diff options
Diffstat (limited to 'spec/requests/api/graphql')
35 files changed, 1155 insertions, 102 deletions
diff --git a/spec/requests/api/graphql/boards/board_lists_query_spec.rb b/spec/requests/api/graphql/boards/board_lists_query_spec.rb index eb206465bce..39ff108a9e1 100644 --- a/spec/requests/api/graphql/boards/board_lists_query_spec.rb +++ b/spec/requests/api/graphql/boards/board_lists_query_spec.rb @@ -96,7 +96,8 @@ RSpec.describe 'get board lists' do context 'when ascending' do it_behaves_like 'sorted paginated query' do - let(:sort_param) { } + include_context 'no sort argument' + let(:first_param) { 2 } let(:all_records) { lists.map { |list| a_graphql_entity_for(list) } } end diff --git a/spec/requests/api/graphql/ci/group_variables_spec.rb b/spec/requests/api/graphql/ci/group_variables_spec.rb new file mode 100644 index 00000000000..5ea6646ec2c --- /dev/null +++ b/spec/requests/api/graphql/ci/group_variables_spec.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Query.group(fullPath).ciVariables' do + include GraphqlHelpers + + let_it_be(:group) { create(:group) } + let_it_be(:user) { create(:user) } + + let(:query) do + %( + query { + group(fullPath: "#{group.full_path}") { + ciVariables { + nodes { + id + key + value + variableType + protected + masked + raw + environmentScope + } + } + } + } + ) + end + + context 'when the user can administer the group' do + before do + group.add_owner(user) + end + + it "returns the group's CI variables" do + variable = create(:ci_group_variable, group: group, key: 'TEST_VAR', value: 'test', + masked: false, protected: true, raw: true, environment_scope: 'staging') + + post_graphql(query, current_user: user) + + expect(graphql_data.dig('group', 'ciVariables', 'nodes')).to contain_exactly({ + 'id' => variable.to_global_id.to_s, + 'key' => 'TEST_VAR', + 'value' => 'test', + 'variableType' => 'ENV_VAR', + 'masked' => false, + 'protected' => true, + 'raw' => true, + 'environmentScope' => 'staging' + }) + end + end + + context 'when the user cannot administer the group' do + it 'returns nothing' do + create(:ci_group_variable, group: group, value: 'verysecret', masked: true) + + group.add_developer(user) + + post_graphql(query, current_user: user) + + expect(graphql_data.dig('group', 'ciVariables')).to be_nil + end + end +end diff --git a/spec/requests/api/graphql/ci/instance_variables_spec.rb b/spec/requests/api/graphql/ci/instance_variables_spec.rb new file mode 100644 index 00000000000..7acf73a4e7a --- /dev/null +++ b/spec/requests/api/graphql/ci/instance_variables_spec.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Query.ciVariables' do + include GraphqlHelpers + + let(:query) do + %( + query { + ciVariables { + nodes { + id + key + value + variableType + protected + masked + raw + environmentScope + } + } + } + ) + end + + context 'when the user is an admin' do + let_it_be(:user) { create(:admin) } + + it "returns the instance's CI variables" do + variable = create(:ci_instance_variable, key: 'TEST_VAR', value: 'test', + masked: false, protected: true, raw: true) + + post_graphql(query, current_user: user) + + expect(graphql_data.dig('ciVariables', 'nodes')).to contain_exactly({ + 'id' => variable.to_global_id.to_s, + 'key' => 'TEST_VAR', + 'value' => 'test', + 'variableType' => 'ENV_VAR', + 'masked' => false, + 'protected' => true, + 'raw' => true, + 'environmentScope' => nil + }) + end + end + + context 'when the user is not an admin' do + let_it_be(:user) { create(:user) } + + 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/job_spec.rb b/spec/requests/api/graphql/ci/job_spec.rb index 2fb90dcd92b..3721155c71b 100644 --- a/spec/requests/api/graphql/ci/job_spec.rb +++ b/spec/requests/api/graphql/ci/job_spec.rb @@ -13,8 +13,8 @@ RSpec.describe 'Query.project(fullPath).pipelines.job(id)' do let_it_be(:project) { create(:project, :repository, :public) } let_it_be(:pipeline) { create(:ci_pipeline, project: project) } - let_it_be(:prepare_stage) { create(:ci_stage_entity, pipeline: pipeline, project: project, name: 'prepare') } - let_it_be(:test_stage) { create(:ci_stage_entity, pipeline: pipeline, project: project, name: 'test') } + let_it_be(:prepare_stage) { create(:ci_stage, pipeline: pipeline, project: project, name: 'prepare') } + let_it_be(:test_stage) { create(:ci_stage, pipeline: pipeline, project: project, name: 'test') } let_it_be(:job_1) { create(:ci_build, pipeline: pipeline, stage: 'prepare', name: 'Job 1') } let_it_be(:job_2) { create(:ci_build, pipeline: pipeline, stage: 'test', name: 'Job 2') } diff --git a/spec/requests/api/graphql/ci/jobs_spec.rb b/spec/requests/api/graphql/ci/jobs_spec.rb index d1737fc22ae..8c4ab13fc35 100644 --- a/spec/requests/api/graphql/ci/jobs_spec.rb +++ b/spec/requests/api/graphql/ci/jobs_spec.rb @@ -14,7 +14,7 @@ RSpec.describe 'Query.project.pipeline' do describe '.stages.groups.jobs' do let(:pipeline) do pipeline = create(:ci_pipeline, project: project, user: user) - stage = create(:ci_stage_entity, project: project, pipeline: pipeline, name: 'first', position: 1) + stage = create(:ci_stage, project: project, pipeline: pipeline, name: 'first', position: 1) create(:ci_build, stage_id: stage.id, pipeline: pipeline, name: 'my test job', scheduling_type: :stage) pipeline @@ -84,8 +84,8 @@ RSpec.describe 'Query.project.pipeline' do context 'when there is more than one stage and job needs' do before do - build_stage = create(:ci_stage_entity, position: 2, name: 'build', project: project, pipeline: pipeline) - test_stage = create(:ci_stage_entity, position: 3, name: 'test', project: project, pipeline: pipeline) + build_stage = create(:ci_stage, position: 2, name: 'build', project: project, pipeline: pipeline) + test_stage = create(:ci_stage, position: 3, name: 'test', project: project, pipeline: pipeline) create(:ci_build, pipeline: pipeline, name: 'docker 1 2', scheduling_type: :stage, stage: build_stage, stage_idx: build_stage.position) create(:ci_build, pipeline: pipeline, name: 'docker 2 2', stage: build_stage, stage_idx: build_stage.position, scheduling_type: :dag) diff --git a/spec/requests/api/graphql/ci/manual_variables_spec.rb b/spec/requests/api/graphql/ci/manual_variables_spec.rb new file mode 100644 index 00000000000..b7aa76511a3 --- /dev/null +++ b/spec/requests/api/graphql/ci/manual_variables_spec.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Query.project(fullPath).pipelines.jobs.manualVariables' do + include GraphqlHelpers + + let_it_be(:project) { create(:project) } + let_it_be(:pipeline) { create(:ci_pipeline, project: project) } + let_it_be(:user) { create(:user) } + + let(:query) do + %( + query { + project(fullPath: "#{project.full_path}") { + pipelines { + nodes { + jobs { + nodes { + manualVariables { + nodes { + key + } + } + } + } + } + } + } + } + ) + end + + before do + project.add_maintainer(user) + end + + it 'returns the manual variables for the jobs' do + job = create(:ci_build, :manual, pipeline: pipeline) + create(:ci_job_variable, key: 'MANUAL_TEST_VAR', job: job) + + post_graphql(query, current_user: user) + + variables_data = graphql_data.dig('project', 'pipelines', 'nodes').first + .dig('jobs', 'nodes').flat_map { |job| job.dig('manualVariables', 'nodes') } + 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) + create(:ci_job_variable, key: 'THIS_VAR_WOULD_SHOULD_NEVER_EXIST', job: job) + + post_graphql(query, current_user: user) + + variables_data = graphql_data.dig('project', 'pipelines', 'nodes').first + .dig('jobs', 'nodes').flat_map { |job| job.dig('manualVariables', 'nodes') } + expect(variables_data).to be_empty + end + + it 'does not fetch job variables for bridges' do + create(:ci_bridge, :manual, pipeline: pipeline) + + post_graphql(query, current_user: user) + + variables_data = graphql_data.dig('project', 'pipelines', 'nodes').first + .dig('jobs', 'nodes').flat_map { |job| job.dig('manualVariables', 'nodes') } + expect(variables_data).to be_empty + end + + it 'does not produce N+1 queries', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/367991' do + second_user = create(:user) + project.add_maintainer(second_user) + job = create(:ci_build, :manual, pipeline: pipeline) + create(:ci_job_variable, key: 'MANUAL_TEST_VAR_1', job: job) + + control_count = ActiveRecord::QueryRecorder.new do + post_graphql(query, current_user: user) + end + + variables_data = graphql_data.dig('project', 'pipelines', 'nodes').first + .dig('jobs', 'nodes').flat_map { |job| job.dig('manualVariables', 'nodes') } + expect(variables_data.map { |var| var['key'] }).to match_array(['MANUAL_TEST_VAR_1']) + + job = create(:ci_build, :manual, pipeline: pipeline) + create(:ci_job_variable, key: 'MANUAL_TEST_VAR_2', job: job) + + expect do + post_graphql(query, current_user: second_user) + end.not_to exceed_query_limit(control_count) + + variables_data = graphql_data.dig('project', 'pipelines', 'nodes').first + .dig('jobs', 'nodes').flat_map { |job| job.dig('manualVariables', 'nodes') } + expect(variables_data.map { |var| var['key'] }).to match_array(%w(MANUAL_TEST_VAR_1 MANUAL_TEST_VAR_2)) + end +end diff --git a/spec/requests/api/graphql/ci/pipelines_spec.rb b/spec/requests/api/graphql/ci/pipelines_spec.rb index 741af676b6d..a968e5508cb 100644 --- a/spec/requests/api/graphql/ci/pipelines_spec.rb +++ b/spec/requests/api/graphql/ci/pipelines_spec.rb @@ -86,8 +86,8 @@ RSpec.describe 'Query.project(fullPath).pipelines' do describe '.stages' do let_it_be(:project) { create(:project, :repository) } let_it_be(:pipeline) { create(:ci_empty_pipeline, project: project) } - let_it_be(:stage) { create(:ci_stage_entity, pipeline: pipeline, project: project) } - let_it_be(:other_stage) { create(:ci_stage_entity, pipeline: pipeline, project: project, name: 'other') } + let_it_be(:stage) { create(:ci_stage, pipeline: pipeline, project: project) } + let_it_be(:other_stage) { create(:ci_stage, pipeline: pipeline, project: project, name: 'other') } let(:first_n) { var('Int') } let(:query_path) do diff --git a/spec/requests/api/graphql/ci/project_variables_spec.rb b/spec/requests/api/graphql/ci/project_variables_spec.rb new file mode 100644 index 00000000000..e61f146b24c --- /dev/null +++ b/spec/requests/api/graphql/ci/project_variables_spec.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Query.project(fullPath).ciVariables' do + include GraphqlHelpers + + let_it_be(:project) { create(:project) } + let_it_be(:user) { create(:user) } + + let(:query) do + %( + query { + project(fullPath: "#{project.full_path}") { + ciVariables { + nodes { + id + key + value + variableType + protected + masked + raw + environmentScope + } + } + } + } + ) + end + + context 'when the user can administer builds' do + before do + project.add_maintainer(user) + end + + it "returns the project's CI variables" do + variable = create(:ci_variable, project: project, key: 'TEST_VAR', value: 'test', + masked: false, protected: true, raw: true, environment_scope: 'production') + + post_graphql(query, current_user: user) + + expect(graphql_data.dig('project', 'ciVariables', 'nodes')).to contain_exactly({ + 'id' => variable.to_global_id.to_s, + 'key' => 'TEST_VAR', + 'value' => 'test', + 'variableType' => 'ENV_VAR', + 'masked' => false, + 'protected' => true, + 'raw' => true, + 'environmentScope' => 'production' + }) + end + end + + context 'when the user cannot administer builds' do + it 'returns nothing' do + create(:ci_variable, project: project, value: 'verysecret', masked: true) + + project.add_developer(user) + + post_graphql(query, current_user: user) + + expect(graphql_data.dig('project', 'ciVariables')).to be_nil + end + end +end diff --git a/spec/requests/api/graphql/ci/runner_spec.rb b/spec/requests/api/graphql/ci/runner_spec.rb index 446d1fb1bdb..e17a83d8e47 100644 --- a/spec/requests/api/graphql/ci/runner_spec.rb +++ b/spec/requests/api/graphql/ci/runner_spec.rb @@ -424,7 +424,7 @@ RSpec.describe 'Query.runner(id)' do let(:user) { create(:user) } before do - group.add_user(user, Gitlab::Access::OWNER) + group.add_member(user, Gitlab::Access::OWNER) end it_behaves_like 'retrieval with no admin url' do diff --git a/spec/requests/api/graphql/ci/stages_spec.rb b/spec/requests/api/graphql/ci/stages_spec.rb index 50d2cf75097..1edd6e58486 100644 --- a/spec/requests/api/graphql/ci/stages_spec.rb +++ b/spec/requests/api/graphql/ci/stages_spec.rb @@ -36,7 +36,7 @@ RSpec.describe 'Query.project.pipeline.stages' do end before_all do - create(:ci_stage_entity, pipeline: pipeline, name: 'deploy') + create(:ci_stage, pipeline: pipeline, name: 'deploy') create_list(:ci_build, 2, pipeline: pipeline, stage: 'deploy') end diff --git a/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb b/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb index 847fa72522e..14c55e61a65 100644 --- a/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb +++ b/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb @@ -71,7 +71,7 @@ RSpec.describe 'container repository details' do with_them do before do project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility.to_s.upcase, false)) - project.add_user(user, role) unless role == :anonymous + project.add_member(user, role) unless role == :anonymous end it 'return the proper response' do diff --git a/spec/requests/api/graphql/crm/contacts_spec.rb b/spec/requests/api/graphql/crm/contacts_spec.rb new file mode 100644 index 00000000000..7e824140894 --- /dev/null +++ b/spec/requests/api/graphql/crm/contacts_spec.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'getting CRM contacts' do + include GraphqlHelpers + + let_it_be(:current_user) { create(:user) } + let_it_be(:group) { create(:group, :crm_enabled) } + + let_it_be(:contact_a) do + create( + :contact, + group: group, + first_name: "ABC", + last_name: "DEF", + email: "ghi@test.com", + description: "LMNO", + state: "inactive" + ) + end + + let_it_be(:contact_b) do + create( + :contact, + group: group, + first_name: "ABC", + last_name: "DEF", + email: "vwx@test.com", + description: "YZ", + state: "active" + ) + end + + let_it_be(:contact_c) do + create( + :contact, + group: group, + first_name: "PQR", + last_name: "STU", + email: "aaa@test.com", + description: "YZ", + state: "active" + ) + end + + before do + group.add_reporter(current_user) + end + + it_behaves_like 'sorted paginated query' do + let(:sort_argument) { {} } + let(:first_param) { 2 } + let(:all_records) { [contact_a, contact_b, contact_c] } + let(:data_path) { [:group, :contacts] } + + def pagination_query(params) + graphql_query_for( + :group, + { full_path: group.full_path }, + query_graphql_field(:contacts, params, "#{page_info} nodes { id }") + ) + end + + def pagination_results_data(nodes) + nodes.map { |item| GlobalID::Locator.locate(item['id']) } + end + end +end 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 39f323b21a3..ef0f32bacf0 100644 --- a/spec/requests/api/graphql/current_user/groups_query_spec.rb +++ b/spec/requests/api/graphql/current_user/groups_query_spec.rb @@ -8,8 +8,9 @@ RSpec.describe 'Query current user groups' do let_it_be(:user) { create(:user) } 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(:public_developer_group) { create(:group, :private, project_creation_level: nil, name: 'c public developer', path: 'c-public-developer') } - let_it_be(:public_maintainer_group) { create(:group, :private, name: 'a public maintainer', path: 'a-public-maintainer') } + 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_owner_group) { create(:group, name: 'a public owner', path: 'a-public-owner') } let(:group_arguments) { {} } let(:current_user) { user } @@ -29,6 +30,7 @@ RSpec.describe 'Query current user groups' do private_maintainer_group.add_maintainer(user) public_developer_group.add_developer(user) public_maintainer_group.add_maintainer(user) + public_owner_group.add_owner(user) end subject { graphql_data.dig('currentUser', 'groups', 'nodes') } @@ -52,6 +54,7 @@ RSpec.describe 'Query current user groups' do is_expected.to match( expected_group_hash( public_maintainer_group, + public_owner_group, private_maintainer_group, public_developer_group, guest_group @@ -66,6 +69,7 @@ RSpec.describe 'Query current user groups' do is_expected.to match( expected_group_hash( public_maintainer_group, + public_owner_group, private_maintainer_group, public_developer_group ) @@ -86,6 +90,32 @@ RSpec.describe 'Query current user groups' do end end + context 'when permission_scope is TRANSFER_PROJECTS' do + let(:group_arguments) { { permission_scope: :TRANSFER_PROJECTS } } + + specify do + is_expected.to match( + expected_group_hash( + public_maintainer_group, + public_owner_group, + private_maintainer_group + ) + ) + end + + context 'when search is provided' do + let(:group_arguments) { { permission_scope: :TRANSFER_PROJECTS, search: 'owner' } } + + specify do + is_expected.to match( + expected_group_hash( + public_owner_group + ) + ) + end + end + end + context 'when search is provided' do let(:group_arguments) { { search: 'maintainer' } } diff --git a/spec/requests/api/graphql/group/container_repositories_spec.rb b/spec/requests/api/graphql/group/container_repositories_spec.rb index be0b866af4a..8ec321c8d7c 100644 --- a/spec/requests/api/graphql/group/container_repositories_spec.rb +++ b/spec/requests/api/graphql/group/container_repositories_spec.rb @@ -82,7 +82,7 @@ RSpec.describe 'getting container repositories in a group' do group.update!(visibility_level: Gitlab::VisibilityLevel.const_get(group_visibility.to_s.upcase, false)) project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(group_visibility.to_s.upcase, false)) - group.add_user(user, role) unless role == :anonymous + group.add_member(user, role) unless role == :anonymous end it 'return the proper response' do diff --git a/spec/requests/api/graphql/group/dependency_proxy_blobs_spec.rb b/spec/requests/api/graphql/group/dependency_proxy_blobs_spec.rb index cdb21512894..daa1483e956 100644 --- a/spec/requests/api/graphql/group/dependency_proxy_blobs_spec.rb +++ b/spec/requests/api/graphql/group/dependency_proxy_blobs_spec.rb @@ -75,7 +75,7 @@ RSpec.describe 'getting dependency proxy blobs in a group' do with_them do before do group.update_column(:visibility_level, Gitlab::VisibilityLevel.const_get(group_visibility.to_s.upcase, false)) - group.add_user(user, role) unless role == :anonymous + group.add_member(user, role) unless role == :anonymous end it 'return the proper response' do diff --git a/spec/requests/api/graphql/group/dependency_proxy_group_setting_spec.rb b/spec/requests/api/graphql/group/dependency_proxy_group_setting_spec.rb index d21c3046c1a..cc706c3051f 100644 --- a/spec/requests/api/graphql/group/dependency_proxy_group_setting_spec.rb +++ b/spec/requests/api/graphql/group/dependency_proxy_group_setting_spec.rb @@ -61,7 +61,7 @@ RSpec.describe 'getting dependency proxy settings for a group' do with_them do before do group.update_column(:visibility_level, Gitlab::VisibilityLevel.const_get(group_visibility.to_s.upcase, false)) - group.add_user(user, role) unless role == :anonymous + group.add_member(user, role) unless role == :anonymous end it 'return the proper response' do diff --git a/spec/requests/api/graphql/group/dependency_proxy_image_ttl_policy_spec.rb b/spec/requests/api/graphql/group/dependency_proxy_image_ttl_policy_spec.rb index 40f4b082072..3b2b04b1322 100644 --- a/spec/requests/api/graphql/group/dependency_proxy_image_ttl_policy_spec.rb +++ b/spec/requests/api/graphql/group/dependency_proxy_image_ttl_policy_spec.rb @@ -60,7 +60,7 @@ RSpec.describe 'getting dependency proxy image ttl policy for a group' do with_them do before do group.update_column(:visibility_level, Gitlab::VisibilityLevel.const_get(group_visibility.to_s.upcase, false)) - group.add_user(user, role) unless role == :anonymous + group.add_member(user, role) unless role == :anonymous end it 'return the proper response' do diff --git a/spec/requests/api/graphql/group/dependency_proxy_manifests_spec.rb b/spec/requests/api/graphql/group/dependency_proxy_manifests_spec.rb index c7149c100b2..37ef7089c2f 100644 --- a/spec/requests/api/graphql/group/dependency_proxy_manifests_spec.rb +++ b/spec/requests/api/graphql/group/dependency_proxy_manifests_spec.rb @@ -73,7 +73,7 @@ RSpec.describe 'getting dependency proxy manifests in a group' do with_them do before do group.update_column(:visibility_level, Gitlab::VisibilityLevel.const_get(group_visibility.to_s.upcase, false)) - group.add_user(user, role) unless role == :anonymous + group.add_member(user, role) unless role == :anonymous end it 'return the proper response' do @@ -125,7 +125,8 @@ RSpec.describe 'getting dependency proxy manifests in a group' do let_it_be(:descending_manifests) { manifests.reverse.map { |manifest| global_id_of(manifest) } } it_behaves_like 'sorted paginated query' do - let(:sort_param) { '' } + include_context 'no sort argument' + let(:first_param) { 2 } let(:all_records) { descending_manifests.map(&:to_s) } end @@ -134,7 +135,7 @@ RSpec.describe 'getting dependency proxy manifests in a group' do def pagination_query(params) # remove sort since the type does not accept sorting, but be future proof graphql_query_for('group', { 'fullPath' => group.full_path }, - query_nodes(:dependencyProxyManifests, :id, include_pagination_info: true, args: params.merge(sort: nil)) + query_nodes(:dependencyProxyManifests, :id, include_pagination_info: true, args: params) ) end end diff --git a/spec/requests/api/graphql/group/group_members_spec.rb b/spec/requests/api/graphql/group/group_members_spec.rb index fec866486ae..1ff5b134e92 100644 --- a/spec/requests/api/graphql/group/group_members_spec.rb +++ b/spec/requests/api/graphql/group/group_members_spec.rb @@ -7,8 +7,8 @@ RSpec.describe 'getting group members information' do let_it_be(:parent_group) { create(:group, :public) } let_it_be(:user) { create(:user) } - let_it_be(:user_1) { create(:user, username: 'user') } - let_it_be(:user_2) { create(:user, username: 'test') } + let_it_be(:user_1) { create(:user, username: 'user', name: 'Same Name') } + let_it_be(:user_2) { create(:user, username: 'test', name: 'Same Name') } before_all do [user_1, user_2].each { |user| parent_group.add_guest(user) } @@ -45,11 +45,44 @@ RSpec.describe 'getting group members information' do expect_array_response(user_1, user_2) end - it 'returns members that match the search query' do - fetch_members(args: { search: 'test' }) + describe 'search argument' do + it 'returns members that match the search query' do + fetch_members(args: { search: 'test' }) - expect(graphql_errors).to be_nil - expect_array_response(user_2) + expect(graphql_errors).to be_nil + expect_array_response(user_2) + end + + context 'when paginating' do + it 'returns correct results' do + fetch_members(args: { search: 'Same Name', first: 1 }) + + expect_array_response(user_1) + + next_cursor = graphql_data_at(:group, :groupMembers, :pageInfo, :endCursor) + fetch_members(args: { search: 'Same Name', first: 1, after: next_cursor }) + + 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 @@ -196,6 +229,9 @@ RSpec.describe 'getting group members information' do } } } + pageInfo { + endCursor + } NODE graphql_query_for("group", diff --git a/spec/requests/api/graphql/mutations/issues/create_spec.rb b/spec/requests/api/graphql/mutations/issues/create_spec.rb index 3d81b456c9c..9345735afe4 100644 --- a/spec/requests/api/graphql/mutations/issues/create_spec.rb +++ b/spec/requests/api/graphql/mutations/issues/create_spec.rb @@ -53,6 +53,42 @@ RSpec.describe 'Create an issue' do let(:mutation_class) { ::Mutations::Issues::Create } end + context 'when creating an issue of type TASK' do + before do + input['type'] = 'TASK' + end + + context 'when work_items feature flag is disabled' do + before do + stub_feature_flags(work_items: false) + end + + it 'creates an issue with the default ISSUE type' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + end.to change(Issue, :count).by(1) + + created_issue = Issue.last + + expect(created_issue.work_item_type.base_type).to eq('issue') + expect(created_issue.issue_type).to eq('issue') + end + end + + context 'when work_items feature flag is enabled' do + it 'creates an issue with TASK type' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + end.to change(Issue, :count).by(1) + + created_issue = Issue.last + + expect(created_issue.work_item_type.base_type).to eq('task') + expect(created_issue.issue_type).to eq('task') + end + end + end + context 'when position params are provided' do let(:existing_issue) { create(:issue, project: project, relative_position: 50) } diff --git a/spec/requests/api/graphql/mutations/notes/create/diff_note_spec.rb b/spec/requests/api/graphql/mutations/notes/create/diff_note_spec.rb index 8f3ae9f26f6..a432fb17a70 100644 --- a/spec/requests/api/graphql/mutations/notes/create/diff_note_spec.rb +++ b/spec/requests/api/graphql/mutations/notes/create/diff_note_spec.rb @@ -10,11 +10,12 @@ RSpec.describe 'Adding a DiffNote' do let(:noteable) { create(:merge_request, source_project: project, target_project: project) } let(:project) { create(:project, :repository) } let(:diff_refs) { noteable.diff_refs } + let(:body) { 'Body text' } let(:base_variables) do { noteable_id: GitlabSchema.id_from_object(noteable).to_s, - body: 'Body text', + body: body, position: { paths: { old_path: 'files/ruby/popen.rb', @@ -65,6 +66,17 @@ RSpec.describe 'Adding a DiffNote' do it_behaves_like 'a Note mutation when the given resource id is not for a Noteable' end + context 'with /merge quick action' do + let(:body) { "Body text \n/merge" } + + it 'merges the merge request', :sidekiq_inline do + post_graphql_mutation(mutation, current_user: current_user) + + expect(noteable.reload.state).to eq('merged') + expect(mutation_response['note']['body']).to eq('Body text') + end + end + it 'returns the note with the correct position' do post_graphql_mutation(mutation, current_user: current_user) diff --git a/spec/requests/api/graphql/mutations/snippets/update_spec.rb b/spec/requests/api/graphql/mutations/snippets/update_spec.rb index eb7e6f840fe..1a5d3620f22 100644 --- a/spec/requests/api/graphql/mutations/snippets/update_spec.rb +++ b/spec/requests/api/graphql/mutations/snippets/update_spec.rb @@ -4,6 +4,7 @@ require 'spec_helper' RSpec.describe 'Updating a Snippet' do include GraphqlHelpers + include SessionHelpers let_it_be(:original_content) { 'Initial content' } let_it_be(:original_description) { 'Initial description' } @@ -162,7 +163,7 @@ RSpec.describe 'Updating a Snippet' do end end - context 'when the author is a member of the project' do + context 'when the author is a member of the project', :snowplow do before do project.add_developer(current_user) end @@ -185,6 +186,20 @@ RSpec.describe 'Updating a Snippet' do it_behaves_like 'has spam protection' do let(:mutation_class) { ::Mutations::Snippets::Update } end + + context 'when not sessionless', :clean_gitlab_redis_sessions do + before do + stub_session('warden.user.user.key' => [[current_user.id], current_user.authenticatable_salt]) + end + + it_behaves_like 'Snowplow event tracking' do + let(:user) { current_user } + let(:namespace) { project.namespace } + let(:category) { 'ide_edit' } + let(:action) { 'g_edit_by_snippet_ide' } + let(:feature_flag_name) { :route_hll_to_snowplow_phase2 } + end + end end it_behaves_like 'when the snippet is not found' 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 8d33f8e1806..b1356bbe6fd 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 @@ -47,6 +47,7 @@ RSpec.describe "Create a work item from a task in a work item's description" do expect(work_item.description).to eq("- [ ] #{created_work_item.to_reference}+") 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(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 6abdaa2c850..911568bc39f 100644 --- a/spec/requests/api/graphql/mutations/work_items/create_spec.rb +++ b/spec/requests/api/graphql/mutations/work_items/create_spec.rb @@ -63,6 +63,95 @@ RSpec.describe 'Create a work item' do let(:mutation_class) { ::Mutations::WorkItems::Create } end + context 'with hierarchy widget input' do + let(:widgets_response) { mutation_response['workItem']['widgets'] } + let(:fields) do + <<~FIELDS + workItem { + widgets { + type + ... on WorkItemWidgetHierarchy { + parent { + id + } + children { + edges { + node { + id + } + } + } + } + } + } + errors + FIELDS + end + + let(:mutation) { graphql_mutation(:workItemCreate, input.merge('projectPath' => project.full_path), fields) } + + context 'when setting parent' do + let_it_be(:parent) { create(:work_item, project: project) } + + let(:input) do + { + title: 'item1', + workItemTypeId: WorkItems::Type.default_by_type(:task).to_global_id.to_s, + hierarchyWidget: { 'parentId' => parent.to_global_id.to_s } + } + end + + it 'updates the work item parent' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(response).to have_gitlab_http_status(:success) + expect(widgets_response).to include( + { + 'children' => { 'edges' => [] }, + 'parent' => { 'id' => parent.to_global_id.to_s }, + 'type' => 'HIERARCHY' + } + ) + end + + context 'when parent work item type is invalid' do + let_it_be(:parent) { create(:work_item, :task, project: project) } + + it 'returns error' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(mutation_response['errors']) + .to contain_exactly(/cannot be added: only Issue and Incident can be parent of Task./) + expect(mutation_response['workItem']).to be_nil + end + end + + context 'when parent work item is not found' do + 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) + + expect(graphql_errors.first['message']).to include('No object found for `parentId') + end + end + end + + context 'when unsupported widget input is sent' do + let(:input) do + { + 'title' => 'new title', + 'description' => 'new description', + 'workItemTypeId' => WorkItems::Type.default_by_type(:test_case).to_global_id.to_s, + 'hierarchyWidget' => {} + } + end + + it_behaves_like 'a mutation that returns top-level errors', + errors: ['Following widget keys are not supported by Test Case type: [:hierarchy_widget]'] + end + end + context 'when the work_items feature flag is disabled' do before do stub_feature_flags(work_items: false) diff --git a/spec/requests/api/graphql/mutations/work_items/delete_task_spec.rb b/spec/requests/api/graphql/mutations/work_items/delete_task_spec.rb index 05d3587d342..e576d0ee7ef 100644 --- a/spec/requests/api/graphql/mutations/work_items/delete_task_spec.rb +++ b/spec/requests/api/graphql/mutations/work_items/delete_task_spec.rb @@ -54,7 +54,7 @@ RSpec.describe "Delete a task in a work item's description" do end.to change(WorkItem, :count).by(-1).and( change(IssueLink, :count).by(-1) ).and( - change(work_item, :description).from("- [ ] #{task.to_reference}+").to('') + change(work_item, :description).from("- [ ] #{task.to_reference}+").to("- [ ] #{task.title}") ) expect(response).to have_gitlab_http_status(:success) 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 71b03103115..77f7b9bacef 100644 --- a/spec/requests/api/graphql/mutations/work_items/update_spec.rb +++ b/spec/requests/api/graphql/mutations/work_items/update_spec.rb @@ -11,8 +11,17 @@ RSpec.describe 'Update a work item' do let(:work_item_event) { 'CLOSE' } let(:input) { { 'stateEvent' => work_item_event, 'title' => 'updated title' } } + let(:fields) do + <<~FIELDS + workItem { + state + title + } + errors + FIELDS + end - let(:mutation) { graphql_mutation(:workItemUpdate, input.merge('id' => work_item.to_global_id.to_s)) } + let(:mutation) { graphql_mutation(:workItemUpdate, input.merge('id' => work_item.to_global_id.to_s), fields) } let(:mutation_response) { graphql_mutation_response(:work_item_update) } @@ -62,6 +71,20 @@ 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' => {} + } + 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 + it_behaves_like 'has spam protection' do let(:mutation_class) { ::Mutations::WorkItems::Update } end @@ -80,5 +103,248 @@ RSpec.describe 'Update a work item' do expect(mutation_response['errors']).to contain_exactly('`work_items` feature flag disabled for this project') end end + + context 'with description widget input' do + let(:fields) do + <<~FIELDS + workItem { + description + widgets { + type + ... on WorkItemWidgetDescription { + description + } + } + } + errors + FIELDS + end + + it_behaves_like 'update work item description widget' do + let(:new_description) { 'updated description' } + let(:input) do + { 'descriptionWidget' => { 'description' => new_description } } + end + end + end + + context 'with weight widget input' do + let(:fields) do + <<~FIELDS + workItem { + widgets { + type + ... on WorkItemWidgetWeight { + weight + } + } + } + errors + FIELDS + end + + it_behaves_like 'update work item weight widget' do + let(:new_weight) { 2 } + + let(:input) do + { 'weightWidget' => { 'weight' => new_weight } } + end + end + end + + context 'with hierarchy widget input' do + let(:widgets_response) { mutation_response['workItem']['widgets'] } + let(:fields) do + <<~FIELDS + workItem { + description + widgets { + type + ... on WorkItemWidgetHierarchy { + parent { + id + } + children { + edges { + node { + id + } + } + } + } + } + } + errors + FIELDS + end + + context 'when updating parent' do + let_it_be(:work_item) { 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) } + + context 'when parent work item type is invalid' do + let(:error) { "#{work_item.to_reference} cannot be added: only Issue and Incident can be parent of Task." } + let(:input) do + { 'hierarchyWidget' => { 'parentId' => invalid_parent.to_global_id.to_s }, 'title' => 'new title' } + end + + it 'returns response with errors' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + work_item.reload + end.to not_change(work_item, :work_item_parent).and(not_change(work_item, :title)) + + expect(mutation_response['workItem']).to be_nil + expect(mutation_response['errors']).to match_array([error]) + end + end + + context 'when parent work item has a valid type' do + let(:input) { { 'hierarchyWidget' => { 'parentId' => valid_parent.to_global_id.to_s } } } + + it 'sets the parent for the work item' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + work_item.reload + end.to change(work_item, :work_item_parent).from(nil).to(valid_parent) + + expect(response).to have_gitlab_http_status(:success) + expect(widgets_response).to include( + { + 'children' => { 'edges' => [] }, + 'parent' => { 'id' => valid_parent.to_global_id.to_s }, + 'type' => 'HIERARCHY' + } + ) + end + + context 'when a parent is already present' do + let_it_be(:existing_parent) { create(:work_item, project: project) } + + before do + work_item.update!(work_item_parent: existing_parent) + end + + it 'is replaced with new parent' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + work_item.reload + end.to change(work_item, :work_item_parent).from(existing_parent).to(valid_parent) + end + end + end + + context 'when parentId is null' do + let(:input) { { 'hierarchyWidget' => { 'parentId' => nil } } } + + context 'when parent is present' do + before do + work_item.update!(work_item_parent: valid_parent) + end + + it 'removes parent and returns success message' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + work_item.reload + end.to change(work_item, :work_item_parent).from(valid_parent).to(nil) + + expect(response).to have_gitlab_http_status(:success) + expect(widgets_response) + .to include( + { + 'children' => { 'edges' => [] }, + 'parent' => nil, + 'type' => 'HIERARCHY' + } + ) + end + end + + context 'when parent is not present' do + before do + work_item.update!(work_item_parent: nil) + end + + it 'does not change work item and returns success message' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + work_item.reload + end.not_to change(work_item, :work_item_parent) + + expect(response).to have_gitlab_http_status(:success) + end + end + end + + context 'when parent work item is not found' do + let(:input) { { 'hierarchyWidget' => { 'parentId' => "gid://gitlab/WorkItem/#{non_existing_record_id}" } } } + + it 'returns a top level error' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(graphql_errors.first['message']).to include('No object found for `parentId') + end + end + end + + context 'when updating children' do + let_it_be(:valid_child1) { create(:work_item, :task, project: project) } + let_it_be(:valid_child2) { create(:work_item, :task, project: project) } + let_it_be(:invalid_child) { create(:work_item, project: project) } + + let(:input) { { 'hierarchyWidget' => { 'childrenIds' => children_ids } } } + let(:error) do + "#{invalid_child.to_reference} cannot be added: only Task can be assigned as a child in hierarchy." + end + + context 'when child work item type is invalid' do + let(:children_ids) { [invalid_child.to_global_id.to_s] } + + it 'returns response with errors' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(mutation_response['workItem']).to be_nil + expect(mutation_response['errors']).to match_array([error]) + end + end + + context 'when there is a mix of existing and non existing work items' do + let(:children_ids) { [valid_child1.to_global_id.to_s, "gid://gitlab/WorkItem/#{non_existing_record_id}"] } + + it 'returns a top level error and does not add valid work item' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + work_item.reload + end.not_to change(work_item.work_item_children, :count) + + expect(graphql_errors.first['message']).to include('No object found for `childrenIds') + end + end + + context 'when child work item type is valid' do + let(:children_ids) { [valid_child1.to_global_id.to_s, valid_child2.to_global_id.to_s] } + + it 'updates the work item children' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + work_item.reload + end.to change(work_item.work_item_children, :count).by(2) + + expect(response).to have_gitlab_http_status(:success) + expect(widgets_response).to include( + { + 'children' => { 'edges' => [ + { 'node' => { 'id' => valid_child2.to_global_id.to_s } }, + { 'node' => { 'id' => valid_child1.to_global_id.to_s } } + ] }, + 'parent' => nil, + 'type' => 'HIERARCHY' + } + ) + end + end + end + end end end diff --git a/spec/requests/api/graphql/mutations/work_items/update_widgets_spec.rb b/spec/requests/api/graphql/mutations/work_items/update_widgets_spec.rb index 595d8fe97ed..2a5cb937a2f 100644 --- a/spec/requests/api/graphql/mutations/work_items/update_widgets_spec.rb +++ b/spec/requests/api/graphql/mutations/work_items/update_widgets_spec.rb @@ -9,16 +9,23 @@ RSpec.describe 'Update work item widgets' do let_it_be(:developer) { create(:user).tap { |user| project.add_developer(user) } } let_it_be(:work_item, refind: true) { create(:work_item, project: project) } - let(:input) do - { - 'descriptionWidget' => { 'description' => 'updated description' } + let(:input) { { 'descriptionWidget' => { 'description' => 'updated description' } } } + let(:mutation_response) { graphql_mutation_response(:work_item_update_widgets) } + let(:mutation) do + graphql_mutation(:workItemUpdateWidgets, input.merge('id' => work_item.to_global_id.to_s), <<~FIELDS) + errors + workItem { + description + widgets { + type + ... on WorkItemWidgetDescription { + description + } + } } + FIELDS end - let(:mutation) { graphql_mutation(:workItemUpdateWidgets, input.merge('id' => work_item.to_global_id.to_s)) } - - let(:mutation_response) { graphql_mutation_response(:work_item_update_widgets) } - context 'the user is not allowed to update a work item' do let(:current_user) { create(:user) } @@ -28,32 +35,8 @@ RSpec.describe 'Update work item widgets' do context 'when user has permissions to update a work item', :aggregate_failures do let(:current_user) { developer } - context 'when the updated work item is not valid' do - it 'returns validation errors without the work item' do - errors = ActiveModel::Errors.new(work_item).tap { |e| e.add(:description, 'error message') } - - allow_next_found_instance_of(::WorkItem) do |instance| - allow(instance).to receive(:valid?).and_return(false) - allow(instance).to receive(:errors).and_return(errors) - end - - post_graphql_mutation(mutation, current_user: current_user) - - expect(mutation_response['workItem']).to be_nil - expect(mutation_response['errors']).to match_array(['Description error message']) - end - end - - it 'updates the work item widgets' do - expect do - post_graphql_mutation(mutation, current_user: current_user) - work_item.reload - end.to change(work_item, :description).from(nil).to('updated description') - - expect(response).to have_gitlab_http_status(:success) - expect(mutation_response['workItem']).to include( - 'title' => work_item.title - ) + it_behaves_like 'update work item description widget' do + let(:new_description) { 'updated description' } end it_behaves_like 'has spam protection' do @@ -69,7 +52,7 @@ RSpec.describe 'Update work item widgets' do expect do post_graphql_mutation(mutation, current_user: current_user) work_item.reload - end.to not_change(work_item, :title) + end.to not_change(work_item, :description) expect(mutation_response['errors']).to contain_exactly('`work_items` feature flag disabled for this project') end diff --git a/spec/requests/api/graphql/project/container_repositories_spec.rb b/spec/requests/api/graphql/project/container_repositories_spec.rb index bbab6012f3f..01b117a89d8 100644 --- a/spec/requests/api/graphql/project/container_repositories_spec.rb +++ b/spec/requests/api/graphql/project/container_repositories_spec.rb @@ -81,7 +81,7 @@ RSpec.describe 'getting container repositories in a project' do with_them do before do project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility.to_s.upcase, false)) - project.add_user(user, role) unless role == :anonymous + project.add_member(user, role) unless role == :anonymous end it 'return the proper response' do diff --git a/spec/requests/api/graphql/project/issues_spec.rb b/spec/requests/api/graphql/project/issues_spec.rb index 69e14eace66..596e023a027 100644 --- a/spec/requests/api/graphql/project/issues_spec.rb +++ b/spec/requests/api/graphql/project/issues_spec.rb @@ -223,6 +223,7 @@ RSpec.describe 'getting an issue list for a project' do end describe 'sorting and pagination' do + let_it_be(:sort_project) { create(:project, :public) } let_it_be(:data_path) { [:project, :issues] } def pagination_query(params) @@ -237,8 +238,38 @@ RSpec.describe 'getting an issue list for a project' do data.map { |issue| issue['iid'].to_i } end + context 'when sorting by severity' do + let_it_be(:severty_issue1) { create(:issue, project: sort_project) } + let_it_be(:severty_issue2) { create(:issue, project: sort_project) } + let_it_be(:severty_issue3) { create(:issue, project: sort_project) } + let_it_be(:severty_issue4) { create(:issue, project: sort_project) } + let_it_be(:severty_issue5) { create(:issue, project: sort_project) } + + before(:all) do + create(:issuable_severity, issue: severty_issue1, severity: :unknown) + create(:issuable_severity, issue: severty_issue2, severity: :low) + create(:issuable_severity, issue: severty_issue4, severity: :critical) + create(:issuable_severity, issue: severty_issue5, severity: :high) + end + + context 'when ascending' do + it_behaves_like 'sorted paginated query' do + let(:sort_param) { :SEVERITY_ASC } + let(:first_param) { 2 } + let(:all_records) { [severty_issue3.iid, severty_issue1.iid, severty_issue2.iid, severty_issue5.iid, severty_issue4.iid] } + end + end + + context 'when descending' do + it_behaves_like 'sorted paginated query' do + let(:sort_param) { :SEVERITY_DESC } + let(:first_param) { 2 } + let(:all_records) { [severty_issue4.iid, severty_issue5.iid, severty_issue2.iid, severty_issue1.iid, severty_issue3.iid] } + end + end + end + 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) } @@ -263,7 +294,6 @@ RSpec.describe 'getting an issue list for a project' do end 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) } @@ -285,7 +315,6 @@ RSpec.describe 'getting an issue list for a project' do end context 'when sorting by priority' do - let_it_be(:sort_project) { create(:project, :public) } let_it_be(:on_project) { { project: sort_project } } let_it_be(:early_milestone) { create(:milestone, **on_project, due_date: 10.days.from_now) } let_it_be(:late_milestone) { create(:milestone, **on_project, due_date: 30.days.from_now) } @@ -321,7 +350,6 @@ RSpec.describe 'getting an issue list for a project' do end 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) } @@ -348,7 +376,6 @@ RSpec.describe 'getting an issue list for a project' do end 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) } diff --git a/spec/requests/api/graphql/project/jobs_spec.rb b/spec/requests/api/graphql/project/jobs_spec.rb index 1a823ede9ac..7d0eb203d60 100644 --- a/spec/requests/api/graphql/project/jobs_spec.rb +++ b/spec/requests/api/graphql/project/jobs_spec.rb @@ -31,8 +31,8 @@ RSpec.describe 'Query.project.jobs' do end it 'does not generate N+1 queries', :request_store, :use_sql_query_cache do - build_stage = create(:ci_stage_entity, position: 1, name: 'build', project: project, pipeline: pipeline) - test_stage = create(:ci_stage_entity, position: 2, name: 'test', project: project, pipeline: pipeline) + build_stage = create(:ci_stage, position: 1, name: 'build', project: project, pipeline: pipeline) + test_stage = create(:ci_stage, position: 2, name: 'test', project: project, pipeline: pipeline) create(:ci_build, pipeline: pipeline, stage_idx: build_stage.position, name: 'docker 1 2', stage: build_stage) create(:ci_build, pipeline: pipeline, stage_idx: build_stage.position, name: 'docker 2 2', stage: build_stage) create(:ci_build, pipeline: pipeline, stage_idx: test_stage.position, name: 'rspec 1 2', stage: test_stage) diff --git a/spec/requests/api/graphql/project/packages_cleanup_policy_spec.rb b/spec/requests/api/graphql/project/packages_cleanup_policy_spec.rb index a025c57d4b8..33e1dbcba27 100644 --- a/spec/requests/api/graphql/project/packages_cleanup_policy_spec.rb +++ b/spec/requests/api/graphql/project/packages_cleanup_policy_spec.rb @@ -61,7 +61,7 @@ RSpec.describe 'getting the packages cleanup policy linked to a project' do with_them do before do project.update!(visibility: visibility.to_s) - project.add_user(current_user, role) unless role == :anonymous + project.add_member(current_user, role) unless role == :anonymous end it 'return the proper response' do diff --git a/spec/requests/api/graphql/project/pipeline_spec.rb b/spec/requests/api/graphql/project/pipeline_spec.rb index ccf97918021..08c6a2d9927 100644 --- a/spec/requests/api/graphql/project/pipeline_spec.rb +++ b/spec/requests/api/graphql/project/pipeline_spec.rb @@ -105,6 +105,62 @@ RSpec.describe 'getting pipeline information nested in a project' do end end + context 'when a job has been retried' do + let_it_be(:retried) do + create(:ci_build, :retried, + name: build_job.name, + pipeline: pipeline, + stage_idx: 0, + stage: build_job.stage) + end + + let(:fields) do + query_graphql_field(:jobs, { retried: retried_argument }, + query_graphql_field(:nodes, {}, all_graphql_fields_for('CiJob', max_depth: 3))) + end + + context 'when we filter out retried jobs' do + let(:retried_argument) { false } + + it 'contains latest jobs' do + post_graphql(query, current_user: current_user) + + expect(graphql_data_at(*path, :jobs, :nodes)).to include( + a_graphql_entity_for(build_job, :name, :duration, :retried) + ) + + expect(graphql_data_at(*path, :jobs, :nodes)).not_to include( + a_graphql_entity_for(retried) + ) + end + end + + context 'when we filter to only retried jobs' do + let(:retried_argument) { true } + + it 'contains only retried jobs' do + post_graphql(query, current_user: current_user) + + expect(graphql_data_at(*path, :jobs, :nodes)).to contain_exactly( + a_graphql_entity_for(retried) + ) + end + end + + context 'when we pass null explicitly' do + let(:retried_argument) { nil } + + it 'contains all jobs' do + post_graphql(query, current_user: current_user) + + expect(graphql_data_at(*path, :jobs, :nodes)).to include( + a_graphql_entity_for(build_job), + a_graphql_entity_for(retried) + ) + end + end + end + context 'when requesting only builds with certain statuses' do let(:variables) do { @@ -290,8 +346,8 @@ RSpec.describe 'getting pipeline information nested in a project' do end it 'does not generate N+1 queries', :request_store, :use_sql_query_cache do - build_stage = create(:ci_stage_entity, position: 1, name: 'build', project: project, pipeline: pipeline) - test_stage = create(:ci_stage_entity, position: 2, name: 'test', project: project, pipeline: pipeline) + build_stage = create(:ci_stage, position: 1, name: 'build', project: project, pipeline: pipeline) + test_stage = create(:ci_stage, position: 2, name: 'test', project: project, pipeline: pipeline) create(:ci_build, pipeline: pipeline, stage_idx: build_stage.position, name: 'docker 1 2', stage: build_stage) create(:ci_build, pipeline: pipeline, stage_idx: build_stage.position, name: 'docker 2 2', stage: build_stage) create(:ci_build, pipeline: pipeline, stage_idx: test_stage.position, name: 'rspec 1 2', stage: test_stage) diff --git a/spec/requests/api/graphql/project/project_members_spec.rb b/spec/requests/api/graphql/project/project_members_spec.rb index c3281b44954..4225c3ad3e8 100644 --- a/spec/requests/api/graphql/project/project_members_spec.rb +++ b/spec/requests/api/graphql/project/project_members_spec.rb @@ -8,8 +8,8 @@ RSpec.describe 'getting project members information' do let_it_be(:parent_group) { create(:group, :public) } let_it_be(:parent_project) { create(:project, :public, group: parent_group) } let_it_be(:user) { create(:user) } - let_it_be(:user_1) { create(:user, username: 'user') } - let_it_be(:user_2) { create(:user, username: 'test') } + let_it_be(:user_1) { create(:user, username: 'user', name: 'Same Name') } + let_it_be(:user_2) { create(:user, username: 'test', name: 'Same Name') } before_all do [user_1, user_2].each { |user| parent_group.add_guest(user) } @@ -29,11 +29,44 @@ RSpec.describe 'getting project members information' do expect_array_response(user_1, user_2) end - it 'returns members that match the search query' do - fetch_members(project: parent_project, args: { search: 'test' }) + describe 'search argument' do + it 'returns members that match the search query' do + fetch_members(project: parent_project, args: { search: 'test' }) - expect(graphql_errors).to be_nil - expect_array_response(user_2) + expect(graphql_errors).to be_nil + expect_array_response(user_2) + end + + context 'when paginating' do + it 'returns correct results' do + fetch_members(project: parent_project, args: { search: 'Same Name', first: 1 }) + + expect_array_response(user_1) + + next_cursor = graphql_data_at(:project, :projectMembers, :pageInfo, :endCursor) + fetch_members(project: parent_project, args: { search: 'Same Name', first: 1, after: next_cursor }) + + 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 @@ -231,6 +264,9 @@ RSpec.describe 'getting project members information' do } } } + pageInfo { + endCursor + } NODE graphql_query_for('project', diff --git a/spec/requests/api/graphql/todo_query_spec.rb b/spec/requests/api/graphql/todo_query_spec.rb new file mode 100644 index 00000000000..3f743f4402a --- /dev/null +++ b/spec/requests/api/graphql/todo_query_spec.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Todo Query' do + include GraphqlHelpers + + let_it_be(:current_user) { nil } + let_it_be(:project) { create(:project) } + let_it_be(:issue) { create(:issue, project: project) } + + let_it_be(:todo_owner) { create(:user) } + + let_it_be(:todo) { create(:todo, user: todo_owner, target: project) } + + before do + project.add_developer(todo_owner) + end + + let(:fields) do + <<~GRAPHQL + id + GRAPHQL + end + + let(:query) do + graphql_query_for(:todo, { id: todo.to_global_id.to_s }, fields) + end + + subject do + result = GitlabSchema.execute(query, context: { current_user: current_user }).to_h + graphql_dig_at(result, :data, :todo) + end + + context 'when requesting user is todo owner' do + let(:current_user) { todo_owner } + + it { is_expected.to include('id' => todo.to_global_id.to_s) } + end + + context 'when requesting user is not todo owner' do + let(:current_user) { create(:user) } + + it { is_expected.to be_nil } + end + + context 'when unauthenticated' do + it { is_expected.to be_nil } + end +end diff --git a/spec/requests/api/graphql/work_item_spec.rb b/spec/requests/api/graphql/work_item_spec.rb index 09bda8ee0d5..f17d2ebbb7e 100644 --- a/spec/requests/api/graphql/work_item_spec.rb +++ b/spec/requests/api/graphql/work_item_spec.rb @@ -8,7 +8,7 @@ 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') } + let_it_be(:work_item) { create(:work_item, project: project, description: '- List item', weight: 1) } 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) } @@ -64,16 +64,13 @@ RSpec.describe 'Query.work_item(id)' do it 'returns widget information' do expect(work_item_data).to include( 'id' => work_item.to_gid.to_s, - 'widgets' => match_array([ + 'widgets' => include( hash_including( 'type' => 'DESCRIPTION', 'description' => work_item.description, 'descriptionHtml' => ::MarkupHelper.markdown_field(work_item, :description, {}) - ), - hash_including( - 'type' => 'HIERARCHY' ) - ]) + ) ) end end @@ -101,10 +98,7 @@ RSpec.describe 'Query.work_item(id)' do it 'returns widget information' do expect(work_item_data).to include( 'id' => work_item.to_gid.to_s, - 'widgets' => match_array([ - hash_including( - 'type' => 'DESCRIPTION' - ), + 'widgets' => include( hash_including( 'type' => 'HIERARCHY', 'parent' => nil, @@ -113,7 +107,7 @@ RSpec.describe 'Query.work_item(id)' do hash_including('id' => child_link2.work_item.to_gid.to_s) ]) } ) - ]) + ) ) end @@ -137,10 +131,7 @@ RSpec.describe 'Query.work_item(id)' do it 'filters out not accessible children or parent' do expect(work_item_data).to include( 'id' => work_item.to_gid.to_s, - 'widgets' => match_array([ - hash_including( - 'type' => 'DESCRIPTION' - ), + 'widgets' => include( hash_including( 'type' => 'HIERARCHY', 'parent' => nil, @@ -148,7 +139,7 @@ RSpec.describe 'Query.work_item(id)' do hash_including('id' => child_link1.work_item.to_gid.to_s) ]) } ) - ]) + ) ) end end @@ -160,20 +151,85 @@ RSpec.describe 'Query.work_item(id)' do it 'returns parent information' do expect(work_item_data).to include( 'id' => work_item.to_gid.to_s, - 'widgets' => match_array([ - hash_including( - 'type' => 'DESCRIPTION' - ), + 'widgets' => include( hash_including( 'type' => 'HIERARCHY', 'parent' => hash_including('id' => parent_link.work_item_parent.to_gid.to_s), 'children' => { 'nodes' => match_array([]) } ) - ]) + ) ) end end end + + describe 'weight widget' do + let(:work_item_fields) do + <<~GRAPHQL + id + widgets { + type + ... on WorkItemWidgetWeight { + weight + } + } + 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' => 'WEIGHT', + 'weight' => work_item.weight + ) + ) + ) + end + end + + 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 WorkItemWidgetAssignees { + allowsMultipleAssignees + canInviteMembers + assignees { + nodes { + id + username + } + } + } + } + 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' => 'ASSIGNEES', + 'allowsMultipleAssignees' => boolean, + 'canInviteMembers' => boolean, + 'assignees' => { + 'nodes' => match_array( + assignees.map { |a| { 'id' => a.to_gid.to_s, 'username' => a.username } } + ) + } + ) + ) + ) + end + end end context 'when an Issue Global ID is provided' do |