diff options
Diffstat (limited to 'spec/requests/api/graphql')
42 files changed, 899 insertions, 382 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 6324db0be4a..484ddc3469b 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 @@ -15,7 +15,7 @@ RSpec.describe 'get board lists' do let_it_be(:group_label2) { create(:group_label, group: group, name: 'Testing') } let(:params) { '' } - let(:board) { } + let(:board) {} let(:confidential) { false } let(:board_parent_type) { board_parent.class.to_s.downcase } let(:board_data) { graphql_data[board_parent_type]['boards']['nodes'][0] } diff --git a/spec/requests/api/graphql/boards/board_lists_query_spec.rb b/spec/requests/api/graphql/boards/board_lists_query_spec.rb index 39ff108a9e1..6fe2e41cf35 100644 --- a/spec/requests/api/graphql/boards/board_lists_query_spec.rb +++ b/spec/requests/api/graphql/boards/board_lists_query_spec.rb @@ -15,7 +15,7 @@ RSpec.describe 'get board lists' do let_it_be(:group_label2) { create(:group_label, group: group, name: 'Testing') } let(:params) { '' } - let(:board) { } + 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'] } diff --git a/spec/requests/api/graphql/ci/instance_variables_spec.rb b/spec/requests/api/graphql/ci/instance_variables_spec.rb index 7acf73a4e7a..c5c88697bf4 100644 --- a/spec/requests/api/graphql/ci/instance_variables_spec.rb +++ b/spec/requests/api/graphql/ci/instance_variables_spec.rb @@ -57,4 +57,16 @@ RSpec.describe 'Query.ciVariables' do expect(graphql_data.dig('ciVariables')).to be_nil end end + + context 'when the user is unauthenticated' do + let_it_be(:user) { nil } + + it 'returns nothing' do + create(:ci_instance_variable, value: 'verysecret', masked: true) + + post_graphql(query, current_user: user) + + expect(graphql_data.dig('ciVariables')).to be_nil + end + end end diff --git a/spec/requests/api/graphql/ci/manual_variables_spec.rb b/spec/requests/api/graphql/ci/manual_variables_spec.rb index b7aa76511a3..a15bac2b8bd 100644 --- a/spec/requests/api/graphql/ci/manual_variables_spec.rb +++ b/spec/requests/api/graphql/ci/manual_variables_spec.rb @@ -35,8 +35,8 @@ RSpec.describe 'Query.project(fullPath).pipelines.jobs.manualVariables' do project.add_maintainer(user) end - it 'returns the manual variables for the jobs' do - job = create(:ci_build, :manual, pipeline: pipeline) + it 'returns the manual variables for actionable jobs' do + job = create(:ci_build, :actionable, pipeline: pipeline) create(:ci_job_variable, key: 'MANUAL_TEST_VAR', job: job) post_graphql(query, current_user: user) @@ -46,8 +46,8 @@ RSpec.describe 'Query.project(fullPath).pipelines.jobs.manualVariables' do expect(variables_data.map { |var| var['key'] }).to match_array(['MANUAL_TEST_VAR']) end - it 'does not fetch job variables for jobs that are not manual' do - job = create(:ci_build, pipeline: pipeline) + it 'does not fetch job variables for jobs that are not actionable' do + job = create(:ci_build, pipeline: pipeline, status: :manual) create(:ci_job_variable, key: 'THIS_VAR_WOULD_SHOULD_NEVER_EXIST', job: job) post_graphql(query, current_user: user) diff --git a/spec/requests/api/graphql/ci/pipelines_spec.rb b/spec/requests/api/graphql/ci/pipelines_spec.rb index a968e5508cb..f471a152603 100644 --- a/spec/requests/api/graphql/ci/pipelines_spec.rb +++ b/spec/requests/api/graphql/ci/pipelines_spec.rb @@ -166,6 +166,35 @@ RSpec.describe 'Query.project(fullPath).pipelines' do end end + describe '.job' do + let(:first_n) { var('Int') } + let(:query_path) do + [ + [:project, { full_path: project.full_path }], + [:pipelines], + [:nodes], + [:job, { name: 'Job 1' }] + ] + end + + let(:query) do + wrap_fields(query_graphql_path(query_path, :status)) + end + + before_all do + pipeline = create(:ci_pipeline, project: project) + create(:ci_build, pipeline: pipeline, name: 'Job 1', status: :failed, retried: true) + create(:ci_build, pipeline: pipeline, name: 'Job 1', status: :success) + end + + it 'fetches the latest job with the given name' do + post_graphql(query, current_user: user) + expect(graphql_data_at(*query_path.map(&:first))).to contain_exactly a_hash_including( + 'status' => 'SUCCESS' + ) + end + end + describe '.jobs' do let(:first_n) { var('Int') } let(:query_path) do diff --git a/spec/requests/api/graphql/ci/runners_spec.rb b/spec/requests/api/graphql/ci/runners_spec.rb index a5b8115286e..749f6839cb5 100644 --- a/spec/requests/api/graphql/ci/runners_spec.rb +++ b/spec/requests/api/graphql/ci/runners_spec.rb @@ -37,7 +37,9 @@ RSpec.describe 'Query.runners' do end before do - allow(Gitlab::Ci::RunnerUpgradeCheck.instance).to receive(:check_runner_upgrade_status) + allow_next_instance_of(::Gitlab::Ci::RunnerUpgradeCheck) do |instance| + allow(instance).to receive(:check_runner_upgrade_suggestion) + end post_graphql(query, current_user: current_user) end diff --git a/spec/requests/api/graphql/crm/contacts_spec.rb b/spec/requests/api/graphql/crm/contacts_spec.rb index 7e824140894..a676e92dc3b 100644 --- a/spec/requests/api/graphql/crm/contacts_spec.rb +++ b/spec/requests/api/graphql/crm/contacts_spec.rb @@ -12,11 +12,11 @@ RSpec.describe 'getting CRM contacts' do create( :contact, group: group, - first_name: "ABC", - last_name: "DEF", - email: "ghi@test.com", - description: "LMNO", - state: "inactive" + first_name: "PQR", + last_name: "STU", + email: "aaa@test.com", + description: "YZ", + state: "active" ) end @@ -26,9 +26,9 @@ RSpec.describe 'getting CRM contacts' do group: group, first_name: "ABC", last_name: "DEF", - email: "vwx@test.com", - description: "YZ", - state: "active" + email: "ghi@test.com", + description: "LMNO", + state: "inactive" ) end @@ -36,9 +36,9 @@ RSpec.describe 'getting CRM contacts' do create( :contact, group: group, - first_name: "PQR", - last_name: "STU", - email: "aaa@test.com", + first_name: "JKL", + last_name: "MNO", + email: "vwx@test.com", description: "YZ", state: "active" ) @@ -51,7 +51,7 @@ RSpec.describe 'getting CRM contacts' do it_behaves_like 'sorted paginated query' do let(:sort_argument) { {} } let(:first_param) { 2 } - let(:all_records) { [contact_a, contact_b, contact_c] } + let(:all_records) { [contact_b, contact_c, contact_a] } let(:data_path) { [:group, :contacts] } def pagination_query(params) diff --git a/spec/requests/api/graphql/current_user/groups_query_spec.rb b/spec/requests/api/graphql/current_user/groups_query_spec.rb index ef0f32bacf0..6e36beb2afc 100644 --- a/spec/requests/api/graphql/current_user/groups_query_spec.rb +++ b/spec/requests/api/graphql/current_user/groups_query_spec.rb @@ -6,10 +6,11 @@ RSpec.describe 'Query current user groups' do include GraphqlHelpers let_it_be(:user) { create(:user) } + let_it_be(:root_group) { create(:group, name: 'Root group', path: 'root-group') } let_it_be(:guest_group) { create(:group, name: 'public guest', path: 'public-guest') } - let_it_be(:private_maintainer_group) { create(:group, :private, name: 'b private maintainer', path: 'b-private-maintainer') } + let_it_be(:private_maintainer_group) { create(:group, :private, name: 'b private maintainer', path: 'b-private-maintainer', parent: root_group) } let_it_be(:public_developer_group) { create(:group, project_creation_level: nil, name: 'c public developer', path: 'c-public-developer') } - let_it_be(:public_maintainer_group) { create(:group, name: 'a public maintainer', path: 'a-public-maintainer') } + let_it_be(:public_maintainer_group) { create(:group, name: 'a public maintainer', path: 'a-public-maintainer', parent: root_group) } let_it_be(:public_owner_group) { create(:group, name: 'a public owner', path: 'a-public-owner') } let(:group_arguments) { {} } @@ -77,7 +78,7 @@ RSpec.describe 'Query current user groups' do end context 'when search is provided' do - let(:group_arguments) { { permission_scope: :CREATE_PROJECTS, search: 'maintainer' } } + let(:group_arguments) { { permission_scope: :CREATE_PROJECTS, search: 'root-group maintainer' } } specify do is_expected.to match( @@ -127,6 +128,18 @@ RSpec.describe 'Query current user groups' do ) ) end + + context 'when searching for a full path (including parent)' do + let(:group_arguments) { { search: 'root-group/b-private-maintainer' } } + + specify do + is_expected.to match( + expected_group_hash( + private_maintainer_group + ) + ) + end + end end def expected_group_hash(*groups) diff --git a/spec/requests/api/graphql/custom_emoji_query_spec.rb b/spec/requests/api/graphql/custom_emoji_query_spec.rb index 874357d9eef..13b7a22e791 100644 --- a/spec/requests/api/graphql/custom_emoji_query_spec.rb +++ b/spec/requests/api/graphql/custom_emoji_query_spec.rb @@ -31,8 +31,8 @@ RSpec.describe 'getting custom emoji within namespace' do post_graphql(custom_emoji_query(group), current_user: current_user) expect(response).to have_gitlab_http_status(:ok) - expect(graphql_data['group']['customEmoji']['nodes'].count). to eq(1) - expect(graphql_data['group']['customEmoji']['nodes'].first['name']). to eq(custom_emoji.name) + expect(graphql_data['group']['customEmoji']['nodes'].count).to eq(1) + expect(graphql_data['group']['customEmoji']['nodes'].first['name']).to eq(custom_emoji.name) end it 'returns nil when unauthorised' do diff --git a/spec/requests/api/graphql/group/group_members_spec.rb b/spec/requests/api/graphql/group/group_members_spec.rb index 1ff5b134e92..bab8d5b770c 100644 --- a/spec/requests/api/graphql/group/group_members_spec.rb +++ b/spec/requests/api/graphql/group/group_members_spec.rb @@ -64,24 +64,6 @@ RSpec.describe 'getting group members information' do expect_array_response(user_2) end - - context 'when the use_keyset_aware_user_search_query FF is off' do - before do - stub_feature_flags(use_keyset_aware_user_search_query: false) - end - - it 'raises error on the 2nd page due to missing cursor data' do - fetch_members(args: { search: 'Same Name', first: 1 }) - - # user_2 because the "old" order was undeterministic (insert order), no tie-breaker column - expect_array_response(user_2) - - next_cursor = graphql_data_at(:group, :groupMembers, :pageInfo, :endCursor) - fetch_members(args: { search: 'Same Name', first: 1, after: next_cursor }) - - expect(graphql_errors.first['message']).to include('PG::UndefinedColumn') - end - end end end end diff --git a/spec/requests/api/graphql/group_query_spec.rb b/spec/requests/api/graphql/group_query_spec.rb index fd0ee5d52b9..8ee5c3c5d73 100644 --- a/spec/requests/api/graphql/group_query_spec.rb +++ b/spec/requests/api/graphql/group_query_spec.rb @@ -122,6 +122,87 @@ RSpec.describe 'getting group information' do end end + context 'with timelog categories' do + let_it_be(:group) { create(:group) } + let_it_be(:timelog_category) { create(:timelog_category, namespace: group, name: 'TimelogCategoryTest') } + + context 'when user is guest' do + it 'includes empty timelog categories array' do + post_graphql(group_query(group), current_user: user2) + + expect(graphql_data_at(:group, :timelogCategories, :nodes)).to match([]) + end + end + + context 'when user has reporter role' do + before do + group.add_reporter(user2) + end + + it 'returns the timelog category with all its fields' do + post_graphql(group_query(group), current_user: user2) + + expect(graphql_data_at(:group, :timelogCategories, :nodes)) + .to contain_exactly(a_graphql_entity_for(timelog_category)) + end + + context 'when timelog_categories flag is disabled' do + before do + stub_feature_flags(timelog_categories: false) + end + + it 'returns no timelog categories' do + post_graphql(group_query(group), current_user: user2) + + expect(graphql_data_at(:group, :timelogCategories)).to be_nil + end + end + end + + context 'for N+1 queries' do + let!(:group1) { create(:group) } + let!(:group2) { create(:group) } + + before do + group1.add_reporter(user2) + group2.add_reporter(user2) + end + + it 'avoids N+1 database queries' do + pending('See: https://gitlab.com/gitlab-org/gitlab/-/issues/369396') + + ctx = { current_user: user2 } + + baseline_query = <<~GQL + query { + a: group(fullPath: "#{group1.full_path}") { ... g } + } + + fragment g on Group { + timelogCategories { nodes { name } } + } + GQL + + query = <<~GQL + query { + a: group(fullPath: "#{group1.full_path}") { ... g } + b: group(fullPath: "#{group2.full_path}") { ... g } + } + + fragment g on Group { + timelogCategories { nodes { name } } + } + GQL + + control = ActiveRecord::QueryRecorder.new do + run_with_clean_state(baseline_query, context: ctx) + end + + expect { run_with_clean_state(query, context: ctx) }.not_to exceed_query_limit(control) + end + end + end + context "when authenticated as admin" do it "returns any existing group" do post_graphql(group_query(private_group), current_user: admin) 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 fdf5503a3a2..3879e58cecf 100644 --- a/spec/requests/api/graphql/mutations/award_emojis/add_spec.rb +++ b/spec/requests/api/graphql/mutations/award_emojis/add_spec.rb @@ -74,7 +74,7 @@ RSpec.describe 'Adding an AwardEmoji' do end describe 'marking Todos as done' do - let(:user) { current_user} + let(:user) { current_user } subject { post_graphql_mutation(mutation, current_user: user) } 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 6b26e37e30c..7ddffa1ab0a 100644 --- a/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb +++ b/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb @@ -84,7 +84,7 @@ RSpec.describe 'Toggling an AwardEmoji' do end describe 'marking Todos as done' do - let(:user) { current_user} + let(:user) { current_user } subject { post_graphql_mutation(mutation, current_user: user) } diff --git a/spec/requests/api/graphql/mutations/boards/destroy_spec.rb b/spec/requests/api/graphql/mutations/boards/destroy_spec.rb index 23e099e94b6..7620da3e7e0 100644 --- a/spec/requests/api/graphql/mutations/boards/destroy_spec.rb +++ b/spec/requests/api/graphql/mutations/boards/destroy_spec.rb @@ -65,15 +65,8 @@ RSpec.describe Mutations::Boards::Destroy 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 + it 'does destroy the board' do + expect { subject }.to change { Board.count }.by(-1) end end end diff --git a/spec/requests/api/graphql/mutations/ci/job_retry_spec.rb b/spec/requests/api/graphql/mutations/ci/job_retry_spec.rb index ef640183bd8..8cf559a372a 100644 --- a/spec/requests/api/graphql/mutations/ci/job_retry_spec.rb +++ b/spec/requests/api/graphql/mutations/ci/job_retry_spec.rb @@ -47,6 +47,38 @@ RSpec.describe 'JobRetry' do expect(new_job).not_to be_retried end + context 'when given CI variables' do + let(:job) { create(:ci_build, :success, :actionable, pipeline: pipeline, name: 'build') } + + let(:mutation) do + variables = { + id: job.to_global_id.to_s, + variables: { key: 'MANUAL_VAR', value: 'test manual var' } + } + + graphql_mutation(:job_retry, variables, + <<-QL + errors + job { + id + } + QL + ) + end + + it 'applies them to a retried manual job' do + post_graphql_mutation(mutation, current_user: user) + + expect(response).to have_gitlab_http_status(:success) + + new_job_id = GitlabSchema.object_from_id(mutation_response['job']['id']).sync.id + new_job = ::Ci::Build.find(new_job_id) + expect(new_job.job_variables.count).to be(1) + expect(new_job.job_variables.first.key).to eq('MANUAL_VAR') + expect(new_job.job_variables.first.value).to eq('test manual var') + end + end + context 'when the job is not retryable' do let(:job) { create(:ci_build, :retried, pipeline: pipeline) } diff --git a/spec/requests/api/graphql/mutations/ci/pipeline_cancel_spec.rb b/spec/requests/api/graphql/mutations/ci/pipeline_cancel_spec.rb index d9106aa42c4..6ec1b7ce9b6 100644 --- a/spec/requests/api/graphql/mutations/ci/pipeline_cancel_spec.rb +++ b/spec/requests/api/graphql/mutations/ci/pipeline_cancel_spec.rb @@ -40,7 +40,7 @@ RSpec.describe 'PipelineCancel' do expect(build).not_to be_canceled end - it "cancels all cancelable builds from a pipeline" do + it 'cancels all cancelable builds from a pipeline', :sidekiq_inline do build = create(:ci_build, :running, pipeline: pipeline) post_graphql_mutation(mutation, current_user: user) diff --git a/spec/requests/api/graphql/mutations/merge_requests/request_attention_spec.rb b/spec/requests/api/graphql/mutations/merge_requests/request_attention_spec.rb deleted file mode 100644 index 9c751913827..00000000000 --- a/spec/requests/api/graphql/mutations/merge_requests/request_attention_spec.rb +++ /dev/null @@ -1,79 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe 'Request attention' do - include GraphqlHelpers - - let_it_be(:current_user) { create(:user) } - let_it_be(:user) { create(:user) } - let_it_be(:merge_request) { create(:merge_request, reviewers: [user]) } - let_it_be(:project) { merge_request.project } - - let(:input) { { user_id: global_id_of(user) } } - - let(:mutation) do - variables = { - project_path: project.full_path, - iid: merge_request.iid.to_s - } - graphql_mutation(:merge_request_request_attention, variables.merge(input), - <<-QL.strip_heredoc - clientMutationId - errors - QL - ) - end - - def mutation_response - graphql_mutation_response(:merge_request_request_attention) - end - - def mutation_errors - mutation_response['errors'] - end - - before_all do - project.add_developer(current_user) - project.add_developer(user) - end - - it 'is successful' do - post_graphql_mutation(mutation, current_user: current_user) - - expect(response).to have_gitlab_http_status(:success) - expect(mutation_errors).to be_empty - end - - context 'when current user is not allowed to update the merge request' do - it 'returns an error' do - post_graphql_mutation(mutation, current_user: create(:user)) - - expect(graphql_errors).not_to be_empty - end - end - - context 'when user is not a reviewer' do - let(:input) { { user_id: global_id_of(create(:user)) } } - - it 'returns an error' do - post_graphql_mutation(mutation, current_user: current_user) - - expect(response).to have_gitlab_http_status(:success) - expect(mutation_errors).not_to be_empty - end - end - - context 'feature flag is disabled' do - before do - stub_feature_flags(mr_attention_requests: false) - end - - it 'returns an error' do - post_graphql_mutation(mutation, current_user: current_user) - - expect(response).to have_gitlab_http_status(:success) - expect(graphql_errors[0]["message"]).to eq "Feature disabled" - end - end -end diff --git a/spec/requests/api/graphql/mutations/merge_requests/set_reviewers_spec.rb b/spec/requests/api/graphql/mutations/merge_requests/set_reviewers_spec.rb new file mode 100644 index 00000000000..be786256ef2 --- /dev/null +++ b/spec/requests/api/graphql/mutations/merge_requests/set_reviewers_spec.rb @@ -0,0 +1,106 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Setting reviewers of a merge request', :assume_throttled do + include GraphqlHelpers + + let_it_be(:project) { create(:project, :repository) } + let_it_be(:current_user) { create(:user, developer_projects: [project]) } + let_it_be(:reviewer) { create(:user) } + let_it_be(:reviewer2) { create(:user) } + let_it_be_with_reload(:merge_request) { create(:merge_request, source_project: project) } + + let(:input) { { reviewer_usernames: [reviewer.username] } } + let(:expected_result) do + [{ 'username' => reviewer.username }] + end + + let(:mutation) do + variables = { + project_path: project.full_path, + iid: merge_request.iid.to_s + } + graphql_mutation(:merge_request_set_reviewers, variables.merge(input), + <<-QL.strip_heredoc + clientMutationId + errors + mergeRequest { + id + reviewers { + nodes { + username + } + } + } + QL + ) + end + + def mutation_response + graphql_mutation_response(:merge_request_set_reviewers) + end + + def mutation_reviewer_nodes + mutation_response['mergeRequest']['reviewers']['nodes'] + end + + def run_mutation! + post_graphql_mutation(mutation, current_user: current_user) + end + + before do + project.add_developer(reviewer) + project.add_developer(reviewer2) + + merge_request.update!(reviewers: []) + end + + it 'returns an error if the user is not allowed to update the merge request' do + post_graphql_mutation(mutation, current_user: create(:user)) + + expect(graphql_errors).not_to be_empty + end + + context 'when the current user does not have permission to add reviewers' do + let(:current_user) { create(:user) } + + it 'does not change the reviewers' do + project.add_guest(current_user) + + expect { run_mutation! }.not_to change { merge_request.reset.reviewers.pluck(:id) } + + expect(graphql_errors).not_to be_empty + end + end + + context 'with reviewers already assigned' do + before do + merge_request.reviewers = [reviewer2] + merge_request.save! + end + + it 'replaces the reviewer' do + run_mutation! + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_reviewer_nodes).to match_array(expected_result) + end + end + + context 'when passing an empty list of reviewers' do + let(:input) { { reviewer_usernames: [] } } + + before do + merge_request.reviewers = [reviewer2] + merge_request.save! + end + + it 'removes reviewer' do + run_mutation! + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_reviewer_nodes).to eq([]) + end + end +end diff --git a/spec/requests/api/graphql/mutations/merge_requests/update_reviewer_state_spec.rb b/spec/requests/api/graphql/mutations/merge_requests/update_reviewer_state_spec.rb deleted file mode 100644 index cf497cb2579..00000000000 --- a/spec/requests/api/graphql/mutations/merge_requests/update_reviewer_state_spec.rb +++ /dev/null @@ -1,65 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe 'Toggle attention requested for reviewer' do - include GraphqlHelpers - - let(:current_user) { create(:user) } - let(:merge_request) { create(:merge_request, reviewers: [user]) } - let(:project) { merge_request.project } - let(:user) { create(:user) } - let(:input) { { user_id: global_id_of(user) } } - - let(:mutation) do - variables = { - project_path: project.full_path, - iid: merge_request.iid.to_s - } - graphql_mutation(:merge_request_toggle_attention_requested, variables.merge(input), - <<-QL.strip_heredoc - clientMutationId - errors - QL - ) - end - - def mutation_response - graphql_mutation_response(:merge_request_toggle_attention_requested) - end - - def mutation_errors - mutation_response['errors'] - end - - before do - project.add_developer(current_user) - project.add_developer(user) - end - - it 'returns an error if the user is not allowed to update the merge request' do - post_graphql_mutation(mutation, current_user: create(:user)) - - expect(graphql_errors).not_to be_empty - end - - describe 'reviewer does not exist' do - let(:input) { { user_id: global_id_of(create(:user)) } } - - it 'returns an error' do - post_graphql_mutation(mutation, current_user: current_user) - - expect(response).to have_gitlab_http_status(:success) - expect(mutation_errors).not_to be_empty - end - end - - describe 'reviewer exists' do - it 'does not return an error' do - post_graphql_mutation(mutation, current_user: current_user) - - expect(response).to have_gitlab_http_status(:success) - expect(mutation_errors).to be_empty - end - end -end diff --git a/spec/requests/api/graphql/mutations/notes/create/note_spec.rb b/spec/requests/api/graphql/mutations/notes/create/note_spec.rb index 22b5f2d5112..9c3842db31a 100644 --- a/spec/requests/api/graphql/mutations/notes/create/note_spec.rb +++ b/spec/requests/api/graphql/mutations/notes/create/note_spec.rb @@ -79,21 +79,29 @@ RSpec.describe 'Adding a Note' do context 'for an issue' do let(:noteable) { create(:issue, project: project) } - let(:mutation) do - variables = { + let(:mutation) { graphql_mutation(:create_note, variables) } + let(:variables) do + { noteable_id: GitlabSchema.id_from_object(noteable).to_s, - body: body, - confidential: true - } - - graphql_mutation(:create_note, variables) + body: body + }.merge(variables_extra) end before do project.add_developer(current_user) end - it_behaves_like 'a Note mutation with confidential notes' + context 'when using internal param' do + let(:variables_extra) { { internal: true } } + + it_behaves_like 'a Note mutation with confidential notes' + end + + context 'when using deprecated confidential param' do + let(:variables_extra) { { confidential: true } } + + it_behaves_like 'a Note mutation with confidential notes' + end end context 'when body only contains quick actions' do diff --git a/spec/requests/api/graphql/mutations/releases/create_spec.rb b/spec/requests/api/graphql/mutations/releases/create_spec.rb index 1e62942c29d..2541072b766 100644 --- a/spec/requests/api/graphql/mutations/releases/create_spec.rb +++ b/spec/requests/api/graphql/mutations/releases/create_spec.rb @@ -16,10 +16,10 @@ RSpec.describe 'Creation of a new release' do let(:mutation_name) { :release_create } - let(:tag_name) { 'v7.12.5'} + let(:tag_name) { 'v7.12.5' } let(:tag_message) { nil } - let(:ref) { 'master'} - let(:name) { 'Version 7.12.5'} + let(:ref) { 'master' } + let(:name) { 'Version 7.12.5' } let(:description) { 'Release 7.12.5 :rocket:' } let(:released_at) { '2018-12-10' } let(:milestones) { [milestone_12_3.title, milestone_12_4.title] } diff --git a/spec/requests/api/graphql/mutations/releases/update_spec.rb b/spec/requests/api/graphql/mutations/releases/update_spec.rb index 0fa3d7de299..33d4e57904c 100644 --- a/spec/requests/api/graphql/mutations/releases/update_spec.rb +++ b/spec/requests/api/graphql/mutations/releases/update_spec.rb @@ -15,7 +15,7 @@ RSpec.describe 'Updating an existing release' do let_it_be(:milestone_12_4) { create(:milestone, project: project, title: '12.4') } let_it_be(:tag_name) { 'v1.1.0' } - let_it_be(:name) { 'Version 7.12.5'} + let_it_be(:name) { 'Version 7.12.5' } let_it_be(:description) { 'Release 7.12.5 :rocket:' } let_it_be(:released_at) { '2018-12-10' } let_it_be(:created_at) { '2018-11-05' } diff --git a/spec/requests/api/graphql/mutations/remove_attention_request_spec.rb b/spec/requests/api/graphql/mutations/remove_attention_request_spec.rb deleted file mode 100644 index 053559b039d..00000000000 --- a/spec/requests/api/graphql/mutations/remove_attention_request_spec.rb +++ /dev/null @@ -1,79 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe 'Remove attention request' do - include GraphqlHelpers - - let_it_be(:current_user) { create(:user) } - let_it_be(:user) { create(:user) } - let_it_be(:merge_request) { create(:merge_request, reviewers: [user]) } - let_it_be(:project) { merge_request.project } - - let(:input) { { user_id: global_id_of(user) } } - - let(:mutation) do - variables = { - project_path: project.full_path, - iid: merge_request.iid.to_s - } - graphql_mutation(:merge_request_remove_attention_request, variables.merge(input), - <<-QL.strip_heredoc - clientMutationId - errors - QL - ) - end - - def mutation_response - graphql_mutation_response(:merge_request_remove_attention_request) - end - - def mutation_errors - mutation_response['errors'] - end - - before_all do - project.add_developer(current_user) - project.add_developer(user) - end - - it 'is successful' do - post_graphql_mutation(mutation, current_user: current_user) - - expect(response).to have_gitlab_http_status(:success) - expect(mutation_errors).to be_empty - end - - context 'when current user is not allowed to update the merge request' do - it 'returns an error' do - post_graphql_mutation(mutation, current_user: create(:user)) - - expect(graphql_errors).not_to be_empty - end - end - - context 'when user is not a reviewer' do - let(:input) { { user_id: global_id_of(create(:user)) } } - - it 'returns an error' do - post_graphql_mutation(mutation, current_user: current_user) - - expect(response).to have_gitlab_http_status(:success) - expect(mutation_errors).not_to be_empty - end - end - - context 'feature flag is disabled' do - before do - stub_feature_flags(mr_attention_requests: false) - end - - it 'returns an error' do - post_graphql_mutation(mutation, current_user: current_user) - - expect(response).to have_gitlab_http_status(:success) - expect(graphql_errors[0]["message"]).to eq "Feature disabled" - end - 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 9a3cea3ca14..264fa5732c3 100644 --- a/spec/requests/api/graphql/mutations/snippets/create_spec.rb +++ b/spec/requests/api/graphql/mutations/snippets/create_spec.rb @@ -12,8 +12,8 @@ RSpec.describe 'Creating a Snippet' do let(:title) { 'Initial title' } 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(: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 } @@ -149,7 +149,7 @@ RSpec.describe 'Creating a Snippet' do end context 'when there non ActiveRecord errors' do - let(:file_1) { { filePath: 'invalid://file/path', content: 'foobar' }} + 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/timelogs/create_spec.rb b/spec/requests/api/graphql/mutations/timelogs/create_spec.rb new file mode 100644 index 00000000000..eea04b89783 --- /dev/null +++ b/spec/requests/api/graphql/mutations/timelogs/create_spec.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Create a timelog' do + include GraphqlHelpers + + let_it_be(:author) { create(:user) } + let_it_be(:project) { create(:project, :public) } + let_it_be(:time_spent) { '1h' } + + let(:current_user) { nil } + let(:users_container) { project } + let(:mutation) do + graphql_mutation(:timelogCreate, { + 'time_spent' => time_spent, + 'spent_at' => '2022-07-08', + 'summary' => 'Test summary', + 'issuable_id' => issuable.to_global_id.to_s + }) + end + + let(:mutation_response) { graphql_mutation_response(:timelog_create) } + + context 'when issuable is an Issue' do + let_it_be(:issuable) { create(:issue, project: project) } + + it_behaves_like 'issuable supports timelog creation mutation' + end + + context 'when issuable is a MergeRequest' do + let_it_be(:issuable) { create(:merge_request, source_project: project) } + + it_behaves_like 'issuable supports timelog creation mutation' + end + + context 'when issuable is a WorkItem' do + let_it_be(:issuable) { create(:work_item, project: project, title: 'WorkItem') } + + it_behaves_like 'issuable supports timelog creation mutation' + end + + context 'when issuable is an Incident' do + let_it_be(:issuable) { create(:incident, project: project) } + + it_behaves_like 'issuable supports timelog creation mutation' + end +end diff --git a/spec/requests/api/graphql/mutations/timelogs/delete_spec.rb b/spec/requests/api/graphql/mutations/timelogs/delete_spec.rb index b674e77f093..d304bfbdf00 100644 --- a/spec/requests/api/graphql/mutations/timelogs/delete_spec.rb +++ b/spec/requests/api/graphql/mutations/timelogs/delete_spec.rb @@ -7,7 +7,7 @@ RSpec.describe 'Delete a timelog' do let_it_be(:author) { create(:user) } let_it_be(:project) { create(:project, :public) } let_it_be(:issue) { create(:issue, project: project) } - let_it_be(:timelog) { create(:timelog, user: author, issue: issue, time_spent: 1800)} + let_it_be(:timelog) { create(:timelog, user: author, issue: issue, time_spent: 1800) } let(:current_user) { nil } let(:mutation) { graphql_mutation(:timelogDelete, { 'id' => timelog.to_global_id.to_s }) } diff --git a/spec/requests/api/graphql/mutations/uploads/delete_spec.rb b/spec/requests/api/graphql/mutations/uploads/delete_spec.rb new file mode 100644 index 00000000000..f44bf179397 --- /dev/null +++ b/spec/requests/api/graphql/mutations/uploads/delete_spec.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Delete an upload' do + include GraphqlHelpers + + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project, group: group) } + let_it_be(:developer) { create(:user).tap { |user| group.add_developer(user) } } + let_it_be(:maintainer) { create(:user).tap { |user| group.add_maintainer(user) } } + + let(:extra_params) { {} } + let(:params) { { filename: File.basename(upload.path), secret: upload.secret }.merge(extra_params) } + let(:mutation) { graphql_mutation(:uploadDelete, params) } + let(:mutation_response) { graphql_mutation_response(:upload_delete) } + + shared_examples_for 'upload deletion' do + context 'when the user is not allowed to delete uploads' do + let(:current_user) { developer } + + it_behaves_like 'a mutation that returns a top-level access error' + end + + context 'when the user is anonymous' do + let(:current_user) { nil } + + it_behaves_like 'a mutation that returns a top-level access error' + end + + context 'when user has permissions to delete uploads' do + let(:current_user) { maintainer } + + it 'deletes the upload' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response['upload']).to include('id' => upload.to_global_id.to_s) + expect(mutation_response['errors']).to be_empty + end + + context 'when upload does not exist' do + let(:params) { { filename: 'invalid', secret: upload.secret }.merge(extra_params) } + + it 'returns an error' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response['upload']).to be_nil + expect(mutation_response['errors']).to match_array([ + "The resource that you are attempting to access does not "\ + "exist or you don't have permission to perform this action." + ]) + end + end + end + end + + context 'when deleting project upload' do + let_it_be_with_reload(:upload) { create(:upload, :issuable_upload, model: project) } + + let(:extra_params) { { project_path: project.full_path } } + + it_behaves_like 'upload deletion' + end + + context 'when deleting group upload' do + let_it_be_with_reload(:upload) { create(:upload, :namespace_upload, model: group) } + + let(:extra_params) { { group_path: group.full_path } } + + it_behaves_like 'upload deletion' + end +end diff --git a/spec/requests/api/graphql/mutations/work_items/create_from_task_spec.rb b/spec/requests/api/graphql/mutations/work_items/create_from_task_spec.rb index b1356bbe6fd..e7f4917ddde 100644 --- a/spec/requests/api/graphql/mutations/work_items/create_from_task_spec.rb +++ b/spec/requests/api/graphql/mutations/work_items/create_from_task_spec.rb @@ -7,7 +7,7 @@ RSpec.describe "Create a work item from a task in a work item's description" do let_it_be(:project) { create(:project) } let_it_be(:developer) { create(:user).tap { |user| project.add_developer(user) } } - let_it_be(:work_item, refind: true) { create(:work_item, project: project, description: '- [ ] A task in a list', lock_version: 3) } + let_it_be(:work_item, refind: true) { create(:work_item, :confidential, project: project, description: '- [ ] A task in a list', lock_version: 3) } let(:lock_version) { work_item.lock_version } let(:input) do @@ -48,6 +48,7 @@ RSpec.describe "Create a work item from a task in a work item's description" do expect(created_work_item.issue_type).to eq('task') expect(created_work_item.work_item_type.base_type).to eq('task') expect(created_work_item.work_item_parent).to eq(work_item) + expect(created_work_item).to be_confidential expect(mutation_response['workItem']).to include('id' => work_item.to_global_id.to_s) expect(mutation_response['newWorkItem']).to include('id' => created_work_item.to_global_id.to_s) end diff --git a/spec/requests/api/graphql/mutations/work_items/create_spec.rb b/spec/requests/api/graphql/mutations/work_items/create_spec.rb index 911568bc39f..8233821053f 100644 --- a/spec/requests/api/graphql/mutations/work_items/create_spec.rb +++ b/spec/requests/api/graphql/mutations/work_items/create_spec.rb @@ -12,6 +12,7 @@ RSpec.describe 'Create a work item' do { 'title' => 'new title', 'description' => 'new description', + 'confidential' => true, 'workItemTypeId' => WorkItems::Type.default_by_type(:task).to_global_id.to_s } end @@ -38,6 +39,7 @@ RSpec.describe 'Create a work item' do expect(response).to have_gitlab_http_status(:success) expect(created_work_item.issue_type).to eq('task') + expect(created_work_item).to be_confidential expect(created_work_item.work_item_type.base_type).to eq('task') expect(mutation_response['workItem']).to include( input.except('workItemTypeId').merge( @@ -127,7 +129,7 @@ RSpec.describe 'Create a work item' do end context 'when parent work item is not found' do - let_it_be(:parent) { build_stubbed(:work_item, id: non_existing_record_id)} + let_it_be(:parent) { build_stubbed(:work_item, id: non_existing_record_id) } it 'returns a top level error' do post_graphql_mutation(mutation, current_user: current_user) diff --git a/spec/requests/api/graphql/mutations/work_items/update_spec.rb b/spec/requests/api/graphql/mutations/work_items/update_spec.rb index 77f7b9bacef..909d6549fa5 100644 --- a/spec/requests/api/graphql/mutations/work_items/update_spec.rb +++ b/spec/requests/api/graphql/mutations/work_items/update_spec.rb @@ -34,6 +34,10 @@ RSpec.describe 'Update a work item' do context 'when user has permissions to update a work item' do let(:current_user) { developer } + it_behaves_like 'has spam protection' do + let(:mutation_class) { ::Mutations::WorkItems::Update } + end + context 'when the work item is open' do it 'closes and updates the work item' do expect do @@ -71,36 +75,48 @@ RSpec.describe 'Update a work item' do end end - context 'when unsupported widget input is sent' do - let_it_be(:test_case) { create(:work_item_type, :default, :test_case, name: 'some_test_case_name') } - let_it_be(:work_item) { create(:work_item, work_item_type: test_case, project: project) } - - let(:input) do - { - 'hierarchyWidget' => {} + context 'when updating confidentiality' do + let(:fields) do + <<~FIELDS + workItem { + confidential } + errors + FIELDS end - it_behaves_like 'a mutation that returns top-level errors', - errors: ["Following widget keys are not supported by some_test_case_name type: [:hierarchy_widget]"] - end + shared_examples 'toggling confidentiality' do + it 'successfully updates work item' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + work_item.reload + end.to change(work_item, :confidential).from(values[:old]).to(values[:new]) - it_behaves_like 'has spam protection' do - let(:mutation_class) { ::Mutations::WorkItems::Update } - end + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response['workItem']).to include( + 'confidential' => values[:new] + ) + end + end - context 'when the work_items feature flag is disabled' do - before do - stub_feature_flags(work_items: false) + context 'when setting as confidential' do + let(:input) { { 'confidential' => true } } + + it_behaves_like 'toggling confidentiality' do + let(:values) { { old: false, new: true } } + end end - it 'does not update the work item and returns and error' do - expect do - post_graphql_mutation(mutation, current_user: current_user) - work_item.reload - end.to not_change(work_item, :title) + context 'when setting as non-confidential' do + let(:input) { { 'confidential' => false } } - expect(mutation_response['errors']).to contain_exactly('`work_items` feature flag disabled for this project') + before do + work_item.update!(confidential: true) + end + + it_behaves_like 'toggling confidentiality' do + let(:values) { { old: true, new: false } } + end end end @@ -128,26 +144,90 @@ RSpec.describe 'Update a work item' do end end - context 'with weight widget input' do + context 'with due and start date widget input' do + let(:start_date) { Date.today } + let(:due_date) { 1.week.from_now.to_date } let(:fields) do <<~FIELDS - workItem { - widgets { - type - ... on WorkItemWidgetWeight { - weight + workItem { + widgets { + type + ... on WorkItemWidgetStartAndDueDate { + startDate + dueDate + } } } - } - errors + errors FIELDS end - it_behaves_like 'update work item weight widget' do - let(:new_weight) { 2 } + let(:input) do + { 'startAndDueDateWidget' => { 'startDate' => start_date.to_s, 'dueDate' => due_date.to_s } } + end - let(:input) do - { 'weightWidget' => { 'weight' => new_weight } } + it 'updates start and due date' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + work_item.reload + end.to change(work_item, :start_date).from(nil).to(start_date).and( + change(work_item, :due_date).from(nil).to(due_date) + ) + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response['workItem']['widgets']).to include( + { + 'startDate' => start_date.to_s, + 'dueDate' => due_date.to_s, + 'type' => 'START_AND_DUE_DATE' + } + ) + end + + context 'when provided input is invalid' do + let(:due_date) { 1.week.ago.to_date } + + it 'returns validation errors without the work item' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(mutation_response['workItem']).to be_nil + expect(mutation_response['errors']).to contain_exactly('Due date must be greater than or equal to start date') + end + end + + context 'when dates were already set for the work item' do + before do + work_item.update!(start_date: start_date, due_date: due_date) + end + + context 'when updating only start date' do + let(:input) do + { 'startAndDueDateWidget' => { 'startDate' => nil } } + end + + it 'allows setting a single date to null' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + work_item.reload + end.to change(work_item, :start_date).from(start_date).to(nil).and( + not_change(work_item, :due_date).from(due_date) + ) + end + end + + context 'when updating only due date' do + let(:input) do + { 'startAndDueDateWidget' => { 'dueDate' => nil } } + end + + it 'allows setting a single date to null' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + work_item.reload + end.to change(work_item, :due_date).from(due_date).to(nil).and( + not_change(work_item, :start_date).from(start_date) + ) + end end end end @@ -179,7 +259,7 @@ RSpec.describe 'Update a work item' do end context 'when updating parent' do - let_it_be(:work_item) { create(:work_item, :task, project: project) } + let_it_be(:work_item, reload: true) { create(:work_item, :task, project: project) } let_it_be(:valid_parent) { create(:work_item, project: project) } let_it_be(:invalid_parent) { create(:work_item, :task, project: project) } @@ -346,5 +426,78 @@ RSpec.describe 'Update a work item' do end end end + + context 'when updating assignees' do + let(:fields) do + <<~FIELDS + workItem { + widgets { + type + ... on WorkItemWidgetAssignees { + assignees { + nodes { + id + username + } + } + } + } + } + errors + FIELDS + end + + let(:input) do + { 'assigneesWidget' => { 'assigneeIds' => [developer.to_global_id.to_s] } } + end + + it 'updates the work item assignee' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + work_item.reload + end.to change(work_item, :assignee_ids).from([]).to([developer.id]) + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response['workItem']['widgets']).to include( + { + 'type' => 'ASSIGNEES', + 'assignees' => { + 'nodes' => [ + { 'id' => developer.to_global_id.to_s, 'username' => developer.username } + ] + } + } + ) + end + end + + context 'when unsupported widget input is sent' do + let_it_be(:test_case) { create(:work_item_type, :default, :test_case, name: 'some_test_case_name') } + let_it_be(:work_item) { create(:work_item, work_item_type: test_case, project: project) } + + let(:input) do + { + 'hierarchyWidget' => {} + } + end + + it_behaves_like 'a mutation that returns top-level errors', + errors: ["Following widget keys are not supported by some_test_case_name type: [:hierarchy_widget]"] + end + + context 'when the work_items feature flag is disabled' do + before do + stub_feature_flags(work_items: false) + end + + it 'does not update the work item and returns and error' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + work_item.reload + end.to not_change(work_item, :title) + + expect(mutation_response['errors']).to contain_exactly('`work_items` feature flag disabled for this project') + end + end end end diff --git a/spec/requests/api/graphql/namespace/root_storage_statistics_spec.rb b/spec/requests/api/graphql/namespace/root_storage_statistics_spec.rb index 37cc502103d..8d8a0baae36 100644 --- a/spec/requests/api/graphql/namespace/root_storage_statistics_spec.rb +++ b/spec/requests/api/graphql/namespace/root_storage_statistics_spec.rb @@ -49,7 +49,7 @@ RSpec.describe 'rendering namespace statistics' do it_behaves_like 'a working namespace with storage statistics query' context 'when the namespace is public' do - let(:group) { create(:group, :public)} + let(:group) { create(:group, :public) } it 'hides statistics for unauthenticated requests' do post_graphql(query, current_user: nil) diff --git a/spec/requests/api/graphql/packages/conan_spec.rb b/spec/requests/api/graphql/packages/conan_spec.rb index 1f3732980d9..5bd5a71bbeb 100644 --- a/spec/requests/api/graphql/packages/conan_spec.rb +++ b/spec/requests/api/graphql/packages/conan_spec.rb @@ -8,7 +8,7 @@ RSpec.describe 'conan package details' do let_it_be(:package) { create(:conan_package, project: project) } let(:metadata) { query_graphql_fragment('ConanMetadata') } - let(:package_files_metadata) {query_graphql_fragment('ConanFileMetadata')} + let(:package_files_metadata) { query_graphql_fragment('ConanFileMetadata') } let(:query) do graphql_query_for(:package, { id: package_global_id }, <<~FIELDS) diff --git a/spec/requests/api/graphql/packages/helm_spec.rb b/spec/requests/api/graphql/packages/helm_spec.rb index 397096f70db..1675b8faa23 100644 --- a/spec/requests/api/graphql/packages/helm_spec.rb +++ b/spec/requests/api/graphql/packages/helm_spec.rb @@ -7,7 +7,7 @@ RSpec.describe 'helm package details' do let_it_be(:package) { create(:helm_package, project: project) } - let(:package_files_metadata) {query_graphql_fragment('HelmFileMetadata')} + let(:package_files_metadata) { query_graphql_fragment('HelmFileMetadata') } let(:query) do graphql_query_for(:package, { id: package_global_id }, <<~FIELDS) diff --git a/spec/requests/api/graphql/packages/package_spec.rb b/spec/requests/api/graphql/packages/package_spec.rb index 0335c1085b4..c28b37db5af 100644 --- a/spec/requests/api/graphql/packages/package_spec.rb +++ b/spec/requests/api/graphql/packages/package_spec.rb @@ -18,7 +18,7 @@ RSpec.describe 'package details' do let(:depth) { 3 } let(:excluded) { %w[metadata apiFuzzingCiConfiguration pipeline packageFiles] } let(:metadata) { query_graphql_fragment('ComposerMetadata') } - let(:package_files) {all_graphql_fields_for('PackageFile')} + let(:package_files) { all_graphql_fields_for('PackageFile') } let(:package_global_id) { global_id_of(composer_package) } let(:package_details) { graphql_data_at(:package) } diff --git a/spec/requests/api/graphql/project/base_service_spec.rb b/spec/requests/api/graphql/project/base_service_spec.rb index 5dc0f55db88..58d10ade8cf 100644 --- a/spec/requests/api/graphql/project/base_service_spec.rb +++ b/spec/requests/api/graphql/project/base_service_spec.rb @@ -26,7 +26,7 @@ RSpec.describe 'query Jira service' do ) end - let(:services) { graphql_data.dig('project', 'services', 'nodes')} + let(:services) { graphql_data.dig('project', 'services', 'nodes') } it_behaves_like 'unauthorized users cannot read services' diff --git a/spec/requests/api/graphql/project/error_tracking/sentry_detailed_error_request_spec.rb b/spec/requests/api/graphql/project/error_tracking/sentry_detailed_error_request_spec.rb index 2b85704f479..2fe5fb593fe 100644 --- a/spec/requests/api/graphql/project/error_tracking/sentry_detailed_error_request_spec.rb +++ b/spec/requests/api/graphql/project/error_tracking/sentry_detailed_error_request_spec.rb @@ -34,6 +34,8 @@ RSpec.describe 'getting a detailed sentry error' do context 'when data is loading via reactive cache' do before do + expect(Gitlab::UsageDataCounters::HLLRedisCounter).not_to receive(:track_event) + post_graphql(query, current_user: current_user) end @@ -48,6 +50,10 @@ RSpec.describe 'getting a detailed sentry error' do .to receive(:issue_details) .and_return({ issue: sentry_detailed_error }) + expect(Gitlab::UsageDataCounters::HLLRedisCounter) + .to receive(:track_event) + .with('error_tracking_view_details', values: current_user.id) + post_graphql(query, current_user: current_user) end diff --git a/spec/requests/api/graphql/project/fork_targets_spec.rb b/spec/requests/api/graphql/project/fork_targets_spec.rb new file mode 100644 index 00000000000..b21a11ff4dc --- /dev/null +++ b/spec/requests/api/graphql/project/fork_targets_spec.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'getting a list of fork targets for a project' do + include GraphqlHelpers + + let_it_be(:group) { create(:group) } + let_it_be(:another_group) { create(:group) } + let_it_be(:project) { create(:project, :private, group: group) } + let_it_be(:user) { create(:user, developer_projects: [project]) } + + let(:current_user) { user } + let(:fields) do + <<~GRAPHQL + forkTargets{ + nodes { id name fullPath visibility } + } + GRAPHQL + end + + let(:query) do + graphql_query_for( + 'project', + { 'fullPath' => project.full_path }, + fields + ) + end + + before_all do + group.add_owner(user) + another_group.add_owner(user) + end + + context 'when user has access to the project' do + before do + post_graphql(query, current_user: current_user) + end + + it_behaves_like 'a working graphql query' + + it 'returns fork targets for the project' do + expect(graphql_data.dig('project', 'forkTargets', 'nodes')).to match_array( + [user.namespace, project.namespace, another_group].map do |target| + hash_including( + { + 'id' => target.to_global_id.to_s, + 'name' => target.name, + 'fullPath' => target.full_path, + 'visibility' => target.visibility + } + ) + end + ) + end + end + + context "when user doesn't have access to the project" do + let(:current_user) { create(:user) } + + before do + post_graphql(query, current_user: current_user) + end + + it 'does not return the project' do + expect(graphql_data).to eq('project' => nil) + 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 98a3f08baa6..202220f4bf6 100644 --- a/spec/requests/api/graphql/project/jira_import_spec.rb +++ b/spec/requests/api/graphql/project/jira_import_spec.rb @@ -56,8 +56,8 @@ RSpec.describe 'query Jira import data' do ) end - let(:jira_imports) { graphql_data.dig('project', 'jiraImports', 'nodes')} - let(:jira_import_status) { graphql_data.dig('project', 'jiraImportStatus')} + let(:jira_imports) { graphql_data.dig('project', 'jiraImports', 'nodes') } + let(:jira_import_status) { graphql_data.dig('project', 'jiraImportStatus') } context 'when user cannot read Jira import data' do before do @@ -89,11 +89,11 @@ RSpec.describe 'query Jira import data' do context 'list of jira imports sorted ascending by scheduledAt time' do it 'retuns list of jira imports' do - jira_proket_keys = jira_imports.map {|ji| ji['jiraProjectKey']} - usernames = jira_imports.map {|ji| ji.dig('scheduledBy', 'username')} - imported_issues_count = jira_imports.map {|ji| ji.dig('importedIssuesCount')} - failed_issues_count = jira_imports.map {|ji| ji.dig('failedToImportCount')} - total_issue_count = jira_imports.map {|ji| ji.dig('totalIssueCount')} + jira_proket_keys = jira_imports.map { |ji| ji['jiraProjectKey'] } + usernames = jira_imports.map { |ji| ji.dig('scheduledBy', 'username') } + imported_issues_count = jira_imports.map { |ji| ji.dig('importedIssuesCount') } + failed_issues_count = jira_imports.map { |ji| ji.dig('failedToImportCount') } + total_issue_count = jira_imports.map { |ji| ji.dig('totalIssueCount') } expect(jira_imports.size).to eq 2 expect(jira_proket_keys).to eq %w(BB AA) diff --git a/spec/requests/api/graphql/project/project_members_spec.rb b/spec/requests/api/graphql/project/project_members_spec.rb index 4225c3ad3e8..97a79ab3b0e 100644 --- a/spec/requests/api/graphql/project/project_members_spec.rb +++ b/spec/requests/api/graphql/project/project_members_spec.rb @@ -48,24 +48,6 @@ RSpec.describe 'getting project members information' do expect_array_response(user_2) end - - context 'when the use_keyset_aware_user_search_query FF is off' do - before do - stub_feature_flags(use_keyset_aware_user_search_query: false) - end - - it 'raises error on the 2nd page due to missing cursor data' do - fetch_members(project: parent_project, args: { search: 'Same Name', first: 1 }) - - # user_2 because the "old" order was undeterministic (insert order), no tie-breaker column - expect_array_response(user_2) - - next_cursor = graphql_data_at(:project, :projectMembers, :pageInfo, :endCursor) - fetch_members(project: parent_project, args: { search: 'Same Name', first: 1, after: next_cursor }) - - expect(graphql_errors.first['message']).to include('PG::UndefinedColumn') - end - end end end end diff --git a/spec/requests/api/graphql/project/work_items_spec.rb b/spec/requests/api/graphql/project/work_items_spec.rb index 66742fcbeb6..6ef28392b8b 100644 --- a/spec/requests/api/graphql/project/work_items_spec.rb +++ b/spec/requests/api/graphql/project/work_items_spec.rb @@ -21,7 +21,7 @@ RSpec.describe 'getting an work item list for a project' do <<~QUERY edges { node { - #{all_graphql_fields_for('workItems'.classify)} + #{all_graphql_fields_for('workItems'.classify, max_depth: 2)} } } QUERY diff --git a/spec/requests/api/graphql/project_query_spec.rb b/spec/requests/api/graphql/project_query_spec.rb index 310a8e9fa33..d1b990629a1 100644 --- a/spec/requests/api/graphql/project_query_spec.rb +++ b/spec/requests/api/graphql/project_query_spec.rb @@ -190,4 +190,100 @@ RSpec.describe 'getting project information' do end end end + + context 'with timelog categories' do + let_it_be(:timelog_category) do + create(:timelog_category, namespace: project.project_namespace, name: 'TimelogCategoryTest') + end + + let(:project_fields) do + <<~GQL + timelogCategories { + nodes { + #{all_graphql_fields_for('TimeTrackingTimelogCategory')} + } + } + GQL + end + + context 'when user is guest and the project is public' do + before do + project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC) + end + + it 'includes empty timelog categories array' do + post_graphql(query, current_user: current_user) + + expect(graphql_data_at(:project, :timelogCategories, :nodes)).to match([]) + end + end + + context 'when user has reporter role' do + before do + project.add_reporter(current_user) + end + + it 'returns the timelog category with all its fields' do + post_graphql(query, current_user: current_user) + + expect(graphql_data_at(:project, :timelogCategories, :nodes)) + .to contain_exactly(a_graphql_entity_for(timelog_category)) + end + + context 'when timelog_categories flag is disabled' do + before do + stub_feature_flags(timelog_categories: false) + end + + it 'returns no timelog categories' do + post_graphql(query, current_user: current_user) + + expect(graphql_data_at(:project, :timelogCategories)).to be_nil + end + end + end + + context 'for N+1 queries' do + let!(:project1) { create(:project) } + let!(:project2) { create(:project) } + + before do + project1.add_reporter(current_user) + project2.add_reporter(current_user) + end + + it 'avoids N+1 database queries' do + pending('See: https://gitlab.com/gitlab-org/gitlab/-/issues/369396') + + ctx = { current_user: current_user } + + baseline_query = <<~GQL + query { + a: project(fullPath: "#{project1.full_path}") { ... p } + } + + fragment p on Project { + timelogCategories { nodes { name } } + } + GQL + + query = <<~GQL + query { + a: project(fullPath: "#{project1.full_path}") { ... p } + b: project(fullPath: "#{project2.full_path}") { ... p } + } + + fragment p on Project { + timelogCategories { nodes { name } } + } + GQL + + control = ActiveRecord::QueryRecorder.new do + run_with_clean_state(baseline_query, context: ctx) + end + + expect { run_with_clean_state(query, context: ctx) }.not_to exceed_query_limit(control) + end + end + end end diff --git a/spec/requests/api/graphql/work_item_spec.rb b/spec/requests/api/graphql/work_item_spec.rb index f17d2ebbb7e..34644e5893a 100644 --- a/spec/requests/api/graphql/work_item_spec.rb +++ b/spec/requests/api/graphql/work_item_spec.rb @@ -8,7 +8,16 @@ RSpec.describe 'Query.work_item(id)' do let_it_be(:developer) { create(:user) } let_it_be(:guest) { create(:user) } let_it_be(:project) { create(:project, :private) } - let_it_be(:work_item) { create(:work_item, project: project, description: '- List item', weight: 1) } + let_it_be(:work_item) do + create( + :work_item, + project: project, + description: '- List item', + start_date: Date.today, + due_date: 1.week.from_now + ) + end + let_it_be(:child_item1) { create(:work_item, :task, project: project) } let_it_be(:child_item2) { create(:work_item, :task, confidential: true, project: project) } let_it_be(:child_link1) { create(:parent_link, work_item_parent: work_item, work_item: child_item1) } @@ -16,7 +25,7 @@ RSpec.describe 'Query.work_item(id)' do let(:current_user) { developer } let(:work_item_data) { graphql_data['workItem'] } - let(:work_item_fields) { all_graphql_fields_for('WorkItem') } + let(:work_item_fields) { all_graphql_fields_for('WorkItem', max_depth: 2) } let(:global_id) { work_item.to_gid.to_s } let(:query) do @@ -41,8 +50,10 @@ RSpec.describe 'Query.work_item(id)' do 'lockVersion' => work_item.lock_version, 'state' => "OPEN", 'title' => work_item.title, + 'confidential' => work_item.confidential, 'workItemType' => hash_including('id' => work_item.work_item_type.to_gid.to_s), - 'userPermissions' => { 'readWorkItem' => true, 'updateWorkItem' => true, 'deleteWorkItem' => false } + 'userPermissions' => { 'readWorkItem' => true, 'updateWorkItem' => true, 'deleteWorkItem' => false }, + 'project' => hash_including('id' => project.to_gid.to_s, 'fullPath' => project.full_path) ) end @@ -163,14 +174,24 @@ RSpec.describe 'Query.work_item(id)' do end end - describe 'weight widget' do + describe 'assignees widget' do + let(:assignees) { create_list(:user, 2) } + let(:work_item) { create(:work_item, project: project, assignees: assignees) } + let(:work_item_fields) do <<~GRAPHQL id widgets { type - ... on WorkItemWidgetWeight { - weight + ... on WorkItemWidgetAssignees { + allowsMultipleAssignees + canInviteMembers + assignees { + nodes { + id + username + } + } } } GRAPHQL @@ -181,30 +202,34 @@ RSpec.describe 'Query.work_item(id)' do 'id' => work_item.to_gid.to_s, 'widgets' => include( hash_including( - 'type' => 'WEIGHT', - 'weight' => work_item.weight + 'type' => 'ASSIGNEES', + 'allowsMultipleAssignees' => boolean, + 'canInviteMembers' => boolean, + 'assignees' => { + 'nodes' => match_array( + assignees.map { |a| { 'id' => a.to_gid.to_s, 'username' => a.username } } + ) + } ) ) ) end end - describe 'assignees widget' do - let(:assignees) { create_list(:user, 2) } - let(:work_item) { create(:work_item, project: project, assignees: assignees) } + describe 'labels widget' do + let(:labels) { create_list(:label, 2, project: project) } + let(:work_item) { create(:work_item, project: project, labels: labels) } let(:work_item_fields) do <<~GRAPHQL id widgets { type - ... on WorkItemWidgetAssignees { - allowsMultipleAssignees - canInviteMembers - assignees { + ... on WorkItemWidgetLabels { + labels { nodes { id - username + title } } } @@ -217,12 +242,10 @@ RSpec.describe 'Query.work_item(id)' do 'id' => work_item.to_gid.to_s, 'widgets' => include( hash_including( - 'type' => 'ASSIGNEES', - 'allowsMultipleAssignees' => boolean, - 'canInviteMembers' => boolean, - 'assignees' => { + 'type' => 'LABELS', + 'labels' => { 'nodes' => match_array( - assignees.map { |a| { 'id' => a.to_gid.to_s, 'username' => a.username } } + labels.map { |a| { 'id' => a.to_gid.to_s, 'title' => a.title } } ) } ) @@ -230,6 +253,34 @@ RSpec.describe 'Query.work_item(id)' do ) end end + + describe 'start and due date widget' do + let(:work_item_fields) do + <<~GRAPHQL + id + widgets { + type + ... on WorkItemWidgetStartAndDueDate { + startDate + dueDate + } + } + GRAPHQL + end + + it 'returns widget information' do + expect(work_item_data).to include( + 'id' => work_item.to_gid.to_s, + 'widgets' => include( + hash_including( + 'type' => 'START_AND_DUE_DATE', + 'startDate' => work_item.start_date.to_s, + 'dueDate' => work_item.due_date.to_s + ) + ) + ) + end + end end context 'when an Issue Global ID is provided' do |