diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-05-20 17:34:42 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-05-20 17:34:42 +0300 |
commit | 9f46488805e86b1bc341ea1620b866016c2ce5ed (patch) | |
tree | f9748c7e287041e37d6da49e0a29c9511dc34768 /spec/requests/api/graphql | |
parent | dfc92d081ea0332d69c8aca2f0e745cb48ae5e6d (diff) |
Add latest changes from gitlab-org/gitlab@13-0-stable-ee
Diffstat (limited to 'spec/requests/api/graphql')
29 files changed, 2219 insertions, 193 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 new file mode 100644 index 00000000000..f0927487f85 --- /dev/null +++ b/spec/requests/api/graphql/boards/board_lists_query_spec.rb @@ -0,0 +1,137 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'get board lists' do + include GraphqlHelpers + + 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_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') } + let_it_be(:group_label2) { create(:group_label, group: group, name: 'Testing') } + + let(:params) { '' } + let(:board) { } + let(:board_parent_type) { board_parent.class.to_s.downcase } + let(:board_data) { graphql_data[board_parent_type]['boards']['edges'].first['node'] } + let(:lists_data) { board_data['lists']['edges'] } + let(:start_cursor) { board_data['lists']['pageInfo']['startCursor'] } + let(:end_cursor) { board_data['lists']['pageInfo']['endCursor'] } + + def query(list_params = params) + graphql_query_for( + board_parent_type, + { 'fullPath' => board_parent.full_path }, + <<~BOARDS + boards(first: 1) { + edges { + node { + #{field_with_params('lists', list_params)} { + pageInfo { + startCursor + endCursor + } + edges { + node { + #{all_graphql_fields_for('board_lists'.classify)} + } + } + } + } + } + } + BOARDS + ) + end + + shared_examples 'group and project board lists query' do + let!(:board) { create(:board, resource_parent: board_parent) } + + context 'when the user does not have access to the board' do + it 'returns nil' do + post_graphql(query, current_user: unauth_user) + + expect(graphql_data[board_parent_type]).to be_nil + end + end + + context 'when user can read the board' do + before do + board_parent.add_reporter(user) + end + + describe 'sorting and pagination' do + context 'when using default sorting' do + let!(:label_list) { create(:list, board: board, label: label, position: 10) } + let!(:label_list2) { create(:list, board: board, label: label2, position: 2) } + let!(:backlog_list) { create(:backlog_list, board: board) } + let(:closed_list) { board.lists.find_by(list_type: :closed) } + + before do + post_graphql(query, current_user: user) + end + + it_behaves_like 'a working graphql query' + + context 'when ascending' do + let(:lists) { [backlog_list, label_list2, label_list, closed_list] } + let(:expected_list_gids) do + lists.map { |list| list.to_global_id.to_s } + end + + it 'sorts lists' do + expect(grab_ids).to eq expected_list_gids + end + + context 'when paginating' do + let(:params) { 'first: 2' } + + it 'sorts boards' do + expect(grab_ids).to eq expected_list_gids.first(2) + + cursored_query = query("after: \"#{end_cursor}\"") + post_graphql(cursored_query, current_user: user) + + response_data = grab_list_data(response.body) + + expect(grab_ids(response_data)).to eq expected_list_gids.drop(2).first(2) + end + end + end + end + end + end + end + + describe 'for a project' do + let(:board_parent) { project } + let(:label) { project_label } + let(:label2) { project_label2 } + + it_behaves_like 'group and project board lists query' + end + + describe 'for a group' do + let(:board_parent) { group } + let(:label) { group_label } + let(:label2) { group_label2 } + + before do + allow(board_parent).to receive(:multiple_issue_boards_available?).and_return(false) + end + + it_behaves_like 'group and project board lists query' + end + + def grab_ids(data = lists_data) + data.map { |list| list.dig('node', 'id') } + end + + def grab_list_data(response_body) + Gitlab::Json.parse(response_body)['data'][board_parent_type]['boards']['edges'][0]['node']['lists']['edges'] + end +end diff --git a/spec/requests/api/graphql/current_user/todos_query_spec.rb b/spec/requests/api/graphql/current_user/todos_query_spec.rb index 82deba0d92c..321e1062a96 100644 --- a/spec/requests/api/graphql/current_user/todos_query_spec.rb +++ b/spec/requests/api/graphql/current_user/todos_query_spec.rb @@ -9,6 +9,7 @@ describe 'Query current user todos' do let_it_be(:commit_todo) { create(:on_commit_todo, user: current_user, project: create(:project, :repository)) } let_it_be(:issue_todo) { create(:todo, user: current_user, target: create(:issue)) } let_it_be(:merge_request_todo) { create(:todo, user: current_user, target: create(:merge_request)) } + let_it_be(:design_todo) { create(:todo, user: current_user, target: create(:design)) } let(:fields) do <<~QUERY @@ -34,7 +35,8 @@ describe 'Query current user todos' do is_expected.to include( a_hash_including('id' => commit_todo.to_global_id.to_s), a_hash_including('id' => issue_todo.to_global_id.to_s), - a_hash_including('id' => merge_request_todo.to_global_id.to_s) + a_hash_including('id' => merge_request_todo.to_global_id.to_s), + a_hash_including('id' => design_todo.to_global_id.to_s) ) end @@ -42,7 +44,8 @@ describe 'Query current user todos' do is_expected.to include( a_hash_including('targetType' => 'COMMIT'), a_hash_including('targetType' => 'ISSUE'), - a_hash_including('targetType' => 'MERGEREQUEST') + a_hash_including('targetType' => 'MERGEREQUEST'), + a_hash_including('targetType' => 'DESIGN') ) end end diff --git a/spec/requests/api/graphql/gitlab_schema_spec.rb b/spec/requests/api/graphql/gitlab_schema_spec.rb index cf409ea6c2d..266c98d6f08 100644 --- a/spec/requests/api/graphql/gitlab_schema_spec.rb +++ b/spec/requests/api/graphql/gitlab_schema_spec.rb @@ -190,7 +190,7 @@ describe 'GitlabSchema configurations' do variables: {}.to_s, complexity: 181, depth: 13, - duration: 7 + duration_s: 7 } expect_any_instance_of(Gitlab::Graphql::QueryAnalyzers::LoggerAnalyzer).to receive(:duration).and_return(7) diff --git a/spec/requests/api/graphql/group/milestones_spec.rb b/spec/requests/api/graphql/group/milestones_spec.rb index f8e3c0026f5..bad0024e7a3 100644 --- a/spec/requests/api/graphql/group/milestones_spec.rb +++ b/spec/requests/api/graphql/group/milestones_spec.rb @@ -7,7 +7,7 @@ describe 'Milestones through GroupQuery' do let_it_be(:user) { create(:user) } let_it_be(:now) { Time.now } - let_it_be(:group) { create(:group, :private) } + let_it_be(:group) { create(:group) } let_it_be(:milestone_1) { create(:milestone, group: group) } let_it_be(:milestone_2) { create(:milestone, group: group, state: :closed, start_date: now, due_date: now + 1.day) } let_it_be(:milestone_3) { create(:milestone, group: group, start_date: now, due_date: now + 2.days) } @@ -17,10 +17,6 @@ describe 'Milestones through GroupQuery' do let(:milestone_data) { graphql_data['group']['milestones']['edges'] } describe 'Get list of milestones from a group' do - before do - group.add_developer(user) - end - context 'when the request is correct' do before do fetch_milestones(user) @@ -51,6 +47,48 @@ describe 'Milestones through GroupQuery' do end end + context 'when including milestones from decendants' do + let_it_be(:accessible_group) { create(:group, :private, parent: group) } + let_it_be(:accessible_project) { create(:project, group: accessible_group) } + let_it_be(:inaccessible_group) { create(:group, :private, parent: group) } + let_it_be(:inaccessible_project) { create(:project, :private, group: group) } + let_it_be(:submilestone_1) { create(:milestone, group: accessible_group) } + let_it_be(:submilestone_2) { create(:milestone, project: accessible_project) } + let_it_be(:submilestone_3) { create(:milestone, group: inaccessible_group) } + let_it_be(:submilestone_4) { create(:milestone, project: inaccessible_project) } + + let(:args) { { include_descendants: true } } + + before do + accessible_group.add_developer(user) + end + + it 'returns milestones also from subgroups and subprojects visible to user' do + fetch_milestones(user, args) + + expect_array_response( + milestone_1.to_global_id.to_s, milestone_2.to_global_id.to_s, + milestone_3.to_global_id.to_s, milestone_4.to_global_id.to_s, + submilestone_1.to_global_id.to_s, submilestone_2.to_global_id.to_s + ) + end + + context 'when group_milestone_descendants is disabled' do + before do + stub_feature_flags(group_milestone_descendants: false) + end + + it 'ignores descendant milestones' do + fetch_milestones(user, args) + + expect_array_response( + milestone_1.to_global_id.to_s, milestone_2.to_global_id.to_s, + milestone_3.to_global_id.to_s, milestone_4.to_global_id.to_s + ) + end + end + end + def fetch_milestones(user = nil, args = {}) post_graphql(milestones_query(args), current_user: user) end diff --git a/spec/requests/api/graphql/metrics/dashboard/annotations_spec.rb b/spec/requests/api/graphql/metrics/dashboard/annotations_spec.rb index f5a5f0a9ec2..cb35411b7a5 100644 --- a/spec/requests/api/graphql/metrics/dashboard/annotations_spec.rb +++ b/spec/requests/api/graphql/metrics/dashboard/annotations_spec.rb @@ -21,6 +21,7 @@ describe 'Getting Metrics Dashboard Annotations' do create(:metrics_dashboard_annotation, environment: environment, starting_at: to.advance(minutes: 5), dashboard_path: path) end + let(:args) { "from: \"#{from}\", to: \"#{to}\"" } let(:fields) do <<~QUERY #{all_graphql_fields_for('MetricsDashboardAnnotation'.classify)} @@ -47,63 +48,40 @@ describe 'Getting Metrics Dashboard Annotations' do ) end - context 'feature flag metrics_dashboard_annotations' do - let(:args) { "from: \"#{from}\", to: \"#{to}\"" } + before do + project.add_developer(current_user) + post_graphql(query, current_user: current_user) + end - before do - project.add_developer(current_user) - end + it_behaves_like 'a working graphql query' - context 'is off' do - before do - stub_feature_flags(metrics_dashboard_annotations: false) - post_graphql(query, current_user: current_user) - end + it 'returns annotations' do + annotations = graphql_data.dig('project', 'environments', 'nodes')[0].dig('metricsDashboard', 'annotations', 'nodes') - it 'returns empty nodes array' do - annotations = graphql_data.dig('project', 'environments', 'nodes')[0].dig('metricsDashboard', 'annotations', 'nodes') + expect(annotations).to match_array [{ + "description" => annotation.description, + "id" => annotation.to_global_id.to_s, + "panelId" => annotation.panel_xid, + "startingAt" => annotation.starting_at.iso8601, + "endingAt" => nil + }] + end - expect(annotations).to be_empty - end - end + context 'arguments' do + context 'from is missing' do + let(:args) { "to: \"#{from}\"" } - context 'is on' do - before do - stub_feature_flags(metrics_dashboard_annotations: true) + it 'returns error' do post_graphql(query, current_user: current_user) - end - it_behaves_like 'a working graphql query' - - it 'returns annotations' do - annotations = graphql_data.dig('project', 'environments', 'nodes')[0].dig('metricsDashboard', 'annotations', 'nodes') - - expect(annotations).to match_array [{ - "description" => annotation.description, - "id" => annotation.to_global_id.to_s, - "panelId" => annotation.panel_xid, - "startingAt" => annotation.starting_at.to_s, - "endingAt" => nil - }] + expect(graphql_errors[0]).to include("message" => "Field 'annotations' is missing required arguments: from") end + end - context 'arguments' do - context 'from is missing' do - let(:args) { "to: \"#{from}\"" } - - it 'returns error' do - post_graphql(query, current_user: current_user) - - expect(graphql_errors[0]).to include("message" => "Field 'annotations' is missing required arguments: from") - end - end - - context 'to is missing' do - let(:args) { "from: \"#{from}\"" } + context 'to is missing' do + let(:args) { "from: \"#{from}\"" } - it_behaves_like 'a working graphql query' - end - end + it_behaves_like 'a working graphql query' end end end diff --git a/spec/requests/api/graphql/mutations/alert_management/alerts/update_alert_status_spec.rb b/spec/requests/api/graphql/mutations/alert_management/alerts/update_alert_status_spec.rb new file mode 100644 index 00000000000..fe50468134c --- /dev/null +++ b/spec/requests/api/graphql/mutations/alert_management/alerts/update_alert_status_spec.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'Setting the status of an alert' do + include GraphqlHelpers + + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project) } + let(:alert) { create(:alert_management_alert, project: project) } + let(:input) { { status: 'ACKNOWLEDGED' } } + + let(:mutation) do + variables = { + project_path: project.full_path, + iid: alert.iid.to_s + } + graphql_mutation(:update_alert_status, variables.merge(input), + <<~QL + clientMutationId + errors + alert { + iid + status + } + QL + ) + end + + let(:mutation_response) { graphql_mutation_response(:update_alert_status) } + + before do + project.add_developer(user) + end + + it 'updates the status of the alert' do + post_graphql_mutation(mutation, current_user: user) + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response['alert']['status']).to eq(input[:status]) + 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 3fdeccc84f9..83dec7dd3e2 100644 --- a/spec/requests/api/graphql/mutations/award_emojis/add_spec.rb +++ b/spec/requests/api/graphql/mutations/award_emojis/add_spec.rb @@ -23,7 +23,7 @@ describe 'Adding an AwardEmoji' do end shared_examples 'a mutation that does not create an AwardEmoji' do - it do + specify do expect do post_graphql_mutation(mutation, current_user: current_user) end.not_to change { AwardEmoji.count } 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 c78f0c7ca27..a2997db6cae 100644 --- a/spec/requests/api/graphql/mutations/award_emojis/remove_spec.rb +++ b/spec/requests/api/graphql/mutations/award_emojis/remove_spec.rb @@ -24,7 +24,7 @@ describe 'Removing an AwardEmoji' do end shared_examples 'a mutation that does not destroy an AwardEmoji' do - it do + specify do expect do post_graphql_mutation(mutation, current_user: current_user) end.not_to change { AwardEmoji.count } 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 bc796b34db4..e1180c85c6b 100644 --- a/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb +++ b/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb @@ -23,7 +23,7 @@ describe 'Toggling an AwardEmoji' do end shared_examples 'a mutation that does not create or destroy an AwardEmoji' do - it do + specify do expect do post_graphql_mutation(mutation, current_user: current_user) end.not_to change { AwardEmoji.count } diff --git a/spec/requests/api/graphql/mutations/branches/create_spec.rb b/spec/requests/api/graphql/mutations/branches/create_spec.rb new file mode 100644 index 00000000000..b3c378ec2bc --- /dev/null +++ b/spec/requests/api/graphql/mutations/branches/create_spec.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'Creation of a new branch' do + include GraphqlHelpers + + let_it_be(:current_user) { create(:user) } + let_it_be(:project) { create(:project, :public, :empty_repo) } + let(:input) { { project_path: project.full_path, name: new_branch, ref: ref } } + let(:new_branch) { 'new_branch' } + let(:ref) { 'master' } + + let(:mutation) { graphql_mutation(:create_branch, input) } + 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'] + end + + context 'when user has permissions to create a branch' do + before do + project.add_developer(current_user) + end + + it 'creates a new branch' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response['branch']).to include( + 'name' => new_branch, + 'commit' => a_hash_including('id') + ) + end + + context 'when ref is not correct' do + let(:new_branch) { 'another_branch' } + let(:ref) { 'unknown' } + + it_behaves_like 'a mutation that returns errors in the response', + errors: ['Invalid reference name: unknown'] + end + end +end diff --git a/spec/requests/api/graphql/mutations/design_management/delete_spec.rb b/spec/requests/api/graphql/mutations/design_management/delete_spec.rb new file mode 100644 index 00000000000..10376305b3e --- /dev/null +++ b/spec/requests/api/graphql/mutations/design_management/delete_spec.rb @@ -0,0 +1,127 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe "deleting designs" do + include GraphqlHelpers + include DesignManagementTestHelpers + + let(:developer) { create(:user) } + let(:current_user) { developer } + let(:issue) { create(:issue) } + let(:project) { issue.project } + let(:designs) { create_designs } + let(:variables) { {} } + + let(:mutation) do + input = { + project_path: project.full_path, + iid: issue.iid, + filenames: designs.map(&:filename) + }.merge(variables) + graphql_mutation(:design_management_delete, input) + end + + let(:mutation_response) { graphql_mutation_response(:design_management_delete) } + + def mutate! + post_graphql_mutation(mutation, current_user: current_user) + end + + before do + enable_design_management + + project.add_developer(developer) + end + + shared_examples 'a failed request' do + let(:the_error) { be_present } + + it 'reports an error' do + mutate! + + expect(graphql_errors).to include(a_hash_including('message' => the_error)) + end + end + + context 'the designs list is empty' do + it_behaves_like 'a failed request' do + let(:designs) { [] } + let(:the_error) { a_string_matching %r/was provided invalid value/ } + end + end + + context 'the designs list contains filenames we cannot find' do + it_behaves_like 'a failed request' do + let(:designs) { %w/foo bar baz/.map { |fn| OpenStruct.new(filename: fn) } } + let(:the_error) { a_string_matching %r/filenames were not found/ } + end + end + + context 'the current user does not have developer access' do + it_behaves_like 'a failed request' do + let(:current_user) { create(:user) } + let(:the_error) { a_string_matching %r/you don't have permission/ } + end + end + + context "when the issue does not exist" do + it_behaves_like 'a failed request' do + let(:variables) { { iid: "1234567890" } } + let(:the_error) { a_string_matching %r/does not exist/ } + end + end + + context "when saving the designs raises an error" do + let(:designs) { create_designs(1) } + + it "responds with errors" do + expect_next_instance_of(::DesignManagement::DeleteDesignsService) do |service| + expect(service) + .to receive(:execute) + .and_return({ status: :error, message: "Something went wrong" }) + end + + mutate! + + expect(mutation_response).to include('errors' => include(eq "Something went wrong")) + end + end + + context 'one of the designs is already deleted' do + let(:designs) do + create_designs(2).push(create(:design, :with_file, deleted: true, issue: issue)) + end + + it 'reports an error' do + mutate! + + expect(graphql_errors).to be_present + end + end + + context 'when the user names designs to delete' do + before do + create_designs(1) + end + + let!(:designs) { create_designs(2) } + + it 'deletes the designs' do + expect { mutate! } + .to change { issue.reset.designs.current.count }.from(3).to(1) + end + + it 'has no errors' do + mutate! + + expect(mutation_response).to include('errors' => be_empty) + end + end + + private + + def create_designs(how_many = 2) + create_list(:design, how_many, :with_file, issue: issue) + end +end diff --git a/spec/requests/api/graphql/mutations/design_management/upload_spec.rb b/spec/requests/api/graphql/mutations/design_management/upload_spec.rb new file mode 100644 index 00000000000..22adc064406 --- /dev/null +++ b/spec/requests/api/graphql/mutations/design_management/upload_spec.rb @@ -0,0 +1,99 @@ +# frozen_string_literal: true +require "spec_helper" + +describe "uploading designs" do + include GraphqlHelpers + include DesignManagementTestHelpers + include WorkhorseHelpers + + let(:current_user) { create(:user) } + let(:issue) { create(:issue) } + let(:project) { issue.project } + let(:files) { [fixture_file_upload("spec/fixtures/dk.png")] } + let(:variables) { {} } + + let(:mutation) do + input = { + project_path: project.full_path, + iid: issue.iid, + files: files + }.merge(variables) + graphql_mutation(:design_management_upload, input) + end + + let(:mutation_response) { graphql_mutation_response(:design_management_upload) } + + before do + enable_design_management + + project.add_developer(current_user) + end + + it "returns an error if the user is not allowed to upload designs" do + post_graphql_mutation(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) + + 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( + a_hash_including("filename" => "dk.png") + ) + ) + end + + it "can respond with skipped designs" do + 2.times do + post_graphql_mutation(mutation, current_user: current_user) + files.each(&:rewind) + end + + expect(mutation_response).to include( + "skippedDesigns" => a_collection_containing_exactly( + a_hash_including("filename" => "dk.png") + ) + ) + end + + context "when the issue does not exist" do + let(:variables) { { iid: "123" } } + + it "returns an error" do + post_graphql_mutation(mutation, current_user: create(:user)) + + expect(graphql_errors).not_to be_empty + end + end + + context "when saving the designs raises an error" do + it "responds with errors" do + expect_next_instance_of(::DesignManagement::SaveDesignsService) do |service| + expect(service).to receive(:execute).and_return({ status: :error, message: "Something went wrong" }) + end + + post_graphql_mutation(mutation, current_user: current_user) + expect(mutation_response["errors"].first).to eq("Something went wrong") + end + end +end diff --git a/spec/requests/api/graphql/mutations/jira_import/start_spec.rb b/spec/requests/api/graphql/mutations/jira_import/start_spec.rb index 014da5d1e1a..84110098400 100644 --- a/spec/requests/api/graphql/mutations/jira_import/start_spec.rb +++ b/spec/requests/api/graphql/mutations/jira_import/start_spec.rb @@ -3,6 +3,7 @@ require 'spec_helper' describe 'Starting a Jira Import' do + include JiraServiceHelper include GraphqlHelpers let_it_be(:user) { create(:user) } @@ -104,6 +105,8 @@ describe 'Starting a Jira Import' do before do project.reload + + stub_jira_service_test end context 'when issues feature are disabled' do @@ -118,7 +121,7 @@ describe 'Starting a Jira Import' do it_behaves_like 'a mutation that returns errors in the response', errors: ['Unable to find Jira project to import data from.'] end - context 'when jira import successfully scheduled' do + context 'when Jira import successfully scheduled' do it 'schedules a Jira import' do post_graphql_mutation(mutation, current_user: current_user) 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 new file mode 100644 index 00000000000..8568dc8ffc0 --- /dev/null +++ b/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/create_spec.rb @@ -0,0 +1,231 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Mutations::Metrics::Dashboard::Annotations::Create do + include GraphqlHelpers + + let_it_be(:current_user) { create(:user) } + let_it_be(:project) { create(:project, :private, :repository) } + let_it_be(:environment) { create(:environment, project: project) } + let_it_be(:cluster) { create(:cluster, projects: [project]) } + let(:dashboard_path) { 'config/prometheus/common_metrics.yml' } + let(:starting_at) { Time.current.iso8601 } + let(:ending_at) { 1.hour.from_now.iso8601 } + let(:description) { 'test description' } + + def mutation_response + graphql_mutation_response(:create_annotation) + end + + specify { expect(described_class).to require_graphql_authorizations(:create_metrics_dashboard_annotation) } + + context 'when annotation source is environment' do + let(:mutation) do + variables = { + environment_id: GitlabSchema.id_from_object(environment).to_s, + starting_at: starting_at, + ending_at: ending_at, + dashboard_path: dashboard_path, + description: description + } + + graphql_mutation(:create_annotation, variables) + end + + context 'when the user does not have permission' do + before do + project.add_reporter(current_user) + end + + it_behaves_like 'a mutation that returns top-level errors', + errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR] + + it 'does not create the annotation' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + end.not_to change { Metrics::Dashboard::Annotation.count } + end + end + + context 'when the user has permission' do + before do + project.add_developer(current_user) + end + + it 'creates the annotation' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + end.to change { Metrics::Dashboard::Annotation.count }.by(1) + end + + it 'returns the created annotation' do + post_graphql_mutation(mutation, current_user: current_user) + + annotation = Metrics::Dashboard::Annotation.first + annotation_id = GitlabSchema.id_from_object(annotation).to_s + + expect(mutation_response['annotation']['description']).to match(description) + expect(mutation_response['annotation']['startingAt'].to_time).to match(starting_at.to_time) + expect(mutation_response['annotation']['endingAt'].to_time).to match(ending_at.to_time) + expect(mutation_response['annotation']['id']).to match(annotation_id) + expect(annotation.environment_id).to eq(environment.id) + end + + context 'when environment_id is missing' do + let(:mutation) do + variables = { + environment_id: nil, + starting_at: starting_at, + ending_at: ending_at, + dashboard_path: dashboard_path, + description: description + } + + graphql_mutation(:create_annotation, variables) + end + + it_behaves_like 'a mutation that returns top-level errors', errors: [described_class::ANNOTATION_SOURCE_ARGUMENT_ERROR] + end + + context 'when environment_id is invalid' do + let(:mutation) do + variables = { + environment_id: 'invalid_id', + starting_at: starting_at, + ending_at: ending_at, + dashboard_path: dashboard_path, + description: description + } + + 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.'] + end + end + end + + context 'when annotation source is cluster' do + let(:mutation) do + variables = { + cluster_id: GitlabSchema.id_from_object(cluster).to_s, + starting_at: starting_at, + ending_at: ending_at, + dashboard_path: dashboard_path, + description: description + } + + graphql_mutation(:create_annotation, variables) + end + + context 'with permission' do + before do + project.add_developer(current_user) + end + + it 'creates the annotation' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + end.to change { Metrics::Dashboard::Annotation.count }.by(1) + end + + it 'returns the created annotation' do + post_graphql_mutation(mutation, current_user: current_user) + + annotation = Metrics::Dashboard::Annotation.first + annotation_id = GitlabSchema.id_from_object(annotation).to_s + + expect(mutation_response['annotation']['description']).to match(description) + expect(mutation_response['annotation']['startingAt'].to_time).to match(starting_at.to_time) + expect(mutation_response['annotation']['endingAt'].to_time).to match(ending_at.to_time) + expect(mutation_response['annotation']['id']).to match(annotation_id) + expect(annotation.cluster_id).to eq(cluster.id) + end + + context 'when cluster_id is missing' do + let(:mutation) do + variables = { + cluster_id: nil, + starting_at: starting_at, + ending_at: ending_at, + dashboard_path: dashboard_path, + description: description + } + + graphql_mutation(:create_annotation, variables) + end + + it_behaves_like 'a mutation that returns top-level errors', errors: [described_class::ANNOTATION_SOURCE_ARGUMENT_ERROR] + end + end + + context 'without permission' do + before do + project.add_guest(current_user) + end + + it_behaves_like 'a mutation that returns top-level errors', + errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR] + + it 'does not create the annotation' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + end.not_to change { Metrics::Dashboard::Annotation.count } + end + end + + context 'when cluster_id is invalid' do + let(:mutation) do + variables = { + cluster_id: 'invalid_id', + starting_at: starting_at, + ending_at: ending_at, + dashboard_path: dashboard_path, + description: description + } + + 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.'] + 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, + starting_at: starting_at, + ending_at: ending_at, + dashboard_path: dashboard_path, + description: description + } + + graphql_mutation(:create_annotation, variables) + end + + it_behaves_like 'a mutation that returns top-level errors', errors: [described_class::ANNOTATION_SOURCE_ARGUMENT_ERROR] + end + + context 'when a non-cluster or environment id is provided' do + 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 + } + + graphql_mutation(:create_annotation, variables) + end + + before do + project.add_developer(current_user) + end + + it_behaves_like 'a mutation that returns top-level errors', errors: [described_class::INVALID_ANNOTATION_SOURCE_ERROR] + end +end diff --git a/spec/requests/api/graphql/mutations/snippets/create_spec.rb b/spec/requests/api/graphql/mutations/snippets/create_spec.rb index cef7fc5cbe3..e1e5fe22887 100644 --- a/spec/requests/api/graphql/mutations/snippets/create_spec.rb +++ b/spec/requests/api/graphql/mutations/snippets/create_spec.rb @@ -13,6 +13,7 @@ describe 'Creating a Snippet' do let(:file_name) { 'Initial file_name' } let(:visibility_level) { 'public' } let(:project_path) { nil } + let(:uploaded_files) { nil } let(:mutation) do variables = { @@ -21,7 +22,8 @@ describe 'Creating a Snippet' do visibility_level: visibility_level, file_name: file_name, title: title, - project_path: project_path + project_path: project_path, + uploaded_files: uploaded_files } graphql_mutation(:create_snippet, variables) @@ -31,6 +33,8 @@ describe 'Creating a Snippet' do graphql_mutation_response(:create_snippet) end + subject { post_graphql_mutation(mutation, current_user: current_user) } + context 'when the user does not have permission' do let(:current_user) { nil } @@ -39,7 +43,7 @@ describe 'Creating a Snippet' do it 'does not create the Snippet' do expect do - post_graphql_mutation(mutation, current_user: current_user) + subject end.not_to change { Snippet.count } end @@ -48,7 +52,7 @@ describe 'Creating a Snippet' do it 'does not create the snippet when the user is not authorized' do expect do - post_graphql_mutation(mutation, current_user: current_user) + subject end.not_to change { Snippet.count } end end @@ -60,12 +64,12 @@ describe 'Creating a Snippet' do context 'with PersonalSnippet' do it 'creates the Snippet' do expect do - post_graphql_mutation(mutation, current_user: current_user) + subject end.to change { Snippet.count }.by(1) end it 'returns the created Snippet' do - post_graphql_mutation(mutation, current_user: current_user) + subject expect(mutation_response['snippet']['blob']['richData']).to be_nil expect(mutation_response['snippet']['blob']['plainData']).to match(content) @@ -86,12 +90,12 @@ describe 'Creating a Snippet' do it 'creates the Snippet' do expect do - post_graphql_mutation(mutation, current_user: current_user) + subject end.to change { Snippet.count }.by(1) end it 'returns the created Snippet' do - post_graphql_mutation(mutation, current_user: current_user) + subject expect(mutation_response['snippet']['blob']['richData']).to be_nil expect(mutation_response['snippet']['blob']['plainData']).to match(content) @@ -106,7 +110,7 @@ describe 'Creating a Snippet' do let(:project_path) { 'foobar' } 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) @@ -117,7 +121,7 @@ describe 'Creating 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) @@ -132,15 +136,41 @@ describe 'Creating a Snippet' do it 'does not create the Snippet' do expect do - post_graphql_mutation(mutation, current_user: current_user) + subject end.not_to change { Snippet.count } end it 'does not return Snippet' do - post_graphql_mutation(mutation, current_user: current_user) + subject expect(mutation_response['snippet']).to be_nil end end + + context 'when there uploaded files' do + shared_examples 'expected files argument' do |file_value, expected_value| + let(:uploaded_files) { file_value } + + it do + expect(::Snippets::CreateService).to receive(:new).with(nil, user, hash_including(files: expected_value)) + + subject + end + end + + it_behaves_like 'expected files argument', nil, nil + it_behaves_like 'expected files argument', %w(foo bar), %w(foo bar) + it_behaves_like 'expected files argument', 'foo', %w(foo) + + context 'when files has an invalid value' do + let(:uploaded_files) { [1] } + + it 'returns an error' do + subject + + expect(json_response['errors']).to be + end + end + end end end diff --git a/spec/requests/api/graphql/mutations/snippets/destroy_spec.rb b/spec/requests/api/graphql/mutations/snippets/destroy_spec.rb index 351d2db8973..cb9aeea74b2 100644 --- a/spec/requests/api/graphql/mutations/snippets/destroy_spec.rb +++ b/spec/requests/api/graphql/mutations/snippets/destroy_spec.rb @@ -6,9 +6,10 @@ describe 'Destroying a Snippet' do include GraphqlHelpers let(:current_user) { snippet.author } + let(:snippet_gid) { snippet.to_global_id.to_s } let(:mutation) do variables = { - id: snippet.to_global_id.to_s + id: snippet_gid } graphql_mutation(:destroy_snippet, variables) @@ -49,9 +50,11 @@ describe 'Destroying a Snippet' do end describe 'PersonalSnippet' do - it_behaves_like 'graphql delete actions' do - let_it_be(:snippet) { create(:personal_snippet) } - end + let_it_be(:snippet) { create(:personal_snippet) } + + it_behaves_like 'graphql delete actions' + + it_behaves_like 'when the snippet is not found' end describe 'ProjectSnippet' do @@ -85,5 +88,7 @@ describe 'Destroying a Snippet' do end end end + + it_behaves_like 'when the snippet is not found' end end diff --git a/spec/requests/api/graphql/mutations/snippets/mark_as_spam_spec.rb b/spec/requests/api/graphql/mutations/snippets/mark_as_spam_spec.rb index 05e3f7e6806..6d4dce3f6f1 100644 --- a/spec/requests/api/graphql/mutations/snippets/mark_as_spam_spec.rb +++ b/spec/requests/api/graphql/mutations/snippets/mark_as_spam_spec.rb @@ -10,9 +10,11 @@ describe 'Mark snippet as spam', :do_not_mock_admin_mode do let_it_be(:snippet) { create(:personal_snippet) } let_it_be(:user_agent_detail) { create(:user_agent_detail, subject: snippet) } let(:current_user) { snippet.author } + + let(:snippet_gid) { snippet.to_global_id.to_s } let(:mutation) do variables = { - id: snippet.to_global_id.to_s + id: snippet_gid } graphql_mutation(:mark_as_spam_snippet, variables) @@ -23,13 +25,15 @@ describe 'Mark snippet as spam', :do_not_mock_admin_mode do end shared_examples 'does not mark the snippet as spam' do - it do + specify do expect do post_graphql_mutation(mutation, current_user: current_user) end.not_to change { snippet.reload.user_agent_detail.submitted } end end + it_behaves_like 'when the snippet is not found' + context 'when the user does not have permission' do let(:current_user) { other_user } diff --git a/spec/requests/api/graphql/mutations/snippets/update_spec.rb b/spec/requests/api/graphql/mutations/snippets/update_spec.rb index 1035e3346e1..968ea5aed52 100644 --- a/spec/requests/api/graphql/mutations/snippets/update_spec.rb +++ b/spec/requests/api/graphql/mutations/snippets/update_spec.rb @@ -15,9 +15,10 @@ describe 'Updating a Snippet' do let(:updated_file_name) { 'Updated file_name' } let(:current_user) { snippet.author } + let(:snippet_gid) { GitlabSchema.id_from_object(snippet).to_s } let(:mutation) do variables = { - id: GitlabSchema.id_from_object(snippet).to_s, + id: snippet_gid, content: updated_content, description: updated_description, visibility_level: 'public', @@ -90,16 +91,18 @@ describe 'Updating a Snippet' do end describe 'PersonalSnippet' do - it_behaves_like 'graphql update actions' do - let(:snippet) do - create(:personal_snippet, - :private, - file_name: original_file_name, - title: original_title, - content: original_content, - description: original_description) - end + let(:snippet) do + create(:personal_snippet, + :private, + file_name: original_file_name, + title: original_title, + content: original_content, + description: original_description) end + + it_behaves_like 'graphql update actions' + + it_behaves_like 'when the snippet is not found' end describe 'ProjectSnippet' do @@ -142,5 +145,7 @@ describe 'Updating a Snippet' do end end end + + it_behaves_like 'when the snippet is not found' end end diff --git a/spec/requests/api/graphql/project/alert_management/alert_status_counts_spec.rb b/spec/requests/api/graphql/project/alert_management/alert_status_counts_spec.rb new file mode 100644 index 00000000000..ffd328429ef --- /dev/null +++ b/spec/requests/api/graphql/project/alert_management/alert_status_counts_spec.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true +require 'spec_helper' + +describe 'getting Alert Management Alert counts by status' do + include GraphqlHelpers + + let_it_be(:project) { create(:project, :repository) } + let_it_be(:current_user) { create(:user) } + let_it_be(:alert_1) { create(:alert_management_alert, :resolved, project: project) } + let_it_be(:alert_2) { create(:alert_management_alert, project: project) } + let_it_be(:other_project_alert) { create(:alert_management_alert) } + let(:params) { {} } + + let(:fields) do + <<~QUERY + #{all_graphql_fields_for('AlertManagementAlertStatusCountsType'.classify)} + QUERY + end + + let(:query) do + graphql_query_for( + 'project', + { 'fullPath' => project.full_path }, + query_graphql_field('alertManagementAlertStatusCounts', params, fields) + ) + end + + context 'with alert data' do + let(:alert_counts) { graphql_data.dig('project', 'alertManagementAlertStatusCounts') } + + context 'without project permissions' do + let(:user) { create(:user) } + + before do + post_graphql(query, current_user: current_user) + end + + it_behaves_like 'a working graphql query' + it { expect(alert_counts).to be nil } + end + + context 'with project permissions' do + before do + project.add_developer(current_user) + post_graphql(query, current_user: current_user) + end + + it_behaves_like 'a working graphql query' + it 'returns the correct counts for each status' do + expect(alert_counts).to eq( + 'open' => 1, + 'all' => 2, + 'triggered' => 1, + 'acknowledged' => 0, + 'resolved' => 1, + 'ignored' => 0 + ) + end + end + end +end diff --git a/spec/requests/api/graphql/project/alert_management/alerts_spec.rb b/spec/requests/api/graphql/project/alert_management/alerts_spec.rb new file mode 100644 index 00000000000..c226e659364 --- /dev/null +++ b/spec/requests/api/graphql/project/alert_management/alerts_spec.rb @@ -0,0 +1,139 @@ +# frozen_string_literal: true +require 'spec_helper' + +describe 'getting Alert Management Alerts' do + include GraphqlHelpers + + let_it_be(:payload) { { 'custom' => { 'alert' => 'payload' } } } + let_it_be(:project) { create(:project, :repository) } + let_it_be(:current_user) { create(:user) } + let_it_be(:resolved_alert) { create(:alert_management_alert, :all_fields, :resolved, project: project, issue: nil, severity: :low) } + let_it_be(:triggered_alert) { create(:alert_management_alert, :all_fields, project: project, severity: :critical, payload: payload) } + let_it_be(:other_project_alert) { create(:alert_management_alert, :all_fields) } + let(:params) { {} } + + let(:fields) do + <<~QUERY + nodes { + #{all_graphql_fields_for('AlertManagementAlert'.classify)} + } + QUERY + end + + let(:query) do + graphql_query_for( + 'project', + { 'fullPath' => project.full_path }, + query_graphql_field('alertManagementAlerts', params, fields) + ) + end + + context 'with alert data' do + let(:alerts) { graphql_data.dig('project', 'alertManagementAlerts', 'nodes') } + + context 'without project permissions' do + let(:user) { create(:user) } + + before do + post_graphql(query, current_user: current_user) + end + + it_behaves_like 'a working graphql query' + + it { expect(alerts).to be nil } + end + + context 'with project permissions' do + before do + project.add_developer(current_user) + post_graphql(query, current_user: current_user) + end + + let(:first_alert) { alerts.first } + let(:second_alert) { alerts.second } + + it_behaves_like 'a working graphql query' + + it { expect(alerts.size).to eq(2) } + + it 'returns the correct properties of the alerts' do + expect(first_alert).to include( + 'iid' => triggered_alert.iid.to_s, + 'issueIid' => triggered_alert.issue_iid.to_s, + 'title' => triggered_alert.title, + 'description' => triggered_alert.description, + 'severity' => triggered_alert.severity.upcase, + 'status' => 'TRIGGERED', + 'monitoringTool' => triggered_alert.monitoring_tool, + 'service' => triggered_alert.service, + 'hosts' => triggered_alert.hosts, + 'eventCount' => triggered_alert.events, + 'startedAt' => triggered_alert.started_at.strftime('%Y-%m-%dT%H:%M:%SZ'), + 'endedAt' => nil, + 'details' => { 'custom.alert' => 'payload' }, + 'createdAt' => triggered_alert.created_at.strftime('%Y-%m-%dT%H:%M:%SZ'), + 'updatedAt' => triggered_alert.updated_at.strftime('%Y-%m-%dT%H:%M:%SZ') + ) + + expect(second_alert).to include( + 'iid' => resolved_alert.iid.to_s, + 'issueIid' => nil, + 'status' => 'RESOLVED', + 'endedAt' => resolved_alert.ended_at.strftime('%Y-%m-%dT%H:%M:%SZ') + ) + end + + context 'with iid given' do + let(:params) { { iid: resolved_alert.iid.to_s } } + + it_behaves_like 'a working graphql query' + + it { expect(alerts.size).to eq(1) } + it { expect(first_alert['iid']).to eq(resolved_alert.iid.to_s) } + end + + context 'with statuses given' do + let(:params) { 'statuses: [TRIGGERED, ACKNOWLEDGED]' } + + it_behaves_like 'a working graphql query' + + it { expect(alerts.size).to eq(1) } + it { expect(first_alert['iid']).to eq(triggered_alert.iid.to_s) } + end + + context 'sorting data given' do + let(:params) { 'sort: SEVERITY_DESC' } + let(:iids) { alerts.map { |a| a['iid'] } } + + it_behaves_like 'a working graphql query' + + it 'sorts in the correct order' do + expect(iids).to eq [resolved_alert.iid.to_s, triggered_alert.iid.to_s] + end + + context 'ascending order' do + let(:params) { 'sort: SEVERITY_ASC' } + + it 'sorts in the correct order' do + expect(iids).to eq [triggered_alert.iid.to_s, resolved_alert.iid.to_s] + end + end + end + + context 'searching' do + let(:params) { { search: resolved_alert.title } } + + it_behaves_like 'a working graphql query' + + it { expect(alerts.size).to eq(1) } + it { expect(first_alert['iid']).to eq(resolved_alert.iid.to_s) } + + context 'unknown criteria' do + let(:params) { { search: 'something random' } } + + it { expect(alerts.size).to eq(0) } + end + end + end + end +end diff --git a/spec/requests/api/graphql/project/grafana_integration_spec.rb b/spec/requests/api/graphql/project/grafana_integration_spec.rb index e7155934b3a..c9bc6c1a68e 100644 --- a/spec/requests/api/graphql/project/grafana_integration_spec.rb +++ b/spec/requests/api/graphql/project/grafana_integration_spec.rb @@ -35,7 +35,7 @@ describe 'Getting Grafana Integration' do it_behaves_like 'a working graphql query' - it { expect(integration_data).to be nil } + specify { expect(integration_data).to be nil } end context 'with project admin permissions' do @@ -45,16 +45,16 @@ describe 'Getting Grafana Integration' do it_behaves_like 'a working graphql query' - it { expect(integration_data['token']).to eql grafana_integration.masked_token } - it { expect(integration_data['grafanaUrl']).to eql grafana_integration.grafana_url } + specify { expect(integration_data['token']).to eql grafana_integration.masked_token } + specify { expect(integration_data['grafanaUrl']).to eql grafana_integration.grafana_url } - it do + specify do expect( integration_data['createdAt'] ).to eql grafana_integration.created_at.strftime('%Y-%m-%dT%H:%M:%SZ') end - it do + specify do expect( integration_data['updatedAt'] ).to eql grafana_integration.updated_at.strftime('%Y-%m-%dT%H:%M:%SZ') diff --git a/spec/requests/api/graphql/project/issue/design_collection/version_spec.rb b/spec/requests/api/graphql/project/issue/design_collection/version_spec.rb new file mode 100644 index 00000000000..04f445b4318 --- /dev/null +++ b/spec/requests/api/graphql/project/issue/design_collection/version_spec.rb @@ -0,0 +1,216 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'Query.project(fullPath).issue(iid).designCollection.version(sha)' do + include GraphqlHelpers + include DesignManagementTestHelpers + + let_it_be(:project) { create(:project) } + let_it_be(:issue) { create(:issue, project: project) } + let_it_be(:developer) { create(:user) } + let_it_be(:stranger) { create(:user) } + let_it_be(:old_version) do + create(:design_version, issue: issue, + created_designs: create_list(:design, 3, issue: issue)) + end + let_it_be(:version) do + create(:design_version, issue: issue, + modified_designs: old_version.designs, + created_designs: create_list(:design, 2, issue: issue)) + end + + let(:current_user) { developer } + + def query(vq = version_fields) + graphql_query_for(:project, { fullPath: project.full_path }, + query_graphql_field(:issue, { iid: issue.iid.to_s }, + query_graphql_field(:design_collection, nil, + query_graphql_field(:version, { sha: version.sha }, vq)))) + end + + let(:post_query) { post_graphql(query, current_user: current_user) } + let(:path_prefix) { %w[project issue designCollection version] } + + let(:data) { graphql_data.dig(*path) } + + before do + enable_design_management + project.add_developer(developer) + end + + describe 'scalar fields' do + let(:path) { path_prefix } + let(:version_fields) { query_graphql_field(:sha) } + + before do + post_query + end + + { id: ->(x) { x.to_global_id.to_s }, sha: ->(x) { x.sha } }.each do |field, value| + describe ".#{field}" do + let(:version_fields) { query_graphql_field(field) } + + it "retrieves the #{field}" do + expect(data).to match(a_hash_including(field.to_s => value[version])) + end + end + end + end + + describe 'design_at_version' do + let(:path) { path_prefix + %w[designAtVersion] } + let(:design) { issue.designs.visible_at_version(version).to_a.sample } + let(:design_at_version) { build(:design_at_version, design: design, version: version) } + + let(:version_fields) do + query_graphql_field(:design_at_version, dav_params, 'id filename') + end + + shared_examples :finds_dav do + it 'finds all the designs as of the given version' do + post_query + + expect(data).to match( + a_hash_including( + 'id' => global_id_of(design_at_version), + 'filename' => design.filename + )) + end + + context 'when the current_user is not authorized' do + let(:current_user) { stranger } + + it 'returns nil' do + post_query + + expect(data).to be_nil + end + end + end + + context 'by ID' do + let(:dav_params) { { id: global_id_of(design_at_version) } } + + include_examples :finds_dav + end + + context 'by filename' do + let(:dav_params) { { filename: design.filename } } + + include_examples :finds_dav + end + + context 'by design_id' do + let(:dav_params) { { design_id: global_id_of(design) } } + + include_examples :finds_dav + end + end + + describe 'designs_at_version' do + let(:path) { path_prefix + %w[designsAtVersion edges] } + let(:version_fields) do + query_graphql_field(:designs_at_version, dav_params, 'edges { node { id filename } }') + end + + let(:dav_params) { nil } + + let(:results) do + issue.designs.visible_at_version(version).map do |d| + dav = build(:design_at_version, design: d, version: version) + { 'id' => global_id_of(dav), 'filename' => d.filename } + end + end + + it 'finds all the designs as of the given version' do + post_query + + expect(data.pluck('node')).to match_array(results) + end + + describe 'filtering' do + let(:designs) { issue.designs.sample(3) } + let(:filenames) { designs.map(&:filename) } + let(:ids) do + designs.map { |d| global_id_of(build(:design_at_version, design: d, version: version)) } + end + + before do + post_query + end + + describe 'by filename' do + let(:dav_params) { { filenames: filenames } } + + it 'finds the designs by filename' do + expect(data.map { |e| e.dig('node', 'id') }).to match_array(ids) + end + end + + describe 'by design-id' do + let(:dav_params) { { ids: designs.map { |d| global_id_of(d) } } } + + it 'finds the designs by id' do + expect(data.map { |e| e.dig('node', 'filename') }).to match_array(filenames) + end + end + end + + describe 'pagination' do + let(:end_cursor) { graphql_data_at(*path_prefix, :designs_at_version, :page_info, :end_cursor) } + + let(:ids) do + ::DesignManagement::Design.visible_at_version(version).order(:id).map do |d| + global_id_of(build(:design_at_version, design: d, version: version)) + end + end + + let(:version_fields) do + query_graphql_field(:designs_at_version, { first: 2 }, fields) + end + + let(:cursored_query) do + frag = query_graphql_field(:designs_at_version, { after: end_cursor }, fields) + query(frag) + end + + let(:fields) { ['pageInfo { endCursor }', 'edges { node { id } }'] } + + def response_values(data = graphql_data) + data.dig(*path).map { |e| e.dig('node', 'id') } + end + + it 'sorts designs for reliable pagination' do + post_graphql(query, current_user: current_user) + + expect(response_values).to match_array(ids.take(2)) + + post_graphql(cursored_query, current_user: current_user) + + new_data = Gitlab::Json.parse(response.body).fetch('data') + + expect(response_values(new_data)).to match_array(ids.drop(2)) + end + end + end + + describe 'designs' do + let(:path) { path_prefix + %w[designs edges] } + let(:version_fields) do + query_graphql_field(:designs, nil, 'edges { node { id filename } }') + end + + let(:results) do + version.designs.map do |design| + { 'id' => global_id_of(design), 'filename' => design.filename } + end + end + + it 'finds all the designs as of the given version' do + post_query + + expect(data.pluck('node')).to match_array(results) + end + end +end diff --git a/spec/requests/api/graphql/project/issue/design_collection/versions_spec.rb b/spec/requests/api/graphql/project/issue/design_collection/versions_spec.rb new file mode 100644 index 00000000000..18787bf925d --- /dev/null +++ b/spec/requests/api/graphql/project/issue/design_collection/versions_spec.rb @@ -0,0 +1,113 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'Getting versions related to an issue' do + include GraphqlHelpers + include DesignManagementTestHelpers + + let_it_be(:issue) { create(:issue) } + + let_it_be(:version_a) do + create(:design_version, issue: issue) + end + let_it_be(:version_b) do + create(:design_version, issue: issue) + end + let_it_be(:version_c) do + create(:design_version, issue: issue) + end + let_it_be(:version_d) do + create(:design_version, issue: issue) + end + + let_it_be(:owner) { issue.project.owner } + + def version_query(params = version_params) + query_graphql_field(:versions, params, version_query_fields) + end + + let(:version_params) { nil } + + let(:version_query_fields) { ['edges { node { sha } }'] } + + let(:project) { issue.project } + let(:current_user) { owner } + + let(:query) { make_query } + + def make_query(vq = version_query) + graphql_query_for(:project, { fullPath: project.full_path }, + query_graphql_field(:issue, { iid: issue.iid.to_s }, + query_graphql_field(:design_collection, {}, vq))) + end + + let(:design_collection) do + graphql_data_at(:project, :issue, :design_collection) + end + + def response_values(data = graphql_data, key = 'sha') + path = %w[project issue designCollection versions edges] + data.dig(*path).map { |e| e.dig('node', key) } + end + + before do + enable_design_management + end + + it 'returns the design filename' do + post_graphql(query, current_user: current_user) + + expect(response_values).to match_array([version_a, version_b, version_c, version_d].map(&:sha)) + end + + describe 'filter by sha' do + let(:sha) { version_b.sha } + + let(:version_params) { { earlier_or_equal_to_sha: sha } } + + it 'finds only those versions at or before the given cut-off' do + post_graphql(query, current_user: current_user) + + expect(response_values).to contain_exactly(version_a.sha, version_b.sha) + end + end + + describe 'filter by id' do + let(:id) { global_id_of(version_c) } + + let(:version_params) { { earlier_or_equal_to_id: id } } + + it 'finds only those versions at or before the given cut-off' do + post_graphql(query, current_user: current_user) + + expect(response_values).to contain_exactly(version_a.sha, version_b.sha, version_c.sha) + end + end + + describe 'pagination' do + let(:end_cursor) { design_collection.dig('versions', 'pageInfo', 'endCursor') } + + let(:ids) { issue.design_collection.versions.ordered.map(&:sha) } + + let(:query) { make_query(version_query(first: 2)) } + + let(:cursored_query) do + make_query(version_query(after: end_cursor)) + end + + let(:version_query_fields) { ['pageInfo { endCursor }', 'edges { node { sha } }'] } + + it 'sorts designs for reliable pagination' do + post_graphql(query, current_user: current_user) + + expect(response_values).to match_array(ids.take(2)) + + post_graphql(cursored_query, current_user: current_user) + + new_data = Gitlab::Json.parse(response.body).fetch('data') + + expect(response_values(new_data)).to match_array(ids.drop(2)) + end + end +end diff --git a/spec/requests/api/graphql/project/issue/designs/designs_spec.rb b/spec/requests/api/graphql/project/issue/designs/designs_spec.rb new file mode 100644 index 00000000000..b6fd0d91bda --- /dev/null +++ b/spec/requests/api/graphql/project/issue/designs/designs_spec.rb @@ -0,0 +1,388 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'Getting designs related to an issue' do + include GraphqlHelpers + include DesignManagementTestHelpers + + let_it_be(:design) { create(:design, :with_smaller_image_versions, versions_count: 1) } + let_it_be(:current_user) { design.project.owner } + let(:design_query) do + <<~NODE + designs { + edges { + node { + id + filename + fullPath + event + image + imageV432x230 + } + } + } + NODE + end + let(:issue) { design.issue } + let(:project) { issue.project } + let(:query) { make_query } + let(:design_collection) do + graphql_data_at(:project, :issue, :design_collection) + end + let(:design_response) do + design_collection.dig('designs', 'edges').first['node'] + end + + def make_query(dq = design_query) + designs_field = query_graphql_field(:design_collection, {}, dq) + issue_field = query_graphql_field(:issue, { iid: issue.iid.to_s }, designs_field) + + graphql_query_for(:project, { fullPath: project.full_path }, issue_field) + end + + def design_image_url(design, ref: nil, size: nil) + Gitlab::UrlBuilder.build(design, ref: ref, size: size) + end + + context 'when the feature is available' do + before do + enable_design_management + end + + it 'returns the design properties correctly' do + version_sha = design.versions.first.sha + + post_graphql(query, current_user: current_user) + + expect(design_response).to eq( + 'id' => design.to_global_id.to_s, + 'event' => 'CREATION', + 'fullPath' => design.full_path, + 'filename' => design.filename, + 'image' => design_image_url(design, ref: version_sha), + 'imageV432x230' => design_image_url(design, ref: version_sha, size: :v432x230) + ) + end + + context 'when the v432x230-sized design image has not been processed' do + before do + allow_next_instance_of(DesignManagement::DesignV432x230Uploader) do |uploader| + allow(uploader).to receive(:file).and_return(nil) + end + end + + it 'returns nil for the v432x230-sized design image' do + post_graphql(query, current_user: current_user) + + expect(design_response['imageV432x230']).to be_nil + end + end + + describe 'pagination' do + before do + create_list(:design, 5, :with_file, issue: issue) + project.add_developer(current_user) + post_graphql(query, current_user: current_user) + end + + let(:issue) { create(:issue) } + + let(:end_cursor) { design_collection.dig('designs', 'pageInfo', 'endCursor') } + + let(:ids) { issue.designs.order(:id).map { |d| global_id_of(d) } } + + let(:query) { make_query(designs_fragment(first: 2)) } + + let(:design_query_fields) { 'pageInfo { endCursor } edges { node { id } }' } + + let(:cursored_query) do + make_query(designs_fragment(after: end_cursor)) + end + + def designs_fragment(params) + query_graphql_field(:designs, params, design_query_fields) + end + + def response_ids(data = graphql_data) + path = %w[project issue designCollection designs edges] + data.dig(*path).map { |e| e.dig('node', 'id') } + end + + it 'sorts designs for reliable pagination' do + expect(response_ids).to match_array(ids.take(2)) + + post_graphql(cursored_query, current_user: current_user) + + new_data = Gitlab::Json.parse(response.body).fetch('data') + + expect(response_ids(new_data)).to match_array(ids.drop(2)) + end + end + + context 'with versions' do + let_it_be(:version) { design.versions.take } + let(:design_query) do + <<~NODE + designs { + edges { + node { + filename + versions { + edges { + node { + id + sha + } + } + } + } + } + } + NODE + end + + it 'includes the version id' do + post_graphql(query, current_user: current_user) + + version_id = design_response['versions']['edges'].first['node']['id'] + + expect(version_id).to eq(version.to_global_id.to_s) + end + + it 'includes the version sha' do + post_graphql(query, current_user: current_user) + + version_sha = design_response['versions']['edges'].first['node']['sha'] + + expect(version_sha).to eq(version.sha) + end + end + + describe 'viewing a design board at a particular version' do + let_it_be(:issue) { design.issue } + let_it_be(:second_design, reload: true) { create(:design, :with_smaller_image_versions, issue: issue, versions_count: 1) } + let_it_be(:deleted_design) { create(:design, :with_versions, issue: issue, deleted: true, versions_count: 1) } + let(:all_versions) { issue.design_versions.ordered.reverse } + let(:design_query) do + <<~NODE + designs(atVersion: "#{version.to_global_id}") { + edges { + node { + id + image + imageV432x230 + event + versions { + edges { + node { + id + } + } + } + } + } + } + NODE + end + let(:design_response) do + design_collection['designs']['edges'] + end + + def global_id(object) + object.to_global_id.to_s + end + + # Filters just design nodes from the larger `design_response` + def design_nodes + design_response.map do |response| + response['node'] + end + end + + # Filters just version nodes from the larger `design_response` + def version_nodes + design_response.map do |response| + response.dig('node', 'versions', 'edges') + end + end + + context 'viewing the original version, when one design was created' do + let(:version) { all_versions.first } + + before do + post_graphql(query, current_user: current_user) + end + + it 'only returns the first design' do + expect(design_nodes).to contain_exactly( + a_hash_including('id' => global_id(design)) + ) + end + + it 'returns the correct full-sized design image' do + expect(design_nodes).to contain_exactly( + a_hash_including('image' => design_image_url(design, ref: version.sha)) + ) + end + + it 'returns the correct v432x230-sized design image' do + expect(design_nodes).to contain_exactly( + a_hash_including('imageV432x230' => design_image_url(design, ref: version.sha, size: :v432x230)) + ) + end + + it 'returns the correct event for the design in this version' do + expect(design_nodes).to contain_exactly( + a_hash_including('event' => 'CREATION') + ) + end + + it 'only returns one version record for the design (the original version)' do + expect(version_nodes).to eq([ + [{ 'node' => { 'id' => global_id(version) } }] + ]) + end + end + + context 'viewing the second version, when one design was created' do + let(:version) { all_versions.second } + + before do + post_graphql(query, current_user: current_user) + end + + it 'only returns the first two designs' do + expect(design_nodes).to contain_exactly( + a_hash_including('id' => global_id(design)), + a_hash_including('id' => global_id(second_design)) + ) + end + + it 'returns the correct full-sized design images' do + expect(design_nodes).to contain_exactly( + a_hash_including('image' => design_image_url(design, ref: version.sha)), + a_hash_including('image' => design_image_url(second_design, ref: version.sha)) + ) + end + + it 'returns the correct v432x230-sized design images' do + expect(design_nodes).to contain_exactly( + a_hash_including('imageV432x230' => design_image_url(design, ref: version.sha, size: :v432x230)), + a_hash_including('imageV432x230' => design_image_url(second_design, ref: version.sha, size: :v432x230)) + ) + end + + it 'returns the correct events for the designs in this version' do + expect(design_nodes).to contain_exactly( + a_hash_including('event' => 'NONE'), + a_hash_including('event' => 'CREATION') + ) + end + + it 'returns the correct versions records for both designs' do + expect(version_nodes).to eq([ + [{ 'node' => { 'id' => global_id(design.versions.first) } }], + [{ 'node' => { 'id' => global_id(second_design.versions.first) } }] + ]) + end + end + + context 'viewing the last version, when one design was deleted and one was updated' do + let(:version) { all_versions.last } + let!(:second_design_update) do + create(:design_action, :with_image_v432x230, design: second_design, version: version, event: 'modification') + end + + before do + post_graphql(query, current_user: current_user) + end + + it 'does not include the deleted design' do + # The design does exist in the version + expect(version.designs).to include(deleted_design) + + # But the GraphQL API does not include it in these results + expect(design_nodes).to contain_exactly( + a_hash_including('id' => global_id(design)), + a_hash_including('id' => global_id(second_design)) + ) + end + + it 'returns the correct full-sized design images' do + expect(design_nodes).to contain_exactly( + a_hash_including('image' => design_image_url(design, ref: version.sha)), + a_hash_including('image' => design_image_url(second_design, ref: version.sha)) + ) + end + + it 'returns the correct v432x230-sized design images' do + expect(design_nodes).to contain_exactly( + a_hash_including('imageV432x230' => design_image_url(design, ref: version.sha, size: :v432x230)), + a_hash_including('imageV432x230' => design_image_url(second_design, ref: version.sha, size: :v432x230)) + ) + end + + it 'returns the correct events for the designs in this version' do + expect(design_nodes).to contain_exactly( + a_hash_including('event' => 'NONE'), + a_hash_including('event' => 'MODIFICATION') + ) + end + + it 'returns all versions records for the designs' do + expect(version_nodes).to eq([ + [ + { 'node' => { 'id' => global_id(design.versions.first) } } + ], + [ + { 'node' => { 'id' => global_id(second_design.versions.second) } }, + { 'node' => { 'id' => global_id(second_design.versions.first) } } + ] + ]) + end + end + end + + describe 'a design with note annotations' do + let_it_be(:note) { create(:diff_note_on_design, noteable: design) } + + let(:design_query) do + <<~NODE + designs { + edges { + node { + notesCount + notes { + edges { + node { + id + } + } + } + } + } + } + NODE + end + + let(:design_response) do + design_collection['designs']['edges'].first['node'] + end + + before do + post_graphql(query, current_user: current_user) + end + + it 'returns the notes for the design' do + expect(design_response.dig('notes', 'edges')).to eq( + ['node' => { 'id' => note.to_global_id.to_s }] + ) + end + + it 'returns a note_count for the design' do + expect(design_response['notesCount']).to eq(1) + 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 new file mode 100644 index 00000000000..0207bb9123a --- /dev/null +++ b/spec/requests/api/graphql/project/issue/designs/notes_spec.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'Getting designs related to an issue' do + include GraphqlHelpers + include DesignManagementTestHelpers + + let_it_be(:project) { create(:project, :public) } + let_it_be(:issue) { create(:issue, project: project) } + let_it_be(:design) { create(:design, :with_file, versions_count: 1, issue: issue) } + let_it_be(:current_user) { project.owner } + let_it_be(:note) { create(:diff_note_on_design, noteable: design, project: project) } + + before do + enable_design_management + + note + end + + it_behaves_like 'a working graphql query' do + before do + post_graphql(query, current_user: current_user) + end + end + + it 'is not too deep for anonymous users' do + note_fields = <<~FIELDS + id + author { name } + FIELDS + + 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'] + + expect(note_data['id']).to eq(note.to_global_id.to_s) + end + + def query(note_fields = all_graphql_fields_for(Note)) + design_node = <<~NODE + designs { + edges { + node { + notes { + edges { + node { + #{note_fields} + } + } + } + } + } + } + NODE + graphql_query_for( + 'project', + { 'fullPath' => design.project.full_path }, + query_graphql_field( + 'issue', + { iid: design.issue.iid.to_s }, + query_graphql_field( + 'designs', {}, design_node + ) + ) + ) + end +end diff --git a/spec/requests/api/graphql/project/issue_spec.rb b/spec/requests/api/graphql/project/issue_spec.rb new file mode 100644 index 00000000000..92d2f9d0d31 --- /dev/null +++ b/spec/requests/api/graphql/project/issue_spec.rb @@ -0,0 +1,189 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'Query.project(fullPath).issue(iid)' do + include GraphqlHelpers + + let_it_be(:project) { create(:project) } + let_it_be(:issue) { create(:issue, project: project) } + let_it_be(:issue_b) { create(:issue, project: project) } + let_it_be(:developer) { create(:user) } + let(:current_user) { developer } + + let_it_be(:project_params) { { 'fullPath' => project.full_path } } + let_it_be(:issue_params) { { 'iid' => issue.iid.to_s } } + let_it_be(:issue_fields) { 'title' } + + let(:query) do + graphql_query_for('project', project_params, project_fields) + end + + let(:project_fields) do + query_graphql_field(:issue, issue_params, issue_fields) + end + + shared_examples 'being able to fetch a design-like object by ID' do + let(:design) { design_a } + let(:path) { %w[project issue designCollection] + [GraphqlHelpers.fieldnamerize(object_field_name)] } + + let(:design_fields) do + [ + query_graphql_field(:filename), + query_graphql_field(:project, nil, query_graphql_field(:id)) + ] + end + + let(:design_collection_fields) do + query_graphql_field(object_field_name, object_params, object_fields) + end + + let(:object_fields) { design_fields } + + context 'the ID is passed' do + let(:object_params) { { id: global_id_of(object) } } + let(:result_fields) { {} } + + let(:expected_fields) do + result_fields.merge({ 'filename' => design.filename, 'project' => id_hash(project) }) + end + + it 'retrieves the object' do + post_query + + data = graphql_data.dig(*path) + + expect(data).to match(a_hash_including(expected_fields)) + end + + context 'the user is unauthorized' do + let(:current_user) { create(:user) } + + it_behaves_like 'a failure to find anything' + end + end + + context 'without parameters' do + let(:object_params) { nil } + + it 'raises an error' do + post_query + + expect(graphql_errors).to include(no_argument_error) + end + end + + context 'attempting to retrieve an object from a different issue' do + let(:object_params) { { id: global_id_of(object_on_other_issue) } } + + it_behaves_like 'a failure to find anything' + end + end + + before do + project.add_developer(developer) + end + + let(:post_query) { post_graphql(query, current_user: current_user) } + + describe '.designCollection' do + include DesignManagementTestHelpers + + let_it_be(:design_a) { create(:design, issue: issue) } + let_it_be(:version_a) { create(:design_version, issue: issue, created_designs: [design_a]) } + + let(:issue_fields) do + query_graphql_field(:design_collection, dc_params, design_collection_fields) + end + + let(:dc_params) { nil } + let(:design_collection_fields) { nil } + + before do + enable_design_management + end + + describe '.design' do + let(:object) { design } + let(:object_field_name) { :design } + + let(:no_argument_error) do + custom_graphql_error(path, a_string_matching(%r/id or filename/)) + end + + let_it_be(:object_on_other_issue) { create(:design, issue: issue_b) } + + it_behaves_like 'being able to fetch a design-like object by ID' + + it_behaves_like 'being able to fetch a design-like object by ID' do + let(:object_params) { { filename: design.filename } } + end + end + + describe '.version' do + let(:version) { version_a } + let(:path) { %w[project issue designCollection version] } + + let(:design_collection_fields) do + query_graphql_field(:version, version_params, 'id sha') + end + + context 'no parameters' do + let(:version_params) { nil } + + it 'raises an error' do + post_query + + expect(graphql_errors).to include(custom_graphql_error(path, a_string_matching(%r/id or sha/))) + end + end + + shared_examples 'a successful query for a version' do + it 'finds the version' do + post_query + + data = graphql_data.dig(*path) + + expect(data).to match( + a_hash_including('id' => global_id_of(version), + 'sha' => version.sha) + ) + end + end + + context '(sha: STRING_TYPE)' do + let(:version_params) { { sha: version.sha } } + + it_behaves_like 'a successful query for a version' + end + + context '(id: ID_TYPE)' do + let(:version_params) { { id: global_id_of(version) } } + + it_behaves_like 'a successful query for a version' + end + end + + describe '.designAtVersion' do + it_behaves_like 'being able to fetch a design-like object by ID' do + let(:object) { build(:design_at_version, design: design, version: version) } + let(:object_field_name) { :design_at_version } + + let(:version) { version_a } + + let(:result_fields) { { 'version' => id_hash(version) } } + let(:object_fields) do + design_fields + [query_graphql_field(:version, nil, query_graphql_field(:id))] + end + + let(:no_argument_error) { missing_required_argument(path, :id) } + + let(:object_on_other_issue) { build(:design_at_version, issue: issue_b) } + end + end + end + + def id_hash(object) + a_hash_including('id' => global_id_of(object)) + end +end diff --git a/spec/requests/api/graphql/project/issues_spec.rb b/spec/requests/api/graphql/project/issues_spec.rb index 4ce7a3912a3..91fce3eed92 100644 --- a/spec/requests/api/graphql/project/issues_spec.rb +++ b/spec/requests/api/graphql/project/issues_spec.rb @@ -45,8 +45,8 @@ describe 'getting an issue list for a project' do it 'includes discussion locked' do post_graphql(query, current_user: current_user) - expect(issues_data[0]['node']['discussionLocked']).to eq false - expect(issues_data[1]['node']['discussionLocked']).to eq true + expect(issues_data[0]['node']['discussionLocked']).to eq(false) + expect(issues_data[1]['node']['discussionLocked']).to eq(true) end context 'when limiting the number of results' do @@ -79,7 +79,7 @@ describe 'getting an issue list for a project' do post_graphql(query) - expect(issues_data).to eq [] + expect(issues_data).to eq([]) end end @@ -118,131 +118,138 @@ describe 'getting an issue list for a project' do end describe 'sorting and pagination' do - let(:start_cursor) { graphql_data['project']['issues']['pageInfo']['startCursor'] } - let(:end_cursor) { graphql_data['project']['issues']['pageInfo']['endCursor'] } + let_it_be(:data_path) { [:project, :issues] } - context 'when sorting by due date' do - let(:sort_project) { create(:project, :public) } - - let!(:due_issue1) { create(:issue, project: sort_project, due_date: 3.days.from_now) } - let!(:due_issue2) { create(:issue, project: sort_project, due_date: nil) } - let!(:due_issue3) { create(:issue, project: sort_project, due_date: 2.days.ago) } - let!(:due_issue4) { create(:issue, project: sort_project, due_date: nil) } - let!(:due_issue5) { create(:issue, project: sort_project, due_date: 1.day.ago) } - - let(:params) { 'sort: DUE_DATE_ASC' } - - def query(issue_params = params) - graphql_query_for( - 'project', - { 'fullPath' => sort_project.full_path }, - <<~ISSUES - issues(#{issue_params}) { - pageInfo { - endCursor - } - edges { - node { - iid - dueDate - } - } - } - ISSUES - ) - end + def pagination_query(params, page_info) + graphql_query_for( + 'project', + { 'fullPath' => sort_project.full_path }, + "issues(#{params}) { #{page_info} edges { node { iid dueDate } } }" + ) + end - before do - post_graphql(query, current_user: current_user) - end + def pagination_results_data(data) + data.map { |issue| issue.dig('node', 'iid').to_i } + end - it_behaves_like 'a working graphql query' + context 'when sorting by due date' do + let_it_be(:sort_project) { create(:project, :public) } + let_it_be(:due_issue1) { create(:issue, project: sort_project, due_date: 3.days.from_now) } + let_it_be(:due_issue2) { create(:issue, project: sort_project, due_date: nil) } + let_it_be(:due_issue3) { create(:issue, project: sort_project, due_date: 2.days.ago) } + let_it_be(:due_issue4) { create(:issue, project: sort_project, due_date: nil) } + let_it_be(:due_issue5) { create(:issue, project: sort_project, due_date: 1.day.ago) } context 'when ascending' do - it 'sorts issues' do - expect(grab_iids).to eq [due_issue3.iid, due_issue5.iid, due_issue1.iid, due_issue4.iid, due_issue2.iid] - end - - context 'when paginating' do - let(:params) { 'sort: DUE_DATE_ASC, first: 2' } - - it 'sorts issues' do - expect(grab_iids).to eq [due_issue3.iid, due_issue5.iid] - - cursored_query = query("sort: DUE_DATE_ASC, after: \"#{end_cursor}\"") - post_graphql(cursored_query, current_user: current_user) - response_data = JSON.parse(response.body)['data']['project']['issues']['edges'] - - expect(grab_iids(response_data)).to eq [due_issue1.iid, due_issue4.iid, due_issue2.iid] - end + it_behaves_like 'sorted paginated query' do + let(:sort_param) { 'DUE_DATE_ASC' } + let(:first_param) { 2 } + let(:expected_results) { [due_issue3.iid, due_issue5.iid, due_issue1.iid, due_issue4.iid, due_issue2.iid] } end end context 'when descending' do - let(:params) { 'sort: DUE_DATE_DESC' } - - it 'sorts issues' do - expect(grab_iids).to eq [due_issue1.iid, due_issue5.iid, due_issue3.iid, due_issue4.iid, due_issue2.iid] + it_behaves_like 'sorted paginated query' do + let(:sort_param) { 'DUE_DATE_DESC' } + let(:first_param) { 2 } + let(:expected_results) { [due_issue1.iid, due_issue5.iid, due_issue3.iid, due_issue4.iid, due_issue2.iid] } end + end + end - context 'when paginating' do - let(:params) { 'sort: DUE_DATE_DESC, first: 2' } - - it 'sorts issues' do - expect(grab_iids).to eq [due_issue1.iid, due_issue5.iid] - - cursored_query = query("sort: DUE_DATE_DESC, after: \"#{end_cursor}\"") - post_graphql(cursored_query, current_user: current_user) - response_data = JSON.parse(response.body)['data']['project']['issues']['edges'] + context 'when sorting by relative position' do + let_it_be(:sort_project) { create(:project, :public) } + let_it_be(:relative_issue1) { create(:issue, project: sort_project, relative_position: 2000) } + let_it_be(:relative_issue2) { create(:issue, project: sort_project, relative_position: nil) } + let_it_be(:relative_issue3) { create(:issue, project: sort_project, relative_position: 1000) } + let_it_be(:relative_issue4) { create(:issue, project: sort_project, relative_position: nil) } + let_it_be(:relative_issue5) { create(:issue, project: sort_project, relative_position: 500) } - expect(grab_iids(response_data)).to eq [due_issue3.iid, due_issue4.iid, due_issue2.iid] - end + context 'when ascending' do + it_behaves_like 'sorted paginated query' do + let(:sort_param) { 'RELATIVE_POSITION_ASC' } + let(:first_param) { 2 } + let(:expected_results) { [relative_issue5.iid, relative_issue3.iid, relative_issue1.iid, relative_issue4.iid, relative_issue2.iid] } end end end - context 'when sorting by relative position' do - let(:sort_project) { create(:project, :public) } - - let!(:relative_issue1) { create(:issue, project: sort_project, relative_position: 2000) } - let!(:relative_issue2) { create(:issue, project: sort_project, relative_position: nil) } - let!(:relative_issue3) { create(:issue, project: sort_project, relative_position: 1000) } - let!(:relative_issue4) { create(:issue, project: sort_project, relative_position: nil) } - let!(:relative_issue5) { create(:issue, project: sort_project, relative_position: 500) } - - let(:params) { 'sort: RELATIVE_POSITION_ASC' } - - def query(issue_params = params) - graphql_query_for( - 'project', - { 'fullPath' => sort_project.full_path }, - "issues(#{issue_params}) { pageInfo { endCursor} edges { node { iid dueDate } } }" - ) + context 'when sorting by priority' do + let_it_be(:sort_project) { create(:project, :public) } + let_it_be(:early_milestone) { create(:milestone, project: sort_project, due_date: 10.days.from_now) } + let_it_be(:late_milestone) { create(:milestone, project: sort_project, due_date: 30.days.from_now) } + let_it_be(:priority_label1) { create(:label, project: sort_project, priority: 1) } + let_it_be(:priority_label2) { create(:label, project: sort_project, priority: 5) } + let_it_be(:priority_issue1) { create(:issue, project: sort_project, labels: [priority_label1], milestone: late_milestone) } + let_it_be(:priority_issue2) { create(:issue, project: sort_project, labels: [priority_label2]) } + let_it_be(:priority_issue3) { create(:issue, project: sort_project, milestone: early_milestone) } + let_it_be(:priority_issue4) { create(:issue, project: sort_project) } + + context 'when ascending' do + it_behaves_like 'sorted paginated query' do + let(:sort_param) { 'PRIORITY_ASC' } + let(:first_param) { 2 } + let(:expected_results) { [priority_issue3.iid, priority_issue1.iid, priority_issue2.iid, priority_issue4.iid] } + end end - before do - post_graphql(query, current_user: current_user) + context 'when descending' do + it_behaves_like 'sorted paginated query' do + let(:sort_param) { 'PRIORITY_DESC' } + let(:first_param) { 2 } + let(:expected_results) { [priority_issue1.iid, priority_issue3.iid, priority_issue2.iid, priority_issue4.iid] } + end end + end - it_behaves_like 'a working graphql query' + context 'when sorting by label priority' do + let_it_be(:sort_project) { create(:project, :public) } + let_it_be(:label1) { create(:label, project: sort_project, priority: 1) } + let_it_be(:label2) { create(:label, project: sort_project, priority: 5) } + let_it_be(:label3) { create(:label, project: sort_project, priority: 10) } + let_it_be(:label_issue1) { create(:issue, project: sort_project, labels: [label1]) } + let_it_be(:label_issue2) { create(:issue, project: sort_project, labels: [label2]) } + let_it_be(:label_issue3) { create(:issue, project: sort_project, labels: [label1, label3]) } + let_it_be(:label_issue4) { create(:issue, project: sort_project) } context 'when ascending' do - it 'sorts issues' do - expect(grab_iids).to eq [relative_issue5.iid, relative_issue3.iid, relative_issue1.iid, relative_issue4.iid, relative_issue2.iid] + it_behaves_like 'sorted paginated query' do + let(:sort_param) { 'LABEL_PRIORITY_ASC' } + let(:first_param) { 2 } + let(:expected_results) { [label_issue3.iid, label_issue1.iid, label_issue2.iid, label_issue4.iid] } end + end - context 'when paginating' do - let(:params) { 'sort: RELATIVE_POSITION_ASC, first: 2' } + context 'when descending' do + it_behaves_like 'sorted paginated query' do + let(:sort_param) { 'LABEL_PRIORITY_DESC' } + let(:first_param) { 2 } + let(:expected_results) { [label_issue2.iid, label_issue3.iid, label_issue1.iid, label_issue4.iid] } + end + end + end - it 'sorts issues' do - expect(grab_iids).to eq [relative_issue5.iid, relative_issue3.iid] + context 'when sorting by milestone due date' do + let_it_be(:sort_project) { create(:project, :public) } + let_it_be(:early_milestone) { create(:milestone, project: sort_project, due_date: 10.days.from_now) } + let_it_be(:late_milestone) { create(:milestone, project: sort_project, due_date: 30.days.from_now) } + let_it_be(:milestone_issue1) { create(:issue, project: sort_project) } + let_it_be(:milestone_issue2) { create(:issue, project: sort_project, milestone: early_milestone) } + let_it_be(:milestone_issue3) { create(:issue, project: sort_project, milestone: late_milestone) } - cursored_query = query("sort: RELATIVE_POSITION_ASC, after: \"#{end_cursor}\"") - post_graphql(cursored_query, current_user: current_user) - response_data = JSON.parse(response.body)['data']['project']['issues']['edges'] + context 'when ascending' do + it_behaves_like 'sorted paginated query' do + let(:sort_param) { 'MILESTONE_DUE_ASC' } + let(:first_param) { 2 } + let(:expected_results) { [milestone_issue2.iid, milestone_issue3.iid, milestone_issue1.iid] } + end + end - expect(grab_iids(response_data)).to eq [relative_issue1.iid, relative_issue4.iid, relative_issue2.iid] - end + context 'when descending' do + it_behaves_like 'sorted paginated query' do + let(:sort_param) { 'MILESTONE_DUE_DESC' } + let(:first_param) { 2 } + let(:expected_results) { [milestone_issue3.iid, milestone_issue2.iid, milestone_issue1.iid] } end end end diff --git a/spec/requests/api/graphql/project/jira_import_spec.rb b/spec/requests/api/graphql/project/jira_import_spec.rb index 43e1bb13342..e063068eb1a 100644 --- a/spec/requests/api/graphql/project/jira_import_spec.rb +++ b/spec/requests/api/graphql/project/jira_import_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe 'query jira import data' do +describe 'query Jira import data' do include GraphqlHelpers let_it_be(:current_user) { create(:user) } @@ -18,6 +18,7 @@ describe 'query jira import data' do jiraImports { nodes { jiraProjectKey + createdAt scheduledAt scheduledBy { username diff --git a/spec/requests/api/graphql/query_spec.rb b/spec/requests/api/graphql/query_spec.rb new file mode 100644 index 00000000000..26b4c6eafd7 --- /dev/null +++ b/spec/requests/api/graphql/query_spec.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'Query' do + include GraphqlHelpers + + let_it_be(:project) { create(:project) } + let_it_be(:issue) { create(:issue, project: project) } + let_it_be(:developer) { create(:user) } + let(:current_user) { developer } + + describe '.designManagement' do + include DesignManagementTestHelpers + + let_it_be(:version) { create(:design_version, issue: issue) } + let_it_be(:design) { version.designs.first } + let(:query_result) { graphql_data.dig(*path) } + let(:query) { graphql_query_for(:design_management, nil, dm_fields) } + + before do + enable_design_management + project.add_developer(developer) + post_graphql(query, current_user: current_user) + end + + shared_examples 'a query that needs authorization' do + context 'the current user is not able to read designs' do + let(:current_user) { create(:user) } + + it 'does not retrieve the record' do + expect(query_result).to be_nil + end + + it 'raises an error' do + expect(graphql_errors).to include( + a_hash_including('message' => a_string_matching(%r{you don't have permission})) + ) + end + end + end + + describe '.version' do + let(:path) { %w[designManagement version] } + + let(:dm_fields) do + query_graphql_field(:version, { 'id' => global_id_of(version) }, 'id sha') + end + + it_behaves_like 'a working graphql query' + it_behaves_like 'a query that needs authorization' + + context 'the current user is able to read designs' do + it 'fetches the expected data' do + expect(query_result).to eq('id' => global_id_of(version), 'sha' => version.sha) + end + end + end + + describe '.designAtVersion' do + let_it_be(:design_at_version) do + ::DesignManagement::DesignAtVersion.new(design: design, version: version) + end + + let(:path) { %w[designManagement designAtVersion] } + + let(:dm_fields) do + query_graphql_field(:design_at_version, { 'id' => global_id_of(design_at_version) }, <<~FIELDS) + id + filename + version { id sha } + design { id } + issue { title iid } + project { id fullPath } + FIELDS + end + + it_behaves_like 'a working graphql query' + it_behaves_like 'a query that needs authorization' + + context 'the current user is able to read designs' do + it 'fetches the expected data, including the correct associations' do + expect(query_result).to eq( + 'id' => global_id_of(design_at_version), + 'filename' => design_at_version.design.filename, + 'version' => { 'id' => global_id_of(version), 'sha' => version.sha }, + 'design' => { 'id' => global_id_of(design) }, + 'issue' => { 'title' => issue.title, 'iid' => issue.iid.to_s }, + 'project' => { 'id' => global_id_of(project), 'fullPath' => project.full_path } + ) + end + end + end + end +end |