diff options
Diffstat (limited to 'spec/requests/api/graphql')
25 files changed, 807 insertions, 76 deletions
diff --git a/spec/requests/api/graphql/boards/board_lists_query_spec.rb b/spec/requests/api/graphql/boards/board_lists_query_spec.rb index 0838900eaba..5d5b963fed5 100644 --- a/spec/requests/api/graphql/boards/board_lists_query_spec.rb +++ b/spec/requests/api/graphql/boards/board_lists_query_spec.rb @@ -7,8 +7,8 @@ RSpec.describe 'get board lists' do let_it_be(:user) { create(:user) } let_it_be(:unauth_user) { create(:user) } - let_it_be(:project) { create(:project, creator_id: user.id, namespace: user.namespace ) } let_it_be(:group) { create(:group, :private) } + let_it_be(:project) { create(:project, creator_id: user.id, group: group) } let_it_be(:project_label) { create(:label, project: project, name: 'Development') } let_it_be(:project_label2) { create(:label, project: project, name: 'Testing') } let_it_be(:group_label) { create(:group_label, group: group, name: 'Development') } @@ -111,12 +111,19 @@ RSpec.describe 'get board lists' do board_parent.add_reporter(user) end - it 'finds the correct list' do + it 'returns the correct list with issue count for matching issue filters' do label_list = create(:list, board: board, label: label, position: 10) + create(:issue, project: project, labels: [label, label2]) + create(:issue, project: project, labels: [label]) - post_graphql(query("id: \"#{global_id_of(label_list)}\""), current_user: user) + post_graphql(query(id: global_id_of(label_list), issueFilters: { labelName: label2.title }), current_user: user) - expect(lists_data[0]['node']['title']).to eq label_list.title + aggregate_failures do + list_node = lists_data[0]['node'] + + expect(list_node['title']).to eq label_list.title + expect(list_node['issuesCount']).to eq 1 + end end end end diff --git a/spec/requests/api/graphql/gitlab_schema_spec.rb b/spec/requests/api/graphql/gitlab_schema_spec.rb index ee7dba545be..fe1c7c15de2 100644 --- a/spec/requests/api/graphql/gitlab_schema_spec.rb +++ b/spec/requests/api/graphql/gitlab_schema_spec.rb @@ -190,7 +190,9 @@ RSpec.describe 'GitlabSchema configurations' do variables: {}.to_s, complexity: 181, depth: 13, - duration_s: 7 + duration_s: 7, + used_fields: an_instance_of(Array), + used_deprecated_fields: an_instance_of(Array) } expect_any_instance_of(Gitlab::Graphql::QueryAnalyzers::LoggerAnalyzer).to receive(:duration).and_return(7) diff --git a/spec/requests/api/graphql/group/merge_requests_spec.rb b/spec/requests/api/graphql/group/merge_requests_spec.rb new file mode 100644 index 00000000000..e9a5e558b1d --- /dev/null +++ b/spec/requests/api/graphql/group/merge_requests_spec.rb @@ -0,0 +1,122 @@ +# frozen_string_literal: true + +require 'spec_helper' + +# Based on ee/spec/requests/api/epics_spec.rb +# Should follow closely in order to ensure all situations are covered +RSpec.describe 'Query.group.mergeRequests' do + include GraphqlHelpers + + let_it_be(:group) { create(:group) } + let_it_be(:sub_group) { create(:group, parent: group) } + + let_it_be(:project_a) { create(:project, :repository, group: group) } + let_it_be(:project_b) { create(:project, :repository, group: group) } + let_it_be(:project_c) { create(:project, :repository, group: sub_group) } + let_it_be(:project_x) { create(:project, :repository) } + let_it_be(:user) { create(:user, developer_projects: [project_x]) } + + let_it_be(:mr_attrs) do + { target_branch: 'master' } + end + + let_it_be(:mr_traits) do + [:unique_branches, :unique_author] + end + + let_it_be(:mrs_a, reload: true) { create_list(:merge_request, 2, *mr_traits, **mr_attrs, source_project: project_a) } + let_it_be(:mrs_b, reload: true) { create_list(:merge_request, 2, *mr_traits, **mr_attrs, source_project: project_b) } + let_it_be(:mrs_c, reload: true) { create_list(:merge_request, 2, *mr_traits, **mr_attrs, source_project: project_c) } + let_it_be(:other_mr) { create(:merge_request, source_project: project_x) } + + let(:mrs_data) { graphql_data_at(:group, :merge_requests, :nodes) } + + before do + group.add_developer(user) + end + + def expected_mrs(mrs) + mrs.map { |mr| a_hash_including('id' => global_id_of(mr)) } + end + + describe 'not passing any arguments' do + let(:query) do + <<~GQL + query($path: ID!) { + group(fullPath: $path) { + mergeRequests { nodes { id } } + } + } + GQL + end + + it 'can find all merge requests in the group, excluding sub-groups' do + post_graphql(query, current_user: user, variables: { path: group.full_path }) + + expect(mrs_data).to match_array(expected_mrs(mrs_a + mrs_b)) + end + end + + describe 'restricting by author' do + let(:query) do + <<~GQL + query($path: ID!, $user: String) { + group(fullPath: $path) { + mergeRequests(authorUsername: $user) { nodes { id author { username } } } + } + } + GQL + end + + let(:author) { mrs_b.first.author } + + it 'can find all merge requests with user as author' do + post_graphql(query, current_user: user, variables: { user: author.username, path: group.full_path }) + + expect(mrs_data).to match_array(expected_mrs([mrs_b.first])) + end + end + + describe 'restricting by assignee' do + let(:query) do + <<~GQL + query($path: ID!, $user: String) { + group(fullPath: $path) { + mergeRequests(assigneeUsername: $user) { nodes { id } } + } + } + GQL + end + + let_it_be(:assignee) { create(:user) } + + before_all do + mrs_b.second.assignees << assignee + mrs_a.first.assignees << assignee + end + + it 'can find all merge requests assigned to user' do + post_graphql(query, current_user: user, variables: { user: assignee.username, path: group.full_path }) + + expect(mrs_data).to match_array(expected_mrs([mrs_a.first, mrs_b.second])) + end + end + + describe 'passing include_subgroups: true' do + let(:query) do + <<~GQL + query($path: ID!) { + group(fullPath: $path) { + mergeRequests(includeSubgroups: true) { nodes { id } } + } + } + GQL + end + + it 'can find all merge requests in the group, including sub-groups' do + post_graphql(query, current_user: user, variables: { path: group.full_path }) + + expect(mrs_data).to match_array(expected_mrs(mrs_a + mrs_b + mrs_c)) + end + end +end diff --git a/spec/requests/api/graphql/instance_statistics_measurements_spec.rb b/spec/requests/api/graphql/instance_statistics_measurements_spec.rb index b8cbe54534a..5d7dbcf2e3c 100644 --- a/spec/requests/api/graphql/instance_statistics_measurements_spec.rb +++ b/spec/requests/api/graphql/instance_statistics_measurements_spec.rb @@ -9,13 +9,16 @@ RSpec.describe 'InstanceStatisticsMeasurements' do let!(:instance_statistics_measurement_1) { create(:instance_statistics_measurement, :project_count, recorded_at: 20.days.ago, count: 5) } let!(:instance_statistics_measurement_2) { create(:instance_statistics_measurement, :project_count, recorded_at: 10.days.ago, count: 10) } - let(:query) { graphql_query_for(:instanceStatisticsMeasurements, 'identifier: PROJECTS', 'nodes { count }') } + let(:query) { graphql_query_for(:instanceStatisticsMeasurements, 'identifier: PROJECTS', 'nodes { count identifier }') } before do post_graphql(query, current_user: current_user) end it 'returns measurement objects' do - expect(graphql_data.dig('instanceStatisticsMeasurements', 'nodes')).to eq([{ "count" => 10 }, { "count" => 5 }]) + expect(graphql_data.dig('instanceStatisticsMeasurements', 'nodes')).to eq([ + { "count" => 10, 'identifier' => 'PROJECTS' }, + { "count" => 5, 'identifier' => 'PROJECTS' } + ]) end end diff --git a/spec/requests/api/graphql/mutations/award_emojis/add_spec.rb b/spec/requests/api/graphql/mutations/award_emojis/add_spec.rb index 1d38bb39d59..3aaebb5095a 100644 --- a/spec/requests/api/graphql/mutations/award_emojis/add_spec.rb +++ b/spec/requests/api/graphql/mutations/award_emojis/add_spec.rb @@ -45,8 +45,9 @@ RSpec.describe 'Adding an AwardEmoji' do it_behaves_like 'a mutation that does not create an AwardEmoji' - it_behaves_like 'a mutation that returns top-level errors', - errors: ['Cannot award emoji to this resource'] + it_behaves_like 'a mutation that returns top-level errors' do + let(:match_errors) { include(/was provided invalid value for awardableId/) } + end end context 'when the given awardable is an Awardable but still cannot be awarded an emoji' do diff --git a/spec/requests/api/graphql/mutations/award_emojis/remove_spec.rb b/spec/requests/api/graphql/mutations/award_emojis/remove_spec.rb index c6e8800de1f..7cd39f93ae7 100644 --- a/spec/requests/api/graphql/mutations/award_emojis/remove_spec.rb +++ b/spec/requests/api/graphql/mutations/award_emojis/remove_spec.rb @@ -50,8 +50,9 @@ RSpec.describe 'Removing an AwardEmoji' do it_behaves_like 'a mutation that does not destroy an AwardEmoji' - it_behaves_like 'a mutation that returns top-level errors', - errors: ['Cannot award emoji to this resource'] + it_behaves_like 'a mutation that returns top-level errors' do + let(:match_errors) { include(/was provided invalid value for awardableId/) } + end end context 'when the given awardable is an Awardable' do diff --git a/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb b/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb index 2df59ce97ca..6910ad80a11 100644 --- a/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb +++ b/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb @@ -44,8 +44,9 @@ RSpec.describe 'Toggling an AwardEmoji' do it_behaves_like 'a mutation that does not create or destroy an AwardEmoji' - it_behaves_like 'a mutation that returns top-level errors', - errors: ['Cannot award emoji to this resource'] + it_behaves_like 'a mutation that returns top-level errors' do + let(:match_errors) { include(/was provided invalid value for awardableId/) } + end end context 'when the given awardable is an Awardable but still cannot be awarded an emoji' do diff --git a/spec/requests/api/graphql/mutations/boards/create_spec.rb b/spec/requests/api/graphql/mutations/boards/create_spec.rb new file mode 100644 index 00000000000..c5f981262ea --- /dev/null +++ b/spec/requests/api/graphql/mutations/boards/create_spec.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Mutations::Boards::Create do + let_it_be(:parent) { create(:project) } + let(:project_path) { parent.full_path } + let(:params) do + { + project_path: project_path, + name: name + } + end + + it_behaves_like 'boards create mutation' +end diff --git a/spec/requests/api/graphql/mutations/boards/lists/destroy_spec.rb b/spec/requests/api/graphql/mutations/boards/lists/destroy_spec.rb new file mode 100644 index 00000000000..42f690f53ed --- /dev/null +++ b/spec/requests/api/graphql/mutations/boards/lists/destroy_spec.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Mutations::Boards::Lists::Destroy do + include GraphqlHelpers + + let_it_be(:current_user, reload: true) { create(:user) } + let_it_be(:project, reload: true) { create(:project) } + let_it_be(:board) { create(:board, project: project) } + let_it_be(:list) { create(:list, board: board) } + let(:mutation) do + variables = { + list_id: GitlabSchema.id_from_object(list).to_s + } + + graphql_mutation(:destroy_board_list, variables) + end + + subject { post_graphql_mutation(mutation, current_user: current_user) } + + def mutation_response + graphql_mutation_response(:destroy_board_list) + end + + context 'when the user does not have permission' do + it_behaves_like 'a mutation that returns a top-level access error' + + it 'does not destroy the list' do + expect { subject }.not_to change { List.count } + end + end + + context 'when the user has permission' do + before do + project.add_maintainer(current_user) + end + + context 'when given id is not for a list' do + let_it_be(:list) { build_stubbed(:issue, project: project) } + + it 'returns an error' do + subject + + expect(graphql_errors.first['message']).to include('does not represent an instance of List') + end + end + + context 'when everything is ok' do + it 'destroys the list' do + expect { subject }.to change { List.count }.from(2).to(1) + end + + it 'returns an empty list' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(mutation_response).to have_key('list') + expect(mutation_response['list']).to be_nil + end + end + + context 'when the list is not destroyable' do + let_it_be(:list) { create(:list, board: board, list_type: :backlog) } + + it 'does not destroy the list' do + expect { subject }.not_to change { List.count }.from(3) + end + + it 'returns an error and not nil list' do + subject + + expect(mutation_response['errors']).not_to be_empty + expect(mutation_response['list']).not_to be_nil + end + end + end +end diff --git a/spec/requests/api/graphql/mutations/issues/create_spec.rb b/spec/requests/api/graphql/mutations/issues/create_spec.rb new file mode 100644 index 00000000000..39b408faa90 --- /dev/null +++ b/spec/requests/api/graphql/mutations/issues/create_spec.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Create an issue' do + include GraphqlHelpers + + let_it_be(:current_user) { create(:user) } + let_it_be(:project) { create(:project) } + let_it_be(:assignee1) { create(:user) } + let_it_be(:assignee2) { create(:user) } + let_it_be(:project_label1) { create(:label, project: project) } + let_it_be(:project_label2) { create(:label, project: project) } + let_it_be(:milestone) { create(:milestone, project: project) } + let_it_be(:new_label1) { FFaker::Lorem.word } + let_it_be(:new_label2) { FFaker::Lorem.word } + + let(:input) do + { + 'title' => 'new title', + 'description' => 'new description', + 'confidential' => true, + 'dueDate' => Date.tomorrow.strftime('%Y-%m-%d') + } + end + + let(:mutation) { graphql_mutation(:createIssue, input.merge('projectPath' => project.full_path, 'locked' => true)) } + + let(:mutation_response) { graphql_mutation_response(:create_issue) } + + context 'the user is not allowed to create an issue' do + it_behaves_like 'a mutation that returns a top-level access error' + end + + context 'when user has permissions to create an issue' do + before do + project.add_developer(current_user) + end + + it 'updates the issue' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response['issue']).to include(input) + expect(mutation_response['issue']).to include('discussionLocked' => true) + end + end +end diff --git a/spec/requests/api/graphql/mutations/issues/move_spec.rb b/spec/requests/api/graphql/mutations/issues/move_spec.rb new file mode 100644 index 00000000000..5bbaff61edd --- /dev/null +++ b/spec/requests/api/graphql/mutations/issues/move_spec.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Moving an issue' do + include GraphqlHelpers + + let_it_be(:user) { create(:user) } + let_it_be(:issue) { create(:issue) } + let_it_be(:target_project) { create(:project) } + + let(:mutation) do + variables = { + project_path: issue.project.full_path, + target_project_path: target_project.full_path, + iid: issue.iid.to_s + } + + graphql_mutation(:issue_move, variables, + <<-QL.strip_heredoc + clientMutationId + errors + issue { + title + } + QL + ) + end + + def mutation_response + graphql_mutation_response(:issue_move) + end + + context 'when the user is not allowed to read source project' do + it 'returns an error' do + error = "The resource that you are attempting to access does not exist or you don't have permission to perform this action" + post_graphql_mutation(mutation, current_user: user) + + expect(response).to have_gitlab_http_status(:success) + expect(graphql_errors).to include(a_hash_including('message' => error)) + end + end + + context 'when the user is not allowed to move issue to target project' do + before do + issue.project.add_developer(user) + end + + it 'returns an error' do + error = "Cannot move issue due to insufficient permissions!" + post_graphql_mutation(mutation, current_user: user) + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response['errors'][0]).to eq(error) + end + end + + context 'when the user is allowed to move issue' do + before do + issue.project.add_developer(user) + target_project.add_developer(user) + end + + it 'moves the issue' do + post_graphql_mutation(mutation, current_user: user) + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response.dig('issue', 'title')).to eq(issue.title) + expect(issue.reload.state).to eq('closed') + expect(target_project.issues.find_by_title(issue.title)).to be_present + end + end +end diff --git a/spec/requests/api/graphql/mutations/issues/update_spec.rb b/spec/requests/api/graphql/mutations/issues/update_spec.rb index af52f9d57a3..71f25dbbe49 100644 --- a/spec/requests/api/graphql/mutations/issues/update_spec.rb +++ b/spec/requests/api/graphql/mutations/issues/update_spec.rb @@ -10,13 +10,15 @@ RSpec.describe 'Update of an existing issue' do let_it_be(:issue) { create(:issue, project: project) } let(:input) do { - project_path: project.full_path, - iid: issue.iid.to_s, - locked: true + 'iid' => issue.iid.to_s, + 'title' => 'new title', + 'description' => 'new description', + 'confidential' => true, + 'dueDate' => Date.tomorrow.strftime('%Y-%m-%d') } end - let(:mutation) { graphql_mutation(:update_issue, input) } + let(:mutation) { graphql_mutation(:update_issue, input.merge(project_path: project.full_path, locked: true)) } let(:mutation_response) { graphql_mutation_response(:update_issue) } context 'the user is not allowed to update issue' do @@ -32,9 +34,8 @@ RSpec.describe 'Update of an existing issue' do post_graphql_mutation(mutation, current_user: current_user) expect(response).to have_gitlab_http_status(:success) - expect(mutation_response['issue']).to include( - 'discussionLocked' => true - ) + expect(mutation_response['issue']).to include(input) + expect(mutation_response['issue']).to include('discussionLocked' => true) end end end diff --git a/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/create_spec.rb b/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/create_spec.rb index 10ca2cf1cf8..81d13b29dde 100644 --- a/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/create_spec.rb +++ b/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/create_spec.rb @@ -101,7 +101,9 @@ RSpec.describe Mutations::Metrics::Dashboard::Annotations::Create do graphql_mutation(:create_annotation, variables) end - it_behaves_like 'a mutation that returns top-level errors', errors: ['invalid_id is not a valid GitLab ID.'] + it_behaves_like 'a mutation that returns top-level errors' do + let(:match_errors) { include(/is not a valid Global ID/) } + end end end end @@ -109,7 +111,7 @@ RSpec.describe Mutations::Metrics::Dashboard::Annotations::Create do context 'when annotation source is cluster' do let(:mutation) do variables = { - cluster_id: GitlabSchema.id_from_object(cluster).to_s, + cluster_id: cluster.to_global_id.to_s, starting_at: starting_at, ending_at: ending_at, dashboard_path: dashboard_path, @@ -188,15 +190,17 @@ RSpec.describe Mutations::Metrics::Dashboard::Annotations::Create do graphql_mutation(:create_annotation, variables) end - it_behaves_like 'a mutation that returns top-level errors', errors: ['invalid_id is not a valid GitLab ID.'] + it_behaves_like 'a mutation that returns top-level errors' do + let(:match_errors) { include(/is not a valid Global ID/) } + end end end context 'when both environment_id and cluster_id are provided' do let(:mutation) do variables = { - environment_id: GitlabSchema.id_from_object(environment).to_s, - cluster_id: GitlabSchema.id_from_object(cluster).to_s, + environment_id: environment.to_global_id.to_s, + cluster_id: cluster.to_global_id.to_s, starting_at: starting_at, ending_at: ending_at, dashboard_path: dashboard_path, @@ -210,14 +214,14 @@ RSpec.describe Mutations::Metrics::Dashboard::Annotations::Create do end context 'when a non-cluster or environment id is provided' do + let(:gid) { { environment_id: project.to_global_id.to_s } } let(:mutation) do variables = { - environment_id: GitlabSchema.id_from_object(project).to_s, starting_at: starting_at, ending_at: ending_at, dashboard_path: dashboard_path, description: description - } + }.merge!(gid) graphql_mutation(:create_annotation, variables) end @@ -226,6 +230,18 @@ RSpec.describe Mutations::Metrics::Dashboard::Annotations::Create do project.add_developer(current_user) end - it_behaves_like 'a mutation that returns top-level errors', errors: [described_class::INVALID_ANNOTATION_SOURCE_ERROR] + describe 'non-environment id' do + it_behaves_like 'a mutation that returns top-level errors' do + let(:match_errors) { include(/does not represent an instance of Environment/) } + end + end + + describe 'non-cluster id' do + let(:gid) { { cluster_id: project.to_global_id.to_s } } + + it_behaves_like 'a mutation that returns top-level errors' do + let(:match_errors) { include(/does not represent an instance of Clusters::Cluster/) } + end + end end end diff --git a/spec/requests/api/graphql/mutations/notes/create/note_spec.rb b/spec/requests/api/graphql/mutations/notes/create/note_spec.rb index 391ced7dc98..6d761eb0a54 100644 --- a/spec/requests/api/graphql/mutations/notes/create/note_spec.rb +++ b/spec/requests/api/graphql/mutations/notes/create/note_spec.rb @@ -60,6 +60,14 @@ RSpec.describe 'Adding a Note' do expect(mutation_response['note']['discussion']['id']).to eq(discussion.to_global_id.to_s) end + + context 'when the discussion_id is not for a Discussion' do + let(:discussion) { create(:issue) } + + it_behaves_like 'a mutation that returns top-level errors' do + let(:match_errors) { include(/ does not represent an instance of Discussion/) } + end + end end end end diff --git a/spec/requests/api/graphql/mutations/notes/update/image_diff_note_spec.rb b/spec/requests/api/graphql/mutations/notes/update/image_diff_note_spec.rb index 0c00906d6bf..efa2ceb65c2 100644 --- a/spec/requests/api/graphql/mutations/notes/update/image_diff_note_spec.rb +++ b/spec/requests/api/graphql/mutations/notes/update/image_diff_note_spec.rb @@ -178,6 +178,12 @@ RSpec.describe 'Updating an image DiffNote' do it_behaves_like 'a mutation that returns top-level errors', errors: ['body or position arguments are required'] end + context 'when the resource is not a Note' do + let(:diff_note) { note } + + it_behaves_like 'a Note mutation when the given resource id is not for a Note' + end + context 'when resource is not a DiffNote on an image' do let!(:diff_note) { create(:diff_note_on_merge_request, note: original_body) } diff --git a/spec/requests/api/graphql/mutations/snippets/create_spec.rb b/spec/requests/api/graphql/mutations/snippets/create_spec.rb index 1bb446de708..d2fa3cfc24f 100644 --- a/spec/requests/api/graphql/mutations/snippets/create_spec.rb +++ b/spec/requests/api/graphql/mutations/snippets/create_spec.rb @@ -76,21 +76,25 @@ RSpec.describe 'Creating a Snippet' do expect(mutation_response['snippet']).to be_nil end + + it_behaves_like 'spam flag is present' end shared_examples 'creates snippet' do - it 'returns the created Snippet' do + it 'returns the created Snippet', :aggregate_failures do expect do subject end.to change { Snippet.count }.by(1) + snippet = Snippet.last + created_file_1 = snippet.repository.blob_at('HEAD', file_1[:filePath]) + created_file_2 = snippet.repository.blob_at('HEAD', file_2[:filePath]) + + expect(created_file_1.data).to match(file_1[:content]) + expect(created_file_2.data).to match(file_2[:content]) expect(mutation_response['snippet']['title']).to eq(title) expect(mutation_response['snippet']['description']).to eq(description) expect(mutation_response['snippet']['visibilityLevel']).to eq(visibility_level) - expect(mutation_response['snippet']['blobs'][0]['plainData']).to match(file_1[:content]) - expect(mutation_response['snippet']['blobs'][0]['fileName']).to match(file_1[:file_path]) - expect(mutation_response['snippet']['blobs'][1]['plainData']).to match(file_2[:content]) - expect(mutation_response['snippet']['blobs'][1]['fileName']).to match(file_2[:file_path]) end context 'when action is invalid' do @@ -101,6 +105,10 @@ RSpec.describe 'Creating a Snippet' do end it_behaves_like 'snippet edit usage data counters' + it_behaves_like 'spam flag is present' + it_behaves_like 'can raise spam flag' do + let(:service) { Snippets::CreateService } + end end context 'with PersonalSnippet' do @@ -140,6 +148,9 @@ RSpec.describe 'Creating a Snippet' do it_behaves_like 'a mutation that returns errors in the response', errors: ["Title can't be blank"] it_behaves_like 'does not create snippet' + it_behaves_like 'can raise spam flag' do + let(:service) { Snippets::CreateService } + end end context 'when there non ActiveRecord errors' do diff --git a/spec/requests/api/graphql/mutations/snippets/update_spec.rb b/spec/requests/api/graphql/mutations/snippets/update_spec.rb index 58ce74b9263..21d403c6f73 100644 --- a/spec/requests/api/graphql/mutations/snippets/update_spec.rb +++ b/spec/requests/api/graphql/mutations/snippets/update_spec.rb @@ -37,6 +37,8 @@ RSpec.describe 'Updating a Snippet' do graphql_mutation_response(:update_snippet) end + subject { post_graphql_mutation(mutation, current_user: current_user) } + shared_examples 'graphql update actions' do context 'when the user does not have permission' do let(:current_user) { create(:user) } @@ -46,14 +48,14 @@ RSpec.describe 'Updating a Snippet' do it 'does not update the Snippet' do expect do - post_graphql_mutation(mutation, current_user: current_user) + subject end.not_to change { snippet.reload } end end context 'when the user has permission' do it 'updates the snippet record' do - post_graphql_mutation(mutation, current_user: current_user) + subject expect(snippet.reload.title).to eq(updated_title) end @@ -65,7 +67,7 @@ RSpec.describe 'Updating a Snippet' do expect(blob_to_update.data).not_to eq updated_content expect(blob_to_delete).to be_present - post_graphql_mutation(mutation, current_user: current_user) + subject blob_to_update = blob_at(updated_file) blob_to_delete = blob_at(deleted_file) @@ -73,20 +75,25 @@ RSpec.describe 'Updating a Snippet' do aggregate_failures do expect(blob_to_update.data).to eq updated_content expect(blob_to_delete).to be_nil - expect(blob_in_mutation_response(updated_file)['plainData']).to match(updated_content) expect(mutation_response['snippet']['title']).to eq(updated_title) expect(mutation_response['snippet']['description']).to eq(updated_description) expect(mutation_response['snippet']['visibilityLevel']).to eq('public') end end + it_behaves_like 'can raise spam flag' do + let(:service) { Snippets::UpdateService } + end + + it_behaves_like 'spam flag is present' + context 'when there are ActiveRecord validation errors' do let(:updated_title) { '' } it_behaves_like 'a mutation that returns errors in the response', errors: ["Title can't be blank"] it 'does not update the Snippet' do - post_graphql_mutation(mutation, current_user: current_user) + subject expect(snippet.reload.title).to eq(original_title) end @@ -95,21 +102,21 @@ RSpec.describe 'Updating a Snippet' do blob_to_update = blob_at(updated_file) blob_to_delete = blob_at(deleted_file) - post_graphql_mutation(mutation, current_user: current_user) + subject aggregate_failures do expect(blob_at(updated_file).data).to eq blob_to_update.data expect(blob_at(deleted_file).data).to eq blob_to_delete.data - expect(blob_in_mutation_response(deleted_file)['plainData']).not_to be_nil expect(mutation_response['snippet']['title']).to eq(original_title) expect(mutation_response['snippet']['description']).to eq(original_description) expect(mutation_response['snippet']['visibilityLevel']).to eq('private') end end - end - def blob_in_mutation_response(filename) - mutation_response['snippet']['blobs'].select { |blob| blob['name'] == filename }[0] + it_behaves_like 'spam flag is present' + it_behaves_like 'can raise spam flag' do + let(:service) { Snippets::UpdateService } + end end def blob_at(filename) @@ -150,7 +157,7 @@ RSpec.describe 'Updating a Snippet' do context 'when the author is not a member of the project' do it 'returns an an error' do - post_graphql_mutation(mutation, current_user: current_user) + subject errors = json_response['errors'] expect(errors.first['message']).to eq(Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR) @@ -168,7 +175,7 @@ RSpec.describe 'Updating a Snippet' do it 'returns an an error' do project.project_feature.update_attribute(:snippets_access_level, ProjectFeature::DISABLED) - post_graphql_mutation(mutation, current_user: current_user) + subject errors = json_response['errors'] expect(errors.first['message']).to eq(Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR) diff --git a/spec/requests/api/graphql/mutations/todos/mark_done_spec.rb b/spec/requests/api/graphql/mutations/todos/mark_done_spec.rb index 8bf8b96aff5..8a9a0b9e845 100644 --- a/spec/requests/api/graphql/mutations/todos/mark_done_spec.rb +++ b/spec/requests/api/graphql/mutations/todos/mark_done_spec.rb @@ -76,15 +76,15 @@ RSpec.describe 'Marking todos done' do end context 'when using an invalid gid' do - let(:input) { { id: 'invalid_gid' } } - let(:invalid_gid_error) { 'invalid_gid is not a valid GitLab ID.' } + let(:input) { { id: GitlabSchema.id_from_object(author).to_s } } + let(:invalid_gid_error) { /"#{input[:id]}" does not represent an instance of #{todo1.class}/ } it 'contains the expected error' do post_graphql_mutation(mutation, current_user: current_user) errors = json_response['errors'] expect(errors).not_to be_blank - expect(errors.first['message']).to eq(invalid_gid_error) + expect(errors.first['message']).to match(invalid_gid_error) expect(todo1.reload.state).to eq('pending') expect(todo2.reload.state).to eq('done') diff --git a/spec/requests/api/graphql/mutations/todos/restore_spec.rb b/spec/requests/api/graphql/mutations/todos/restore_spec.rb index 8451dcdf587..a58c7fc69fc 100644 --- a/spec/requests/api/graphql/mutations/todos/restore_spec.rb +++ b/spec/requests/api/graphql/mutations/todos/restore_spec.rb @@ -76,15 +76,15 @@ RSpec.describe 'Restoring Todos' do end context 'when using an invalid gid' do - let(:input) { { id: 'invalid_gid' } } - let(:invalid_gid_error) { 'invalid_gid is not a valid GitLab ID.' } + let(:input) { { id: GitlabSchema.id_from_object(author).to_s } } + let(:invalid_gid_error) { /"#{input[:id]}" does not represent an instance of #{todo1.class}/ } it 'contains the expected error' do post_graphql_mutation(mutation, current_user: current_user) errors = json_response['errors'] expect(errors).not_to be_blank - expect(errors.first['message']).to eq(invalid_gid_error) + expect(errors.first['message']).to match(invalid_gid_error) expect(todo1.reload.state).to eq('done') expect(todo2.reload.state).to eq('pending') diff --git a/spec/requests/api/graphql/project/alert_management/alerts_spec.rb b/spec/requests/api/graphql/project/alert_management/alerts_spec.rb index d3a2e6a1deb..8deed75a466 100644 --- a/spec/requests/api/graphql/project/alert_management/alerts_spec.rb +++ b/spec/requests/api/graphql/project/alert_management/alerts_spec.rb @@ -139,6 +139,19 @@ RSpec.describe 'getting Alert Management Alerts' do it { expect(alerts.size).to eq(0) } end end + + context 'assignee_username' do + let(:alert) { triggered_alert } + let(:assignee) { alert.assignees.first! } + let(:params) { { assignee_username: assignee.username } } + + it_behaves_like 'a working graphql query' + + specify do + expect(alerts.size).to eq(1) + expect(first_alert['iid']).to eq(alert.iid.to_s) + end + end end end end diff --git a/spec/requests/api/graphql/project/issue/designs/notes_spec.rb b/spec/requests/api/graphql/project/issue/designs/notes_spec.rb index 65191e057c7..e25453510d5 100644 --- a/spec/requests/api/graphql/project/issue/designs/notes_spec.rb +++ b/spec/requests/api/graphql/project/issue/designs/notes_spec.rb @@ -31,8 +31,8 @@ RSpec.describe 'Getting designs related to an issue' do post_graphql(query(note_fields), current_user: nil) designs_data = graphql_data['project']['issue']['designs']['designs'] - design_data = designs_data['edges'].first['node'] - note_data = design_data['notes']['edges'].first['node'] + design_data = designs_data['nodes'].first + note_data = design_data['notes']['nodes'].first expect(note_data['id']).to eq(note.to_global_id.to_s) end @@ -40,14 +40,10 @@ RSpec.describe 'Getting designs related to an issue' do def query(note_fields = all_graphql_fields_for(Note)) design_node = <<~NODE designs { - edges { - node { - notes { - edges { - node { - #{note_fields} - } - } + nodes { + notes { + nodes { + #{note_fields} } } } diff --git a/spec/requests/api/graphql/project/issues_spec.rb b/spec/requests/api/graphql/project/issues_spec.rb index 5d4276f47ca..40fec6ba068 100644 --- a/spec/requests/api/graphql/project/issues_spec.rb +++ b/spec/requests/api/graphql/project/issues_spec.rb @@ -53,16 +53,37 @@ RSpec.describe 'getting an issue list for a project' do context 'when limiting the number of results' do let(:query) do - graphql_query_for( - 'project', - { 'fullPath' => project.full_path }, - "issues(first: 1) { #{fields} }" - ) + <<~GQL + query($path: ID!, $n: Int) { + project(fullPath: $path) { + issues(first: $n) { #{fields} } + } + } + GQL + end + + let(:issue_limit) { 1 } + let(:variables) do + { path: project.full_path, n: issue_limit } end it_behaves_like 'a working graphql query' do before do - post_graphql(query, current_user: current_user) + post_graphql(query, current_user: current_user, variables: variables) + end + + it 'only returns N issues' do + expect(issues_data.size).to eq(issue_limit) + end + end + + context 'no limit is provided' do + let(:issue_limit) { nil } + + it 'returns all issues' do + post_graphql(query, current_user: current_user, variables: variables) + + expect(issues_data.size).to be > 1 end end @@ -71,7 +92,7 @@ RSpec.describe 'getting an issue list for a project' do # Newest first, we only want to see the newest checked expect(Ability).not_to receive(:allowed?).with(current_user, :read_issue, issues.first) - post_graphql(query, current_user: current_user) + post_graphql(query, current_user: current_user, variables: variables) end end diff --git a/spec/requests/api/graphql/project/merge_requests_spec.rb b/spec/requests/api/graphql/project/merge_requests_spec.rb index 22b003501a1..c737e0b8caf 100644 --- a/spec/requests/api/graphql/project/merge_requests_spec.rb +++ b/spec/requests/api/graphql/project/merge_requests_spec.rb @@ -13,6 +13,7 @@ RSpec.describe 'getting merge request listings nested in a project' do let_it_be(:merge_request_b) { create(:merge_request, :closed, :unique_branches, source_project: project) } let_it_be(:merge_request_c) { create(:labeled_merge_request, :closed, :unique_branches, source_project: project, labels: [label]) } let_it_be(:merge_request_d) { create(:merge_request, :locked, :unique_branches, source_project: project) } + let_it_be(:merge_request_e) { create(:merge_request, :unique_branches, source_project: project) } let(:results) { graphql_data.dig('project', 'mergeRequests', 'nodes') } @@ -118,7 +119,7 @@ RSpec.describe 'getting merge request listings nested in a project' do context 'there are no search params' do let(:search_params) { nil } - let(:mrs) { [merge_request_a, merge_request_b, merge_request_c, merge_request_d] } + let(:mrs) { [merge_request_a, merge_request_b, merge_request_c, merge_request_d, merge_request_e] } it_behaves_like 'searching with parameters' end @@ -172,6 +173,28 @@ RSpec.describe 'getting merge request listings nested in a project' do it_behaves_like 'searching with parameters' end + context 'when requesting `approved_by`' do + let(:search_params) { { iids: [merge_request_a.iid.to_s, merge_request_b.iid.to_s] } } + let(:extra_iid_for_second_query) { merge_request_c.iid.to_s } + let(:requested_fields) { query_graphql_field(:approved_by, nil, query_graphql_field(:nodes, nil, [:username])) } + + def execute_query + query = query_merge_requests(requested_fields) + post_graphql(query, current_user: current_user) + end + + it 'exposes approver username' do + merge_request_a.approved_by_users << current_user + + execute_query + + user_data = { 'username' => current_user.username } + expect(results).to include(a_hash_including('approvedBy' => { 'nodes' => array_including(user_data) })) + end + + include_examples 'N+1 query check' + end + describe 'fields' do let(:requested_fields) { nil } let(:extra_iid_for_second_query) { merge_request_c.iid.to_s } @@ -209,7 +232,19 @@ RSpec.describe 'getting merge request listings nested in a project' do include_examples 'N+1 query check' end + + context 'when requesting `user_notes_count`' do + let(:requested_fields) { [:user_notes_count] } + + before do + create_list(:note_on_merge_request, 2, noteable: merge_request_a, project: project) + create(:note_on_merge_request, noteable: merge_request_c, project: project) + end + + include_examples 'N+1 query check' + end end + describe 'sorting and pagination' do let(:data_path) { [:project, :mergeRequests] } @@ -241,16 +276,50 @@ RSpec.describe 'getting merge request listings nested in a project' do let(:expected_results) do [ merge_request_b, - merge_request_c, merge_request_d, + merge_request_c, + merge_request_e, merge_request_a ].map(&:to_gid).map(&:to_s) end before do - merge_request_c.metrics.update!(merged_at: 5.days.ago) + five_days_ago = 5.days.ago + + merge_request_d.metrics.update!(merged_at: five_days_ago) + + # same merged_at, the second order column will decide (merge_request.id) + merge_request_c.metrics.update!(merged_at: five_days_ago) + merge_request_b.metrics.update!(merged_at: 1.day.ago) end + + context 'when paginating backwards' do + let(:params) { 'first: 2, sort: MERGED_AT_DESC' } + let(:page_info) { 'pageInfo { startCursor endCursor }' } + + before do + post_graphql(pagination_query(params, page_info), current_user: current_user) + end + + it 'paginates backwards correctly' do + # first page + first_page_response_data = graphql_dig_at(Gitlab::Json.parse(response.body), :data, *data_path, :edges) + end_cursor = graphql_dig_at(Gitlab::Json.parse(response.body), :data, :project, :mergeRequests, :pageInfo, :endCursor) + + # second page + params = "first: 2, after: \"#{end_cursor}\", sort: MERGED_AT_DESC" + post_graphql(pagination_query(params, page_info), current_user: current_user) + start_cursor = graphql_dig_at(Gitlab::Json.parse(response.body), :data, :project, :mergeRequests, :pageInfo, :start_cursor) + + # going back to the first page + + params = "last: 2, before: \"#{start_cursor}\", sort: MERGED_AT_DESC" + post_graphql(pagination_query(params, page_info), current_user: current_user) + backward_paginated_response_data = graphql_dig_at(Gitlab::Json.parse(response.body), :data, *data_path, :edges) + expect(first_page_response_data).to eq(backward_paginated_response_data) + end + end end end end diff --git a/spec/requests/api/graphql/project/milestones_spec.rb b/spec/requests/api/graphql/project/milestones_spec.rb new file mode 100644 index 00000000000..2fede4c7285 --- /dev/null +++ b/spec/requests/api/graphql/project/milestones_spec.rb @@ -0,0 +1,202 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'getting milestone listings nested in a project' do + include GraphqlHelpers + + let_it_be(:today) { Time.now.utc.to_date } + let_it_be(:project) { create(:project, :repository, :public) } + let_it_be(:current_user) { create(:user) } + + let_it_be(:no_dates) { create(:milestone, project: project, title: 'no dates') } + let_it_be(:no_end) { create(:milestone, project: project, title: 'no end', start_date: today - 10.days) } + let_it_be(:no_start) { create(:milestone, project: project, title: 'no start', due_date: today - 5.days) } + let_it_be(:fully_past) { create(:milestone, project: project, title: 'past', start_date: today - 10.days, due_date: today - 5.days) } + let_it_be(:covers_today) { create(:milestone, project: project, title: 'present', start_date: today - 5.days, due_date: today + 5.days) } + let_it_be(:fully_future) { create(:milestone, project: project, title: 'future', start_date: today + 5.days, due_date: today + 10.days) } + let_it_be(:closed) { create(:milestone, :closed, project: project) } + + let(:results) { graphql_data_at(:project, :milestones, :nodes) } + + let(:search_params) { nil } + + def query_milestones(fields) + graphql_query_for( + :project, + { full_path: project.full_path }, + query_graphql_field(:milestones, search_params, [ + query_graphql_field(:nodes, nil, %i[id title]) + ]) + ) + end + + def result_list(expected) + expected.map do |milestone| + a_hash_including('id' => global_id_of(milestone)) + end + end + + let(:query) do + query_milestones(all_graphql_fields_for('Milestone', max_depth: 1)) + end + + let(:all_milestones) do + [no_dates, no_end, no_start, fully_past, fully_future, covers_today, closed] + end + + it_behaves_like 'a working graphql query' do + before do + post_graphql(query, current_user: current_user) + end + end + + shared_examples 'searching with parameters' do + it 'finds the right milestones' do + post_graphql(query, current_user: current_user) + + expect(results).to match_array(result_list(expected)) + end + end + + context 'there are no search params' do + let(:search_params) { nil } + let(:expected) { all_milestones } + + it_behaves_like 'searching with parameters' + end + + context 'the search params do not match anything' do + let(:search_params) { { title: 'wibble' } } + let(:expected) { [] } + + it_behaves_like 'searching with parameters' + end + + context 'searching by state:closed' do + let(:search_params) { { state: :closed } } + let(:expected) { [closed] } + + it_behaves_like 'searching with parameters' + end + + context 'searching by state:active' do + let(:search_params) { { state: :active } } + let(:expected) { all_milestones - [closed] } + + it_behaves_like 'searching with parameters' + end + + context 'searching by title' do + let(:search_params) { { title: 'no start' } } + let(:expected) { [no_start] } + + it_behaves_like 'searching with parameters' + end + + context 'searching by search_title' do + let(:search_params) { { search_title: 'no' } } + let(:expected) { [no_dates, no_start, no_end] } + + it_behaves_like 'searching with parameters' + end + + context 'searching by containing_date' do + let(:search_params) { { containing_date: (today - 7.days).iso8601 } } + let(:expected) { [no_start, no_end, fully_past] } + + it_behaves_like 'searching with parameters' + end + + context 'searching by containing_date = today' do + let(:search_params) { { containing_date: today.iso8601 } } + let(:expected) { [no_end, covers_today] } + + it_behaves_like 'searching with parameters' + end + + context 'searching by custom range' do + let(:expected) { [no_end, fully_future] } + let(:search_params) do + { + start_date: (today + 6.days).iso8601, + end_date: (today + 7.days).iso8601 + } + end + + it_behaves_like 'searching with parameters' + end + + context 'using timeframe argument' do + let(:expected) { [no_end, fully_future] } + let(:search_params) do + { + timeframe: { + start: (today + 6.days).iso8601, + end: (today + 7.days).iso8601 + } + } + end + + it_behaves_like 'searching with parameters' + end + + describe 'timeframe validations' do + let(:vars) do + { + path: project.full_path, + start: (today + 6.days).iso8601, + end: (today + 7.days).iso8601 + } + end + + it_behaves_like 'a working graphql query' do + before do + query = <<~GQL + query($path: ID!, $start: Date!, $end: Date!) { + project(fullPath: $path) { + milestones(timeframe: { start: $start, end: $end }) { + nodes { id } + } + } + } + GQL + + post_graphql(query, current_user: current_user, variables: vars) + end + end + + it 'is invalid to provide timeframe and start_date/end_date' do + query = <<~GQL + query($path: ID!, $tstart: Date!, $tend: Date!, $start: Time!, $end: Time!) { + project(fullPath: $path) { + milestones(timeframe: { start: $tstart, end: $tend }, startDate: $start, endDate: $end) { + nodes { id } + } + } + } + GQL + + post_graphql(query, current_user: current_user, + variables: vars.merge(vars.transform_keys { |k| :"t#{k}" })) + + expect(graphql_errors).to contain_exactly(a_hash_including('message' => include('deprecated in favor of timeframe'))) + end + + it 'is invalid to invert the timeframe arguments' do + query = <<~GQL + query($path: ID!, $start: Date!, $end: Date!) { + project(fullPath: $path) { + milestones(timeframe: { start: $end, end: $start }) { + nodes { id } + } + } + } + GQL + + post_graphql(query, current_user: current_user, variables: vars) + + expect(graphql_errors).to contain_exactly(a_hash_including('message' => include('start must be before end'))) + end + end +end diff --git a/spec/requests/api/graphql/user_query_spec.rb b/spec/requests/api/graphql/user_query_spec.rb index 2f4dc0a9160..79debd0b7ef 100644 --- a/spec/requests/api/graphql/user_query_spec.rb +++ b/spec/requests/api/graphql/user_query_spec.rb @@ -29,15 +29,15 @@ RSpec.describe 'getting user information' do let_it_be(:unauthorized_user) { create(:user) } let_it_be(:assigned_mr) do - create(:merge_request, :unique_branches, + create(:merge_request, :unique_branches, :unique_author, source_project: project_a, assignees: [user]) end let_it_be(:assigned_mr_b) do - create(:merge_request, :unique_branches, + create(:merge_request, :unique_branches, :unique_author, source_project: project_b, assignees: [user]) end let_it_be(:assigned_mr_c) do - create(:merge_request, :unique_branches, + create(:merge_request, :unique_branches, :unique_author, source_project: project_b, assignees: [user]) end let_it_be(:authored_mr) do @@ -133,6 +133,17 @@ RSpec.describe 'getting user information' do ) end end + + context 'filtering by author' do + let(:author) { assigned_mr_b.author } + let(:mr_args) { { author_username: author.username } } + + it 'finds the authored mrs' do + expect(assigned_mrs).to contain_exactly( + a_hash_including('id' => global_id_of(assigned_mr_b)) + ) + end + end end context 'the current user does not have access' do @@ -172,6 +183,23 @@ RSpec.describe 'getting user information' do end end + context 'filtering by assignee' do + let(:assignee) { create(:user) } + let(:mr_args) { { assignee_username: assignee.username } } + + it 'finds the assigned mrs' do + authored_mr.assignees << assignee + authored_mr_c.assignees << assignee + + post_graphql(query, current_user: current_user) + + expect(authored_mrs).to contain_exactly( + a_hash_including('id' => global_id_of(authored_mr)), + a_hash_including('id' => global_id_of(authored_mr_c)) + ) + end + end + context 'filtering by project path and IID' do let(:mr_args) do { project_path: project_b.full_path, iids: [authored_mr_b.iid.to_s] } @@ -253,8 +281,10 @@ RSpec.describe 'getting user information' do let(:current_user) { user } it 'can be found' do - expect(assigned_mrs).to include( - a_hash_including('id' => global_id_of(assigned_mr)) + expect(assigned_mrs).to contain_exactly( + a_hash_including('id' => global_id_of(assigned_mr)), + a_hash_including('id' => global_id_of(assigned_mr_b)), + a_hash_including('id' => global_id_of(assigned_mr_c)) ) end end |