diff options
Diffstat (limited to 'spec/graphql')
63 files changed, 1871 insertions, 138 deletions
diff --git a/spec/graphql/features/authorization_spec.rb b/spec/graphql/features/authorization_spec.rb index 4f54695e5be..6e5a8b9f4be 100644 --- a/spec/graphql/features/authorization_spec.rb +++ b/spec/graphql/features/authorization_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe 'Gitlab::Graphql::Authorization' do +RSpec.describe 'Gitlab::Graphql::Authorization' do include GraphqlHelpers let_it_be(:user) { create(:user) } diff --git a/spec/graphql/features/feature_flag_spec.rb b/spec/graphql/features/feature_flag_spec.rb index 51914cf0ca8..b484663d675 100644 --- a/spec/graphql/features/feature_flag_spec.rb +++ b/spec/graphql/features/feature_flag_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe 'Graphql Field feature flags' do +RSpec.describe 'Graphql Field feature flags' do include GraphqlHelpers let_it_be(:user) { create(:user) } diff --git a/spec/graphql/gitlab_schema_spec.rb b/spec/graphql/gitlab_schema_spec.rb index 8960ad91543..5d6aa863994 100644 --- a/spec/graphql/gitlab_schema_spec.rb +++ b/spec/graphql/gitlab_schema_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe GitlabSchema do +RSpec.describe GitlabSchema do let_it_be(:connections) { GitlabSchema.connections.all_wrappers } let(:user) { build :user } @@ -46,12 +46,6 @@ describe GitlabSchema do expect(connection).to eq(Gitlab::Graphql::Pagination::ExternallyPaginatedArrayConnection) end - it 'paginates FilterableArray using `Pagination::FilterableArrayConnection`' do - connection = connections[Gitlab::Graphql::FilterableArray] - - expect(connection).to eq(Gitlab::Graphql::Pagination::FilterableArrayConnection) - end - describe '.execute' do context 'for different types of users' do context 'when no context' do diff --git a/spec/graphql/mutations/alert_management/alerts/set_assignees_spec.rb b/spec/graphql/mutations/alert_management/alerts/set_assignees_spec.rb new file mode 100644 index 00000000000..a025b3d344a --- /dev/null +++ b/spec/graphql/mutations/alert_management/alerts/set_assignees_spec.rb @@ -0,0 +1,167 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Mutations::AlertManagement::Alerts::SetAssignees do + let_it_be(:starting_assignee) { create(:user) } + let_it_be(:unassigned_user) { create(:user) } + let_it_be(:alert) { create(:alert_management_alert, assignees: [starting_assignee]) } + let_it_be(:project) { alert.project } + + let(:current_user) { starting_assignee } + let(:assignee_usernames) { [unassigned_user.username] } + let(:operation_mode) { nil } + + let(:args) do + { + project_path: project.full_path, + iid: alert.iid, + assignee_usernames: assignee_usernames, + operation_mode: operation_mode + } + end + + before_all do + project.add_developer(starting_assignee) + project.add_developer(unassigned_user) + end + + specify { expect(described_class).to require_graphql_authorizations(:update_alert_management_alert) } + + describe '#resolve' do + let(:expected_assignees) { [unassigned_user] } + + subject(:resolve) { mutation_for(project, current_user).resolve(args) } + + shared_examples 'successful resolution' do + after do + alert.assignees = [starting_assignee] + end + + it 'successfully resolves' do + expect(resolve).to eq(alert: alert.reload, errors: []) + expect(alert.assignees).to eq(expected_assignees) + end + end + + shared_examples 'noop' do + it 'makes no changes' do + original_assignees = alert.assignees + + expect(resolve).to eq(alert: alert.reload, errors: []) + expect(alert.assignees).to eq(original_assignees) + end + end + + context 'when operation mode is not specified' do + it_behaves_like 'successful resolution' + end + + context 'when user does not have permission to update alerts' do + let(:current_user) { create(:user) } + + it 'raises an error if the resource is not accessible to the user' do + expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + end + end + + context 'for APPEND operation' do + let(:operation_mode) { Types::MutationOperationModeEnum.enum[:append] } + + # Only allow a single assignee + context 'when a different user is already assigned' do + it_behaves_like 'noop' + end + + context 'when no users are specified' do + let(:assignee_usernames) { [] } + + it_behaves_like 'noop' + end + + context 'when a user is specified and no user is assigned' do + before do + alert.assignees = [] + end + + it_behaves_like 'successful resolution' + end + + context 'when the specified user is already assigned to the alert' do + let(:assignee_usernames) { [starting_assignee.username] } + + it_behaves_like 'noop' + end + end + + context 'for REPLACE operation' do + let(:operation_mode) { Types::MutationOperationModeEnum.enum[:replace] } + + context 'when a different user is already assigned' do + it_behaves_like 'successful resolution' + end + + context 'when no users are specified' do + let(:assignee_usernames) { [] } + let(:expected_assignees) { [] } + + it_behaves_like 'successful resolution' + end + + context 'when a user is specified and no user is assigned' do + before do + alert.assignees = [] + end + + it_behaves_like 'successful resolution' + end + + context 'when the specified user is already assigned to the alert' do + let(:assignee_usernames) { [starting_assignee.username] } + + it_behaves_like 'noop' + end + + context 'when multiple users are specified' do + let(:assignees) { [starting_assignee, unassigned_user] } + let(:assignee_usernames) { assignees.map(&:username) } + let(:expected_assignees) { [assignees.last] } + + it_behaves_like 'successful resolution' + end + end + + context 'for REMOVE operation' do + let(:operation_mode) { Types::MutationOperationModeEnum.enum[:remove] } + + context 'when a different user is already assigned' do + it_behaves_like 'noop' + end + + context 'when no users are specified' do + let(:assignee_usernames) { [] } + + it_behaves_like 'noop' + end + + context 'when a user is specified and no user is assigned' do + before do + alert.assignees = [] + end + + it_behaves_like 'noop' + end + + context 'when the specified user is already assigned to the alert' do + let(:assignee_usernames) { [starting_assignee.username] } + let(:expected_assignees) { [] } + + it_behaves_like 'successful resolution' + end + end + end + + def mutation_for(project, user) + described_class.new(object: project, context: { current_user: user }, field: nil) + end +end diff --git a/spec/graphql/mutations/alert_management/create_alert_issue_spec.rb b/spec/graphql/mutations/alert_management/create_alert_issue_spec.rb index 1e51767cf0e..fa5a84b4fcc 100644 --- a/spec/graphql/mutations/alert_management/create_alert_issue_spec.rb +++ b/spec/graphql/mutations/alert_management/create_alert_issue_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe Mutations::AlertManagement::CreateAlertIssue do +RSpec.describe Mutations::AlertManagement::CreateAlertIssue do let_it_be(:current_user) { create(:user) } let_it_be(:project) { create(:project) } let_it_be(:alert) { create(:alert_management_alert, project: project, status: 'triggered') } diff --git a/spec/graphql/mutations/alert_management/update_alert_status_spec.rb b/spec/graphql/mutations/alert_management/update_alert_status_spec.rb index 8b9abd9497d..68513c02040 100644 --- a/spec/graphql/mutations/alert_management/update_alert_status_spec.rb +++ b/spec/graphql/mutations/alert_management/update_alert_status_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe Mutations::AlertManagement::UpdateAlertStatus do +RSpec.describe Mutations::AlertManagement::UpdateAlertStatus do let_it_be(:current_user) { create(:user) } let_it_be(:alert) { create(:alert_management_alert, :triggered) } let_it_be(:project) { alert.project } @@ -33,7 +33,7 @@ describe Mutations::AlertManagement::UpdateAlertStatus do context 'error occurs when updating' do it 'returns the alert with errors' do # Stub an error on the alert - allow_next_instance_of(Resolvers::AlertManagementAlertResolver) do |resolver| + allow_next_instance_of(Resolvers::AlertManagement::AlertResolver) do |resolver| allow(resolver).to receive(:resolve).and_return(alert) end diff --git a/spec/graphql/mutations/branches/create_spec.rb b/spec/graphql/mutations/branches/create_spec.rb index 744f8f1f2bc..e378a8e3d41 100644 --- a/spec/graphql/mutations/branches/create_spec.rb +++ b/spec/graphql/mutations/branches/create_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe Mutations::Branches::Create do +RSpec.describe Mutations::Branches::Create do subject(:mutation) { described_class.new(object: nil, context: context, field: nil) } let_it_be(:project) { create(:project, :public, :repository) } diff --git a/spec/graphql/mutations/commits/create_spec.rb b/spec/graphql/mutations/commits/create_spec.rb new file mode 100644 index 00000000000..bb0b8c577b0 --- /dev/null +++ b/spec/graphql/mutations/commits/create_spec.rb @@ -0,0 +1,180 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Mutations::Commits::Create do + subject(:mutation) { described_class.new(object: nil, context: context, field: nil) } + + let_it_be(:project) { create(:project, :public, :repository) } + let_it_be(:user) { create(:user) } + let(:context) do + GraphQL::Query::Context.new( + query: OpenStruct.new(schema: nil), + values: { current_user: user }, + object: nil + ) + end + + specify { expect(described_class).to require_graphql_authorizations(:push_code) } + + describe '#resolve' do + subject { mutation.resolve(project_path: project.full_path, branch: branch, message: message, actions: actions) } + + let(:branch) { 'master' } + let(:message) { 'Commit message' } + let(:actions) do + [ + { + action: 'create', + file_path: 'NEW_FILE.md', + content: 'Hello' + } + ] + end + + let(:mutated_commit) { subject[:commit] } + + it 'raises an error if the resource is not accessible to the user' do + expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + end + + context 'when user does not have enough permissions' do + before do + project.add_guest(user) + end + + it 'raises an error' do + expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + end + end + + context 'when user is a maintainer of a different project' do + before do + create(:project_empty_repo).add_maintainer(user) + end + + it 'raises an error' do + expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + end + end + + context 'when the user can create a commit' do + let(:deltas) { mutated_commit.raw_deltas } + + before_all do + project.add_developer(user) + end + + context 'when service successfully creates a new commit' do + it 'returns a new commit' do + expect(mutated_commit).to have_attributes(message: message, project: project) + expect(subject[:errors]).to be_empty + + expect_to_contain_deltas([ + a_hash_including(a_mode: '0', b_mode: '100644', new_file: true, new_path: 'NEW_FILE.md') + ]) + end + end + + context 'when request has multiple actions' do + let(:actions) do + [ + { + action: 'create', + file_path: 'foo/foobar', + content: 'some content' + }, + { + action: 'delete', + file_path: 'README.md' + }, + { + action: 'move', + file_path: "LICENSE.md", + previous_path: "LICENSE", + content: "some content" + }, + { + action: 'update', + file_path: 'VERSION', + content: 'new content' + }, + { + action: 'chmod', + file_path: 'CHANGELOG', + execute_filemode: true + } + ] + end + + it 'returns a new commit' do + expect(mutated_commit).to have_attributes(message: message, project: project) + expect(subject[:errors]).to be_empty + + expect_to_contain_deltas([ + a_hash_including(a_mode: '0', b_mode: '100644', new_path: 'foo/foobar'), + a_hash_including(deleted_file: true, new_path: 'README.md'), + a_hash_including(deleted_file: true, new_path: 'LICENSE'), + a_hash_including(new_file: true, new_path: 'LICENSE.md'), + a_hash_including(new_file: false, new_path: 'VERSION'), + a_hash_including(a_mode: '100644', b_mode: '100755', new_path: 'CHANGELOG') + ]) + end + end + + context 'when actions are not defined' do + let(:actions) { [] } + + it 'returns a new commit' do + expect(mutated_commit).to have_attributes(message: message, project: project) + expect(subject[:errors]).to be_empty + + expect_to_contain_deltas([]) + end + end + + context 'when branch does not exist' do + let(:branch) { 'unknown' } + + it 'returns errors' do + expect(mutated_commit).to be_nil + expect(subject[:errors]).to eq(['You can only create or edit files when you are on a branch']) + end + end + + context 'when message is not set' do + let(:message) { nil } + + it 'returns errors' do + expect(mutated_commit).to be_nil + expect(subject[:errors]).to eq(['3:UserCommitFiles: empty CommitMessage']) + end + end + + context 'when actions are incorrect' do + let(:actions) { [{ action: 'unknown', file_path: 'test.md', content: '' }] } + + it 'returns errors' do + expect(mutated_commit).to be_nil + expect(subject[:errors]).to eq(['Unknown action \'unknown\'']) + end + end + + context 'when branch is protected' do + before do + create(:protected_branch, project: project, name: branch) + end + + it 'returns errors' do + expect(mutated_commit).to be_nil + expect(subject[:errors]).to eq(['You are not allowed to push into this branch']) + end + end + end + end + + def expect_to_contain_deltas(expected_deltas) + expect(deltas.count).to eq(expected_deltas.count) + expect(deltas).to include(*expected_deltas) + end +end diff --git a/spec/graphql/mutations/concerns/mutations/resolves_group_spec.rb b/spec/graphql/mutations/concerns/mutations/resolves_group_spec.rb index 51d3c4f5d6b..6bed3a752ed 100644 --- a/spec/graphql/mutations/concerns/mutations/resolves_group_spec.rb +++ b/spec/graphql/mutations/concerns/mutations/resolves_group_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe Mutations::ResolvesGroup do +RSpec.describe Mutations::ResolvesGroup do let(:mutation_class) do Class.new(Mutations::BaseMutation) do include Mutations::ResolvesGroup diff --git a/spec/graphql/mutations/concerns/mutations/resolves_issuable_spec.rb b/spec/graphql/mutations/concerns/mutations/resolves_issuable_spec.rb index 145e42e2a51..706a54931ea 100644 --- a/spec/graphql/mutations/concerns/mutations/resolves_issuable_spec.rb +++ b/spec/graphql/mutations/concerns/mutations/resolves_issuable_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe Mutations::ResolvesIssuable do +RSpec.describe Mutations::ResolvesIssuable do let_it_be(:mutation_class) do Class.new(Mutations::BaseMutation) do include Mutations::ResolvesIssuable diff --git a/spec/graphql/mutations/concerns/mutations/resolves_project_spec.rb b/spec/graphql/mutations/concerns/mutations/resolves_project_spec.rb deleted file mode 100644 index b5c349f6284..00000000000 --- a/spec/graphql/mutations/concerns/mutations/resolves_project_spec.rb +++ /dev/null @@ -1,22 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -describe Mutations::ResolvesProject do - let(:mutation_class) do - Class.new(Mutations::BaseMutation) do - include Mutations::ResolvesProject - end - end - - let(:context) { double } - - subject(:mutation) { mutation_class.new(object: nil, context: context, field: nil) } - - it 'uses the ProjectsResolver to resolve projects by path' do - project = create(:project) - - expect(Resolvers::ProjectResolver).to receive(:new).with(object: nil, context: context, field: nil).and_call_original - expect(mutation.resolve_project(full_path: project.full_path).sync).to eq(project) - end -end diff --git a/spec/graphql/mutations/container_expiration_policies/update_spec.rb b/spec/graphql/mutations/container_expiration_policies/update_spec.rb new file mode 100644 index 00000000000..fc90f437576 --- /dev/null +++ b/spec/graphql/mutations/container_expiration_policies/update_spec.rb @@ -0,0 +1,96 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Mutations::ContainerExpirationPolicies::Update do + using RSpec::Parameterized::TableSyntax + + let_it_be(:project, reload: true) { create(:project) } + let_it_be(:user) { create(:user) } + + let(:container_expiration_policy) { project.container_expiration_policy } + let(:params) { { project_path: project.full_path, cadence: '3month', keep_n: 100, older_than: '14d' } } + + specify { expect(described_class).to require_graphql_authorizations(:destroy_container_image) } + + describe '#resolve' do + subject { described_class.new(object: project, context: { current_user: user }, field: nil).resolve(params) } + + RSpec.shared_examples 'returning a success' do + it 'returns the container expiration policy with no errors' do + expect(subject).to eq( + container_expiration_policy: container_expiration_policy, + errors: [] + ) + end + end + + RSpec.shared_examples 'updating the container expiration policy' do + it_behaves_like 'updating the container expiration policy attributes', mode: :update, from: { cadence: '1d', keep_n: 10, older_than: '90d' }, to: { cadence: '3month', keep_n: 100, older_than: '14d' } + + it_behaves_like 'returning a success' + + context 'with invalid params' do + let_it_be(:params) { { project_path: project.full_path, cadence: '20d' } } + + it_behaves_like 'not creating the container expiration policy' + + it "doesn't update the cadence" do + expect { subject } + .not_to change { container_expiration_policy.reload.cadence } + end + + it 'returns an error' do + expect(subject).to eq( + container_expiration_policy: nil, + errors: ['Cadence is not included in the list'] + ) + end + end + end + + RSpec.shared_examples 'denying access to container expiration policy' do + it 'raises Gitlab::Graphql::Errors::ResourceNotAvailable' do + expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + end + end + + context 'with existing container expiration policy' do + where(:user_role, :shared_examples_name) do + :maintainer | 'updating the container expiration policy' + :developer | 'updating the container expiration policy' + :reporter | 'denying access to container expiration policy' + :guest | 'denying access to container expiration policy' + :anonymous | 'denying access to container expiration policy' + end + + with_them do + before do + project.send("add_#{user_role}", user) unless user_role == :anonymous + end + + it_behaves_like params[:shared_examples_name] + end + end + + context 'without existing container expiration policy' do + let_it_be(:project, reload: true) { create(:project, :without_container_expiration_policy) } + + where(:user_role, :shared_examples_name) do + :maintainer | 'creating the container expiration policy' + :developer | 'creating the container expiration policy' + :reporter | 'denying access to container expiration policy' + :guest | 'denying access to container expiration policy' + :anonymous | 'denying access to container expiration policy' + end + + with_them do + before do + project.send("add_#{user_role}", user) unless user_role == :anonymous + end + + it_behaves_like params[:shared_examples_name] + end + end + end +end diff --git a/spec/graphql/mutations/design_management/delete_spec.rb b/spec/graphql/mutations/design_management/delete_spec.rb index 60be6dad62a..3efa865c64b 100644 --- a/spec/graphql/mutations/design_management/delete_spec.rb +++ b/spec/graphql/mutations/design_management/delete_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe Mutations::DesignManagement::Delete do +RSpec.describe Mutations::DesignManagement::Delete do include DesignManagementTestHelpers let(:issue) { create(:issue) } diff --git a/spec/graphql/mutations/design_management/upload_spec.rb b/spec/graphql/mutations/design_management/upload_spec.rb index 783af70448c..326d88cea80 100644 --- a/spec/graphql/mutations/design_management/upload_spec.rb +++ b/spec/graphql/mutations/design_management/upload_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require 'spec_helper' -describe Mutations::DesignManagement::Upload do +RSpec.describe Mutations::DesignManagement::Upload do include DesignManagementTestHelpers include ConcurrentHelpers diff --git a/spec/graphql/mutations/discussions/toggle_resolve_spec.rb b/spec/graphql/mutations/discussions/toggle_resolve_spec.rb new file mode 100644 index 00000000000..9ac4d6ab165 --- /dev/null +++ b/spec/graphql/mutations/discussions/toggle_resolve_spec.rb @@ -0,0 +1,155 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Mutations::Discussions::ToggleResolve do + subject(:mutation) do + described_class.new(object: nil, context: { current_user: user }, field: nil) + end + + let_it_be(:project) { create(:project, :repository) } + + describe '#resolve' do + subject do + mutation.resolve({ id: id_arg, resolve: resolve_arg }) + end + + let(:id_arg) { discussion.to_global_id.to_s } + let(:resolve_arg) { true } + let(:mutated_discussion) { subject[:discussion] } + let(:errors) { subject[:errors] } + + shared_examples 'a working resolve method' do + context 'when the user does not have permission' do + let_it_be(:user) { create(:user) } + + it 'raises an error if the resource is not accessible to the user' do + expect { subject }.to raise_error( + Gitlab::Graphql::Errors::ResourceNotAvailable, + "The resource that you are attempting to access does not exist or you don't have permission to perform this action" + ) + end + end + + context 'when the user has permission' do + let_it_be(:user) { create(:user, developer_projects: [project]) } + + context 'when discussion cannot be found' do + let(:id_arg) { "#{discussion.to_global_id}foo" } + + it 'raises an error' do + expect { subject }.to raise_error( + Gitlab::Graphql::Errors::ResourceNotAvailable, + "The resource that you are attempting to access does not exist or you don't have permission to perform this action" + ) + end + end + + context 'when discussion is not a Discussion' do + let(:discussion) { create(:note, noteable: noteable, project: project) } + + it 'raises an error' do + expect { subject }.to raise_error( + Gitlab::Graphql::Errors::ArgumentError, + "#{discussion.to_global_id} is not a valid id for Discussion." + ) + end + end + + shared_examples 'returns a resolved discussion without errors' do + it 'returns a resolved discussion' do + expect(mutated_discussion).to be_resolved + end + + it 'returns empty errors' do + expect(errors).to be_empty + end + end + + shared_examples 'returns an unresolved discussion without errors' do + it 'returns an unresolved discussion' do + expect(mutated_discussion).not_to be_resolved + end + + it 'returns empty errors' do + expect(errors).to be_empty + end + end + + context 'when the `resolve` argument is true' do + include_examples 'returns a resolved discussion without errors' + + context 'when the discussion is already resolved' do + before do + discussion.resolve!(user) + end + + include_examples 'returns a resolved discussion without errors' + end + + context 'when the service raises an `ActiveRecord::RecordNotSaved` error' do + before do + allow_next_instance_of(::Discussions::ResolveService) do |service| + allow(service).to receive(:execute).and_raise(ActiveRecord::RecordNotSaved) + end + end + + it 'does not resolve the discussion' do + expect(mutated_discussion).not_to be_resolved + end + + it 'returns errors' do + expect(errors).to contain_exactly('Discussion failed to be resolved') + end + end + end + + context 'when the `resolve` argument is false' do + let(:resolve_arg) { false } + + context 'when the discussion is resolved' do + before do + discussion.resolve!(user) + end + + include_examples 'returns an unresolved discussion without errors' + + context 'when the service raises an `ActiveRecord::RecordNotSaved` error' do + before do + allow_next_instance_of(discussion.class) do |instance| + allow(instance).to receive(:unresolve!).and_raise(ActiveRecord::RecordNotSaved) + end + end + + it 'does not unresolve the discussion' do + expect(mutated_discussion).to be_resolved + end + + it 'returns errors' do + expect(errors).to contain_exactly('Discussion failed to be unresolved') + end + end + end + + context 'when the discussion is already unresolved' do + include_examples 'returns an unresolved discussion without errors' + end + end + end + end + + context 'when discussion is on a merge request' do + let_it_be(:noteable) { create(:merge_request, source_project: project) } + let(:discussion) { create(:diff_note_on_merge_request, noteable: noteable, project: project).to_discussion } + + it_behaves_like 'a working resolve method' + end + + context 'when discussion is on a design' do + let_it_be(:noteable) { create(:design, :with_file, issue: create(:issue, project: project)) } + let(:discussion) { create(:diff_note_on_design, noteable: noteable, project: project).to_discussion } + + it_behaves_like 'a working resolve method' + end + end +end diff --git a/spec/graphql/mutations/issues/set_confidential_spec.rb b/spec/graphql/mutations/issues/set_confidential_spec.rb index c90ce2658d6..820f9aa5e17 100644 --- a/spec/graphql/mutations/issues/set_confidential_spec.rb +++ b/spec/graphql/mutations/issues/set_confidential_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe Mutations::Issues::SetConfidential do +RSpec.describe Mutations::Issues::SetConfidential do let(:issue) { create(:issue) } let(:user) { create(:user) } diff --git a/spec/graphql/mutations/issues/set_due_date_spec.rb b/spec/graphql/mutations/issues/set_due_date_spec.rb index 84df6fce7c7..a638971d966 100644 --- a/spec/graphql/mutations/issues/set_due_date_spec.rb +++ b/spec/graphql/mutations/issues/set_due_date_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe Mutations::Issues::SetDueDate do +RSpec.describe Mutations::Issues::SetDueDate do let(:issue) { create(:issue) } let(:user) { create(:user) } diff --git a/spec/graphql/mutations/merge_requests/create_spec.rb b/spec/graphql/mutations/merge_requests/create_spec.rb new file mode 100644 index 00000000000..88acd3ed5b6 --- /dev/null +++ b/spec/graphql/mutations/merge_requests/create_spec.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Mutations::MergeRequests::Create do + subject(:mutation) { described_class.new(object: nil, context: context, field: nil) } + + let_it_be(:project) { create(:project, :public, :repository) } + let_it_be(:user) { create(:user) } + let_it_be(:context) do + GraphQL::Query::Context.new( + query: OpenStruct.new(schema: nil), + values: { current_user: user }, + object: nil + ) + end + + describe '#resolve' do + subject do + mutation.resolve( + project_path: project.full_path, + title: title, + source_branch: source_branch, + target_branch: target_branch, + description: description + ) + end + + let(:title) { 'MergeRequest' } + let(:source_branch) { 'feature' } + let(:target_branch) { 'master' } + let(:description) { nil } + + let(:mutated_merge_request) { subject[:merge_request] } + + it 'raises an error if the resource is not accessible to the user' do + expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + end + + context 'when user does not have enough permissions to create a merge request' do + before do + project.add_guest(user) + end + + it 'raises an error if the resource is not accessible to the user' do + expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + end + end + + context 'when the user can create a merge request' do + before_all do + project.add_developer(user) + end + + it 'creates a new merge request' do + expect { mutated_merge_request }.to change(MergeRequest, :count).by(1) + end + + it 'returns a new merge request' do + expect(mutated_merge_request.title).to eq(title) + expect(subject[:errors]).to be_empty + end + + context 'when optional description field is set' do + let(:description) { 'content' } + + it 'returns a new merge request with a description' do + expect(mutated_merge_request.description).to eq(description) + expect(subject[:errors]).to be_empty + end + end + + context 'when service cannot create a merge request' do + let(:title) { nil } + + it 'does not create a new merge request' do + expect { mutated_merge_request }.not_to change(MergeRequest, :count) + end + + it 'returns errors' do + expect(mutated_merge_request).to be_nil + expect(subject[:errors]).to eq(['Title can\'t be blank']) + end + end + end + end +end diff --git a/spec/graphql/resolvers/alert_management_alert_resolver_spec.rb b/spec/graphql/resolvers/alert_management/alert_resolver_spec.rb index 971a81a826d..6c12f765e69 100644 --- a/spec/graphql/resolvers/alert_management_alert_resolver_spec.rb +++ b/spec/graphql/resolvers/alert_management/alert_resolver_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe Resolvers::AlertManagementAlertResolver do +describe Resolvers::AlertManagement::AlertResolver do include GraphqlHelpers let_it_be(:current_user) { create(:user) } @@ -45,11 +45,11 @@ describe Resolvers::AlertManagementAlertResolver do let_it_be(:alert_count_3) { create(:alert_management_alert, project: project, events: 3) } it 'sorts alerts ascending' do - expect(resolve_alerts(sort: :events_count_asc)).to eq [alert_2, alert_1, alert_count_3, alert_count_6] + expect(resolve_alerts(sort: :event_count_asc)).to eq [alert_2, alert_1, alert_count_3, alert_count_6] end it 'sorts alerts descending' do - expect(resolve_alerts(sort: :events_count_desc)).to eq [alert_count_6, alert_count_3, alert_1, alert_2] + expect(resolve_alerts(sort: :event_count_desc)).to eq [alert_count_6, alert_count_3, alert_1, alert_2] end end end diff --git a/spec/graphql/resolvers/base_resolver_spec.rb b/spec/graphql/resolvers/base_resolver_spec.rb index 0a21b2797ee..6c384349577 100644 --- a/spec/graphql/resolvers/base_resolver_spec.rb +++ b/spec/graphql/resolvers/base_resolver_spec.rb @@ -41,9 +41,35 @@ describe Resolvers::BaseResolver do end end + context 'when the resolver returns early' do + let(:resolver) do + Class.new(described_class) do + def ready?(**args) + [false, %w(early return)] + end + + def resolve(**args) + raise 'Should not get here' + end + end + end + + it 'runs correctly in our test framework' do + expect(resolve(resolver)).to contain_exactly('early', 'return') + end + + it 'single selects the first early return value' do + expect(resolve(resolver.single)).to eq('early') + end + + it 'last selects the last early return value' do + expect(resolve(resolver.last)).to eq('return') + end + end + describe '.last' do it 'returns a subclass from the resolver' do - expect(last_resolver.last.superclass).to eq(last_resolver) + expect(last_resolver.last.ancestors).to include(last_resolver) end it 'returns the same subclass every time' do @@ -95,4 +121,28 @@ describe Resolvers::BaseResolver do end end end + + describe '#synchronized_object' do + let(:object) { double(foo: :the_foo) } + + let(:resolver) do + Class.new(described_class) do + def resolve(**args) + [synchronized_object.foo] + end + end + end + + it 'handles raw objects' do + expect(resolve(resolver, obj: object)).to contain_exactly(:the_foo) + end + + it 'handles lazy objects' do + delayed = BatchLoader::GraphQL.for(1).batch do |_, loader| + loader.call(1, object) + end + + expect(resolve(resolver, obj: delayed)).to contain_exactly(:the_foo) + end + end end diff --git a/spec/graphql/resolvers/concerns/looks_ahead_spec.rb b/spec/graphql/resolvers/concerns/looks_ahead_spec.rb new file mode 100644 index 00000000000..8b83f887846 --- /dev/null +++ b/spec/graphql/resolvers/concerns/looks_ahead_spec.rb @@ -0,0 +1,177 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe LooksAhead do + include GraphqlHelpers + + let_it_be(:the_user) { create(:user) } + let_it_be(:label_a) { create(:label) } + let_it_be(:label_b) { create(:label) } + let_it_be(:issue_a) { create(:issue, author: the_user, labels: [label_a, label_b]) } + let_it_be(:issue_b) { create(:issue, author: the_user, labels: [label_a]) } + let_it_be(:issue_c) { create(:issue, author: the_user, labels: [label_b]) } + + # Simplified schema to test lookahead + let_it_be(:schema) do + issues_resolver = Class.new(Resolvers::BaseResolver) do + include LooksAhead + + def resolve_with_lookahead(**args) + apply_lookahead(object.issues) + end + + def preloads + { labels: [:labels] } + end + end + + label = Class.new(GraphQL::Schema::Object) do + graphql_name 'Label' + field :id, Integer, null: false + end + issue = Class.new(GraphQL::Schema::Object) do + graphql_name 'Issue' + field :title, String, null: true + field :labels, label.connection_type, null: true + end + 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, + extras: [:lookahead], + resolver: issues_resolver, + null: true + end + + Class.new(GraphQL::Schema) do + query(Class.new(GraphQL::Schema::Object) do + graphql_name 'Query' + field :find_user, user, null: true do + argument :username, String, required: true + end + + def find_user(username:) + context[:user_db].find { |u| u.username == username } + end + end) + end + end + + def query(doc = document) + GraphQL::Query.new(schema, + document: doc, + context: { user_db: [the_user] }, + variables: { username: the_user.username }) + end + + let(:document) do + GraphQL.parse <<-GRAPHQL + query($username: String!){ + findUser(username: $username) { + name + issues { + nodes { + title + labels { nodes { id } } + } + } + issuesWithLookahead { + nodes { + title + labels { nodes { id } } + } + } + } + } + GRAPHQL + end + + def run_query(gql_query) + query(GraphQL.parse(gql_query)).result + end + + shared_examples 'a working query on the test schema' do + it 'has a good test setup', :aggregate_failures do + expected_label_ids = [label_a, label_b].cycle.take(4).map(&:id) + issue_titles = [issue_a, issue_b, issue_c].map(&:title) + + res = query.result + + expect(res['errors']).to be_blank + expect(res.dig('data', 'findUser', 'name')).to eq(the_user.name) + %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 + end + end + + it_behaves_like 'a working query on the test schema' + + it 'preloads labels on issues' do + expect(the_user.issues).to receive(:preload).with(:labels) + + query.result + end + + context 'the feature flag is off' do + before do + stub_feature_flags(described_class::FEATURE_FLAG => false) + end + + it_behaves_like 'a working query on the test schema' + + it 'does not preload labels on issues' do + expect(the_user.issues).not_to receive(:preload).with(:labels) + + query.result + end + end + + it 'issues fewer queries than the naive approach' do + the_user.reload # ensure no attributes are loaded before we begin + naive = <<-GQL + query($username: String!){ + findUser(username: $username) { + name + issues { + nodes { + labels { nodes { id } } + } + } + } + } + GQL + with_lookahead = <<-GQL + query($username: String!){ + findUser(username: $username) { + name + issuesWithLookahead { + nodes { + labels { nodes { id } } + } + } + } + } + GQL + + expect { run_query(with_lookahead) }.to issue_fewer_queries_than { run_query(naive) } + end + + private + + def all_label_ids(result, field_name) + result.dig('data', 'findUser', field_name, 'nodes').flat_map do |node| + node.dig('labels', 'nodes').map { |n| n['id'] } + end + end + + def all_issue_titles(result, field_name) + result.dig('data', 'findUser', field_name, 'nodes').map do |node| + node['title'] + end + end +end diff --git a/spec/graphql/resolvers/concerns/resolves_project_spec.rb b/spec/graphql/resolvers/concerns/resolves_project_spec.rb new file mode 100644 index 00000000000..f29f54483d6 --- /dev/null +++ b/spec/graphql/resolvers/concerns/resolves_project_spec.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe ResolvesProject do + include GraphqlHelpers + + let(:implementing_class) do + Class.new do + include ResolvesProject + end + end + + subject(:instance) { implementing_class.new } + + let_it_be(:project) { create(:project) } + + it 'can resolve projects by path' do + expect(sync(instance.resolve_project(full_path: project.full_path))).to eq(project) + end + + it 'can resolve projects by id' do + expect(sync(instance.resolve_project(project_id: global_id_of(project)))).to eq(project) + end + + it 'complains when both are present' do + expect do + instance.resolve_project(full_path: project.full_path, project_id: global_id_of(project)) + end.to raise_error(::Gitlab::Graphql::Errors::ArgumentError) + end + + it 'complains when neither is present' do + expect do + instance.resolve_project(full_path: nil, project_id: nil) + end.to raise_error(::Gitlab::Graphql::Errors::ArgumentError) + end +end diff --git a/spec/graphql/resolvers/merge_requests_resolver_spec.rb b/spec/graphql/resolvers/merge_requests_resolver_spec.rb index 4217d257ab3..6ff7e1ecac6 100644 --- a/spec/graphql/resolvers/merge_requests_resolver_spec.rb +++ b/spec/graphql/resolvers/merge_requests_resolver_spec.rb @@ -6,61 +6,164 @@ describe Resolvers::MergeRequestsResolver do include GraphqlHelpers let_it_be(:project) { create(:project, :repository) } - let_it_be(:merge_request_1) { create(:merge_request, :simple, source_project: project, target_project: project) } - let_it_be(:merge_request_2) { create(:merge_request, :rebased, source_project: project, target_project: project) } + let_it_be(:current_user) { create(:user) } + let_it_be(:common_attrs) { { author: current_user, source_project: project, target_project: project } } + let_it_be(:merge_request_1) { create(:merge_request, :simple, **common_attrs) } + let_it_be(:merge_request_2) { create(:merge_request, :rebased, **common_attrs) } + 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), **common_attrs) } 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(:iid_1) { merge_request_1.iid } let(:iid_2) { merge_request_2.iid } let(:other_iid) { other_merge_request.iid } + before do + project.add_developer(current_user) + end + describe '#resolve' do - it 'batch-resolves by target project full path and individual IID' do - result = batch_sync(max_queries: 2) do - resolve_mr(project, iid: iid_1) + resolve_mr(project, iid: iid_2) + context 'no 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) end - expect(result).to contain_exactly(merge_request_1, merge_request_2) + it 'returns only merge requests that the current user can see' do + result = resolve_mr(project, {}, user: build(:user)) + + expect(result).to be_empty + end end - it 'batch-resolves by target project full path and IIDS' do - result = batch_sync(max_queries: 2) do - resolve_mr(project, iids: [iid_1, iid_2]) + context 'by iid alone' do + it 'batch-resolves by target project full path and individual IID' do + result = batch_sync(max_queries: 2) do + [iid_1, iid_2].map { |iid| resolve_mr_single(project, iid) } + end + + expect(result).to contain_exactly(merge_request_1, merge_request_2) + end + + it 'batch-resolves by target project full path and IIDS' do + result = batch_sync(max_queries: 2) do + resolve_mr(project, iids: [iid_1, iid_2]) + end + + expect(result).to contain_exactly(merge_request_1, merge_request_2) + end + + it 'can batch-resolve merge requests from different projects' do + result = batch_sync(max_queries: 3) do + resolve_mr(project, iids: iid_1) + + resolve_mr(project, iids: iid_2) + + resolve_mr(other_project, iids: other_iid) + end + + expect(result).to contain_exactly(merge_request_1, merge_request_2, other_merge_request) + end + + it 'resolves an unknown iid to be empty' do + result = batch_sync { resolve_mr_single(project, -1) } + + expect(result).to be_nil end - expect(result).to contain_exactly(merge_request_1, merge_request_2) + it 'resolves empty iids to be empty' do + result = batch_sync { resolve_mr(project, iids: []) } + + expect(result).to be_empty + end + + it 'resolves an unknown project to be nil when single' do + result = batch_sync { resolve_mr_single(nil, iid_1) } + + expect(result).to be_nil + end + + it 'resolves an unknown project to be empty' do + result = batch_sync { resolve_mr(nil, iids: [iid_1]) } + + expect(result).to be_empty + end end - it 'can batch-resolve merge requests from different projects' do - result = batch_sync(max_queries: 3) do - resolve_mr(project, iid: iid_1) + - resolve_mr(project, iid: iid_2) + - resolve_mr(other_project, iid: other_iid) + context 'by source branches' do + it 'takes one argument' do + result = resolve_mr(project, source_branch: [merge_request_3.source_branch]) + + expect(result).to contain_exactly(merge_request_3) end - expect(result).to contain_exactly(merge_request_1, merge_request_2, other_merge_request) + it 'takes more than one argument' do + mrs = [merge_request_3, merge_request_4] + branches = mrs.map(&:source_branch) + result = resolve_mr(project, source_branch: branches ) + + expect(result).to match_array(mrs) + end end - it 'resolves an unknown iid to be empty' do - result = batch_sync { resolve_mr(project, iid: -1) } + context 'by target branches' do + it 'takes one argument' do + result = resolve_mr(project, target_branch: [merge_request_3.target_branch]) + + expect(result).to contain_exactly(merge_request_3) + end - expect(result.compact).to be_empty + it 'takes more than one argument' do + mrs = [merge_request_3, merge_request_4] + branches = mrs.map(&:target_branch) + result = resolve_mr(project, target_branch: branches ) + + expect(result.compact).to match_array(mrs) + end end - it 'resolves empty iids to be empty' do - result = batch_sync { resolve_mr(project, iids: []) } + context 'by state' do + it 'takes one argument' do + result = resolve_mr(project, state: 'locked') - expect(result).to be_empty + expect(result).to contain_exactly(merge_request_4, merge_request_5) + end end - it 'resolves an unknown project to be empty' do - result = batch_sync { resolve_mr(nil, iid: iid_1) } + context 'by label' do + let_it_be(:label) { merge_request_6.labels.first } + let_it_be(:with_label) { create(:labeled_merge_request, :closed, labels: [label], **common_attrs) } - expect(result.compact).to be_empty + it 'takes one argument' do + result = resolve_mr(project, label_name: [label.title]) + + expect(result).to contain_exactly(merge_request_6, with_label) + end + + it 'takes multiple arguments, with semantics of ALL MUST MATCH' do + result = resolve_mr(project, label_name: merge_request_6.labels.map(&:title)) + + expect(result).to contain_exactly(merge_request_6) + 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) + + result = resolve_mr(project, source_branch: [merge_request_4.source_branch], state: 'locked') + + expect(result.compact).to contain_exactly(merge_request_4) + end end end - def resolve_mr(project, args) - resolve(described_class, obj: project, args: args) + def resolve_mr_single(project, iid) + resolve_mr(project, { iids: iid }, resolver: described_class.single) + end + + def resolve_mr(project, args, resolver: described_class, user: current_user) + resolve(resolver, obj: project, args: args, ctx: { current_user: user }) end end diff --git a/spec/graphql/resolvers/project_members_resolver_spec.rb b/spec/graphql/resolvers/project_members_resolver_spec.rb new file mode 100644 index 00000000000..3209838850b --- /dev/null +++ b/spec/graphql/resolvers/project_members_resolver_spec.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Resolvers::ProjectMembersResolver do + include GraphqlHelpers + + context "with a group" do + let_it_be(:root_group) { create(:group) } + let_it_be(:group_1) { create(:group, parent: root_group) } + let_it_be(:group_2) { create(:group, parent: root_group) } + let_it_be(:project) { create(:project, :public, group: group_1) } + + let_it_be(:user_1) { create(:user, name: 'test user') } + let_it_be(:user_2) { create(:user, name: 'test user 2') } + let_it_be(:user_3) { create(:user, name: 'another user 1') } + let_it_be(:user_4) { create(:user, name: 'another user 2') } + + let_it_be(:project_member) { create(:project_member, user: user_1, project: project) } + let_it_be(:group_1_member) { create(:group_member, user: user_2, group: group_1) } + let_it_be(:group_2_member) { create(:group_member, user: user_3, group: group_2) } + let_it_be(:root_group_member) { create(:group_member, user: user_4, group: root_group) } + + let(:args) { {} } + + subject do + resolve(described_class, obj: project, args: args, ctx: { context: user_4 }) + end + + describe '#resolve' do + it 'finds all project members' do + expect(subject).to contain_exactly(project_member, group_1_member, root_group_member) + end + + context 'with search' do + context 'when the search term matches a user' do + let(:args) { { search: 'test' } } + + it 'searches users by user name' do + expect(subject).to contain_exactly(project_member, group_1_member) + end + end + + context 'when the search term does not match any user' do + let(:args) { { search: 'nothing' } } + + it 'is empty' do + expect(subject).to be_empty + end + end + end + + context 'when project is nil' do + let(:project) { nil } + + it 'returns nil' do + expect(subject).to be_empty + end + end + end + end +end diff --git a/spec/graphql/resolvers/project_pipeline_resolver_spec.rb b/spec/graphql/resolvers/project_pipeline_resolver_spec.rb new file mode 100644 index 00000000000..72049f16d7d --- /dev/null +++ b/spec/graphql/resolvers/project_pipeline_resolver_spec.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Resolvers::ProjectPipelineResolver do + include GraphqlHelpers + + let_it_be(:project) { create(:project) } + let_it_be(:pipeline) { create(:ci_pipeline, project: project, iid: '1234') } + let_it_be(:other_pipeline) { create(:ci_pipeline) } + let(:current_user) { create(:user) } + + def resolve_pipeline(project, args) + resolve(described_class, obj: project, args: args, ctx: { current_user: current_user }) + end + + it 'resolves pipeline for the passed iid' do + result = batch_sync do + resolve_pipeline(project, { iid: '1234' }) + end + + expect(result).to eq(pipeline) + end + + it 'does not resolve a pipeline outside the project' do + result = batch_sync do + resolve_pipeline(other_pipeline.project, { iid: '1234' }) + end + + expect(result).to be_nil + end + + it 'errors when no iid is passed' do + expect { resolve_pipeline(project, {}) }.to raise_error(ArgumentError) + end +end diff --git a/spec/graphql/resolvers/projects/jira_imports_resolver_spec.rb b/spec/graphql/resolvers/projects/jira_imports_resolver_spec.rb index 7146bfb441b..9811075a613 100644 --- a/spec/graphql/resolvers/projects/jira_imports_resolver_spec.rb +++ b/spec/graphql/resolvers/projects/jira_imports_resolver_spec.rb @@ -40,16 +40,6 @@ describe Resolvers::Projects::JiraImportsResolver 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) } - context 'when feature flag disabled' do - let(:current_user) { user } - - before do - stub_feature_flags(jira_issue_import: false) - end - - it_behaves_like 'no Jira import access' - end - context 'when user cannot read Jira imports' do context 'when anonymous user' do let(:current_user) { nil } diff --git a/spec/graphql/resolvers/projects/jira_projects_resolver_spec.rb b/spec/graphql/resolvers/projects/jira_projects_resolver_spec.rb new file mode 100644 index 00000000000..364e2aa6ca8 --- /dev/null +++ b/spec/graphql/resolvers/projects/jira_projects_resolver_spec.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Resolvers::Projects::JiraProjectsResolver do + include GraphqlHelpers + + describe '#resolve' do + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project) } + + shared_examples 'no project service access' do + it 'raises error' do + expect do + resolve_jira_projects + end.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + end + end + + context 'when project has no jira service' do + let_it_be(:jira_service) { nil } + + context 'when user is a maintainer' do + before do + project.add_maintainer(user) + end + + it_behaves_like 'no project service access' + end + end + + context 'when project has jira service' do + let(:jira_service) { create(:jira_service, project: project) } + + context 'when user is a developer' do + before do + project.add_developer(user) + end + + it_behaves_like 'no project service access' + end + + context 'when user is a maintainer' do + before do + project.add_maintainer(user) + end + + context 'when Jira connection is valid' do + include_context 'jira projects request context' + + it 'returns jira projects' do + jira_projects = resolve_jira_projects + project_keys = jira_projects.map(&:key) + project_names = jira_projects.map(&:name) + project_ids = jira_projects.map(&:id) + + expect(jira_projects.size).to eq 2 + expect(project_keys).to eq(%w(EX ABC)) + expect(project_names).to eq(%w(Example Alphabetical)) + expect(project_ids).to eq(%w(10000 10001)) + end + end + + context 'when Jira connection is not valid' do + before do + WebMock.stub_request(:get, 'https://jira.example.com/rest/api/2/project/search?maxResults=50&query=&startAt=0') + .to_raise(JIRA::HTTPError.new(double(message: 'Some failure.'))) + end + + it 'raises failure error' do + expect { resolve_jira_projects }.to raise_error('Jira request error: Some failure.') + end + end + end + end + end + + def resolve_jira_projects(args = {}, context = { current_user: user }) + resolve(described_class, obj: jira_service, args: args, ctx: context) + end +end diff --git a/spec/graphql/resolvers/user_resolver_spec.rb b/spec/graphql/resolvers/user_resolver_spec.rb new file mode 100644 index 00000000000..45a8816bf26 --- /dev/null +++ b/spec/graphql/resolvers/user_resolver_spec.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Resolvers::UserResolver do + include GraphqlHelpers + + describe '#resolve' do + let_it_be(:user) { create(:user) } + + context 'when neither an ID or a username is provided' do + it 'raises an ArgumentError' do + expect { resolve_user } + .to raise_error(Gitlab::Graphql::Errors::ArgumentError) + end + end + + it 'raises an ArgumentError when both an ID and username are provided' do + expect { resolve_user(id: user.to_global_id, username: user.username) } + .to raise_error(Gitlab::Graphql::Errors::ArgumentError) + end + + context 'by username' do + it 'returns the correct user' do + expect( + resolve_user(username: user.username) + ).to eq(user) + end + end + + context 'by ID' do + it 'returns the correct user' do + expect( + resolve_user(id: user.to_global_id) + ).to eq(user) + end + end + end + + private + + def resolve_user(args = {}) + sync(resolve(described_class, args: args)) + end +end diff --git a/spec/graphql/resolvers/users_resolver_spec.rb b/spec/graphql/resolvers/users_resolver_spec.rb new file mode 100644 index 00000000000..e752500d52f --- /dev/null +++ b/spec/graphql/resolvers/users_resolver_spec.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Resolvers::UsersResolver do + include GraphqlHelpers + + let_it_be(:user1) { create(:user) } + let_it_be(:user2) { create(:user) } + + describe '#resolve' do + it 'raises an error when read_users_list is not authorized' do + expect(Ability).to receive(:allowed?).with(nil, :read_users_list).and_return(false) + + expect { resolve_users }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + end + + context 'when no arguments are passed' do + it 'returns all users' do + expect(resolve_users).to contain_exactly(user1, user2) + end + end + + context 'when both ids and usernames are passed ' do + it 'raises an error' do + expect { resolve_users(ids: [user1.to_global_id.to_s], usernames: [user1.username]) } + .to raise_error(Gitlab::Graphql::Errors::ArgumentError) + end + end + + context 'when a set of IDs is passed' do + it 'returns those users' do + expect( + resolve_users(ids: [user1.to_global_id.to_s, user2.to_global_id.to_s]) + ).to contain_exactly(user1, user2) + end + end + + context 'when a set of usernames is passed' do + it 'returns those users' do + expect( + resolve_users(usernames: [user1.username, user2.username]) + ).to contain_exactly(user1, user2) + end + end + end + + def resolve_users(args = {}) + resolve(described_class, args: args) + end +end diff --git a/spec/graphql/types/access_level_enum_spec.rb b/spec/graphql/types/access_level_enum_spec.rb new file mode 100644 index 00000000000..05a6d6d5545 --- /dev/null +++ b/spec/graphql/types/access_level_enum_spec.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe GitlabSchema.types['AccessLevelEnum'] do + specify { expect(described_class.graphql_name).to eq('AccessLevelEnum') } + + it 'exposes all the existing access levels' do + expect(described_class.values.keys).to match_array(%w[NO_ACCESS GUEST REPORTER DEVELOPER MAINTAINER OWNER]) + end +end diff --git a/spec/graphql/types/access_level_type_spec.rb b/spec/graphql/types/access_level_type_spec.rb new file mode 100644 index 00000000000..b9711a9aa4b --- /dev/null +++ b/spec/graphql/types/access_level_type_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true +require 'spec_helper' + +describe GitlabSchema.types['AccessLevel'] do + specify { expect(described_class.graphql_name).to eq('AccessLevel') } + specify { expect(described_class).to require_graphql_authorizations(nil) } + + it 'has expected fields' do + expected_fields = [:integer_value, :string_value] + + expect(described_class).to have_graphql_fields(*expected_fields) + end +end diff --git a/spec/graphql/types/alert_management/alert_type_spec.rb b/spec/graphql/types/alert_management/alert_type_spec.rb index 9c326f30e3c..5acbf8ebb7a 100644 --- a/spec/graphql/types/alert_management/alert_type_spec.rb +++ b/spec/graphql/types/alert_management/alert_type_spec.rb @@ -24,6 +24,9 @@ describe GitlabSchema.types['AlertManagementAlert'] do details created_at updated_at + assignees + notes + discussions ] expect(described_class).to have_graphql_fields(*expected_fields) diff --git a/spec/graphql/types/base_field_spec.rb b/spec/graphql/types/base_field_spec.rb index daed5725e26..3ec33c75803 100644 --- a/spec/graphql/types/base_field_spec.rb +++ b/spec/graphql/types/base_field_spec.rb @@ -135,25 +135,6 @@ describe Types::BaseField do it 'returns true if the feature is enabled' do expect(field.visible?(context)).to eq(true) end - - context 'falsey feature_flag values' do - using RSpec::Parameterized::TableSyntax - - where(:flag, :feature_value, :visible) do - '' | false | true - '' | true | true - nil | false | true - nil | true | true - end - - with_them do - it 'returns the correct value' do - stub_feature_flags(flag => feature_value) - - expect(field.visible?(context)).to eq(visible) - end - end - end end end end diff --git a/spec/graphql/types/commit_action_mode_enum_spec.rb b/spec/graphql/types/commit_action_mode_enum_spec.rb new file mode 100644 index 00000000000..9e1a27ea254 --- /dev/null +++ b/spec/graphql/types/commit_action_mode_enum_spec.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe GitlabSchema.types['CommitActionMode'] do + it { expect(described_class.graphql_name).to eq('CommitActionMode') } + + it 'exposes all the existing commit actions' do + expect(described_class.values.keys).to match_array(%w[CREATE UPDATE MOVE DELETE CHMOD]) + end +end diff --git a/spec/graphql/types/commit_encoding_enum_spec.rb b/spec/graphql/types/commit_encoding_enum_spec.rb new file mode 100644 index 00000000000..30686a0c712 --- /dev/null +++ b/spec/graphql/types/commit_encoding_enum_spec.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe GitlabSchema.types['CommitEncoding'] do + it { expect(described_class.graphql_name).to eq('CommitEncoding') } + + it 'exposes all the existing encoding option' do + expect(described_class.values.keys).to match_array(%w[TEXT BASE64]) + end +end diff --git a/spec/graphql/types/container_expiration_policy_cadence_enum_spec.rb b/spec/graphql/types/container_expiration_policy_cadence_enum_spec.rb new file mode 100644 index 00000000000..08c777cd365 --- /dev/null +++ b/spec/graphql/types/container_expiration_policy_cadence_enum_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe GitlabSchema.types['ContainerExpirationPolicyCadenceEnum'] do + let_it_be(:expected_values) { %w[EVERY_DAY EVERY_WEEK EVERY_TWO_WEEKS EVERY_MONTH EVERY_THREE_MONTHS] } + + it_behaves_like 'exposing container expiration policy option', :cadence +end diff --git a/spec/graphql/types/container_expiration_policy_keep_enum_spec.rb b/spec/graphql/types/container_expiration_policy_keep_enum_spec.rb new file mode 100644 index 00000000000..1a5b4bdd3bb --- /dev/null +++ b/spec/graphql/types/container_expiration_policy_keep_enum_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe GitlabSchema.types['ContainerExpirationPolicyKeepEnum'] do + let_it_be(:expected_values) { %w[ONE_TAG FIVE_TAGS TEN_TAGS TWENTY_FIVE_TAGS FIFTY_TAGS ONE_HUNDRED_TAGS] } + + it_behaves_like 'exposing container expiration policy option', :keep_n +end diff --git a/spec/graphql/types/container_expiration_policy_older_than_enum_spec.rb b/spec/graphql/types/container_expiration_policy_older_than_enum_spec.rb new file mode 100644 index 00000000000..47f0ca22522 --- /dev/null +++ b/spec/graphql/types/container_expiration_policy_older_than_enum_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe GitlabSchema.types['ContainerExpirationPolicyOlderThanEnum'] do + let_it_be(:expected_values) { %w[SEVEN_DAYS FOURTEEN_DAYS THIRTY_DAYS NINETY_DAYS] } + + it_behaves_like 'exposing container expiration policy option', :older_than +end diff --git a/spec/graphql/types/container_expiration_policy_type_spec.rb b/spec/graphql/types/container_expiration_policy_type_spec.rb new file mode 100644 index 00000000000..8924ab67847 --- /dev/null +++ b/spec/graphql/types/container_expiration_policy_type_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe GitlabSchema.types['ContainerExpirationPolicy'] do + specify { expect(described_class.graphql_name).to eq('ContainerExpirationPolicy') } + + specify { expect(described_class.description).to eq('A tag expiration policy designed to keep only the images that matter most') } + + specify { expect(described_class).to require_graphql_authorizations(:destroy_container_image) } + + describe 'older_than field' do + subject { described_class.fields['olderThan'] } + + it 'returns older_than enum' do + is_expected.to have_graphql_type(Types::ContainerExpirationPolicyOlderThanEnum) + end + end + + describe 'keep n field' do + subject { described_class.fields['keepN'] } + + it 'returns keep enum' do + is_expected.to have_graphql_type(Types::ContainerExpirationPolicyKeepEnum) + end + end +end diff --git a/spec/graphql/types/evidence_type_spec.rb b/spec/graphql/types/evidence_type_spec.rb new file mode 100644 index 00000000000..4a11f7bcda9 --- /dev/null +++ b/spec/graphql/types/evidence_type_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe GitlabSchema.types['ReleaseEvidence'] do + it { expect(described_class).to require_graphql_authorizations(:download_code) } + + it 'has the expected fields' do + expected_fields = %w[ + id sha filepath collected_at + ] + + expect(described_class).to include_graphql_fields(*expected_fields) + end +end diff --git a/spec/graphql/types/group_member_type_spec.rb b/spec/graphql/types/group_member_type_spec.rb new file mode 100644 index 00000000000..5d09e60d21c --- /dev/null +++ b/spec/graphql/types/group_member_type_spec.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Types::GroupMemberType do + specify { expect(described_class).to expose_permissions_using(Types::PermissionTypes::Group) } + + specify { expect(described_class.graphql_name).to eq('GroupMember') } + + specify { expect(described_class).to require_graphql_authorizations(:read_group) } + + it 'has the expected fields' do + expected_fields = %w[ + access_level created_by created_at updated_at expires_at group + ] + + expect(described_class).to include_graphql_fields(*expected_fields) + end +end diff --git a/spec/graphql/types/group_type_spec.rb b/spec/graphql/types/group_type_spec.rb index a834a9038db..c56cd40ef12 100644 --- a/spec/graphql/types/group_type_spec.rb +++ b/spec/graphql/types/group_type_spec.rb @@ -29,4 +29,6 @@ describe GitlabSchema.types['Group'] do is_expected.to have_graphql_type(Types::BoardType.connection_type) end end + + it_behaves_like 'a GraphQL type with labels' end diff --git a/spec/graphql/types/jira_import_type_spec.rb b/spec/graphql/types/jira_import_type_spec.rb index ac1aa672e30..fa1152aec41 100644 --- a/spec/graphql/types/jira_import_type_spec.rb +++ b/spec/graphql/types/jira_import_type_spec.rb @@ -6,6 +6,9 @@ describe GitlabSchema.types['JiraImport'] do specify { expect(described_class.graphql_name).to eq('JiraImport') } it 'has the expected fields' do - expect(described_class).to have_graphql_fields(:jira_project_key, :createdAt, :scheduled_at, :scheduled_by) + expect(described_class).to have_graphql_fields( + :jira_project_key, :created_at, :scheduled_at, :scheduled_by, + :failed_to_import_count, :imported_issues_count, :total_issue_count + ) end end diff --git a/spec/graphql/types/merge_request_type_spec.rb b/spec/graphql/types/merge_request_type_spec.rb index e7ab2100084..0f48264c99f 100644 --- a/spec/graphql/types/merge_request_type_spec.rb +++ b/spec/graphql/types/merge_request_type_spec.rb @@ -19,10 +19,11 @@ describe GitlabSchema.types['MergeRequest'] do force_remove_source_branch merge_status in_progress_merge_commit_sha merge_error allow_collaboration should_be_rebased rebase_commit_sha rebase_in_progress merge_commit_message default_merge_commit_message - merge_ongoing source_branch_exists mergeable_discussions_state web_url + merge_ongoing mergeable_discussions_state web_url + source_branch_exists target_branch_exists upvotes downvotes head_pipeline pipelines task_completion_status milestone assignees participants subscribed labels discussion_locked time_estimate - total_time_spent reference + total_time_spent reference author merged_at ] expect(described_class).to have_graphql_fields(*expected_fields) diff --git a/spec/graphql/types/metrics/dashboard_type_spec.rb b/spec/graphql/types/metrics/dashboard_type_spec.rb index 81219c596a7..0dbd0d8b38d 100644 --- a/spec/graphql/types/metrics/dashboard_type_spec.rb +++ b/spec/graphql/types/metrics/dashboard_type_spec.rb @@ -7,7 +7,7 @@ describe GitlabSchema.types['MetricsDashboard'] do it 'has the expected fields' do expected_fields = %w[ - path annotations + path annotations schema_validation_warnings ] expect(described_class).to have_graphql_fields(*expected_fields) diff --git a/spec/graphql/types/notes/diff_position_type_spec.rb b/spec/graphql/types/notes/diff_position_type_spec.rb index 01f355cb278..87f3810d55c 100644 --- a/spec/graphql/types/notes/diff_position_type_spec.rb +++ b/spec/graphql/types/notes/diff_position_type_spec.rb @@ -1,11 +1,22 @@ # frozen_string_literal: true + require 'spec_helper' describe GitlabSchema.types['DiffPosition'] do it 'exposes the expected fields' do - expected_fields = [:diff_refs, :file_path, :old_path, - :new_path, :position_type, :old_line, :new_line, :x, :y, - :width, :height] + expected_fields = %i[ + diff_refs + file_path + height + new_line + new_path + old_line + old_path + position_type + width + x + y + ] expect(described_class).to have_graphql_fields(*expected_fields) end diff --git a/spec/graphql/types/notes/discussion_type_spec.rb b/spec/graphql/types/notes/discussion_type_spec.rb index 44774594d17..177000b01b2 100644 --- a/spec/graphql/types/notes/discussion_type_spec.rb +++ b/spec/graphql/types/notes/discussion_type_spec.rb @@ -1,8 +1,22 @@ # frozen_string_literal: true + require 'spec_helper' describe GitlabSchema.types['Discussion'] do - specify { expect(described_class).to have_graphql_fields(:id, :created_at, :notes, :reply_id) } + it 'exposes the expected fields' do + expected_fields = %i[ + created_at + id + notes + reply_id + resolvable + resolved + resolved_at + resolved_by + ] + + expect(described_class).to have_graphql_fields(*expected_fields) + end specify { expect(described_class).to require_graphql_authorizations(:read_note) } end diff --git a/spec/graphql/types/notes/note_type_spec.rb b/spec/graphql/types/notes/note_type_spec.rb index 019f742ee77..d6cd0800234 100644 --- a/spec/graphql/types/notes/note_type_spec.rb +++ b/spec/graphql/types/notes/note_type_spec.rb @@ -1,11 +1,27 @@ # frozen_string_literal: true + require 'spec_helper' describe GitlabSchema.types['Note'] do it 'exposes the expected fields' do - expected_fields = [:id, :project, :author, :body, :created_at, - :updated_at, :discussion, :resolvable, :position, :user_permissions, - :resolved_by, :resolved_at, :system, :body_html, :confidential] + expected_fields = %i[ + author + body + body_html + confidential + created_at + discussion + id + position + project + resolvable + resolved + resolved_at + resolved_by + system + updated_at + user_permissions + ] expect(described_class).to have_graphql_fields(*expected_fields) end diff --git a/spec/graphql/types/notes/noteable_type_spec.rb b/spec/graphql/types/notes/noteable_type_spec.rb index 4a81f45bd4e..88d8eae56d1 100644 --- a/spec/graphql/types/notes/noteable_type_spec.rb +++ b/spec/graphql/types/notes/noteable_type_spec.rb @@ -1,14 +1,23 @@ # frozen_string_literal: true + require 'spec_helper' describe Types::Notes::NoteableType do - specify { expect(described_class).to have_graphql_fields(:notes, :discussions) } + it 'exposes the expected fields' do + expected_fields = %i[ + discussions + notes + ] + + expect(described_class).to have_graphql_fields(*expected_fields) + end describe ".resolve_type" do it 'knows the correct type for objects' do expect(described_class.resolve_type(build(:issue), {})).to eq(Types::IssueType) expect(described_class.resolve_type(build(:merge_request), {})).to eq(Types::MergeRequestType) expect(described_class.resolve_type(build(:design), {})).to eq(Types::DesignManagement::DesignType) + expect(described_class.resolve_type(build(:alert_management_alert), {})).to eq(Types::AlertManagement::AlertType) end end end diff --git a/spec/graphql/types/project_member_type_spec.rb b/spec/graphql/types/project_member_type_spec.rb new file mode 100644 index 00000000000..1b1f6c24a32 --- /dev/null +++ b/spec/graphql/types/project_member_type_spec.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Types::ProjectMemberType do + specify { expect(described_class).to expose_permissions_using(Types::PermissionTypes::Project) } + + specify { expect(described_class.graphql_name).to eq('ProjectMember') } + + specify { expect(described_class).to require_graphql_authorizations(:read_project) } + + it 'has the expected fields' do + expected_fields = %w[ + access_level created_by created_at updated_at expires_at project user + ] + + expect(described_class).to include_graphql_fields(*expected_fields) + end +end diff --git a/spec/graphql/types/project_type_spec.rb b/spec/graphql/types/project_type_spec.rb index 6368f743720..8ee9aa9cf3a 100644 --- a/spec/graphql/types/project_type_spec.rb +++ b/spec/graphql/types/project_type_spec.rb @@ -26,6 +26,7 @@ describe GitlabSchema.types['Project'] do grafanaIntegration autocloseReferencedIssues suggestion_commit_message environments boards jira_import_status jira_imports services releases release alert_management_alerts alert_management_alert alert_management_alert_status_counts + container_expiration_policy ] expect(described_class).to include_graphql_fields(*expected_fields) @@ -45,18 +46,32 @@ describe GitlabSchema.types['Project'] do it { is_expected.to have_graphql_resolver(Resolvers::IssuesResolver) } end - describe 'merge_requests field' do + describe 'merge_request field' do subject { described_class.fields['mergeRequest'] } it { is_expected.to have_graphql_type(Types::MergeRequestType) } it { is_expected.to have_graphql_resolver(Resolvers::MergeRequestsResolver.single) } + it { is_expected.to have_graphql_arguments(:iid) } end - describe 'merge_request field' do + describe 'merge_requests field' do subject { described_class.fields['mergeRequests'] } it { is_expected.to have_graphql_type(Types::MergeRequestType.connection_type) } it { is_expected.to have_graphql_resolver(Resolvers::MergeRequestsResolver) } + + it do + is_expected.to have_graphql_arguments(:iids, + :source_branches, + :target_branches, + :state, + :labels, + :before, + :after, + :first, + :last + ) + end end describe 'snippets field' do @@ -80,6 +95,13 @@ describe GitlabSchema.types['Project'] do it { is_expected.to have_graphql_resolver(Resolvers::EnvironmentsResolver) } end + describe 'members field' do + subject { described_class.fields['projectMembers'] } + + it { is_expected.to have_graphql_type(Types::ProjectMemberType.connection_type) } + it { is_expected.to have_graphql_resolver(Resolvers::ProjectMembersResolver) } + end + describe 'boards field' do subject { described_class.fields['boards'] } @@ -111,4 +133,12 @@ describe GitlabSchema.types['Project'] do it { is_expected.to have_graphql_type(Types::ReleaseType.connection_type) } it { is_expected.to have_graphql_resolver(Resolvers::ReleasesResolver) } end + + describe 'container expiration policy field' do + subject { described_class.fields['containerExpirationPolicy'] } + + it { is_expected.to have_graphql_type(Types::ContainerExpirationPolicyType) } + end + + it_behaves_like 'a GraphQL type with labels' end diff --git a/spec/graphql/types/projects/jira_project_type_spec.rb b/spec/graphql/types/projects/jira_project_type_spec.rb new file mode 100644 index 00000000000..cbb01117717 --- /dev/null +++ b/spec/graphql/types/projects/jira_project_type_spec.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe GitlabSchema.types['JiraProject'] do + it { expect(described_class.graphql_name).to eq('JiraProject') } + + it 'has basic expected fields' do + expect(described_class).to have_graphql_fields(:key, :project_id, :name) + end +end diff --git a/spec/graphql/types/projects/jira_service_type_spec.rb b/spec/graphql/types/projects/jira_service_type_spec.rb index 91d7e4586cb..fad0c91caab 100644 --- a/spec/graphql/types/projects/jira_service_type_spec.rb +++ b/spec/graphql/types/projects/jira_service_type_spec.rb @@ -6,7 +6,7 @@ describe GitlabSchema.types['JiraService'] do specify { expect(described_class.graphql_name).to eq('JiraService') } it 'has basic expected fields' do - expect(described_class).to have_graphql_fields(:type, :active) + expect(described_class).to have_graphql_fields(:type, :active, :projects) end specify { expect(described_class).to require_graphql_authorizations(:admin_project) } diff --git a/spec/graphql/types/query_type_spec.rb b/spec/graphql/types/query_type_spec.rb index 1f269a80d00..1194391c26a 100644 --- a/spec/graphql/types/query_type_spec.rb +++ b/spec/graphql/types/query_type_spec.rb @@ -8,7 +8,18 @@ describe GitlabSchema.types['Query'] do end it 'has the expected fields' do - expected_fields = %i[project namespace group echo metadata current_user snippets design_management] + expected_fields = %i[ + project + namespace + group + echo + metadata + current_user + snippets + design_management + user + users + ] expect(described_class).to have_graphql_fields(*expected_fields).at_least end diff --git a/spec/graphql/types/release_assets_type_spec.rb b/spec/graphql/types/release_assets_type_spec.rb new file mode 100644 index 00000000000..58f0f7ee697 --- /dev/null +++ b/spec/graphql/types/release_assets_type_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe GitlabSchema.types['ReleaseAssets'] do + it { expect(described_class).to require_graphql_authorizations(:read_release) } + + it 'has the expected fields' do + expected_fields = %w[ + assets_count links sources + ] + + expect(described_class).to include_graphql_fields(*expected_fields) + end + + describe 'links field' do + subject { described_class.fields['links'] } + + it { is_expected.to have_graphql_type(Types::ReleaseLinkType.connection_type) } + end + + describe 'sources field' do + subject { described_class.fields['sources'] } + + it { is_expected.to have_graphql_type(Types::ReleaseSourceType.connection_type) } + end +end diff --git a/spec/graphql/types/release_links_type_spec.rb b/spec/graphql/types/release_links_type_spec.rb new file mode 100644 index 00000000000..49e04e120f4 --- /dev/null +++ b/spec/graphql/types/release_links_type_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe GitlabSchema.types['ReleaseLink'] do + it { expect(described_class).to require_graphql_authorizations(:read_release) } + + it 'has the expected fields' do + expected_fields = %w[ + id name url external link_type + ] + + expect(described_class).to include_graphql_fields(*expected_fields) + end +end diff --git a/spec/graphql/types/release_source_type_spec.rb b/spec/graphql/types/release_source_type_spec.rb new file mode 100644 index 00000000000..e471ac1a5ac --- /dev/null +++ b/spec/graphql/types/release_source_type_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe GitlabSchema.types['ReleaseSource'] do + it { expect(described_class).to require_graphql_authorizations(:read_release_sources) } + + it 'has the expected fields' do + expected_fields = %w[ + format url + ] + + expect(described_class).to include_graphql_fields(*expected_fields) + end +end diff --git a/spec/graphql/types/release_type_spec.rb b/spec/graphql/types/release_type_spec.rb index d22a0b4f0fa..feafe5ed519 100644 --- a/spec/graphql/types/release_type_spec.rb +++ b/spec/graphql/types/release_type_spec.rb @@ -9,19 +9,31 @@ describe GitlabSchema.types['Release'] do expected_fields = %w[ tag_name tag_path description description_html - name milestones author commit + name assets milestones evidences author commit created_at released_at ] expect(described_class).to include_graphql_fields(*expected_fields) end + describe 'assets field' do + subject { described_class.fields['assets'] } + + it { is_expected.to have_graphql_type(Types::ReleaseAssetsType) } + end + describe 'milestones field' do subject { described_class.fields['milestones'] } it { is_expected.to have_graphql_type(Types::MilestoneType.connection_type) } end + describe 'evidences field' do + subject { described_class.fields['evidences'] } + + it { is_expected.to have_graphql_type(Types::EvidenceType.connection_type) } + end + describe 'author field' do subject { described_class.fields['author'] } diff --git a/spec/graphql/types/resolvable_interface_spec.rb b/spec/graphql/types/resolvable_interface_spec.rb new file mode 100644 index 00000000000..231287f9969 --- /dev/null +++ b/spec/graphql/types/resolvable_interface_spec.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Types::ResolvableInterface do + it 'exposes the expected fields' do + expected_fields = %i[ + resolvable + resolved + resolved_at + resolved_by + ] + + expect(described_class).to have_graphql_fields(*expected_fields) + end +end diff --git a/spec/graphql/types/snippet_type_spec.rb b/spec/graphql/types/snippet_type_spec.rb index adc13d4d651..f24419ce9cc 100644 --- a/spec/graphql/types/snippet_type_spec.rb +++ b/spec/graphql/types/snippet_type_spec.rb @@ -11,11 +11,49 @@ describe GitlabSchema.types['Snippet'] do :visibility_level, :created_at, :updated_at, :web_url, :raw_url, :ssh_url_to_repo, :http_url_to_repo, :notes, :discussions, :user_permissions, - :description_html, :blob] + :description_html, :blob, :blobs] expect(described_class).to have_graphql_fields(*expected_fields) end + context 'when restricted visibility level is set to public' do + let_it_be(:snippet) { create(:personal_snippet, :repository, :public, author: user) } + + let(:current_user) { user } + let(:query) do + %( + { + snippets { + nodes { + author { + id + } + } + } + } + ) + end + let(:response) { subject.dig('data', 'snippets', 'nodes')[0] } + + subject { GitlabSchema.execute(query, context: { current_user: current_user }).as_json } + + before do + stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC]) + end + + it 'returns snippet author' do + expect(response['author']).to be_present + end + + context 'when user is not logged in' do + let(:current_user) { nil } + + it 'returns snippet author as nil' do + expect(response['author']).to be_nil + end + end + end + describe 'authorizations' do specify { expect(described_class).to require_graphql_authorizations(:read_snippet) } end @@ -76,30 +114,14 @@ describe GitlabSchema.types['Snippet'] do describe '#blob' do let(:query_blob) { subject.dig('data', 'snippets', 'edges')[0]['node']['blob'] } - let(:query) do - %( - { - snippets { - edges { - node { - blob { - name - path - } - } - } - } - } - ) - end - subject { GitlabSchema.execute(query, context: { current_user: user }).as_json } + subject { GitlabSchema.execute(snippet_query_for(field: 'blob'), context: { current_user: user }).as_json } context 'when snippet has repository' do let!(:snippet) { create(:personal_snippet, :repository, :public, author: user) } let(:blob) { snippet.blobs.first } - it 'returns blob from the repository' do + it 'returns the first blob from the repository' do expect(query_blob['name']).to eq blob.name expect(query_blob['path']).to eq blob.path end @@ -115,4 +137,58 @@ describe GitlabSchema.types['Snippet'] do end end end + + describe '#blobs' do + let_it_be(:snippet) { create(:personal_snippet, :public, author: user) } + let(:query_blobs) { subject.dig('data', 'snippets', 'edges')[0]['node']['blobs'] } + + subject { GitlabSchema.execute(snippet_query_for(field: 'blobs'), context: { current_user: user }).as_json } + + shared_examples 'an array' do + it 'returns an array of snippet blobs' do + expect(query_blobs).to be_an(Array) + end + end + + context 'when snippet does not have a repository' do + let(:blob) { snippet.blob } + + it_behaves_like 'an array' + + it 'contains the first blob from the snippet' do + expect(query_blobs.first['name']).to eq blob.name + expect(query_blobs.first['path']).to eq blob.path + end + end + + 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' + + it 'contains all the blobs from the repository' do + resulting_blobs_names = query_blobs.map { |b| b['name'] } + + expect(resulting_blobs_names).to match_array(blobs.map(&:name)) + end + end + end + + def snippet_query_for(field:) + %( + { + snippets { + edges { + node { + #{field} { + name + path + } + } + } + } + } + ) + end end diff --git a/spec/graphql/types/snippets/file_input_action_enum_spec.rb b/spec/graphql/types/snippets/file_input_action_enum_spec.rb new file mode 100644 index 00000000000..2ccc8b04b8f --- /dev/null +++ b/spec/graphql/types/snippets/file_input_action_enum_spec.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Types::Snippets::FileInputActionEnum do + specify { expect(described_class.graphql_name).to eq('SnippetFileInputActionEnum') } + + it 'exposes all file input action types' do + expect(described_class.values.keys).to eq(%w[create update delete move]) + end +end diff --git a/spec/graphql/types/snippets/file_input_type_spec.rb b/spec/graphql/types/snippets/file_input_type_spec.rb new file mode 100644 index 00000000000..62e5caf20b7 --- /dev/null +++ b/spec/graphql/types/snippets/file_input_type_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Types::Snippets::FileInputType do + specify { expect(described_class.graphql_name).to eq('SnippetFileInputType') } + + it 'has the correct arguments' do + expect(described_class.arguments.keys).to match_array(%w[filePath action previousPath content]) + end + + it 'sets the type of action argument to FileInputActionEnum' do + expect(described_class.arguments['action'].type.of_type).to eq(Types::Snippets::FileInputActionEnum) + end +end diff --git a/spec/graphql/types/user_type_spec.rb b/spec/graphql/types/user_type_spec.rb index cf1e91afb80..7b34588b0ff 100644 --- a/spec/graphql/types/user_type_spec.rb +++ b/spec/graphql/types/user_type_spec.rb @@ -9,7 +9,19 @@ describe GitlabSchema.types['User'] do it 'has the expected fields' do expected_fields = %w[ - id user_permissions snippets name username avatarUrl webUrl todos state + id + user_permissions + snippets + name + username + avatarUrl + webUrl + todos + state + authoredMergeRequests + assignedMergeRequests + groupMemberships + projectMemberships ] expect(described_class).to have_graphql_fields(*expected_fields) |