diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-09-19 04:45:44 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-09-19 04:45:44 +0300 |
commit | 85dc423f7090da0a52c73eb66faf22ddb20efff9 (patch) | |
tree | 9160f299afd8c80c038f08e1545be119f5e3f1e1 /spec/requests/api/graphql | |
parent | 15c2c8c66dbe422588e5411eee7e68f1fa440bb8 (diff) |
Add latest changes from gitlab-org/gitlab@13-4-stable-ee
Diffstat (limited to 'spec/requests/api/graphql')
46 files changed, 1283 insertions, 287 deletions
diff --git a/spec/requests/api/graphql/boards/board_list_issues_query_spec.rb b/spec/requests/api/graphql/boards/board_list_issues_query_spec.rb index ae1abb50a40..3628171fcc1 100644 --- a/spec/requests/api/graphql/boards/board_list_issues_query_spec.rb +++ b/spec/requests/api/graphql/boards/board_list_issues_query_spec.rb @@ -30,7 +30,7 @@ RSpec.describe 'get board lists' do nodes { lists { nodes { - issues { + issues(filters: {labelName: "#{label2.title}"}) { count nodes { #{all_graphql_fields_for('issues'.classify)} @@ -51,8 +51,8 @@ RSpec.describe 'get board lists' do shared_examples 'group and project board list issues query' do let!(:board) { create(:board, resource_parent: board_parent) } let!(:label_list) { create(:list, board: board, label: label, position: 10) } - let!(:issue1) { create(:issue, project: issue_project, labels: [label], relative_position: 9) } - let!(:issue2) { create(:issue, project: issue_project, labels: [label], relative_position: 2) } + let!(:issue1) { create(:issue, project: issue_project, labels: [label, label2], relative_position: 9) } + let!(:issue2) { create(:issue, project: issue_project, labels: [label, label2], relative_position: 2) } let!(:issue3) { create(:issue, project: issue_project, labels: [label], relative_position: 9) } let!(:issue4) { create(:issue, project: issue_project, labels: [label2], relative_position: 432) } @@ -72,7 +72,7 @@ RSpec.describe 'get board lists' do it 'can access the issues' do post_graphql(query("id: \"#{global_id_of(label_list)}\""), current_user: user) - expect(issue_titles).to eq([issue2.title, issue3.title, issue1.title]) + expect(issue_titles).to eq([issue2.title, issue1.title]) end end end diff --git a/spec/requests/api/graphql/current_user_todos_spec.rb b/spec/requests/api/graphql/current_user_todos_spec.rb new file mode 100644 index 00000000000..b657f15d0e9 --- /dev/null +++ b/spec/requests/api/graphql/current_user_todos_spec.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'A Todoable that implements the CurrentUserTodos interface' do + include GraphqlHelpers + + let_it_be(:current_user) { create(:user) } + let_it_be(:project) { create(:project, :public) } + let_it_be(:todoable) { create(:issue, project: project) } + let_it_be(:done_todo) { create(:todo, state: :done, target: todoable, user: current_user) } + let_it_be(:pending_todo) { create(:todo, state: :pending, target: todoable, user: current_user) } + let(:state) { 'null' } + + let(:todoable_response) do + graphql_data_at(:project, :issue, :currentUserTodos, :nodes) + end + + let(:query) do + <<~GQL + { + project(fullPath: "#{project.full_path}") { + issue(iid: "#{todoable.iid}") { + currentUserTodos(state: #{state}) { + nodes { + #{all_graphql_fields_for('Todo', max_depth: 1)} + } + } + } + } + } + GQL + end + + it 'returns todos of the current user' do + post_graphql(query, current_user: current_user) + + expect(todoable_response).to contain_exactly( + a_hash_including('id' => global_id_of(done_todo)), + a_hash_including('id' => global_id_of(pending_todo)) + ) + end + + it 'does not return todos of another user', :aggregate_failures do + post_graphql(query, current_user: create(:user)) + + expect(response).to have_gitlab_http_status(:success) + expect(todoable_response).to be_empty + end + + it 'does not error when there is no logged in user', :aggregate_failures do + post_graphql(query) + + expect(response).to have_gitlab_http_status(:success) + expect(todoable_response).to be_empty + end + + context 'when `state` argument is `pending`' do + let(:state) { 'pending' } + + it 'returns just the pending todo' do + post_graphql(query, current_user: current_user) + + expect(todoable_response).to contain_exactly( + a_hash_including('id' => global_id_of(pending_todo)) + ) + end + end + + context 'when `state` argument is `done`' do + let(:state) { 'done' } + + it 'returns just the done todo' do + post_graphql(query, current_user: current_user) + + expect(todoable_response).to contain_exactly( + a_hash_including('id' => global_id_of(done_todo)) + ) + end + end +end diff --git a/spec/requests/api/graphql/group/group_members_spec.rb b/spec/requests/api/graphql/group/group_members_spec.rb new file mode 100644 index 00000000000..84b2fd63d46 --- /dev/null +++ b/spec/requests/api/graphql/group/group_members_spec.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'getting group members information' do + include GraphqlHelpers + + let_it_be(:group) { create(:group, :public) } + let_it_be(:user) { create(:user) } + let_it_be(:user_1) { create(:user, username: 'user') } + let_it_be(:user_2) { create(:user, username: 'test') } + + let(:member_data) { graphql_data['group']['groupMembers']['edges'] } + + before do + [user_1, user_2].each { |user| group.add_guest(user) } + end + + context 'when the request is correct' do + it_behaves_like 'a working graphql query' do + before do + fetch_members(user) + end + end + + it 'returns group members successfully' do + fetch_members(user) + + expect(graphql_errors).to be_nil + expect_array_response(user_1.to_global_id.to_s, user_2.to_global_id.to_s) + end + + it 'returns members that match the search query' do + fetch_members(user, { search: 'test' }) + + expect(graphql_errors).to be_nil + expect_array_response(user_2.to_global_id.to_s) + end + end + + def fetch_members(user = nil, args = {}) + post_graphql(members_query(args), current_user: user) + end + + def members_query(args = {}) + members_node = <<~NODE + edges { + node { + user { + id + } + } + } + NODE + + graphql_query_for("group", + { full_path: group.full_path }, + [query_graphql_field("groupMembers", args, members_node)] + ) + end + + def expect_array_response(*items) + expect(response).to have_gitlab_http_status(:success) + expect(member_data).to be_an Array + expect(member_data.map { |node| node["node"]["user"]["id"] }).to match_array(items) + end +end diff --git a/spec/requests/api/graphql/group_query_spec.rb b/spec/requests/api/graphql/group_query_spec.rb index d99bff2e349..83180c7d7a5 100644 --- a/spec/requests/api/graphql/group_query_spec.rb +++ b/spec/requests/api/graphql/group_query_spec.rb @@ -89,18 +89,13 @@ RSpec.describe 'getting group information', :do_not_mock_admin_mode do end it 'avoids N+1 queries' do - control_count = ActiveRecord::QueryRecorder.new do - post_graphql(group_query(group1), current_user: admin) - end.count + pending('See: https://gitlab.com/gitlab-org/gitlab/-/issues/245272') queries = [{ query: group_query(group1) }, { query: group_query(group2) }] - expect do - post_multiplex(queries, current_user: admin) - end.not_to exceed_query_limit(control_count) - - expect(graphql_errors).to contain_exactly(nil, nil) + expect { post_multiplex(queries, current_user: admin) } + .to issue_same_number_of_queries_as { post_graphql(group_query(group1), current_user: admin) } end end diff --git a/spec/requests/api/graphql/instance_statistics_measurements_spec.rb b/spec/requests/api/graphql/instance_statistics_measurements_spec.rb new file mode 100644 index 00000000000..b8cbe54534a --- /dev/null +++ b/spec/requests/api/graphql/instance_statistics_measurements_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'InstanceStatisticsMeasurements' do + include GraphqlHelpers + + let(:current_user) { create(:user, :admin) } + 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 }') } + + 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 }]) + end +end diff --git a/spec/requests/api/graphql/issue/issue_spec.rb b/spec/requests/api/graphql/issue/issue_spec.rb new file mode 100644 index 00000000000..1c9d6b25856 --- /dev/null +++ b/spec/requests/api/graphql/issue/issue_spec.rb @@ -0,0 +1,126 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Query.issue(id)' do + include GraphqlHelpers + + let_it_be(:project) { create(:project) } + let_it_be(:issue) { create(:issue, project: project) } + let_it_be(:current_user) { create(:user) } + + let(:issue_data) { graphql_data['issue'] } + + let_it_be(:issue_params) { { 'id' => issue.to_global_id.to_s } } + let(:issue_fields) { all_graphql_fields_for('Issue'.classify) } + + let(:query) do + graphql_query_for('issue', issue_params, issue_fields) + end + + it_behaves_like 'a working graphql query' do + before do + post_graphql(query, current_user: current_user) + end + end + + context 'when the user does not have access to the issue' do + it 'returns nil' do + project.project_feature.update!(issues_access_level: ProjectFeature::PRIVATE) + + post_graphql(query) + + expect(issue_data).to be nil + end + end + + context 'when the user does have access' do + before do + project.add_guest(current_user) + end + + it 'returns the issue' do + post_graphql(query, current_user: current_user) + + expect(issue_data).to include( + 'title' => issue.title, + 'description' => issue.description + ) + end + + context 'selecting any single field' do + where(:field) do + scalar_fields_of('Issue').map { |name| [name] } + end + + with_them do + it_behaves_like 'a working graphql query' do + let(:issue_fields) do + field + end + + before do + post_graphql(query, current_user: current_user) + end + + it "returns the Issue and field #{params['field']}" do + expect(issue_data.keys).to eq([field]) + end + end + end + end + + context 'selecting multiple fields' do + let(:issue_fields) { %w(title description) } + + it 'returns the Issue with the specified fields' do + post_graphql(query, current_user: current_user) + + expect(issue_data.keys).to eq( %w(title description) ) + expect(issue_data['title']).to eq(issue.title) + expect(issue_data['description']).to eq(issue.description) + end + end + + context 'when passed a non-Issue gid' do + let(:mr) {create(:merge_request)} + + it 'returns an error' do + gid = mr.to_global_id.to_s + issue_params['id'] = gid + + post_graphql(query, current_user: current_user) + + expect(graphql_errors).not_to be nil + expect(graphql_errors.first['message']).to eq("\"#{gid}\" does not represent an instance of Issue") + end + end + end + + context 'when there is a confidential issue' do + let!(:confidential_issue) do + create(:issue, :confidential, project: project) + end + + let(:issue_params) { { 'id' => confidential_issue.to_global_id.to_s } } + + context 'when the user cannot see confidential issues' do + it 'returns nil ' do + post_graphql(query, current_user: current_user) + + expect(issue_data).to be nil + end + end + + context 'when the user can see confidential issues' do + it 'returns the confidential issue' do + project.add_developer(current_user) + + post_graphql(query, current_user: current_user) + + expect(graphql_data.count).to eq(1) + expect(issue_data['confidential']).to be(true) + end + end + end +end diff --git a/spec/requests/api/graphql/metrics/dashboard_query_spec.rb b/spec/requests/api/graphql/metrics/dashboard_query_spec.rb index 456b0a5dea1..e01f59ee6a0 100644 --- a/spec/requests/api/graphql/metrics/dashboard_query_spec.rb +++ b/spec/requests/api/graphql/metrics/dashboard_query_spec.rb @@ -7,7 +7,7 @@ RSpec.describe 'Getting Metrics Dashboard' do let_it_be(:current_user) { create(:user) } let(:project) { create(:project) } - let!(:environment) { create(:environment, project: project) } + let(:environment) { create(:environment, project: project) } let(:query) do graphql_query_for( @@ -25,73 +25,156 @@ RSpec.describe 'Getting Metrics Dashboard' do ) end - context 'for anonymous user' do + context 'with metrics_dashboard_exhaustive_validations feature flag off' do before do - post_graphql(query, current_user: current_user) + stub_feature_flags(metrics_dashboard_exhaustive_validations: false) end - context 'requested dashboard is available' do - let(:path) { 'config/prometheus/common_metrics.yml' } + context 'for anonymous user' do + before do + post_graphql(query, current_user: current_user) + end + + context 'requested dashboard is available' do + let(:path) { 'config/prometheus/common_metrics.yml' } + + it_behaves_like 'a working graphql query' + + it 'returns nil' do + dashboard = graphql_data.dig('project', 'environments', 'nodes') + + expect(dashboard).to be_nil + end + end + end + + context 'for user with developer access' do + before do + project.add_developer(current_user) + post_graphql(query, current_user: current_user) + end + + context 'requested dashboard is available' do + let(:path) { 'config/prometheus/common_metrics.yml' } + + it_behaves_like 'a working graphql query' + + it 'returns metrics dashboard' do + dashboard = graphql_data.dig('project', 'environments', 'nodes', 0, 'metricsDashboard') + + expect(dashboard).to eql("path" => path, "schemaValidationWarnings" => nil) + end + + context 'invalid dashboard' do + let(:path) { '.gitlab/dashboards/metrics.yml' } + let(:project) { create(:project, :repository, :custom_repo, namespace: current_user.namespace, files: { path => "---\ndashboard: 'test'" }) } + + it 'returns metrics dashboard' do + dashboard = graphql_data.dig('project', 'environments', 'nodes', 0, 'metricsDashboard') + + expect(dashboard).to eql("path" => path, "schemaValidationWarnings" => ["panel_groups: should be an array of panel_groups objects"]) + end + end + + context 'empty dashboard' do + let(:path) { '.gitlab/dashboards/metrics.yml' } + let(:project) { create(:project, :repository, :custom_repo, namespace: current_user.namespace, files: { path => "" }) } + + it 'returns metrics dashboard' do + dashboard = graphql_data.dig('project', 'environments', 'nodes', 0, 'metricsDashboard') - it_behaves_like 'a working graphql query' + expect(dashboard).to eql("path" => path, "schemaValidationWarnings" => ["dashboard: can't be blank", "panel_groups: should be an array of panel_groups objects"]) + end + end + end + + context 'requested dashboard can not be found' do + let(:path) { 'config/prometheus/i_am_not_here.yml' } - it 'returns nil' do - dashboard = graphql_data.dig('project', 'environments', 'nodes') + it_behaves_like 'a working graphql query' - expect(dashboard).to be_nil + it 'returns nil' do + dashboard = graphql_data.dig('project', 'environments', 'nodes', 0, 'metricsDashboard') + + expect(dashboard).to be_nil + end end end end - context 'for user with developer access' do + context 'with metrics_dashboard_exhaustive_validations feature flag on' do before do - project.add_developer(current_user) - post_graphql(query, current_user: current_user) + stub_feature_flags(metrics_dashboard_exhaustive_validations: true) end - context 'requested dashboard is available' do - let(:path) { 'config/prometheus/common_metrics.yml' } + context 'for anonymous user' do + before do + post_graphql(query, current_user: current_user) + end + + context 'requested dashboard is available' do + let(:path) { 'config/prometheus/common_metrics.yml' } + + it_behaves_like 'a working graphql query' - it_behaves_like 'a working graphql query' + it 'returns nil' do + dashboard = graphql_data.dig('project', 'environments', 'nodes') - it 'returns metrics dashboard' do - dashboard = graphql_data.dig('project', 'environments', 'nodes')[0]['metricsDashboard'] + expect(dashboard).to be_nil + end + end + end - expect(dashboard).to eql("path" => path, "schemaValidationWarnings" => nil) + context 'for user with developer access' do + before do + project.add_developer(current_user) + post_graphql(query, current_user: current_user) end - context 'invalid dashboard' do - let(:path) { '.gitlab/dashboards/metrics.yml' } - let(:project) { create(:project, :repository, :custom_repo, namespace: current_user.namespace, files: { path => "---\ndashboard: 'test'" }) } + context 'requested dashboard is available' do + let(:path) { 'config/prometheus/common_metrics.yml' } + + it_behaves_like 'a working graphql query' it 'returns metrics dashboard' do dashboard = graphql_data.dig('project', 'environments', 'nodes', 0, 'metricsDashboard') - expect(dashboard).to eql("path" => path, "schemaValidationWarnings" => ["panel_groups: should be an array of panel_groups objects"]) + expect(dashboard).to eql("path" => path, "schemaValidationWarnings" => nil) end - end - context 'empty dashboard' do - let(:path) { '.gitlab/dashboards/metrics.yml' } - let(:project) { create(:project, :repository, :custom_repo, namespace: current_user.namespace, files: { path => "" }) } + context 'invalid dashboard' do + let(:path) { '.gitlab/dashboards/metrics.yml' } + let(:project) { create(:project, :repository, :custom_repo, namespace: current_user.namespace, files: { path => "---\ndashboard: 'test'" }) } - it 'returns metrics dashboard' do - dashboard = graphql_data.dig('project', 'environments', 'nodes', 0, 'metricsDashboard') + it 'returns metrics dashboard' do + dashboard = graphql_data.dig('project', 'environments', 'nodes', 0, 'metricsDashboard') - expect(dashboard).to eql("path" => path, "schemaValidationWarnings" => ["dashboard: can't be blank", "panel_groups: should be an array of panel_groups objects"]) + expect(dashboard).to eql("path" => path, "schemaValidationWarnings" => ["root is missing required keys: panel_groups"]) + end + end + + context 'empty dashboard' do + let(:path) { '.gitlab/dashboards/metrics.yml' } + let(:project) { create(:project, :repository, :custom_repo, namespace: current_user.namespace, files: { path => "" }) } + + it 'returns metrics dashboard' do + dashboard = graphql_data.dig('project', 'environments', 'nodes', 0, 'metricsDashboard') + + expect(dashboard).to eql("path" => path, "schemaValidationWarnings" => ["root is missing required keys: dashboard, panel_groups"]) + end end end - end - context 'requested dashboard can not be found' do - let(:path) { 'config/prometheus/i_am_not_here.yml' } + context 'requested dashboard can not be found' do + let(:path) { 'config/prometheus/i_am_not_here.yml' } - it_behaves_like 'a working graphql query' + it_behaves_like 'a working graphql query' - it 'returns nil' do - dashboard = graphql_data.dig('project', 'environments', 'nodes')[0]['metricsDashboard'] + it 'returns nil' do + dashboard = graphql_data.dig('project', 'environments', 'nodes', 0, 'metricsDashboard') - expect(dashboard).to be_nil + expect(dashboard).to be_nil + end end 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 1891300dace..1d38bb39d59 100644 --- a/spec/requests/api/graphql/mutations/award_emojis/add_spec.rb +++ b/spec/requests/api/graphql/mutations/award_emojis/add_spec.rb @@ -32,9 +32,7 @@ RSpec.describe 'Adding an AwardEmoji' do context 'when the user does not have permission' do it_behaves_like 'a mutation that does not create an AwardEmoji' - - it_behaves_like 'a mutation that returns top-level errors', - errors: ['The resource that you are attempting to access does not exist or you don\'t have permission to perform this action'] + it_behaves_like 'a mutation that returns a top-level access error' end context 'when the user has permission' 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 665b511abb8..c6e8800de1f 100644 --- a/spec/requests/api/graphql/mutations/award_emojis/remove_spec.rb +++ b/spec/requests/api/graphql/mutations/award_emojis/remove_spec.rb @@ -33,9 +33,7 @@ RSpec.describe 'Removing an AwardEmoji' do shared_examples 'a mutation that does not authorize the user' do it_behaves_like 'a mutation that does not destroy an AwardEmoji' - - it_behaves_like 'a mutation that returns top-level errors', - errors: ['The resource that you are attempting to access does not exist or you don\'t have permission to perform this action'] + it_behaves_like 'a mutation that returns a top-level access error' end context 'when the current_user does not own the award emoji' 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 ab4a213fde3..2df59ce97ca 100644 --- a/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb +++ b/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb @@ -143,8 +143,6 @@ RSpec.describe 'Toggling an AwardEmoji' do context 'when the user does not have permission' 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: ['The resource that you are attempting to access does not exist or you don\'t have permission to perform this action'] + it_behaves_like 'a mutation that returns a top-level access error' end end diff --git a/spec/requests/api/graphql/mutations/boards/destroy_spec.rb b/spec/requests/api/graphql/mutations/boards/destroy_spec.rb new file mode 100644 index 00000000000..a6d894e698d --- /dev/null +++ b/spec/requests/api/graphql/mutations/boards/destroy_spec.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Mutations::Boards::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(:other_board) { create(:board, project: project) } + let(:mutation) do + variables = { + id: GitlabSchema.id_from_object(board).to_s + } + + graphql_mutation(:destroy_board, variables) + end + + subject { post_graphql_mutation(mutation, current_user: current_user) } + + def mutation_response + graphql_mutation_response(:destroy_board) + 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 board' do + expect { subject }.not_to change { Board.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 board' do + let_it_be(:board) { build_stubbed(:issue, project: project) } + + it 'returns an error' do + subject + + expect(graphql_errors.first['message']).to include('does not represent an instance of Board') + end + end + + context 'when everything is ok' do + it 'destroys the board' do + expect { subject }.to change { Board.count }.from(2).to(1) + end + + it 'returns an empty board' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(mutation_response).to have_key('board') + expect(mutation_response['board']).to be_nil + end + end + + context 'when there is only 1 board for the parent' do + before do + other_board.destroy! + end + + it 'does not destroy the board' do + expect { subject }.not_to change { Board.count }.from(1) + end + + it 'returns an error and not nil board' do + subject + + expect(mutation_response['errors']).not_to be_empty + expect(mutation_response['board']).not_to be_nil + end + end + end +end diff --git a/spec/requests/api/graphql/mutations/boards/lists/create_spec.rb b/spec/requests/api/graphql/mutations/boards/lists/create_spec.rb new file mode 100644 index 00000000000..328f4fb7b6e --- /dev/null +++ b/spec/requests/api/graphql/mutations/boards/lists/create_spec.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Create a label or backlog board list' do + include GraphqlHelpers + + let_it_be(:group) { create(:group, :private) } + let_it_be(:board) { create(:board, group: group) } + let_it_be(:user) { create(:user) } + let_it_be(:dev_label) do + create(:group_label, title: 'Development', color: '#FFAABB', group: group) + end + + let(:current_user) { user } + let(:mutation) { graphql_mutation(:board_list_create, input) } + let(:mutation_response) { graphql_mutation_response(:board_list_create) } + + context 'the user is not allowed to read board lists' do + let(:input) { { board_id: board.to_global_id.to_s, backlog: true } } + + it_behaves_like 'a mutation that returns a top-level access error' + end + + context 'when user has permissions to admin board lists' do + before do + group.add_reporter(current_user) + end + + describe 'backlog list' do + let(:input) { { board_id: board.to_global_id.to_s, backlog: true } } + + it 'creates the list' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response['list']) + .to include('position' => nil, 'listType' => 'backlog') + end + end + + describe 'label list' do + let(:input) { { board_id: board.to_global_id.to_s, label_id: dev_label.to_global_id.to_s } } + + it 'creates the list' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response['list']) + .to include('position' => 0, 'listType' => 'label', 'label' => include('title' => 'Development')) + end + end + end +end diff --git a/spec/requests/api/graphql/mutations/boards/lists/update_spec.rb b/spec/requests/api/graphql/mutations/boards/lists/update_spec.rb index 8a6d2cb3994..8e24e053211 100644 --- a/spec/requests/api/graphql/mutations/boards/lists/update_spec.rb +++ b/spec/requests/api/graphql/mutations/boards/lists/update_spec.rb @@ -15,8 +15,7 @@ RSpec.describe 'Update of an existing board list' do let(:mutation_response) { graphql_mutation_response(:update_board_list) } context 'the user is not allowed to read board lists' do - it_behaves_like 'a mutation that returns top-level errors', - errors: ['The resource that you are attempting to access does not exist or you don\'t have permission to perform this action'] + it_behaves_like 'a mutation that returns a top-level access error' end before do diff --git a/spec/requests/api/graphql/mutations/branches/create_spec.rb b/spec/requests/api/graphql/mutations/branches/create_spec.rb index 082b445bf3e..fc09f57a389 100644 --- a/spec/requests/api/graphql/mutations/branches/create_spec.rb +++ b/spec/requests/api/graphql/mutations/branches/create_spec.rb @@ -15,8 +15,7 @@ RSpec.describe 'Creation of a new branch' do let(:mutation_response) { graphql_mutation_response(:create_branch) } context 'the user is not allowed to create a branch' do - it_behaves_like 'a mutation that returns top-level errors', - errors: ['The resource that you are attempting to access does not exist or you don\'t have permission to perform this action'] + it_behaves_like 'a mutation that returns a top-level access error' end context 'when user has permissions to create a branch' do diff --git a/spec/requests/api/graphql/mutations/ci/pipeline_cancel_spec.rb b/spec/requests/api/graphql/mutations/ci/pipeline_cancel_spec.rb new file mode 100644 index 00000000000..a20ac823550 --- /dev/null +++ b/spec/requests/api/graphql/mutations/ci/pipeline_cancel_spec.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'PipelineCancel' do + include GraphqlHelpers + + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project) } + let_it_be(:pipeline) { create(:ci_pipeline, project: project, user: user) } + + let(:mutation) do + variables = { + id: pipeline.to_global_id.to_s + } + graphql_mutation(:pipeline_cancel, variables, 'errors') + end + + let(:mutation_response) { graphql_mutation_response(:pipeline_cancel) } + + before_all do + project.add_maintainer(user) + end + + it 'does not cancel any pipelines not owned by the current user' do + build = create(:ci_build, :running, pipeline: pipeline) + + post_graphql_mutation(mutation, current_user: create(:user)) + + expect(graphql_errors).not_to be_empty + expect(build).not_to be_canceled + end + + it 'returns a error if the pipline cannot be be canceled' do + build = create(:ci_build, :success, pipeline: pipeline) + + post_graphql_mutation(mutation, current_user: user) + + expect(mutation_response).to include('errors' => include(eq 'Pipeline is not cancelable')) + expect(build).not_to be_canceled + end + + it "cancels all cancelable builds from a pipeline" do + build = create(:ci_build, :running, pipeline: pipeline) + + post_graphql_mutation(mutation, current_user: user) + + expect(response).to have_gitlab_http_status(:success) + expect(build.reload).to be_canceled + end +end diff --git a/spec/requests/api/graphql/mutations/ci/pipeline_destroy_spec.rb b/spec/requests/api/graphql/mutations/ci/pipeline_destroy_spec.rb new file mode 100644 index 00000000000..08959d354e2 --- /dev/null +++ b/spec/requests/api/graphql/mutations/ci/pipeline_destroy_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'PipelineDestroy' do + include GraphqlHelpers + + let_it_be(:project) { create(:project) } + let_it_be(:user) { project.owner } + let_it_be(:pipeline) { create(:ci_pipeline, :success, project: project, user: user) } + + let(:mutation) do + variables = { + id: pipeline.to_global_id.to_s + } + graphql_mutation(:pipeline_destroy, variables, 'errors') + end + + it 'returns an error if the user is not allowed to destroy the pipeline' do + post_graphql_mutation(mutation, current_user: create(:user)) + + expect(graphql_errors).not_to be_empty + end + + it 'destroys a pipeline' do + post_graphql_mutation(mutation, current_user: user) + + expect(response).to have_gitlab_http_status(:success) + expect { pipeline.reload }.to raise_error(ActiveRecord::RecordNotFound) + end +end diff --git a/spec/requests/api/graphql/mutations/ci/pipeline_retry_spec.rb b/spec/requests/api/graphql/mutations/ci/pipeline_retry_spec.rb new file mode 100644 index 00000000000..f6acf29c321 --- /dev/null +++ b/spec/requests/api/graphql/mutations/ci/pipeline_retry_spec.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'PipelineRetry' do + include GraphqlHelpers + + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project) } + let_it_be(:pipeline) { create(:ci_pipeline, project: project, user: user) } + + let(:mutation) do + variables = { + id: pipeline.to_global_id.to_s + } + graphql_mutation(:pipeline_retry, variables, + <<-QL + errors + pipeline { + id + } + QL + ) + end + + let(:mutation_response) { graphql_mutation_response(:pipeline_retry) } + + before_all do + project.add_maintainer(user) + end + + it 'returns an error if the user is not allowed to retry the pipeline' do + post_graphql_mutation(mutation, current_user: create(:user)) + + expect(graphql_errors).not_to be_empty + end + + it 'retries a pipeline' do + pipeline_id = ::Gitlab::GlobalId.build(pipeline, id: pipeline.id).to_s + post_graphql_mutation(mutation, current_user: user) + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response['pipeline']['id']).to eq(pipeline_id) + end +end diff --git a/spec/requests/api/graphql/mutations/commits/create_spec.rb b/spec/requests/api/graphql/mutations/commits/create_spec.rb index 9e4a96700bb..ac4fa7cfe83 100644 --- a/spec/requests/api/graphql/mutations/commits/create_spec.rb +++ b/spec/requests/api/graphql/mutations/commits/create_spec.rb @@ -24,8 +24,7 @@ RSpec.describe 'Creation of a new commit' do let(:mutation_response) { graphql_mutation_response(:commit_create) } context 'the user is not allowed to create a commit' do - it_behaves_like 'a mutation that returns top-level errors', - errors: ['The resource that you are attempting to access does not exist or you don\'t have permission to perform this action'] + it_behaves_like 'a mutation that returns a top-level access error' end context 'when user has permissions to create a commit' do diff --git a/spec/requests/api/graphql/mutations/design_management/upload_spec.rb b/spec/requests/api/graphql/mutations/design_management/upload_spec.rb index 9a9c7107b20..2189ae3c519 100644 --- a/spec/requests/api/graphql/mutations/design_management/upload_spec.rb +++ b/spec/requests/api/graphql/mutations/design_management/upload_spec.rb @@ -12,11 +12,11 @@ RSpec.describe "uploading designs" do let(:files) { [fixture_file_upload("spec/fixtures/dk.png")] } let(:variables) { {} } - let(:mutation) do + def mutation input = { project_path: project.full_path, iid: issue.iid, - files: files + files: files.dup }.merge(variables) graphql_mutation(:design_management_upload, input) end @@ -30,31 +30,15 @@ RSpec.describe "uploading designs" do end it "returns an error if the user is not allowed to upload designs" do - post_graphql_mutation(mutation, current_user: create(:user)) + post_graphql_mutation_with_uploads(mutation, current_user: create(:user)) expect(graphql_errors).to be_present end - it "succeeds (backward compatibility)" do - post_graphql_mutation(mutation, current_user: current_user) + it "succeeds, and responds with the created designs" do + post_graphql_mutation_with_uploads(mutation, current_user: current_user) expect(graphql_errors).not_to be_present - end - - it 'succeeds' do - file_path_in_params = ['designManagementUploadInput', 'files', 0] - params = mutation_to_apollo_uploads_param(mutation, files: [file_path_in_params]) - - workhorse_post_with_file(api('/', current_user, version: 'graphql'), - params: params, - file_key: '1' - ) - - expect(graphql_errors).not_to be_present - end - - it "responds with the created designs" do - post_graphql_mutation(mutation, current_user: current_user) expect(mutation_response).to include( "designs" => a_collection_containing_exactly( @@ -65,7 +49,7 @@ RSpec.describe "uploading designs" do it "can respond with skipped designs" do 2.times do - post_graphql_mutation(mutation, current_user: current_user) + post_graphql_mutation_with_uploads(mutation, current_user: current_user) files.each(&:rewind) end @@ -80,7 +64,7 @@ RSpec.describe "uploading designs" do let(:variables) { { iid: "123" } } it "returns an error" do - post_graphql_mutation(mutation, current_user: create(:user)) + post_graphql_mutation_with_uploads(mutation, current_user: create(:user)) expect(graphql_errors).not_to be_empty end @@ -92,7 +76,7 @@ RSpec.describe "uploading designs" do expect(service).to receive(:execute).and_return({ status: :error, message: "Something went wrong" }) end - post_graphql_mutation(mutation, current_user: current_user) + post_graphql_mutation_with_uploads(mutation, current_user: current_user) expect(mutation_response["errors"].first).to eq("Something went wrong") end end diff --git a/spec/requests/api/graphql/mutations/discussions/toggle_resolve_spec.rb b/spec/requests/api/graphql/mutations/discussions/toggle_resolve_spec.rb index 457c37e900b..450996bf76b 100644 --- a/spec/requests/api/graphql/mutations/discussions/toggle_resolve_spec.rb +++ b/spec/requests/api/graphql/mutations/discussions/toggle_resolve_spec.rb @@ -20,8 +20,7 @@ RSpec.describe 'Toggling the resolve status of a discussion' do context 'when the user does not have permission' do let_it_be(:current_user) { create(:user) } - it_behaves_like 'a mutation that returns top-level errors', - errors: ["The resource that you are attempting to access does not exist or you don't have permission to perform this action"] + it_behaves_like 'a mutation that returns a top-level access error' end context 'when user has permission' do diff --git a/spec/requests/api/graphql/mutations/issues/set_locked_spec.rb b/spec/requests/api/graphql/mutations/issues/set_locked_spec.rb index f1d55430e02..4989d096925 100644 --- a/spec/requests/api/graphql/mutations/issues/set_locked_spec.rb +++ b/spec/requests/api/graphql/mutations/issues/set_locked_spec.rb @@ -32,12 +32,7 @@ RSpec.describe 'Setting an issue as locked' do end context 'when the user is not allowed to update the issue' 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: current_user) - - expect(graphql_errors).to include(a_hash_including('message' => error)) - end + it_behaves_like 'a mutation that returns a top-level access error' end context 'when user is allowed to update the issue' do diff --git a/spec/requests/api/graphql/mutations/issues/set_severity_spec.rb b/spec/requests/api/graphql/mutations/issues/set_severity_spec.rb new file mode 100644 index 00000000000..96fd2368765 --- /dev/null +++ b/spec/requests/api/graphql/mutations/issues/set_severity_spec.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Setting severity level of an incident' do + include GraphqlHelpers + + let_it_be(:user) { create(:user) } + let(:incident) { create(:incident) } + let(:project) { incident.project } + let(:input) { { severity: 'CRITICAL' } } + + let(:mutation) do + variables = { + project_path: project.full_path, + iid: incident.iid.to_s + } + + graphql_mutation(:issue_set_severity, variables.merge(input), + <<-QL.strip_heredoc + clientMutationId + errors + issue { + iid + severity + } + QL + ) + end + + def mutation_response + graphql_mutation_response(:issue_set_severity) + end + + context 'when the user is not allowed to update the incident' 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 allowed to update the incident' do + before do + project.add_developer(user) + end + + it 'updates the issue' do + post_graphql_mutation(mutation, current_user: user) + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response.dig('issue', 'severity')).to eq('CRITICAL') + 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 fd983c683be..af52f9d57a3 100644 --- a/spec/requests/api/graphql/mutations/issues/update_spec.rb +++ b/spec/requests/api/graphql/mutations/issues/update_spec.rb @@ -20,8 +20,7 @@ RSpec.describe 'Update of an existing issue' do let(:mutation_response) { graphql_mutation_response(:update_issue) } context 'the user is not allowed to update issue' do - it_behaves_like 'a mutation that returns top-level errors', - errors: ['The resource that you are attempting to access does not exist or you don\'t have permission to perform this action'] + it_behaves_like 'a mutation that returns a top-level access error' end context 'when user has permissions to update issue' do diff --git a/spec/requests/api/graphql/mutations/merge_requests/create_spec.rb b/spec/requests/api/graphql/mutations/merge_requests/create_spec.rb index 9297ca054c7..bf759521dc0 100644 --- a/spec/requests/api/graphql/mutations/merge_requests/create_spec.rb +++ b/spec/requests/api/graphql/mutations/merge_requests/create_spec.rb @@ -24,8 +24,7 @@ RSpec.describe 'Creation of a new merge request' do let(:mutation_response) { graphql_mutation_response(:merge_request_create) } context 'the user is not allowed to create a branch' do - it_behaves_like 'a mutation that returns top-level errors', - errors: ['The resource that you are attempting to access does not exist or you don\'t have permission to perform this action'] + it_behaves_like 'a mutation that returns a top-level access error' end context 'when user has permissions to create a merge request' do 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 0e2da94f0f9..10ca2cf1cf8 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,7 @@ 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', errors: ['invalid_id is not a valid GitLab ID.'] end end end @@ -188,7 +188,7 @@ 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', errors: ['invalid_id is not a valid GitLab ID.'] end end diff --git a/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/delete_spec.rb b/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/delete_spec.rb index 2459a6f3828..7357f3e1e35 100644 --- a/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/delete_spec.rb +++ b/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/delete_spec.rb @@ -45,7 +45,7 @@ RSpec.describe Mutations::Metrics::Dashboard::Annotations::Delete do graphql_mutation(:delete_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', errors: ['invalid_id is not a valid GitLab ID.'] end context 'when the delete fails' do diff --git a/spec/requests/api/graphql/mutations/notes/create/diff_note_spec.rb b/spec/requests/api/graphql/mutations/notes/create/diff_note_spec.rb index e847c46be1b..21da1332465 100644 --- a/spec/requests/api/graphql/mutations/notes/create/diff_note_spec.rb +++ b/spec/requests/api/graphql/mutations/notes/create/diff_note_spec.rb @@ -44,7 +44,7 @@ RSpec.describe 'Adding a DiffNote' do it_behaves_like 'a Note mutation when there are active record validation errors', model: DiffNote context do - let(:diff_refs) { build(:merge_request).diff_refs } # Allow fake diff refs so arguments are valid + let(:diff_refs) { build(:commit).diff_refs } # Allow fake diff refs so arguments are valid it_behaves_like 'a Note mutation when the given resource id is not for a Noteable' end diff --git a/spec/requests/api/graphql/mutations/notes/create/image_diff_note_spec.rb b/spec/requests/api/graphql/mutations/notes/create/image_diff_note_spec.rb index 896a398e308..8bc68e6017c 100644 --- a/spec/requests/api/graphql/mutations/notes/create/image_diff_note_spec.rb +++ b/spec/requests/api/graphql/mutations/notes/create/image_diff_note_spec.rb @@ -47,7 +47,7 @@ RSpec.describe 'Adding an image DiffNote' do it_behaves_like 'a Note mutation when there are active record validation errors', model: DiffNote context do - let(:diff_refs) { build(:merge_request).diff_refs } # Allow fake diff refs so arguments are valid + let(:diff_refs) { build(:commit).diff_refs } # Allow fake diff refs so arguments are valid it_behaves_like 'a Note mutation when the given resource id is not for a Noteable' end diff --git a/spec/requests/api/graphql/mutations/notes/destroy_spec.rb b/spec/requests/api/graphql/mutations/notes/destroy_spec.rb index 6002a5b5b9d..49f09fadfea 100644 --- a/spec/requests/api/graphql/mutations/notes/destroy_spec.rb +++ b/spec/requests/api/graphql/mutations/notes/destroy_spec.rb @@ -21,8 +21,7 @@ RSpec.describe 'Destroying a Note' do context 'when the user does not have permission' do let(:current_user) { create(:user) } - it_behaves_like 'a mutation that returns top-level errors', - errors: ['The resource that you are attempting to access does not exist or you don\'t have permission to perform this action'] + it_behaves_like 'a mutation that returns a top-level access error' it 'does not destroy the Note' do expect do 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 463a872d95d..0c00906d6bf 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 @@ -59,8 +59,7 @@ RSpec.describe 'Updating an image DiffNote' do context 'when the user does not have permission' do let_it_be(:current_user) { create(:user) } - it_behaves_like 'a mutation that returns top-level errors', - errors: ['The resource that you are attempting to access does not exist or you don\'t have permission to perform this action'] + it_behaves_like 'a mutation that returns a top-level access error' it 'does not update the DiffNote' do post_graphql_mutation(mutation, current_user: current_user) diff --git a/spec/requests/api/graphql/mutations/notes/update/note_spec.rb b/spec/requests/api/graphql/mutations/notes/update/note_spec.rb index 0d93afe9434..5a92ffe61b8 100644 --- a/spec/requests/api/graphql/mutations/notes/update/note_spec.rb +++ b/spec/requests/api/graphql/mutations/notes/update/note_spec.rb @@ -22,8 +22,7 @@ RSpec.describe 'Updating a Note' do context 'when the user does not have permission' do let_it_be(:current_user) { create(:user) } - it_behaves_like 'a mutation that returns top-level errors', - errors: ['The resource that you are attempting to access does not exist or you don\'t have permission to perform this action'] + it_behaves_like 'a mutation that returns a top-level access error' it 'does not update the Note' do post_graphql_mutation(mutation, current_user: current_user) diff --git a/spec/requests/api/graphql/mutations/snippets/create_spec.rb b/spec/requests/api/graphql/mutations/snippets/create_spec.rb index 56a5f4907c1..1bb446de708 100644 --- a/spec/requests/api/graphql/mutations/snippets/create_spec.rb +++ b/spec/requests/api/graphql/mutations/snippets/create_spec.rb @@ -7,22 +7,24 @@ RSpec.describe 'Creating a Snippet' do let_it_be(:user) { create(:user) } let_it_be(:project) { create(:project) } - let(:content) { 'Initial content' } + let(:description) { 'Initial description' } let(:title) { 'Initial title' } - let(:file_name) { 'Initial file_name' } let(:visibility_level) { 'public' } + let(:action) { :create } + let(:file_1) { { filePath: 'example_file1', content: 'This is the example file 1' }} + let(:file_2) { { filePath: 'example_file2', content: 'This is the example file 2' }} + let(:actions) { [{ action: action }.merge(file_1), { action: action }.merge(file_2)] } let(:project_path) { nil } let(:uploaded_files) { nil } let(:mutation_vars) do { - content: content, description: description, visibility_level: visibility_level, - file_name: file_name, title: title, project_path: project_path, - uploaded_files: uploaded_files + uploaded_files: uploaded_files, + blob_actions: actions } end @@ -62,24 +64,47 @@ RSpec.describe 'Creating a Snippet' do context 'when the user has permission' do let(:current_user) { user } - context 'with PersonalSnippet' do - it 'creates the Snippet' do + shared_examples 'does not create snippet' do + it 'does not create the Snippet' do expect do subject - end.to change { Snippet.count }.by(1) + end.not_to change { Snippet.count } end - it 'returns the created Snippet' do + it 'does not return Snippet' do subject - expect(mutation_response['snippet']['blob']['richData']).to be_nil - expect(mutation_response['snippet']['blob']['plainData']).to match(content) + expect(mutation_response['snippet']).to be_nil + end + end + + shared_examples 'creates snippet' do + it 'returns the created Snippet' do + expect do + subject + end.to change { Snippet.count }.by(1) + expect(mutation_response['snippet']['title']).to eq(title) expect(mutation_response['snippet']['description']).to eq(description) - expect(mutation_response['snippet']['fileName']).to eq(file_name) expect(mutation_response['snippet']['visibilityLevel']).to eq(visibility_level) - expect(mutation_response['snippet']['project']).to be_nil + 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 + let(:file_1) { { filePath: 'example_file1' }} + + it_behaves_like 'a mutation that returns errors in the response', errors: ['Snippet actions have invalid data'] + it_behaves_like 'does not create snippet' + end + + it_behaves_like 'snippet edit usage data counters' + end + + context 'with PersonalSnippet' do + it_behaves_like 'creates snippet' end context 'with ProjectSnippet' do @@ -89,23 +114,7 @@ RSpec.describe 'Creating a Snippet' do project.add_developer(current_user) end - it 'creates the Snippet' do - expect do - subject - end.to change { Snippet.count }.by(1) - end - - it 'returns the created Snippet' do - subject - - expect(mutation_response['snippet']['blob']['richData']).to be_nil - expect(mutation_response['snippet']['blob']['plainData']).to match(content) - expect(mutation_response['snippet']['title']).to eq(title) - expect(mutation_response['snippet']['description']).to eq(description) - expect(mutation_response['snippet']['fileName']).to eq(file_name) - expect(mutation_response['snippet']['visibilityLevel']).to eq(visibility_level) - expect(mutation_response['snippet']['project']['fullPath']).to eq(project_path) - end + it_behaves_like 'creates snippet' context 'when the project path is invalid' do let(:project_path) { 'foobar' } @@ -122,61 +131,8 @@ RSpec.describe 'Creating a Snippet' do it_behaves_like 'a mutation that returns top-level errors', errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR] end - end - - shared_examples 'does not create snippet' do - it 'does not create the Snippet' do - expect do - subject - end.not_to change { Snippet.count } - end - - it 'does not return Snippet' do - subject - - expect(mutation_response['snippet']).to be_nil - end - end - - context 'when snippet is created using the files param' do - let(:action) { :create } - let(:file_1) { { filePath: 'example_file1', content: 'This is the example file 1' }} - let(:file_2) { { filePath: 'example_file2', content: 'This is the example file 2' }} - let(:actions) { [{ action: action }.merge(file_1), { action: action }.merge(file_2)] } - let(:mutation_vars) do - { - description: description, - visibility_level: visibility_level, - project_path: project_path, - title: title, - blob_actions: actions - } - end - - it 'creates the Snippet' do - expect do - subject - end.to change { Snippet.count }.by(1) - end - - it 'returns the created Snippet' do - subject - 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 - let(:file_1) { { filePath: 'example_file1' }} - - it_behaves_like 'a mutation that returns errors in the response', errors: ['Snippet actions have invalid data'] - it_behaves_like 'does not create snippet' - end + it_behaves_like 'snippet edit usage data counters' end context 'when there are ActiveRecord validation errors' do @@ -187,7 +143,7 @@ RSpec.describe 'Creating a Snippet' do end context 'when there non ActiveRecord errors' do - let(:file_name) { 'invalid://file/path' } + let(:file_1) { { filePath: 'invalid://file/path', content: 'foobar' }} it_behaves_like 'a mutation that returns errors in the response', errors: ['Repository Error creating the snippet - Invalid file name'] it_behaves_like 'does not create snippet' diff --git a/spec/requests/api/graphql/mutations/snippets/destroy_spec.rb b/spec/requests/api/graphql/mutations/snippets/destroy_spec.rb index c861564c66b..b71f87d2702 100644 --- a/spec/requests/api/graphql/mutations/snippets/destroy_spec.rb +++ b/spec/requests/api/graphql/mutations/snippets/destroy_spec.rb @@ -56,7 +56,7 @@ RSpec.describe 'Destroying a Snippet' do post_graphql_mutation(mutation, current_user: current_user) expect(graphql_errors) - .to include(a_hash_including('message' => "#{snippet_gid} is not a valid id for Snippet.")) + .to include(a_hash_including('message' => "#{snippet_gid} is not a valid ID for Snippet.")) end it 'does not destroy the Snippet' do diff --git a/spec/requests/api/graphql/mutations/snippets/update_spec.rb b/spec/requests/api/graphql/mutations/snippets/update_spec.rb index 3f39c0ab851..58ce74b9263 100644 --- a/spec/requests/api/graphql/mutations/snippets/update_spec.rb +++ b/spec/requests/api/graphql/mutations/snippets/update_spec.rb @@ -12,18 +12,20 @@ RSpec.describe 'Updating a Snippet' do let(:updated_content) { 'Updated content' } let(:updated_description) { 'Updated description' } let(:updated_title) { 'Updated_title' } - let(:updated_file_name) { 'Updated file_name' } let(:current_user) { snippet.author } - + let(:updated_file) { 'CHANGELOG' } + let(:deleted_file) { 'README' } let(:snippet_gid) { GitlabSchema.id_from_object(snippet).to_s } let(:mutation_vars) do { id: snippet_gid, - content: updated_content, description: updated_description, visibility_level: 'public', - file_name: updated_file_name, - title: updated_title + title: updated_title, + blob_actions: [ + { action: :update, filePath: updated_file, content: updated_content }, + { action: :delete, filePath: deleted_file } + ] } end @@ -50,21 +52,32 @@ RSpec.describe 'Updating a Snippet' do end context 'when the user has permission' do - it 'updates the Snippet' do + it 'updates the snippet record' do post_graphql_mutation(mutation, current_user: current_user) expect(snippet.reload.title).to eq(updated_title) end - it 'returns the updated Snippet' do + it 'updates the Snippet' do + blob_to_update = blob_at(updated_file) + blob_to_delete = blob_at(deleted_file) + + 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) - expect(mutation_response['snippet']['blob']['richData']).to be_nil - expect(mutation_response['snippet']['blob']['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']['fileName']).to eq(updated_file_name) - expect(mutation_response['snippet']['visibilityLevel']).to eq('public') + blob_to_update = blob_at(updated_file) + blob_to_delete = blob_at(deleted_file) + + 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 context 'when there are ActiveRecord validation errors' do @@ -79,16 +92,29 @@ RSpec.describe 'Updating a Snippet' do end it 'returns the Snippet with its original values' do + blob_to_update = blob_at(updated_file) + blob_to_delete = blob_at(deleted_file) + post_graphql_mutation(mutation, current_user: current_user) - expect(mutation_response['snippet']['blob']['richData']).to be_nil - expect(mutation_response['snippet']['blob']['plainData']).to match(original_content) - expect(mutation_response['snippet']['title']).to eq(original_title) - expect(mutation_response['snippet']['description']).to eq(original_description) - expect(mutation_response['snippet']['fileName']).to eq(original_file_name) - expect(mutation_response['snippet']['visibilityLevel']).to eq('private') + 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] + end + + def blob_at(filename) + snippet.repository.blob_at('HEAD', filename) + end end end @@ -96,6 +122,7 @@ RSpec.describe 'Updating a Snippet' do let(:snippet) do create(:personal_snippet, :private, + :repository, file_name: original_file_name, title: original_title, content: original_content, @@ -104,6 +131,7 @@ RSpec.describe 'Updating a Snippet' do it_behaves_like 'graphql update actions' it_behaves_like 'when the snippet is not found' + it_behaves_like 'snippet edit usage data counters' end describe 'ProjectSnippet' do @@ -111,6 +139,7 @@ RSpec.describe 'Updating a Snippet' do let(:snippet) do create(:project_snippet, :private, + :repository, project: project, author: create(:user), file_name: original_file_name, @@ -145,44 +174,10 @@ RSpec.describe 'Updating a Snippet' do expect(errors.first['message']).to eq(Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR) end end - end - - it_behaves_like 'when the snippet is not found' - end - - context 'when using the files params' do - let!(:snippet) { create(:personal_snippet, :private, :repository) } - let(:updated_content) { 'updated_content' } - let(:updated_file) { 'CHANGELOG' } - let(:deleted_file) { 'README' } - let(:mutation_vars) do - { - id: snippet_gid, - blob_actions: [ - { action: :update, filePath: updated_file, content: updated_content }, - { action: :delete, filePath: deleted_file } - ] - } - end - it 'updates the Snippet' do - blob_to_update = blob_at(updated_file) - expect(blob_to_update.data).not_to eq updated_content - - blob_to_delete = blob_at(deleted_file) - expect(blob_to_delete).to be_present - - post_graphql_mutation(mutation, current_user: current_user) - - blob_to_update = blob_at(updated_file) - expect(blob_to_update.data).to eq updated_content - - blob_to_delete = blob_at(deleted_file) - expect(blob_to_delete).to be_nil + it_behaves_like 'snippet edit usage data counters' end - def blob_at(filename) - snippet.repository.blob_at('HEAD', filename) - end + it_behaves_like 'when the snippet is not found' end end diff --git a/spec/requests/api/graphql/mutations/todos/mark_all_done_spec.rb b/spec/requests/api/graphql/mutations/todos/mark_all_done_spec.rb index ed5552f3e30..705ef28ffd4 100644 --- a/spec/requests/api/graphql/mutations/todos/mark_all_done_spec.rb +++ b/spec/requests/api/graphql/mutations/todos/mark_all_done_spec.rb @@ -59,7 +59,6 @@ RSpec.describe 'Marking all todos done' do context 'when user is not logged in' do let(:current_user) { nil } - it_behaves_like 'a mutation that returns top-level errors', - errors: ['The resource that you are attempting to access does not exist or you don\'t have permission to perform this action'] + it_behaves_like 'a mutation that returns a top-level access error' end end 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 9c4733f6769..8bf8b96aff5 100644 --- a/spec/requests/api/graphql/mutations/todos/mark_done_spec.rb +++ b/spec/requests/api/graphql/mutations/todos/mark_done_spec.rb @@ -63,14 +63,11 @@ RSpec.describe 'Marking todos done' do context 'when todo does not belong to requesting user' do let(:input) { { id: other_user_todo.to_global_id.to_s } } - let(:access_error) { 'The resource that you are attempting to access does not exist or you don\'t have permission to perform this action' } - it 'contains the expected error' do - post_graphql_mutation(mutation, current_user: current_user) + it_behaves_like 'a mutation that returns a top-level access error' - errors = json_response['errors'] - expect(errors).not_to be_blank - expect(errors.first['message']).to eq(access_error) + it 'results in the correct todo states' do + post_graphql_mutation(mutation, current_user: current_user) expect(todo1.reload.state).to eq('pending') expect(todo2.reload.state).to eq('done') @@ -80,7 +77,7 @@ RSpec.describe 'Marking todos done' do 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(:invalid_gid_error) { 'invalid_gid is not a valid GitLab ID.' } it 'contains the expected error' do post_graphql_mutation(mutation, current_user: current_user) diff --git a/spec/requests/api/graphql/mutations/todos/restore_spec.rb b/spec/requests/api/graphql/mutations/todos/restore_spec.rb index 6dedde56e13..8451dcdf587 100644 --- a/spec/requests/api/graphql/mutations/todos/restore_spec.rb +++ b/spec/requests/api/graphql/mutations/todos/restore_spec.rb @@ -63,14 +63,11 @@ RSpec.describe 'Restoring Todos' do context 'when todo does not belong to requesting user' do let(:input) { { id: other_user_todo.to_global_id.to_s } } - let(:access_error) { 'The resource that you are attempting to access does not exist or you don\'t have permission to perform this action' } - it 'contains the expected error' do - post_graphql_mutation(mutation, current_user: current_user) + it_behaves_like 'a mutation that returns a top-level access error' - errors = json_response['errors'] - expect(errors).not_to be_blank - expect(errors.first['message']).to eq(access_error) + it 'results in the correct todo states' do + post_graphql_mutation(mutation, current_user: current_user) expect(todo1.reload.state).to eq('done') expect(todo2.reload.state).to eq('pending') @@ -80,7 +77,7 @@ RSpec.describe 'Restoring Todos' do 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(:invalid_gid_error) { 'invalid_gid is not a valid GitLab ID.' } it 'contains the expected error' do post_graphql_mutation(mutation, current_user: current_user) diff --git a/spec/requests/api/graphql/namespace/projects_spec.rb b/spec/requests/api/graphql/namespace/projects_spec.rb index 0b634e6b689..03160719389 100644 --- a/spec/requests/api/graphql/namespace/projects_spec.rb +++ b/spec/requests/api/graphql/namespace/projects_spec.rb @@ -78,4 +78,43 @@ RSpec.describe 'getting projects' do it_behaves_like 'a graphql namespace' end + + describe 'sorting and pagination' do + let(:data_path) { [:namespace, :projects] } + + def pagination_query(params, page_info) + graphql_query_for( + 'namespace', + { 'fullPath' => subject.full_path }, + <<~QUERY + projects(includeSubgroups: #{include_subgroups}, search: "#{search}", #{params}) { + #{page_info} edges { + node { + #{all_graphql_fields_for('Project')} + } + } + } + QUERY + ) + end + + def pagination_results_data(data) + data.map { |project| project.dig('node', 'name') } + end + + context 'when sorting by similarity' do + let!(:project_1) { create(:project, name: 'Project', path: 'project', namespace: subject) } + let!(:project_2) { create(:project, name: 'Test Project', path: 'test-project', namespace: subject) } + let!(:project_3) { create(:project, name: 'Test', path: 'test', namespace: subject) } + let!(:project_4) { create(:project, name: 'Test Project Other', path: 'other-test-project', namespace: subject) } + let(:search) { 'test' } + let(:current_user) { user } + + it_behaves_like 'sorted paginated query' do + let(:sort_param) { 'SIMILARITY' } + let(:first_param) { 2 } + let(:expected_results) { [project_3.name, project_2.name, project_4.name] } + 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 ae5c8363d0f..65191e057c7 100644 --- a/spec/requests/api/graphql/project/issue/designs/notes_spec.rb +++ b/spec/requests/api/graphql/project/issue/designs/notes_spec.rb @@ -14,8 +14,6 @@ RSpec.describe 'Getting designs related to an issue' do before do enable_design_management - - note end it_behaves_like 'a working graphql query' do diff --git a/spec/requests/api/graphql/project/issues_spec.rb b/spec/requests/api/graphql/project/issues_spec.rb index 06e613a09bc..5d4276f47ca 100644 --- a/spec/requests/api/graphql/project/issues_spec.rb +++ b/spec/requests/api/graphql/project/issues_spec.rb @@ -5,12 +5,13 @@ require 'spec_helper' RSpec.describe 'getting an issue list for a project' do include GraphqlHelpers - let(:project) { create(:project, :repository, :public) } - let(:current_user) { create(:user) } let(:issues_data) { graphql_data['project']['issues']['edges'] } - let!(:issues) do + + let_it_be(:project) { create(:project, :repository, :public) } + let_it_be(:current_user) { create(:user) } + let_it_be(:issues, reload: true) do [create(:issue, project: project, discussion_locked: true), - create(:issue, project: project)] + create(:issue, :with_alert, project: project)] end let(:fields) do @@ -85,7 +86,7 @@ RSpec.describe 'getting an issue list for a project' do end context 'when there is a confidential issue' do - let!(:confidential_issue) do + let_it_be(:confidential_issue) do create(:issue, :confidential, project: project) end @@ -256,9 +257,140 @@ RSpec.describe 'getting an issue list for a project' do end end - def grab_iids(data = issues_data) - data.map do |issue| - issue.dig('node', 'iid').to_i + context 'fetching alert management alert' do + let(:fields) do + <<~QUERY + edges { + node { + iid + alertManagementAlert { + title + } + } + } + QUERY + end + + # Alerts need to have developer permission and above + before do + project.add_developer(current_user) + end + + it 'avoids N+1 queries' do + control = ActiveRecord::QueryRecorder.new { post_graphql(query, current_user: current_user) } + + create(:alert_management_alert, :with_issue, project: project ) + + expect { post_graphql(query, current_user: current_user) }.not_to exceed_query_limit(control) + end + + it 'returns the alert data' do + post_graphql(query, current_user: current_user) + + alert_titles = issues_data.map { |issue| issue.dig('node', 'alertManagementAlert', 'title') } + expected_titles = issues.map { |issue| issue.alert_management_alert&.title } + + expect(alert_titles).to contain_exactly(*expected_titles) + end + end + + context 'fetching labels' do + let(:fields) do + <<~QUERY + edges { + node { + id + labels { + nodes { + id + } + } + } + } + QUERY + end + + before do + issues.each do |issue| + # create a label for each issue we have to properly test N+1 + label = create(:label, project: project) + issue.update!(labels: [label]) + end + end + + def response_label_ids(response_data) + response_data.map do |edge| + edge['node']['labels']['nodes'].map { |u| u['id'] } + end.flatten + end + + def labels_as_global_ids(issues) + issues.map(&:labels).flatten.map(&:to_global_id).map(&:to_s) + end + + it 'avoids N+1 queries', :aggregate_failures do + control = ActiveRecord::QueryRecorder.new { post_graphql(query, current_user: current_user) } + expect(issues_data.count).to eq(2) + expect(response_label_ids(issues_data)).to match_array(labels_as_global_ids(issues)) + + new_issues = issues + [create(:issue, project: project, labels: [create(:label, project: project)])] + + expect { post_graphql(query, current_user: current_user) }.not_to exceed_query_limit(control) + # graphql_data is memoized (see spec/support/helpers/graphql_helpers.rb) + # so we have to parse the body ourselves the second time + issues_data = Gitlab::Json.parse(response.body)['data']['project']['issues']['edges'] + expect(issues_data.count).to eq(3) + expect(response_label_ids(issues_data)).to match_array(labels_as_global_ids(new_issues)) + end + end + + context 'fetching assignees' do + let(:fields) do + <<~QUERY + edges { + node { + id + assignees { + nodes { + id + } + } + } + } + QUERY + end + + before do + issues.each do |issue| + # create an assignee for each issue we have to properly test N+1 + assignee = create(:user) + issue.update!(assignees: [assignee]) + end + end + + def response_assignee_ids(response_data) + response_data.map do |edge| + edge['node']['assignees']['nodes'].map { |node| node['id'] } + end.flatten + end + + def assignees_as_global_ids(issues) + issues.map(&:assignees).flatten.map(&:to_global_id).map(&:to_s) + end + + it 'avoids N+1 queries', :aggregate_failures do + control = ActiveRecord::QueryRecorder.new { post_graphql(query, current_user: current_user) } + expect(issues_data.count).to eq(2) + expect(response_assignee_ids(issues_data)).to match_array(assignees_as_global_ids(issues)) + + new_issues = issues + [create(:issue, project: project, assignees: [create(:user)])] + + expect { post_graphql(query, current_user: current_user) }.not_to exceed_query_limit(control) + # graphql_data is memoized (see spec/support/helpers/graphql_helpers.rb) + # so we have to parse the body ourselves the second time + issues_data = Gitlab::Json.parse(response.body)['data']['project']['issues']['edges'] + expect(issues_data.count).to eq(3) + expect(response_assignee_ids(issues_data)).to match_array(assignees_as_global_ids(new_issues)) end end end diff --git a/spec/requests/api/graphql/project/merge_request_spec.rb b/spec/requests/api/graphql/project/merge_request_spec.rb index c39358a2db1..fae52fe814d 100644 --- a/spec/requests/api/graphql/project/merge_request_spec.rb +++ b/spec/requests/api/graphql/project/merge_request_spec.rb @@ -124,7 +124,8 @@ RSpec.describe 'getting merge request information nested in a project' do 'removeSourceBranch' => false, 'cherryPickOnCurrentMergeRequest' => false, 'revertOnCurrentMergeRequest' => false, - 'updateMergeRequest' => false + 'updateMergeRequest' => false, + 'canMerge' => false } post_graphql(query, current_user: current_user) diff --git a/spec/requests/api/graphql/project/merge_requests_spec.rb b/spec/requests/api/graphql/project/merge_requests_spec.rb index bb63a5994b0..22b003501a1 100644 --- a/spec/requests/api/graphql/project/merge_requests_spec.rb +++ b/spec/requests/api/graphql/project/merge_requests_spec.rb @@ -8,7 +8,7 @@ RSpec.describe 'getting merge request listings nested in a project' do let_it_be(:project) { create(:project, :repository, :public) } let_it_be(:current_user) { create(:user) } - let_it_be(:label) { create(:label) } + let_it_be(:label) { create(:label, project: project) } let_it_be(:merge_request_a) { create(:labeled_merge_request, :unique_branches, source_project: project, labels: [label]) } 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]) } @@ -210,4 +210,48 @@ RSpec.describe 'getting merge request listings nested in a project' do include_examples 'N+1 query check' end end + describe 'sorting and pagination' do + let(:data_path) { [:project, :mergeRequests] } + + def pagination_query(params, page_info) + graphql_query_for( + :project, + { full_path: project.full_path }, + <<~QUERY + mergeRequests(#{params}) { + #{page_info} edges { + node { + id + } + } + } + QUERY + ) + end + + def pagination_results_data(data) + data.map { |project| project.dig('node', 'id') } + end + + context 'when sorting by merged_at DESC' do + it_behaves_like 'sorted paginated query' do + let(:sort_param) { 'MERGED_AT_DESC' } + let(:first_param) { 2 } + + let(:expected_results) do + [ + merge_request_b, + merge_request_c, + merge_request_d, + merge_request_a + ].map(&:to_gid).map(&:to_s) + end + + before do + merge_request_c.metrics.update!(merged_at: 5.days.ago) + merge_request_b.metrics.update!(merged_at: 1.day.ago) + end + end + end + end end diff --git a/spec/requests/api/graphql/project/release_spec.rb b/spec/requests/api/graphql/project/release_spec.rb index f9c19d9747d..8fce29d0dc6 100644 --- a/spec/requests/api/graphql/project/release_spec.rb +++ b/spec/requests/api/graphql/project/release_spec.rb @@ -10,6 +10,8 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do let_it_be(:guest) { create(:user) } let_it_be(:reporter) { create(:user) } let_it_be(:stranger) { create(:user) } + let_it_be(:link_filepath) { '/direct/asset/link/path' } + let_it_be(:released_at) { Time.now - 1.day } let(:params_for_issues_and_mrs) { { scope: 'all', state: 'opened', release_tag: release.tag } } let(:post_query) { post_graphql(query, current_user: current_user) } @@ -38,6 +40,7 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do name createdAt releasedAt + upcomingRelease }) end @@ -53,7 +56,8 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do 'descriptionHtml' => release.description_html, 'name' => release.name, 'createdAt' => release.created_at.iso8601, - 'releasedAt' => release.released_at.iso8601 + 'releasedAt' => release.released_at.iso8601, + 'upcomingRelease' => false }) end end @@ -127,7 +131,7 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do let(:release_fields) do query_graphql_field(:assets, nil, - query_graphql_field(:links, nil, 'nodes { id name url external }')) + query_graphql_field(:links, nil, 'nodes { id name url external, directAssetUrl }')) end it 'finds all release links' do @@ -138,7 +142,8 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do 'id' => global_id_of(link), 'name' => link.name, 'url' => link.url, - 'external' => link.external? + 'external' => link.external?, + 'directAssetUrl' => link.filepath ? Gitlab::Routing.url_helpers.project_release_url(project, release) << link.filepath : link.url } end @@ -268,9 +273,9 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do let_it_be(:project) { create(:project, :repository, :private) } let_it_be(:milestone_1) { create(:milestone, project: project) } let_it_be(:milestone_2) { create(:milestone, project: project) } - let_it_be(:release) { create(:release, :with_evidence, project: project, milestones: [milestone_1, milestone_2]) } + let_it_be(:release) { create(:release, :with_evidence, project: project, milestones: [milestone_1, milestone_2], released_at: released_at) } let_it_be(:release_link_1) { create(:release_link, release: release) } - let_it_be(:release_link_2) { create(:release_link, release: release) } + let_it_be(:release_link_2) { create(:release_link, release: release, filepath: link_filepath) } before_all do project.add_developer(developer) @@ -309,9 +314,9 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do let_it_be(:project) { create(:project, :repository, :public) } let_it_be(:milestone_1) { create(:milestone, project: project) } let_it_be(:milestone_2) { create(:milestone, project: project) } - let_it_be(:release) { create(:release, :with_evidence, project: project, milestones: [milestone_1, milestone_2]) } + let_it_be(:release) { create(:release, :with_evidence, project: project, milestones: [milestone_1, milestone_2], released_at: released_at) } let_it_be(:release_link_1) { create(:release_link, release: release) } - let_it_be(:release_link_2) { create(:release_link, release: release) } + let_it_be(:release_link_2) { create(:release_link, release: release, filepath: link_filepath) } before_all do project.add_developer(developer) @@ -371,4 +376,45 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do it_behaves_like 'no access to the release field' end end + + describe 'upcoming release' do + let(:path) { path_prefix } + let(:project) { create(:project, :repository, :private) } + let(:release) { create(:release, :with_evidence, project: project, released_at: released_at) } + let(:current_user) { developer } + + let(:release_fields) do + query_graphql_field(%{ + releasedAt + upcomingRelease + }) + end + + before do + project.add_developer(developer) + post_query + end + + context 'future release' do + let(:released_at) { Time.now + 1.day } + + it 'finds all release data' do + expect(data).to eq({ + 'releasedAt' => release.released_at.iso8601, + 'upcomingRelease' => true + }) + end + end + + context 'past release' do + let(:released_at) { Time.now - 1.day } + + it 'finds all release data' do + expect(data).to eq({ + 'releasedAt' => release.released_at.iso8601, + 'upcomingRelease' => false + }) + end + end + end end diff --git a/spec/requests/api/graphql/project/releases_spec.rb b/spec/requests/api/graphql/project/releases_spec.rb index 7e418bbaa5b..7c57c0e9177 100644 --- a/spec/requests/api/graphql/project/releases_spec.rb +++ b/spec/requests/api/graphql/project/releases_spec.rb @@ -14,6 +14,7 @@ RSpec.describe 'Query.project(fullPath).releases()' do graphql_query_for(:project, { fullPath: project.full_path }, %{ releases { + count nodes { tagName tagPath @@ -53,6 +54,20 @@ RSpec.describe 'Query.project(fullPath).releases()' do stub_default_url_options(host: 'www.example.com') end + shared_examples 'correct total count' do + let(:data) { graphql_data.dig('project', 'releases') } + + before do + create_list(:release, 2, project: project) + + post_query + end + + it 'returns the total count' do + expect(data['count']).to eq(project.releases.count) + end + end + shared_examples 'full access to all repository-related fields' do describe 'repository-related fields' do before do @@ -92,6 +107,8 @@ RSpec.describe 'Query.project(fullPath).releases()' do ) end end + + it_behaves_like 'correct total count' end shared_examples 'no access to any repository-related fields' do @@ -119,6 +136,8 @@ RSpec.describe 'Query.project(fullPath).releases()' do ) end end + + it_behaves_like 'correct total count' end # editUrl is tested separately becuase its permissions diff --git a/spec/requests/api/graphql/project_query_spec.rb b/spec/requests/api/graphql/project_query_spec.rb index c6049e098be..4b8ffb0675c 100644 --- a/spec/requests/api/graphql/project_query_spec.rb +++ b/spec/requests/api/graphql/project_query_spec.rb @@ -5,7 +5,8 @@ require 'spec_helper' RSpec.describe 'getting project information' do include GraphqlHelpers - let(:project) { create(:project, :repository) } + let(:group) { create(:group) } + let(:project) { create(:project, :repository, group: group) } let(:current_user) { create(:user) } let(:query) do @@ -60,6 +61,51 @@ RSpec.describe 'getting project information' do expect(graphql_data['project']['pipelines']['edges'].size).to eq(1) end end + + it 'includes inherited members in project_members' do + group_member = create(:group_member, group: group) + project_member = create(:project_member, project: project) + member_query = <<~GQL + query { + project(fullPath: "#{project.full_path}") { + projectMembers { + nodes { + id + user { + username + } + ... on ProjectMember { + project { + id + } + } + ... on GroupMember { + group { + id + } + } + } + } + } + } + GQL + + post_graphql(member_query, current_user: current_user) + + member_ids = graphql_data.dig('project', 'projectMembers', 'nodes') + expect(member_ids).to include( + a_hash_including( + 'id' => group_member.to_global_id.to_s, + 'group' => { 'id' => group.to_global_id.to_s } + ) + ) + expect(member_ids).to include( + a_hash_including( + 'id' => project_member.to_global_id.to_s, + 'project' => { 'id' => project.to_global_id.to_s } + ) + ) + end end describe 'performance' do diff --git a/spec/requests/api/graphql/user/starred_projects_query_spec.rb b/spec/requests/api/graphql/user/starred_projects_query_spec.rb new file mode 100644 index 00000000000..8a1bd3d172f --- /dev/null +++ b/spec/requests/api/graphql/user/starred_projects_query_spec.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Getting starredProjects of the user' do + include GraphqlHelpers + + let(:query) do + graphql_query_for(:user, user_params, user_fields) + end + + let(:user_params) { { username: user.username } } + + let_it_be(:project_a) { create(:project, :public) } + let_it_be(:project_b) { create(:project, :private) } + let_it_be(:project_c) { create(:project, :private) } + let_it_be(:user, reload: true) { create(:user) } + + let(:user_fields) { 'starredProjects { nodes { id } }' } + let(:starred_projects) { graphql_data_at(:user, :starred_projects, :nodes) } + + before do + project_b.add_reporter(user) + project_c.add_reporter(user) + + user.toggle_star(project_a) + user.toggle_star(project_b) + user.toggle_star(project_c) + + post_graphql(query) + end + + it_behaves_like 'a working graphql query' + + it 'found only public project' do + expect(starred_projects).to contain_exactly( + a_hash_including('id' => global_id_of(project_a)) + ) + end + + context 'the current user is the user' do + let(:current_user) { user } + + before do + post_graphql(query, current_user: current_user) + end + + it 'found all projects' do + expect(starred_projects).to contain_exactly( + a_hash_including('id' => global_id_of(project_a)), + a_hash_including('id' => global_id_of(project_b)), + a_hash_including('id' => global_id_of(project_c)) + ) + end + end + + context 'the current user is a member of a private project the user starred' do + let_it_be(:other_user) { create(:user) } + + before do + project_b.add_reporter(other_user) + + post_graphql(query, current_user: other_user) + end + + it 'finds public and member projects' do + expect(starred_projects).to contain_exactly( + a_hash_including('id' => global_id_of(project_a)), + a_hash_including('id' => global_id_of(project_b)) + ) + end + end +end |