diff options
Diffstat (limited to 'spec/requests/api/graphql')
39 files changed, 1522 insertions, 424 deletions
diff --git a/spec/requests/api/graphql/abuse_report_spec.rb b/spec/requests/api/graphql/abuse_report_spec.rb index f74b1fb4061..8ab0e92d838 100644 --- a/spec/requests/api/graphql/abuse_report_spec.rb +++ b/spec/requests/api/graphql/abuse_report_spec.rb @@ -25,11 +25,7 @@ RSpec.describe 'Querying an Abuse Report', feature_category: :insider_threat do it 'returns all fields' do expect(abuse_report_data).to include( - 'id' => global_id, - 'userPermissions' => { - 'readAbuseReport' => true, - 'createNote' => true - } + 'id' => global_id ) end end diff --git a/spec/requests/api/graphql/ci/catalog/resource_spec.rb b/spec/requests/api/graphql/ci/catalog/resource_spec.rb index fce773f320b..9fe73e7ba45 100644 --- a/spec/requests/api/graphql/ci/catalog/resource_spec.rb +++ b/spec/requests/api/graphql/ci/catalog/resource_spec.rb @@ -15,11 +15,14 @@ RSpec.describe 'Query.ciCatalogResource', feature_category: :pipeline_compositio description: 'A simple component', namespace: namespace, star_count: 1, - files: { 'README.md' => '[link](README.md)' } + files: { + 'README.md' => '[link](README.md)', + 'templates/secret-detection.yml' => "spec:\n inputs:\n website:\n---\nimage: alpine_1" + } ) end - let_it_be(:resource) { create(:ci_catalog_resource, project: project) } + let_it_be(:resource) { create(:ci_catalog_resource, :published, project: project) } let(:query) do <<~GQL @@ -33,10 +36,12 @@ RSpec.describe 'Query.ciCatalogResource', feature_category: :pipeline_compositio subject(:post_query) { post_graphql(query, current_user: user) } + before_all do + namespace.add_developer(user) + end + context 'when the current user has permission to read the namespace catalog' do it 'returns the resource with the expected data' do - namespace.add_developer(user) - post_query expect(graphql_data_at(:ciCatalogResource)).to match( @@ -45,7 +50,6 @@ RSpec.describe 'Query.ciCatalogResource', feature_category: :pipeline_compositio icon: project.avatar_path, webPath: "/#{project.full_path}", starCount: project.star_count, - forksCount: project.forks_count, readmeHtml: a_string_including( "#{project.full_path}/-/blob/#{project.default_branch}/README.md" ) @@ -64,15 +68,94 @@ RSpec.describe 'Query.ciCatalogResource', feature_category: :pipeline_compositio end end - describe 'versions' do - before_all do - namespace.add_developer(user) + describe 'components' do + let(:query) do + <<~GQL + query { + ciCatalogResource(id: "#{resource.to_global_id}") { + id + versions { + nodes { + id + components { + nodes { + id + name + path + inputs { + name + default + required + } + } + } + } + } + } + } + GQL end - before do - stub_licensed_features(ci_namespace_catalog: true) + context 'when the catalog resource has components' do + let_it_be(:inputs) do + { + website: nil, + environment: { + default: 'test' + }, + tags: { + type: 'array' + } + } + end + + let_it_be(:version) do + create(:release, :with_catalog_resource_version, project: project).catalog_resource_version + end + + let_it_be(:components) do + create_list(:ci_catalog_resource_component, 2, version: version, inputs: inputs, path: 'templates/comp.yml') + end + + it 'returns the resource with the component data' do + post_query + + expect(graphql_data_at(:ciCatalogResource)).to match(a_graphql_entity_for(resource)) + + expect(graphql_data_at(:ciCatalogResource, :versions, :nodes, :components, :nodes)).to contain_exactly( + a_graphql_entity_for( + components.first, + name: components.first.name, + path: components.first.path, + inputs: [ + a_graphql_entity_for( + name: 'tags', + default: nil, + required: true + ), + a_graphql_entity_for( + name: 'website', + default: nil, + required: true + ), + a_graphql_entity_for( + name: 'environment', + default: 'test', + required: false + ) + ] + ), + a_graphql_entity_for( + components.last, + name: components.last.name, + path: components.last.path + ) + ) + end end + end + describe 'versions' do let(:query) do <<~GQL query { @@ -82,6 +165,7 @@ RSpec.describe 'Query.ciCatalogResource', feature_category: :pipeline_compositio nodes { id tagName + tagPath releasedAt author { id @@ -99,11 +183,13 @@ RSpec.describe 'Query.ciCatalogResource', feature_category: :pipeline_compositio let_it_be(:author) { create(:user, name: 'author') } let_it_be(:version1) do - create(:release, project: project, released_at: '2023-01-01T00:00:00Z', author: author) + create(:release, :with_catalog_resource_version, project: project, released_at: '2023-01-01T00:00:00Z', + author: author).catalog_resource_version end let_it_be(:version2) do - create(:release, project: project, released_at: '2023-02-01T00:00:00Z', author: author) + create(:release, :with_catalog_resource_version, project: project, released_at: '2023-02-01T00:00:00Z', + author: author).catalog_resource_version end it 'returns the resource with the versions data' do @@ -116,13 +202,15 @@ RSpec.describe 'Query.ciCatalogResource', feature_category: :pipeline_compositio expect(graphql_data_at(:ciCatalogResource, :versions, :nodes)).to contain_exactly( a_graphql_entity_for( version1, - tagName: version1.tag, + tagName: version1.name, + tagPath: project_tag_path(project, version1.name), releasedAt: version1.released_at, author: a_graphql_entity_for(author, :name) ), a_graphql_entity_for( version2, - tagName: version2.tag, + tagName: version2.name, + tagPath: project_tag_path(project, version2.name), releasedAt: version2.released_at, author: a_graphql_entity_for(author, :name) ) @@ -142,14 +230,6 @@ RSpec.describe 'Query.ciCatalogResource', feature_category: :pipeline_compositio end describe 'latestVersion' do - before_all do - namespace.add_developer(user) - end - - before do - stub_licensed_features(ci_namespace_catalog: true) - end - let(:query) do <<~GQL query { @@ -158,6 +238,7 @@ RSpec.describe 'Query.ciCatalogResource', feature_category: :pipeline_compositio latestVersion { id tagName + tagPath releasedAt author { id @@ -174,12 +255,14 @@ RSpec.describe 'Query.ciCatalogResource', feature_category: :pipeline_compositio let_it_be(:author) { create(:user, name: 'author') } let_it_be(:latest_version) do - create(:release, project: project, released_at: '2023-02-01T00:00:00Z', author: author) + create(:release, :with_catalog_resource_version, project: project, released_at: '2023-02-01T00:00:00Z', + author: author).catalog_resource_version end before_all do - # Previous version of the project - create(:release, project: project, released_at: '2023-01-01T00:00:00Z', author: author) + # Previous version of the catalog resource + create(:release, :with_catalog_resource_version, project: project, released_at: '2023-01-01T00:00:00Z', + author: author) end it 'returns the resource with the latest version data' do @@ -190,7 +273,8 @@ RSpec.describe 'Query.ciCatalogResource', feature_category: :pipeline_compositio resource, latestVersion: a_graphql_entity_for( latest_version, - tagName: latest_version.tag, + tagName: latest_version.name, + tagPath: project_tag_path(project, latest_version.name), releasedAt: latest_version.released_at, author: a_graphql_entity_for(author, :name) ) @@ -210,47 +294,7 @@ RSpec.describe 'Query.ciCatalogResource', feature_category: :pipeline_compositio end end - describe 'rootNamespace' do - before_all do - namespace.add_developer(user) - end - - before do - stub_licensed_features(ci_namespace_catalog: true) - end - - let(:query) do - <<~GQL - query { - ciCatalogResource(id: "#{resource.to_global_id}") { - id - rootNamespace { - id - name - path - } - } - } - GQL - end - - it 'returns the correct root namespace data' do - post_query - - expect(graphql_data_at(:ciCatalogResource)).to match( - a_graphql_entity_for( - resource, - rootNamespace: a_graphql_entity_for(namespace, :name, :path) - ) - ) - end - end - describe 'openIssuesCount' do - before do - stub_licensed_features(ci_namespace_catalog: true) - end - context 'when open_issue_count is requested' do let(:query) do <<~GQL @@ -266,8 +310,6 @@ RSpec.describe 'Query.ciCatalogResource', feature_category: :pipeline_compositio create(:issue, :opened, project: project) create(:issue, :opened, project: project) - namespace.add_developer(user) - post_query expect(graphql_data_at(:ciCatalogResource)).to match( @@ -279,8 +321,6 @@ RSpec.describe 'Query.ciCatalogResource', feature_category: :pipeline_compositio context 'when open_issue_count is zero' do it 'returns zero' do - namespace.add_developer(user) - post_query expect(graphql_data_at(:ciCatalogResource)).to match( @@ -294,10 +334,6 @@ RSpec.describe 'Query.ciCatalogResource', feature_category: :pipeline_compositio end describe 'openMergeRequestsCount' do - before do - stub_licensed_features(ci_namespace_catalog: true) - end - context 'when merge_requests_count is requested' do let(:query) do <<~GQL @@ -312,8 +348,6 @@ RSpec.describe 'Query.ciCatalogResource', feature_category: :pipeline_compositio it 'returns the correct count' do create(:merge_request, :opened, source_project: project) - namespace.add_developer(user) - post_query expect(graphql_data_at(:ciCatalogResource)).to match( @@ -325,8 +359,6 @@ RSpec.describe 'Query.ciCatalogResource', feature_category: :pipeline_compositio context 'when open merge_requests_count is zero' do it 'returns zero' do - namespace.add_developer(user) - post_query expect(graphql_data_at(:ciCatalogResource)).to match( diff --git a/spec/requests/api/graphql/ci/catalog/resources_spec.rb b/spec/requests/api/graphql/ci/catalog/resources_spec.rb index 7c955a1202c..49a3f3be1d7 100644 --- a/spec/requests/api/graphql/ci/catalog/resources_spec.rb +++ b/spec/requests/api/graphql/ci/catalog/resources_spec.rb @@ -29,8 +29,11 @@ RSpec.describe 'Query.ciCatalogResources', feature_category: :pipeline_compositi ) end - let_it_be(:resource1) { create(:ci_catalog_resource, project: project1, latest_released_at: '2023-01-01T00:00:00Z') } - let_it_be(:public_resource) { create(:ci_catalog_resource, project: public_project) } + let_it_be(:resource1) do + create(:ci_catalog_resource, :published, project: project1, latest_released_at: '2023-01-01T00:00:00Z') + end + + let_it_be(:public_resource) { create(:ci_catalog_resource, :published, project: public_project) } let(:query) do <<~GQL @@ -44,7 +47,6 @@ RSpec.describe 'Query.ciCatalogResources', feature_category: :pipeline_compositi webPath latestReleasedAt starCount - forksCount readmeHtml } } @@ -58,11 +60,11 @@ RSpec.describe 'Query.ciCatalogResources', feature_category: :pipeline_compositi it do ctx = { current_user: user } - control_count = ActiveRecord::QueryRecorder.new do + control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do run_with_clean_state(query, context: ctx) end - create(:ci_catalog_resource, project: project2) + create(:ci_catalog_resource, :published, project: project2) expect do run_with_clean_state(query, context: ctx) @@ -83,7 +85,6 @@ RSpec.describe 'Query.ciCatalogResources', feature_category: :pipeline_compositi icon: project1.avatar_path, webPath: "/#{project1.full_path}", starCount: project1.star_count, - forksCount: project1.forks_count, readmeHtml: a_string_including('Test</strong>'), latestReleasedAt: resource1.latest_released_at ), @@ -121,7 +122,7 @@ RSpec.describe 'Query.ciCatalogResources', feature_category: :pipeline_compositi end it 'limits the request to 1 resource at a time' do - create(:ci_catalog_resource, project: project2) + create(:ci_catalog_resource, :published, project: project2) post_query @@ -135,11 +136,13 @@ RSpec.describe 'Query.ciCatalogResources', feature_category: :pipeline_compositi let_it_be(:author2) { create(:user, name: 'author2') } let_it_be(:latest_version1) do - create(:release, project: project1, released_at: '2023-02-01T00:00:00Z', author: author1) + create(:release, :with_catalog_resource_version, project: project1, released_at: '2023-02-01T00:00:00Z', + author: author1).catalog_resource_version end let_it_be(:latest_version2) do - create(:release, project: public_project, released_at: '2023-02-01T00:00:00Z', author: author2) + create(:release, :with_catalog_resource_version, project: public_project, released_at: '2023-02-01T00:00:00Z', + author: author2).catalog_resource_version end let(:query) do @@ -167,9 +170,11 @@ RSpec.describe 'Query.ciCatalogResources', feature_category: :pipeline_compositi before_all do namespace.add_developer(user) - # Previous versions of the projects - create(:release, project: project1, released_at: '2023-01-01T00:00:00Z', author: author1) - create(:release, project: public_project, released_at: '2023-01-01T00:00:00Z', author: author2) + # Previous versions of the catalog resources + create(:release, :with_catalog_resource_version, project: project1, released_at: '2023-01-01T00:00:00Z', + author: author1) + create(:release, :with_catalog_resource_version, project: public_project, released_at: '2023-01-01T00:00:00Z', + author: author2) end it 'returns all resources with the latest version data' do @@ -180,7 +185,7 @@ RSpec.describe 'Query.ciCatalogResources', feature_category: :pipeline_compositi resource1, latestVersion: a_graphql_entity_for( latest_version1, - tagName: latest_version1.tag, + tagName: latest_version1.name, releasedAt: latest_version1.released_at, author: a_graphql_entity_for(author1, :name) ) @@ -189,7 +194,7 @@ RSpec.describe 'Query.ciCatalogResources', feature_category: :pipeline_compositi public_resource, latestVersion: a_graphql_entity_for( latest_version2, - tagName: latest_version2.tag, + tagName: latest_version2.name, releasedAt: latest_version2.released_at, author: a_graphql_entity_for(author2, :name) ) @@ -197,43 +202,7 @@ RSpec.describe 'Query.ciCatalogResources', feature_category: :pipeline_compositi ) end - # TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/430350 - # it_behaves_like 'avoids N+1 queries' - end - - describe 'rootNamespace' do - before_all do - namespace.add_developer(user) - end - - let(:query) do - <<~GQL - query { - ciCatalogResources { - nodes { - id - rootNamespace { - id - name - path - } - } - } - } - GQL - end - - it 'returns the correct root namespace data' do - post_query - - expect(graphql_data_at(:ciCatalogResources, :nodes)).to contain_exactly( - a_graphql_entity_for( - resource1, - rootNamespace: a_graphql_entity_for(namespace, :name, :path) - ), - a_graphql_entity_for(public_resource, rootNamespace: nil) - ) - end + it_behaves_like 'avoids N+1 queries' end describe 'openIssuesCount' do @@ -326,8 +295,8 @@ RSpec.describe 'Query.ciCatalogResources', feature_category: :pipeline_compositi end it 'returns catalog resources with the expected data' do - resource2 = create(:ci_catalog_resource, project: project2) - _resource_in_another_namespace = create(:ci_catalog_resource) + resource2 = create(:ci_catalog_resource, :published, project: project2) + _resource_in_another_namespace = create(:ci_catalog_resource, :published) post_query @@ -338,7 +307,6 @@ RSpec.describe 'Query.ciCatalogResources', feature_category: :pipeline_compositi icon: project2.avatar_path, webPath: "/#{project2.full_path}", starCount: project2.star_count, - forksCount: project2.forks_count, readmeHtml: '', latestReleasedAt: resource2.latest_released_at ) diff --git a/spec/requests/api/graphql/ci/runner_spec.rb b/spec/requests/api/graphql/ci/runner_spec.rb index 6f1eb77fa9b..8262640b283 100644 --- a/spec/requests/api/graphql/ci/runner_spec.rb +++ b/spec/requests/api/graphql/ci/runner_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe 'Query.runner(id)', :freeze_time, feature_category: :runner_fleet do +RSpec.describe 'Query.runner(id)', :freeze_time, feature_category: :fleet_visibility do include GraphqlHelpers using RSpec::Parameterized::TableSyntax diff --git a/spec/requests/api/graphql/ci/runner_web_url_edge_spec.rb b/spec/requests/api/graphql/ci/runner_web_url_edge_spec.rb index 76e2dda4ce2..8e3efb67ee5 100644 --- a/spec/requests/api/graphql/ci/runner_web_url_edge_spec.rb +++ b/spec/requests/api/graphql/ci/runner_web_url_edge_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require 'spec_helper' -RSpec.describe 'RunnerWebUrlEdge', feature_category: :runner_fleet do +RSpec.describe 'RunnerWebUrlEdge', feature_category: :fleet_visibility do include GraphqlHelpers describe 'inside a Query.group' do diff --git a/spec/requests/api/graphql/ci/runners_spec.rb b/spec/requests/api/graphql/ci/runners_spec.rb index 0e2712d742d..0fe14bef778 100644 --- a/spec/requests/api/graphql/ci/runners_spec.rb +++ b/spec/requests/api/graphql/ci/runners_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require 'spec_helper' -RSpec.describe 'Query.runners', feature_category: :runner_fleet do +RSpec.describe 'Query.runners', feature_category: :fleet_visibility do include GraphqlHelpers let_it_be(:current_user) { create_default(:user, :admin) } @@ -35,17 +35,19 @@ RSpec.describe 'Query.runners', feature_category: :runner_fleet do end context 'with filters' do - shared_examples 'a working graphql query returning expected runner' do + shared_examples 'a working graphql query returning expected runners' do it_behaves_like 'a working graphql query' do before do post_graphql(query, current_user: current_user) end end - it 'returns expected runner' do + it 'returns expected runners' do post_graphql(query, current_user: current_user) - expect(runners_graphql_data['nodes']).to contain_exactly(a_graphql_entity_for(expected_runner)) + expect(runners_graphql_data['nodes']).to contain_exactly( + *Array(expected_runners).map { |expected_runner| a_graphql_entity_for(expected_runner) } + ) end it 'does not execute more queries per runner', :aggregate_failures do @@ -95,24 +97,36 @@ RSpec.describe 'Query.runners', feature_category: :runner_fleet do let(:runner_type) { 'INSTANCE_TYPE' } let(:status) { 'ACTIVE' } - let!(:expected_runner) { instance_runner } + let(:expected_runners) { instance_runner } - it_behaves_like 'a working graphql query returning expected runner' + it_behaves_like 'a working graphql query returning expected runners' end context 'runner_type is PROJECT_TYPE and status is NEVER_CONTACTED' do let(:runner_type) { 'PROJECT_TYPE' } let(:status) { 'NEVER_CONTACTED' } - let!(:expected_runner) { project_runner } + let(:expected_runners) { project_runner } - it_behaves_like 'a working graphql query returning expected runner' + it_behaves_like 'a working graphql query returning expected runners' end end context 'when filtered on version prefix' do - let_it_be(:version_runner) { create(:ci_runner, :project, active: false, description: 'Runner with machine') } - let_it_be(:version_runner_machine) { create(:ci_runner_machine, runner: version_runner, version: '15.11.0') } + let_it_be(:runner_15_10_1) { create_ci_runner(version: '15.10.1') } + + let_it_be(:runner_15_11_0) { create_ci_runner(version: '15.11.0') } + let_it_be(:runner_15_11_1) { create_ci_runner(version: '15.11.1') } + + let_it_be(:runner_16_1_0) { create_ci_runner(version: '16.1.0') } + + let(:fields) do + <<~QUERY + nodes { + id + } + QUERY + end let(:query) do %( @@ -124,12 +138,44 @@ RSpec.describe 'Query.runners', feature_category: :runner_fleet do ) end - context 'version_prefix is "15."' do + context 'when version_prefix is "15."' do let(:version_prefix) { '15.' } - let!(:expected_runner) { version_runner } + it_behaves_like 'a working graphql query returning expected runners' do + let(:expected_runners) { [runner_15_10_1, runner_15_11_0, runner_15_11_1] } + end + end + + context 'when version_prefix is "15.11."' do + let(:version_prefix) { '15.11.' } - it_behaves_like 'a working graphql query returning expected runner' + it_behaves_like 'a working graphql query returning expected runners' do + let(:expected_runners) { [runner_15_11_0, runner_15_11_1] } + end + end + + context 'when version_prefix is "15.11.0"' do + let(:version_prefix) { '15.11.0' } + + it_behaves_like 'a working graphql query returning expected runners' do + let(:expected_runners) { runner_15_11_0 } + end + end + + context 'when version_prefix is not digits' do + let(:version_prefix) { 'a.b' } + + it_behaves_like 'a working graphql query returning expected runners' do + let(:expected_runners) do + [instance_runner, project_runner, runner_15_10_1, runner_15_11_0, runner_15_11_1, runner_16_1_0] + end + end + end + + def create_ci_runner(args = {}, version:) + create(:ci_runner, :project, **args).tap do |runner| + create(:ci_runner_machine, runner: runner, version: version) + end end end 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 20277c7e27b..2acdd509355 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 @@ -11,11 +11,12 @@ RSpec.describe 'container repository details', feature_category: :container_regi let_it_be_with_reload(:project) { create(:project) } let_it_be_with_reload(:container_repository) { create(:container_repository, project: project) } + let(:excluded) { %w[pipeline size agentConfigurations iterations iterationCadences productAnalyticsState] } let(:query) do graphql_query_for( 'containerRepository', { id: container_repository_global_id }, - all_graphql_fields_for('ContainerRepositoryDetails', excluded: %w[pipeline size]) + all_graphql_fields_for('ContainerRepositoryDetails', excluded: excluded, max_depth: 4) ) end diff --git a/spec/requests/api/graphql/custom_emoji_query_spec.rb b/spec/requests/api/graphql/custom_emoji_query_spec.rb index 1858ea831dd..c89ad0002b4 100644 --- a/spec/requests/api/graphql/custom_emoji_query_spec.rb +++ b/spec/requests/api/graphql/custom_emoji_query_spec.rb @@ -35,14 +35,14 @@ RSpec.describe 'getting custom emoji within namespace', feature_category: :share expect(graphql_data['group']['customEmoji']['nodes'].first['name']).to eq(custom_emoji.name) end - it 'returns nil custom emoji when the custom_emoji feature flag is disabled' do + it 'returns empty array when the custom_emoji feature flag is disabled' do stub_feature_flags(custom_emoji: false) post_graphql(custom_emoji_query(group), current_user: current_user) expect(response).to have_gitlab_http_status(:ok) expect(graphql_data['group']).to be_present - expect(graphql_data['group']['customEmoji']).to be_nil + expect(graphql_data['group']['customEmoji']['nodes']).to eq([]) end it 'returns nil group when unauthorised' do diff --git a/spec/requests/api/graphql/group/issues_spec.rb b/spec/requests/api/graphql/group/issues_spec.rb index 95aeed32558..1da6abf3cac 100644 --- a/spec/requests/api/graphql/group/issues_spec.rb +++ b/spec/requests/api/graphql/group/issues_spec.rb @@ -15,6 +15,8 @@ RSpec.describe 'getting an issue list for a group', feature_category: :team_plan let_it_be(:issue2) { create(:issue, project: project2) } let_it_be(:issue3) { create(:issue, project: project3) } + let_it_be(:group_level_issue) { create(:issue, :epic, :group_level, namespace: group1) } + let(:issue1_gid) { issue1.to_global_id.to_s } let(:issue2_gid) { issue2.to_global_id.to_s } let(:issues_data) { graphql_data['group']['issues']['edges'] } @@ -142,6 +144,40 @@ RSpec.describe 'getting an issue list for a group', feature_category: :team_plan end end + context 'when querying epic types' do + let(:query) do + graphql_query_for( + 'group', + { 'fullPath' => group1.full_path }, + "issues(types: [EPIC]) { #{fields} }" + ) + end + + before_all do + group1.add_developer(current_user) + end + + it 'returns group-level epics' do + post_graphql(query, current_user: current_user) + + expect_graphql_errors_to_be_empty + expect(issues_ids).to contain_exactly(group_level_issue.to_global_id.to_s) + end + + context 'when namespace_level_work_items is disabled' do + before do + stub_feature_flags(namespace_level_work_items: false) + end + + it 'returns no epics' do + post_graphql(query, current_user: current_user) + + expect_graphql_errors_to_be_empty + expect(issues_ids).to be_empty + end + end + end + def issues_ids graphql_dig_at(issues_data, :node, :id) end diff --git a/spec/requests/api/graphql/group/work_item_state_counts_spec.rb b/spec/requests/api/graphql/group/work_item_state_counts_spec.rb new file mode 100644 index 00000000000..2ae623c39f2 --- /dev/null +++ b/spec/requests/api/graphql/group/work_item_state_counts_spec.rb @@ -0,0 +1,107 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'request_store' + +RSpec.describe 'getting Work Item counts by state', feature_category: :portfolio_management do + include GraphqlHelpers + + let_it_be(:current_user) { create(:user) } + let_it_be(:group) { create(:group, :private) } + let_it_be(:work_item_opened1) { create(:work_item, namespace: group) } + let_it_be(:work_item_opened2) { create(:work_item, namespace: group, author: current_user) } + let_it_be(:work_item_closed1) { create(:work_item, :closed, namespace: group) } + let_it_be(:work_item_closed2) { create(:work_item, :closed, namespace: group) } + + let(:params) { {} } + + subject(:query_counts) { post_graphql(query, current_user: current_user) } + + context 'with work items count data' do + let(:work_item_counts) { graphql_data.dig('group', 'workItemStateCounts') } + + context 'with group permissions' do + before_all do + group.add_developer(current_user) + end + + it_behaves_like 'a working graphql query' do + before do + query_counts + end + end + + it 'returns the correct counts for each state' do + query_counts + + expect(work_item_counts).to eq( + 'all' => 4, + 'opened' => 2, + 'closed' => 2 + ) + end + + context 'when filters are provided' do + context 'when filtering by author username' do + let(:params) { { 'authorUsername' => current_user.username } } + + it 'returns the correct counts for each state' do + query_counts + + expect(work_item_counts).to eq( + 'all' => 1, + 'opened' => 1, + 'closed' => 0 + ) + end + end + + context 'when filtering by search' do + let(:params) { { search: 'foo', in: [:TITLE] } } + + it 'returns an error for filters that are not supported' do + query_counts + + expect(graphql_errors).to contain_exactly( + hash_including('message' => 'Searching is not available for work items at the namespace level yet') + ) + end + end + end + + context 'when the namespace_level_work_items feature flag is disabled' do + before do + stub_feature_flags(namespace_level_work_items: false) + end + + it 'does not return work item counts' do + query_counts + + expect_graphql_errors_to_be_empty + expect(work_item_counts).to be_nil + end + end + end + + context 'without group permissions' do + it 'does not return work item counts' do + query_counts + + expect_graphql_errors_to_be_empty + expect(work_item_counts).to be_nil + end + end + end + + def query(args: params) + fields = <<~QUERY + #{all_graphql_fields_for('WorkItemStateCountsType'.classify)} + QUERY + + graphql_query_for( + 'group', + { 'fullPath' => group.full_path }, + query_graphql_field('workItemStateCounts', args, fields) + ) + end +end diff --git a/spec/requests/api/graphql/group/work_item_types_spec.rb b/spec/requests/api/graphql/group/work_item_types_spec.rb index 791c0fb9524..fbebcdad389 100644 --- a/spec/requests/api/graphql/group/work_item_types_spec.rb +++ b/spec/requests/api/graphql/group/work_item_types_spec.rb @@ -5,56 +5,19 @@ require 'spec_helper' RSpec.describe 'getting a list of work item types for a group', feature_category: :team_planning do include GraphqlHelpers - let_it_be(:developer) { create(:user) } let_it_be(:group) { create(:group, :private) } + let_it_be(:developer) { create(:user).tap { |u| group.add_developer(u) } } - before_all do - group.add_developer(developer) - end - - let(:current_user) { developer } - - let(:fields) do - <<~GRAPHQL - workItemTypes{ - nodes { id name iconName } - } - GRAPHQL - end - - let(:query) do - graphql_query_for( - 'group', - { 'fullPath' => group.full_path }, - fields - ) - end - - context 'when user has access to the group' do - before do - post_graphql(query, current_user: current_user) - end + it_behaves_like 'graphql work item type list request spec' do + let(:current_user) { developer } + let(:parent_key) { :group } - it_behaves_like 'a working graphql query' - - it 'returns all default work item types' do - expect(graphql_data.dig('group', 'workItemTypes', 'nodes')).to match_array( - WorkItems::Type.default.map do |type| - hash_including('id' => type.to_global_id.to_s, 'name' => type.name, 'iconName' => type.icon_name) - end + let(:query) do + graphql_query_for( + 'group', + { 'fullPath' => group.full_path }, + query_nodes('WorkItemTypes', work_item_type_fields) ) end end - - context "when user doesn't have access to the group" do - let(:current_user) { create(:user) } - - before do - post_graphql(query, current_user: current_user) - end - - it 'does not return the group' do - expect(graphql_data).to eq('group' => nil) - end - end end diff --git a/spec/requests/api/graphql/milestone_spec.rb b/spec/requests/api/graphql/milestone_spec.rb index 2cea9fd0408..0dc2eabc3e1 100644 --- a/spec/requests/api/graphql/milestone_spec.rb +++ b/spec/requests/api/graphql/milestone_spec.rb @@ -151,4 +151,18 @@ RSpec.describe 'Querying a Milestone', feature_category: :team_planning do end end end + + context 'for common GraphQL/REST' do + it_behaves_like 'group milestones including ancestors and descendants' + + def query_group_milestone_ids(params) + query = graphql_query_for('group', { 'fullPath' => group.full_path }, + query_graphql_field('milestones', params, query_graphql_path([:nodes], :id)) + ) + + post_graphql(query, current_user: current_user) + + graphql_data_at(:group, :milestones, :nodes).pluck('id').map { |gid| GlobalID.parse(gid).model_id.to_i } + end + end end diff --git a/spec/requests/api/graphql/mutations/admin/sidekiq_queues/delete_jobs_spec.rb b/spec/requests/api/graphql/mutations/admin/sidekiq_queues/delete_jobs_spec.rb index 316b0f3755d..808dcefb84d 100644 --- a/spec/requests/api/graphql/mutations/admin/sidekiq_queues/delete_jobs_spec.rb +++ b/spec/requests/api/graphql/mutations/admin/sidekiq_queues/delete_jobs_spec.rb @@ -26,7 +26,7 @@ RSpec.describe 'Deleting Sidekiq jobs', :clean_gitlab_redis_queues, feature_cate context 'when the user is an admin' do let(:current_user) { admin } - context 'valid request' do + context 'when valid request' do around do |example| Sidekiq::Queue.new(queue).clear Sidekiq::Testing.disable!(&example) @@ -40,7 +40,7 @@ RSpec.describe 'Deleting Sidekiq jobs', :clean_gitlab_redis_queues, feature_cate 'args' => args, 'meta.user' => user.username ) - raise 'Not enqueued!' if Sidekiq::Queue.new(queue).size.zero? + raise 'Not enqueued!' if Sidekiq::Queue.new(queue).size.zero? # rubocop:disable Style/ZeroLengthPredicate -- Sidekiq::Queue doesn't implement #blank? or #empty? end it 'returns info about the deleted jobs' do diff --git a/spec/requests/api/graphql/mutations/branch_rules/update_spec.rb b/spec/requests/api/graphql/mutations/branch_rules/update_spec.rb new file mode 100644 index 00000000000..14874bdfaa8 --- /dev/null +++ b/spec/requests/api/graphql/mutations/branch_rules/update_spec.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'BranchRuleUpdate', feature_category: :source_code_management do + include GraphqlHelpers + + let_it_be(:project) { create(:project, :public) } + let_it_be(:user) { create(:user) } + let!(:branch_rule_1) { create(:protected_branch, project: project, name: name_1) } + let!(:branch_rule_2) { create(:protected_branch, project: project, name: name_2) } + let(:name_1) { "name_1" } + let(:name_2) { "name_2" } + let(:new_name) { "new name" } + let(:id) { branch_rule_1.to_global_id } + let(:project_path) { project.full_path } + let(:name) { new_name } + let(:params) do + { + id: id, + project_path: project_path, + name: name + } + end + + let(:mutation) { graphql_mutation(:branch_rule_update, params) } + + subject(:post_mutation) { post_graphql_mutation(mutation, current_user: user) } + + def mutation_response + graphql_mutation_response(:branch_rule_update) + end + + context 'when the user does not have permission' do + before_all do + project.add_developer(user) + end + + it 'does not update the branch rule' do + expect { post_mutation }.not_to change { branch_rule_1 } + end + end + + context 'when the user can update a branch rules' do + let(:current_user) { user } + + before_all do + project.add_maintainer(user) + end + + it 'updates the protected branch' do + post_mutation + + expect(branch_rule_1.reload.name).to eq(new_name) + end + + it 'returns the updated branch rule' do + post_mutation + + expect(mutation_response).to have_key('branchRule') + expect(mutation_response['branchRule']['name']).to eq(new_name) + expect(mutation_response['errors']).to be_empty + end + + context 'when name already exists for the project' do + let(:params) do + { + id: id, + project_path: project_path, + name: name_2 + } + end + + it 'returns an error' do + post_mutation + + expect(mutation_response['errors'].first).to eq('Name has already been taken') + end + end + + context 'when the protected branch cannot be found' do + let(:id) { "gid://gitlab/ProtectedBranch/#{non_existing_record_id}" } + + it_behaves_like 'a mutation that returns top-level errors', + errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR] + end + + context 'when the project cannot be found' do + let(:project_path) { 'not a project path' } + + it_behaves_like 'a mutation that returns top-level errors', + errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR] + end + end +end diff --git a/spec/requests/api/graphql/mutations/ci/catalog/resources/destroy_spec.rb b/spec/requests/api/graphql/mutations/ci/catalog/resources/destroy_spec.rb new file mode 100644 index 00000000000..3b278f973b7 --- /dev/null +++ b/spec/requests/api/graphql/mutations/ci/catalog/resources/destroy_spec.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'CatalogResourceDestroy', feature_category: :pipeline_composition do + include GraphqlHelpers + + let_it_be(:current_user) { create(:user) } + let_it_be(:project) { create(:project, :catalog_resource_with_components) } + let_it_be(:catalog_resource) { create(:ci_catalog_resource, project: project) } + + let(:mutation) do + variables = { + project_path: project.full_path + } + graphql_mutation(:catalog_resources_destroy, variables, + <<-QL.strip_heredoc + errors + QL + ) + end + + context 'when unauthorized' do + it_behaves_like 'a mutation that returns a top-level access error' + end + + context 'when authorized' do + before do + catalog_resource.project.add_owner(current_user) + end + + it 'destroys the catalog resource' do + expect(project.catalog_resource).to eq(catalog_resource) + + post_graphql_mutation(mutation, current_user: current_user) + + expect(project.reload.catalog_resource).to be_nil + expect_graphql_errors_to_be_empty + end + end +end diff --git a/spec/requests/api/graphql/mutations/ci/catalog/unpublish_spec.rb b/spec/requests/api/graphql/mutations/ci/catalog/unpublish_spec.rb deleted file mode 100644 index 07465777263..00000000000 --- a/spec/requests/api/graphql/mutations/ci/catalog/unpublish_spec.rb +++ /dev/null @@ -1,52 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe 'CatalogResourceUnpublish', feature_category: :pipeline_composition do - include GraphqlHelpers - - let_it_be(:current_user) { create(:user) } - let_it_be_with_reload(:resource) { create(:ci_catalog_resource) } - - let(:mutation) do - graphql_mutation( - :catalog_resource_unpublish, - id: resource.to_gid.to_s - ) - end - - subject(:post_query) { post_graphql_mutation(mutation, current_user: current_user) } - - context 'when unauthorized' do - it_behaves_like 'a mutation that returns a top-level access error' - end - - context 'when authorized' do - before_all do - resource.project.add_owner(current_user) - end - - context 'when the catalog resource is in published state' do - it 'updates the state to draft' do - resource.update!(state: :published) - expect(resource.state).to eq('published') - - post_query - - expect(resource.reload.state).to eq('draft') - expect_graphql_errors_to_be_empty - end - end - - context 'when the catalog resource is already in draft state' do - it 'leaves the state as draft' do - expect(resource.state).to eq('draft') - - post_query - - expect(resource.reload.state).to eq('draft') - expect_graphql_errors_to_be_empty - end - end - end -end diff --git a/spec/requests/api/graphql/mutations/ci/runner/create_spec.rb b/spec/requests/api/graphql/mutations/ci/runner/create_spec.rb index b697b9f73b7..567ef12df2b 100644 --- a/spec/requests/api/graphql/mutations/ci/runner/create_spec.rb +++ b/spec/requests/api/graphql/mutations/ci/runner/create_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe 'RunnerCreate', feature_category: :runner_fleet do +RSpec.describe 'RunnerCreate', feature_category: :fleet_visibility do include GraphqlHelpers let_it_be(:user) { create(:user) } diff --git a/spec/requests/api/graphql/mutations/ci/runners_registration_token/reset_spec.rb b/spec/requests/api/graphql/mutations/ci/runners_registration_token/reset_spec.rb index 752242c3ab3..ef752448966 100644 --- a/spec/requests/api/graphql/mutations/ci/runners_registration_token/reset_spec.rb +++ b/spec/requests/api/graphql/mutations/ci/runners_registration_token/reset_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe 'RunnersRegistrationTokenReset', feature_category: :runner_fleet do +RSpec.describe 'RunnersRegistrationTokenReset', feature_category: :fleet_visibility do include GraphqlHelpers let(:mutation) { graphql_mutation(:runners_registration_token_reset, input) } diff --git a/spec/requests/api/graphql/mutations/container_registry/protection/rule/create_spec.rb b/spec/requests/api/graphql/mutations/container_registry/protection/rule/create_spec.rb index 0c708c3dc41..71b8c99c1c0 100644 --- a/spec/requests/api/graphql/mutations/container_registry/protection/rule/create_spec.rb +++ b/spec/requests/api/graphql/mutations/container_registry/protection/rule/create_spec.rb @@ -15,7 +15,7 @@ RSpec.describe 'Creating the container registry protection rule', :aggregate_fai let(:kwargs) do { project_path: project.full_path, - container_path_pattern: container_registry_protection_rule_attributes.container_path_pattern, + repository_path_pattern: container_registry_protection_rule_attributes.repository_path_pattern, push_protected_up_to_access_level: 'MAINTAINER', delete_protected_up_to_access_level: 'MAINTAINER' } @@ -26,7 +26,7 @@ RSpec.describe 'Creating the container registry protection rule', :aggregate_fai <<~QUERY containerRegistryProtectionRule { id - containerPathPattern + repositoryPathPattern } clientMutationId errors @@ -48,7 +48,7 @@ RSpec.describe 'Creating the container registry protection rule', :aggregate_fai 'errors' => be_blank, 'containerRegistryProtectionRule' => { 'id' => be_present, - 'containerPathPattern' => kwargs[:container_path_pattern] + 'repositoryPathPattern' => kwargs[:repository_path_pattern] } ) end @@ -57,7 +57,7 @@ RSpec.describe 'Creating the container registry protection rule', :aggregate_fai expect { subject }.to change { ::ContainerRegistry::Protection::Rule.count }.by(1) expect(::ContainerRegistry::Protection::Rule.where(project: project, - container_path_pattern: kwargs[:container_path_pattern])).to exist + repository_path_pattern: kwargs[:repository_path_pattern])).to exist end end @@ -84,9 +84,9 @@ RSpec.describe 'Creating the container registry protection rule', :aggregate_fai } end - context 'with invalid input field `containerPathPattern`' do + context 'with invalid input field `repositoryPathPattern`' do let(:kwargs) do - super().merge(container_path_pattern: '') + super().merge(repository_path_pattern: '') end it_behaves_like 'an erroneous response' @@ -95,7 +95,7 @@ RSpec.describe 'Creating the container registry protection rule', :aggregate_fai it { subject.tap do - expect(mutation_response['errors']).to eq ["Container path pattern can't be blank"] + expect(mutation_response['errors']).to eq ["Repository path pattern can't be blank"] end } end @@ -108,9 +108,9 @@ RSpec.describe 'Creating the container registry protection rule', :aggregate_fai context 'when container name pattern is slightly different' do let(:kwargs) do - # The field `container_path_pattern` is unique; this is why we change the value in a minimum way + # The field `repository_path_pattern` is unique; this is why we change the value in a minimum way super().merge( - container_path_pattern: "#{existing_container_registry_protection_rule.container_path_pattern}-unique" + repository_path_pattern: "#{existing_container_registry_protection_rule.repository_path_pattern}-unique" ) end @@ -121,9 +121,9 @@ RSpec.describe 'Creating the container registry protection rule', :aggregate_fai end end - context 'when field `container_path_pattern` is taken' do + context 'when field `repository_path_pattern` is taken' do let(:kwargs) do - super().merge(container_path_pattern: existing_container_registry_protection_rule.container_path_pattern, + super().merge(repository_path_pattern: existing_container_registry_protection_rule.repository_path_pattern, push_protected_up_to_access_level: 'MAINTAINER') end @@ -134,12 +134,12 @@ RSpec.describe 'Creating the container registry protection rule', :aggregate_fai it 'returns without error' do subject - expect(mutation_response['errors']).to eq ['Container path pattern has already been taken'] + expect(mutation_response['errors']).to eq ['Repository path pattern has already been taken'] end it 'does not create new container protection rules' do expect(::ContainerRegistry::Protection::Rule.where(project: project, - container_path_pattern: kwargs[:container_path_pattern], + repository_path_pattern: kwargs[:repository_path_pattern], push_protected_up_to_access_level: Gitlab::Access::MAINTAINER)).not_to exist end end diff --git a/spec/requests/api/graphql/mutations/container_registry/protection/rule/delete_spec.rb b/spec/requests/api/graphql/mutations/container_registry/protection/rule/delete_spec.rb new file mode 100644 index 00000000000..dd661c302ff --- /dev/null +++ b/spec/requests/api/graphql/mutations/container_registry/protection/rule/delete_spec.rb @@ -0,0 +1,102 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Deleting a container registry protection rule', :aggregate_failures, feature_category: :container_registry do + include GraphqlHelpers + + let_it_be(:project) { create(:project, :repository) } + let_it_be_with_refind(:container_protection_rule) do + create(:container_registry_protection_rule, project: project) + end + + let_it_be(:current_user) { create(:user, maintainer_projects: [project]) } + + let(:mutation) { graphql_mutation(:delete_container_registry_protection_rule, input) } + let(:mutation_response) { graphql_mutation_response(:delete_container_registry_protection_rule) } + let(:input) { { id: container_protection_rule.to_global_id } } + + subject(:post_graphql_mutation_delete_container_registry_protection_rule) do + post_graphql_mutation(mutation, current_user: current_user) + end + + shared_examples 'an erroneous response' do + it { post_graphql_mutation_delete_container_registry_protection_rule.tap { expect(mutation_response).to be_blank } } + + it do + expect { post_graphql_mutation_delete_container_registry_protection_rule } + .not_to change { ::ContainerRegistry::Protection::Rule.count } + end + end + + it_behaves_like 'a working GraphQL mutation' + + it 'responds with deleted container registry protection rule' do + expect { post_graphql_mutation_delete_container_registry_protection_rule } + .to change { ::ContainerRegistry::Protection::Rule.count }.from(1).to(0) + + expect_graphql_errors_to_be_empty + + expect(mutation_response).to include( + 'errors' => be_blank, + 'containerRegistryProtectionRule' => { + 'id' => container_protection_rule.to_global_id.to_s, + 'repositoryPathPattern' => container_protection_rule.repository_path_pattern, + 'deleteProtectedUpToAccessLevel' => container_protection_rule.delete_protected_up_to_access_level.upcase, + 'pushProtectedUpToAccessLevel' => container_protection_rule.push_protected_up_to_access_level.upcase + } + ) + end + + context 'with existing container registry protection rule belonging to other project' do + let_it_be(:container_protection_rule) do + create(:container_registry_protection_rule, repository_path_pattern: 'protection_rule_other_project') + end + + it_behaves_like 'an erroneous response' + + it { is_expected.tap { expect_graphql_errors_to_include(/you don't have permission to perform this action/) } } + end + + context 'with deleted container registry protection rule' do + let!(:container_protection_rule) do + create(:container_registry_protection_rule, project: project, + repository_path_pattern: 'protection_rule_deleted').destroy! + end + + it_behaves_like 'an erroneous response' + + it { is_expected.tap { expect_graphql_errors_to_include(/you don't have permission to perform this action/) } } + end + + context 'when current_user does not have permission' do + let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } } + let_it_be(:reporter) { create(:user).tap { |u| project.add_reporter(u) } } + let_it_be(:guest) { create(:user).tap { |u| project.add_guest(u) } } + let_it_be(:anonymous) { create(:user) } + + where(:current_user) do + [ref(:developer), ref(:reporter), ref(:guest), ref(:anonymous)] + end + + with_them do + it_behaves_like 'an erroneous response' + + it { is_expected.tap { expect_graphql_errors_to_include(/you don't have permission to perform this action/) } } + end + end + + context "when feature flag ':container_registry_protected_containers' disabled" do + before do + stub_feature_flags(container_registry_protected_containers: false) + end + + it_behaves_like 'an erroneous response' + + it do + post_graphql_mutation_delete_container_registry_protection_rule + + expect_graphql_errors_to_include(/'container_registry_protected_containers' feature flag is disabled/) + end + end +end diff --git a/spec/requests/api/graphql/mutations/container_registry/protection/rule/update_spec.rb b/spec/requests/api/graphql/mutations/container_registry/protection/rule/update_spec.rb new file mode 100644 index 00000000000..cd2c8b9f0a2 --- /dev/null +++ b/spec/requests/api/graphql/mutations/container_registry/protection/rule/update_spec.rb @@ -0,0 +1,143 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Updating the container registry protection rule', :aggregate_failures, feature_category: :container_registry do + include GraphqlHelpers + + let_it_be(:project) { create(:project) } + let_it_be_with_reload(:container_registry_protection_rule) do + create(:container_registry_protection_rule, project: project, push_protected_up_to_access_level: :developer) + end + + let_it_be(:current_user) { create(:user, maintainer_projects: [project]) } + + let(:container_registry_protection_rule_attributes) do + build_stubbed(:container_registry_protection_rule, project: project) + end + + let(:mutation) do + graphql_mutation(:update_container_registry_protection_rule, input, + <<~QUERY + containerRegistryProtectionRule { + repositoryPathPattern + deleteProtectedUpToAccessLevel + pushProtectedUpToAccessLevel + } + clientMutationId + errors + QUERY + ) + end + + let(:input) do + { + id: container_registry_protection_rule.to_global_id, + repository_path_pattern: "#{container_registry_protection_rule.repository_path_pattern}-updated", + delete_protected_up_to_access_level: 'OWNER', + push_protected_up_to_access_level: 'MAINTAINER' + } + end + + let(:mutation_response) { graphql_mutation_response(:update_container_registry_protection_rule) } + + subject { post_graphql_mutation(mutation, current_user: current_user) } + + shared_examples 'a successful response' do + it { subject.tap { expect_graphql_errors_to_be_empty } } + + it 'returns the updated container registry protection rule' do + subject + + expect(mutation_response).to include( + 'containerRegistryProtectionRule' => { + 'repositoryPathPattern' => input[:repository_path_pattern], + 'deleteProtectedUpToAccessLevel' => input[:delete_protected_up_to_access_level], + 'pushProtectedUpToAccessLevel' => input[:push_protected_up_to_access_level] + } + ) + end + + it do + subject.tap do + expect(container_registry_protection_rule.reload).to have_attributes( + repository_path_pattern: input[:repository_path_pattern], + push_protected_up_to_access_level: input[:push_protected_up_to_access_level].downcase + ) + end + end + end + + shared_examples 'an erroneous reponse' do + it { subject.tap { expect(mutation_response).to be_blank } } + it { expect { subject }.not_to change { container_registry_protection_rule.reload.updated_at } } + end + + it_behaves_like 'a successful response' + + context 'with other existing container registry protection rule with same repository_path_pattern' do + let_it_be_with_reload(:other_existing_container_registry_protection_rule) do + create(:container_registry_protection_rule, project: project, + repository_path_pattern: "#{container_registry_protection_rule.repository_path_pattern}-other") + end + + let(:input) do + super().merge(repository_path_pattern: other_existing_container_registry_protection_rule.repository_path_pattern) + end + + it { is_expected.tap { expect_graphql_errors_to_be_empty } } + + it 'returns a blank container registry protection rule' do + is_expected.tap { expect(mutation_response['containerRegistryProtectionRule']).to be_blank } + end + + it 'includes error message in response' do + is_expected.tap { expect(mutation_response['errors']).to eq ['Repository path pattern has already been taken'] } + end + end + + context 'with invalid input param `pushProtectedUpToAccessLevel`' do + let(:input) { super().merge(push_protected_up_to_access_level: nil) } + + it_behaves_like 'an erroneous reponse' + + it { is_expected.tap { expect_graphql_errors_to_include(/pushProtectedUpToAccessLevel can't be blank/) } } + end + + context 'with invalid input param `repositoryPathPattern`' do + let(:input) { super().merge(repository_path_pattern: '') } + + it_behaves_like 'an erroneous reponse' + + it { is_expected.tap { expect_graphql_errors_to_include(/repositoryPathPattern can't be blank/) } } + end + + context 'when current_user does not have permission' do + let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } } + let_it_be(:reporter) { create(:user).tap { |u| project.add_reporter(u) } } + let_it_be(:guest) { create(:user).tap { |u| project.add_guest(u) } } + let_it_be(:anonymous) { create(:user) } + + where(:current_user) do + [ref(:developer), ref(:reporter), ref(:guest), ref(:anonymous)] + end + + with_them do + it { is_expected.tap { expect_graphql_errors_to_include(/you don't have permission to perform this action/) } } + end + end + + context "when feature flag ':container_registry_protected_containers' disabled" do + before do + stub_feature_flags(container_registry_protected_containers: false) + end + + it_behaves_like 'an erroneous reponse' + + it 'returns error of disabled feature flag' do + is_expected.tap do + expect_graphql_errors_to_include(/'container_registry_protected_containers' feature flag is disabled/) + end + end + end +end diff --git a/spec/requests/api/graphql/mutations/merge_requests/set_assignees_spec.rb b/spec/requests/api/graphql/mutations/merge_requests/set_assignees_spec.rb index cb7bac771b3..1bd239ecd87 100644 --- a/spec/requests/api/graphql/mutations/merge_requests/set_assignees_spec.rb +++ b/spec/requests/api/graphql/mutations/merge_requests/set_assignees_spec.rb @@ -127,7 +127,7 @@ RSpec.describe 'Setting assignees of a merge request', :assume_throttled, featur context 'when passing append as true' do let(:mode) { Types::MutationOperationModeEnum.enum[:append] } let(:input) { { assignee_usernames: [assignee2.username], operation_mode: mode } } - let(:db_query_limit) { 23 } + let(:db_query_limit) { 25 } before do # In CE, APPEND is a NOOP as you can't have multiple assignees @@ -147,7 +147,7 @@ RSpec.describe 'Setting assignees of a merge request', :assume_throttled, featur end context 'when passing remove as true' do - let(:db_query_limit) { 31 } + let(:db_query_limit) { 33 } let(:mode) { Types::MutationOperationModeEnum.enum[:remove] } let(:input) { { assignee_usernames: [assignee.username], operation_mode: mode } } let(:expected_result) { [] } diff --git a/spec/requests/api/graphql/mutations/namespace/package_settings/update_spec.rb b/spec/requests/api/graphql/mutations/namespace/package_settings/update_spec.rb index 738dc3078e7..05c1a2d96d9 100644 --- a/spec/requests/api/graphql/mutations/namespace/package_settings/update_spec.rb +++ b/spec/requests/api/graphql/mutations/namespace/package_settings/update_spec.rb @@ -22,7 +22,8 @@ RSpec.describe 'Updating the package settings', feature_category: :package_regis npm_package_requests_forwarding: true, lock_npm_package_requests_forwarding: true, pypi_package_requests_forwarding: true, - lock_pypi_package_requests_forwarding: true + lock_pypi_package_requests_forwarding: true, + nuget_symbol_server_enabled: true } end @@ -42,6 +43,7 @@ RSpec.describe 'Updating the package settings', feature_category: :package_regis lockNpmPackageRequestsForwarding pypiPackageRequestsForwarding lockPypiPackageRequestsForwarding + nugetSymbolServerEnabled } errors QL @@ -70,6 +72,7 @@ RSpec.describe 'Updating the package settings', feature_category: :package_regis expect(package_settings_response['lockPypiPackageRequestsForwarding']).to eq(params[:lock_pypi_package_requests_forwarding]) expect(package_settings_response['npmPackageRequestsForwarding']).to eq(params[:npm_package_requests_forwarding]) expect(package_settings_response['lockNpmPackageRequestsForwarding']).to eq(params[:lock_npm_package_requests_forwarding]) + expect(package_settings_response['nugetSymbolServerEnabled']).to eq(params[:nuget_symbol_server_enabled]) end end @@ -111,7 +114,8 @@ RSpec.describe 'Updating the package settings', feature_category: :package_regis npm_package_requests_forwarding: nil, lock_npm_package_requests_forwarding: false, pypi_package_requests_forwarding: nil, - lock_pypi_package_requests_forwarding: false + lock_pypi_package_requests_forwarding: false, + nuget_symbol_server_enabled: false }, to: { maven_duplicates_allowed: false, maven_duplicate_exception_regex: 'foo-.*', @@ -124,7 +128,8 @@ RSpec.describe 'Updating the package settings', feature_category: :package_regis npm_package_requests_forwarding: true, lock_npm_package_requests_forwarding: true, pypi_package_requests_forwarding: true, - lock_pypi_package_requests_forwarding: true + lock_pypi_package_requests_forwarding: true, + nuget_symbol_server_enabled: true } it_behaves_like 'returning a success' diff --git a/spec/requests/api/graphql/mutations/organizations/create_spec.rb b/spec/requests/api/graphql/mutations/organizations/create_spec.rb index ac6b04104ba..8ab80685822 100644 --- a/spec/requests/api/graphql/mutations/organizations/create_spec.rb +++ b/spec/requests/api/graphql/mutations/organizations/create_spec.rb @@ -4,20 +4,24 @@ require 'spec_helper' RSpec.describe Mutations::Organizations::Create, feature_category: :cell do include GraphqlHelpers + include WorkhorseHelpers let_it_be(:user) { create(:user) } let(:mutation) { graphql_mutation(:organization_create, params) } let(:name) { 'Name' } let(:path) { 'path' } + let(:description) { nil } + let(:avatar) { fixture_file_upload("spec/fixtures/dk.png") } let(:params) do { name: name, - path: path + path: path, + avatar: avatar } end - subject(:create_organization) { post_graphql_mutation(mutation, current_user: current_user) } + subject(:create_organization) { post_graphql_mutation_with_uploads(mutation, current_user: current_user) } it { expect(described_class).to require_graphql_authorizations(:create_organization) } @@ -27,6 +31,7 @@ RSpec.describe Mutations::Organizations::Create, feature_category: :cell do context 'when the user does not have permission' do let(:current_user) { nil } + let(:avatar) { nil } it_behaves_like 'a mutation that returns a top-level access error' @@ -48,17 +53,35 @@ RSpec.describe Mutations::Organizations::Create, feature_category: :cell do end end - it 'creates an organization' do - expect { create_organization }.to change { Organizations::Organization.count }.by(1) + shared_examples 'creating an organization' do + it 'creates an organization' do + expect { create_organization }.to change { Organizations::Organization.count }.by(1) + end + + it 'returns the new organization' do + create_organization + + expect(graphql_data_at(:organization_create, :organization)).to match a_hash_including( + 'name' => name, + 'path' => path, + 'description' => description + ) + end end - it 'returns the new organization' do - create_organization + context 'with description' do + let(:description) { 'Organization description' } + let(:params) do + { + name: name, + path: path, + description: description + } + end - expect(graphql_data_at(:organization_create, :organization)).to match a_hash_including( - 'name' => name, - 'path' => path - ) + include_examples 'creating an organization' end + + include_examples 'creating an organization' end end diff --git a/spec/requests/api/graphql/mutations/organizations/update_spec.rb b/spec/requests/api/graphql/mutations/organizations/update_spec.rb new file mode 100644 index 00000000000..4e819c280d0 --- /dev/null +++ b/spec/requests/api/graphql/mutations/organizations/update_spec.rb @@ -0,0 +1,120 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Mutations::Organizations::Update, feature_category: :cell do + include GraphqlHelpers + include WorkhorseHelpers + + let_it_be(:user) { create(:user) } + let_it_be_with_reload(:organization) do + create(:organization) { |org| create(:organization_user, organization: org, user: user) } + end + + let(:mutation) { graphql_mutation(:organization_update, params) } + let(:name) { 'Name' } + let(:path) { 'path' } + let(:description) { 'org-description' } + let(:avatar) { nil } + let(:params) do + { + id: organization.to_global_id.to_s, + name: name, + path: path, + description: description, + avatar: avatar + } + end + + subject(:update_organization) { post_graphql_mutation_with_uploads(mutation, current_user: current_user) } + + it { expect(described_class).to require_graphql_authorizations(:admin_organization) } + + def mutation_response + graphql_mutation_response(:organization_update) + end + + context 'when the user does not have permission' do + let(:current_user) { nil } + + it_behaves_like 'a mutation that returns a top-level access error' + + it 'does not update the organization' do + initial_name = organization.name + initial_path = organization.path + + update_organization + organization.reset + + expect(organization.name).to eq(initial_name) + expect(organization.path).to eq(initial_path) + end + end + + context 'when the user has permission' do + let(:current_user) { user } + + context 'when the params are invalid' do + let(:name) { '' } + + it 'returns the validation error' do + update_organization + + expect(mutation_response).to include('errors' => ["Name can't be blank"]) + end + end + + context 'when single attribute is update' do + using RSpec::Parameterized::TableSyntax + + where(attribute: %w[name path description]) + + with_them do + let(:value) { "new-#{attribute}" } + let(:attribute_hash) { { attribute => value } } + let(:params) { { id: organization.to_global_id.to_s }.merge(attribute_hash) } + + it 'updates the given field' do + update_organization + + expect(graphql_data_at(:organization_update, :organization)).to match a_hash_including(attribute_hash) + expect(mutation_response['errors']).to be_empty + end + end + end + + it 'returns the updated organization' do + update_organization + + expect(graphql_data_at(:organization_update, :organization)).to match a_hash_including( + 'name' => name, + 'path' => path, + 'description' => description + ) + expect(mutation_response['errors']).to be_empty + end + + context 'with a new avatar' do + let(:filename) { 'spec/fixtures/dk.png' } + let(:avatar) { fixture_file_upload(filename) } + + it 'returns the updated organization' do + update_organization + + expect( + graphql_data_at(:organization_update, :organization) + ).to( + match( + a_hash_including( + 'name' => name, + 'path' => path, + 'description' => description + ) + ) + ) + expect(File.basename(organization.reload.avatar.file.file)).to eq(File.basename(filename)) + expect(mutation_response['errors']).to be_empty + end + end + end +end diff --git a/spec/requests/api/graphql/mutations/packages/bulk_destroy_spec.rb b/spec/requests/api/graphql/mutations/packages/bulk_destroy_spec.rb index d0980a2b43d..084958be1fb 100644 --- a/spec/requests/api/graphql/mutations/packages/bulk_destroy_spec.rb +++ b/spec/requests/api/graphql/mutations/packages/bulk_destroy_spec.rb @@ -38,6 +38,26 @@ RSpec.describe 'Destroying multiple packages', feature_category: :package_regist end it_behaves_like 'returning response status', :success + + context 'when npm package' do + let_it_be_with_reload(:packages1) { create_list(:npm_package, 3, project: project1, name: 'test-package-1') } + let_it_be_with_reload(:packages2) { create_list(:npm_package, 2, project: project2, name: 'test-package-2') } + + it 'enqueues the worker to sync a metadata cache' do + arguments = [] + + expect(Packages::Npm::CreateMetadataCacheWorker) + .to receive(:bulk_perform_async_with_contexts).and_wrap_original do |original_method, *args| + packages = args.first + arguments = packages.map(&args.second[:arguments_proc]).uniq + original_method.call(*args) + end + + mutation_request + + expect(arguments).to contain_exactly([project1.id, 'test-package-1'], [project2.id, 'test-package-2']) + end + end end shared_examples 'denying the mutation request' do diff --git a/spec/requests/api/graphql/mutations/packages/destroy_spec.rb b/spec/requests/api/graphql/mutations/packages/destroy_spec.rb index 86167e7116f..6e0e5bd8aae 100644 --- a/spec/requests/api/graphql/mutations/packages/destroy_spec.rb +++ b/spec/requests/api/graphql/mutations/packages/destroy_spec.rb @@ -35,6 +35,17 @@ RSpec.describe 'Destroying a package', feature_category: :package_registry do .to change { ::Packages::Package.pending_destruction.count }.by(1) end + context 'when npm package' do + let_it_be_with_reload(:package) { create(:npm_package) } + + it 'enqueues the worker to sync a metadata cache' do + expect(Packages::Npm::CreateMetadataCacheWorker) + .to receive(:perform_async).with(project.id, package.name) + + mutation_request + end + end + it_behaves_like 'returning response status', :success end diff --git a/spec/requests/api/graphql/mutations/packages/protection/rule/delete_spec.rb b/spec/requests/api/graphql/mutations/packages/protection/rule/delete_spec.rb index 1d94d520674..6c300f8ce57 100644 --- a/spec/requests/api/graphql/mutations/packages/protection/rule/delete_spec.rb +++ b/spec/requests/api/graphql/mutations/packages/protection/rule/delete_spec.rb @@ -15,7 +15,7 @@ RSpec.describe 'Deleting a package protection rule', :aggregate_failures, featur subject { post_graphql_mutation(mutation, current_user: current_user) } - shared_examples 'an erroneous reponse' do + shared_examples 'an erroneous response' do it { subject.tap { expect(mutation_response).to be_blank } } it { expect { subject }.not_to change { ::Packages::Protection::Rule.count } } end @@ -44,7 +44,7 @@ RSpec.describe 'Deleting a package protection rule', :aggregate_failures, featur create(:package_protection_rule, package_name_pattern: 'protection_rule_other_project') end - it_behaves_like 'an erroneous reponse' + it_behaves_like 'an erroneous response' it { subject.tap { expect_graphql_errors_to_include(/you don't have permission to perform this action/) } } end @@ -54,7 +54,7 @@ RSpec.describe 'Deleting a package protection rule', :aggregate_failures, featur create(:package_protection_rule, project: project, package_name_pattern: 'protection_rule_deleted').destroy! end - it_behaves_like 'an erroneous reponse' + it_behaves_like 'an erroneous response' it { subject.tap { expect_graphql_errors_to_include(/you don't have permission to perform this action/) } } end @@ -70,7 +70,7 @@ RSpec.describe 'Deleting a package protection rule', :aggregate_failures, featur end with_them do - it_behaves_like 'an erroneous reponse' + it_behaves_like 'an erroneous response' it { subject.tap { expect_graphql_errors_to_include(/you don't have permission to perform this action/) } } end @@ -81,7 +81,7 @@ RSpec.describe 'Deleting a package protection rule', :aggregate_failures, featur stub_feature_flags(packages_protected_packages: false) end - it_behaves_like 'an erroneous reponse' + it_behaves_like 'an erroneous response' it { subject.tap { expect_graphql_errors_to_include(/'packages_protected_packages' feature flag is disabled/) } } end diff --git a/spec/requests/api/graphql/mutations/packages/protection/rule/update_spec.rb b/spec/requests/api/graphql/mutations/packages/protection/rule/update_spec.rb new file mode 100644 index 00000000000..efc919062d6 --- /dev/null +++ b/spec/requests/api/graphql/mutations/packages/protection/rule/update_spec.rb @@ -0,0 +1,134 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Updating the packages protection rule', :aggregate_failures, feature_category: :package_registry do + include GraphqlHelpers + + let_it_be(:project) { create(:project) } + let_it_be_with_reload(:package_protection_rule) do + create(:package_protection_rule, project: project, push_protected_up_to_access_level: :developer) + end + + let_it_be(:current_user) { create(:user, maintainer_projects: [project]) } + + let(:package_protection_rule_attributes) { build_stubbed(:package_protection_rule, project: project) } + + let(:mutation) do + graphql_mutation(:update_packages_protection_rule, input, + <<~QUERY + packageProtectionRule { + packageNamePattern + pushProtectedUpToAccessLevel + } + clientMutationId + errors + QUERY + ) + end + + let(:input) do + { + id: package_protection_rule.to_global_id, + package_name_pattern: "#{package_protection_rule.package_name_pattern}-updated", + push_protected_up_to_access_level: 'MAINTAINER' + } + end + + let(:mutation_response) { graphql_mutation_response(:update_packages_protection_rule) } + + subject { post_graphql_mutation(mutation, current_user: current_user) } + + shared_examples 'a successful response' do + it { subject.tap { expect_graphql_errors_to_be_empty } } + + it 'returns the updated package protection rule' do + subject + + expect(mutation_response).to include( + 'packageProtectionRule' => { + 'packageNamePattern' => input[:package_name_pattern], + 'pushProtectedUpToAccessLevel' => input[:push_protected_up_to_access_level] + } + ) + end + + it do + subject.tap do + expect(package_protection_rule.reload).to have_attributes( + package_name_pattern: input[:package_name_pattern], + push_protected_up_to_access_level: input[:push_protected_up_to_access_level].downcase + ) + end + end + end + + shared_examples 'an erroneous response' do + it { subject.tap { expect(mutation_response).to be_blank } } + it { expect { subject }.not_to change { package_protection_rule.reload.updated_at } } + end + + it_behaves_like 'a successful response' + + context 'with other existing package protection rule with same package_name_pattern' do + let_it_be_with_reload(:other_existing_package_protection_rule) do + create(:package_protection_rule, project: project, + package_name_pattern: "#{package_protection_rule.package_name_pattern}-other") + end + + let(:input) { super().merge(package_name_pattern: other_existing_package_protection_rule.package_name_pattern) } + + it { is_expected.tap { expect_graphql_errors_to_be_empty } } + + it 'returns a blank package protection rule' do + is_expected.tap { expect(mutation_response['packageProtectionRule']).to be_blank } + end + + it 'includes error message in response' do + is_expected.tap { expect(mutation_response['errors']).to eq ['Package name pattern has already been taken'] } + end + end + + context 'with invalid input param `pushProtectedUpToAccessLevel`' do + let(:input) { super().merge(push_protected_up_to_access_level: nil) } + + it_behaves_like 'an erroneous response' + + it { is_expected.tap { expect_graphql_errors_to_include(/pushProtectedUpToAccessLevel can't be blank/) } } + end + + context 'with invalid input param `packageNamePattern`' do + let(:input) { super().merge(package_name_pattern: '') } + + it_behaves_like 'an erroneous response' + + it { is_expected.tap { expect_graphql_errors_to_include(/packageNamePattern can't be blank/) } } + end + + context 'when current_user does not have permission' do + let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } } + let_it_be(:reporter) { create(:user).tap { |u| project.add_reporter(u) } } + let_it_be(:guest) { create(:user).tap { |u| project.add_guest(u) } } + let_it_be(:anonymous) { create(:user) } + + where(:current_user) do + [ref(:developer), ref(:reporter), ref(:guest), ref(:anonymous)] + end + + with_them do + it { is_expected.tap { expect_graphql_errors_to_include(/you don't have permission to perform this action/) } } + end + end + + context "when feature flag ':packages_protected_packages' disabled" do + before do + stub_feature_flags(packages_protected_packages: false) + end + + it_behaves_like 'an erroneous response' + + it 'returns error of disabled feature flag' do + is_expected.tap { expect_graphql_errors_to_include(/'packages_protected_packages' feature flag is disabled/) } + end + end +end diff --git a/spec/requests/api/graphql/mutations/user_preferences/update_spec.rb b/spec/requests/api/graphql/mutations/user_preferences/update_spec.rb index 65b8083c74f..b1cd3259eeb 100644 --- a/spec/requests/api/graphql/mutations/user_preferences/update_spec.rb +++ b/spec/requests/api/graphql/mutations/user_preferences/update_spec.rb @@ -12,7 +12,8 @@ RSpec.describe Mutations::UserPreferences::Update, feature_category: :user_profi let(:input) do { 'issuesSort' => sort_value, - 'visibilityPipelineIdType' => 'IID' + 'visibilityPipelineIdType' => 'IID', + 'useWebIdeExtensionMarketplace' => true } end @@ -26,19 +27,26 @@ RSpec.describe Mutations::UserPreferences::Update, feature_category: :user_profi expect(response).to have_gitlab_http_status(:success) expect(mutation_response['userPreferences']['issuesSort']).to eq(sort_value) expect(mutation_response['userPreferences']['visibilityPipelineIdType']).to eq('IID') + expect(mutation_response['userPreferences']['useWebIdeExtensionMarketplace']).to eq(true) expect(current_user.user_preference.persisted?).to eq(true) expect(current_user.user_preference.issues_sort).to eq(Types::IssueSortEnum.values[sort_value].value.to_s) expect(current_user.user_preference.visibility_pipeline_id_type).to eq('iid') + expect(current_user.user_preference.use_web_ide_extension_marketplace).to eq(true) end end context 'when user has existing preference' do - before do - current_user.create_user_preference!( + let(:init_user_preference) do + { issues_sort: Types::IssueSortEnum.values['TITLE_DESC'].value, - visibility_pipeline_id_type: 'id' - ) + visibility_pipeline_id_type: 'id', + use_web_ide_extension_marketplace: true + } + end + + before do + current_user.create_user_preference!(init_user_preference) end it 'updates the existing value' do @@ -53,5 +61,29 @@ RSpec.describe Mutations::UserPreferences::Update, feature_category: :user_profi expect(current_user.user_preference.issues_sort).to eq(Types::IssueSortEnum.values[sort_value].value.to_s) expect(current_user.user_preference.visibility_pipeline_id_type).to eq('iid') end + + context 'when input has nil attributes' do + let(:input) do + { + 'issuesSort' => nil, + 'visibilityPipelineIdType' => nil, + 'useWebIdeExtensionMarketplace' => nil + } + end + + it 'updates only nullable attributes' do + post_graphql_mutation(mutation, current_user: current_user) + + current_user.user_preference.reload + + expect(current_user.user_preference).to have_attributes({ + # These are nullable and are exepcted to change + issues_sort: nil, + # These should not have changed + visibility_pipeline_id_type: init_user_preference[:visibility_pipeline_id_type], + use_web_ide_extension_marketplace: init_user_preference[:use_web_ide_extension_marketplace] + }) + end + end end end 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 deleted file mode 100644 index b1828de046f..00000000000 --- a/spec/requests/api/graphql/mutations/work_items/delete_task_spec.rb +++ /dev/null @@ -1,79 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe "Delete a task in a work item's description", feature_category: :team_planning do - include GraphqlHelpers - - let_it_be(:project) { create(:project) } - let_it_be(:developer) { create(:user).tap { |user| project.add_developer(user) } } - let_it_be(:task) { create(:work_item, :task, project: project, author: developer) } - let_it_be(:work_item, refind: true) do - create(:work_item, project: project, description: "- [ ] #{task.to_reference}+", lock_version: 3) - end - - before_all do - create(:issue_link, source_id: work_item.id, target_id: task.id) - end - - let(:lock_version) { work_item.lock_version } - let(:input) do - { - 'id' => work_item.to_global_id.to_s, - 'lockVersion' => lock_version, - 'taskData' => { - 'id' => task.to_global_id.to_s, - 'lineNumberStart' => 1, - 'lineNumberEnd' => 1 - } - } - end - - let(:mutation) { graphql_mutation(:workItemDeleteTask, input) } - let(:mutation_response) { graphql_mutation_response(:work_item_delete_task) } - - context 'the user is not allowed to update a work item' do - let(:current_user) { create(:user) } - - it_behaves_like 'a mutation that returns a top-level access error' - end - - context 'when user can update the description but not delete the task' do - let(:current_user) { create(:user).tap { |u| project.add_developer(u) } } - - it_behaves_like 'a mutation that returns a top-level access error' - end - - context 'when user has permissions to remove a task' do - let(:current_user) { developer } - - it 'removes the task from the work item' do - expect do - post_graphql_mutation(mutation, current_user: current_user) - work_item.reload - end.to change(WorkItem, :count).by(-1).and( - change(IssueLink, :count).by(-1) - ).and( - change(work_item, :description).from("- [ ] #{task.to_reference}+").to("- [ ] #{task.title}") - ) - - expect(response).to have_gitlab_http_status(:success) - expect(mutation_response['workItem']).to include('id' => work_item.to_global_id.to_s) - end - - context 'when removing the task fails' do - let(:lock_version) { 2 } - - it 'makes no changes to the DB and returns an error message' do - expect do - post_graphql_mutation(mutation, current_user: current_user) - work_item.reload - end.to not_change(WorkItem, :count).and( - not_change(work_item, :description) - ) - - expect(mutation_response['errors']).to contain_exactly('Stale work item. Check lock version') - end - end - end -end diff --git a/spec/requests/api/graphql/organizations/organization_query_spec.rb b/spec/requests/api/graphql/organizations/organization_query_spec.rb index c243e0613ad..c485e3b170d 100644 --- a/spec/requests/api/graphql/organizations/organization_query_spec.rb +++ b/spec/requests/api/graphql/organizations/organization_query_spec.rb @@ -23,7 +23,8 @@ RSpec.describe 'getting organization information', feature_category: :cell do let_it_be(:organization_user) { create(:organization_user) } let_it_be(:organization) { organization_user.organization } let_it_be(:user) { organization_user.user } - let_it_be(:public_group) { create(:group, name: 'public-group', organization: organization) } + let_it_be(:parent_group) { create(:group, name: 'parent-group', organization: organization) } + let_it_be(:public_group) { create(:group, name: 'public-group', parent: parent_group, organization: organization) } let_it_be(:other_group) { create(:group, name: 'other-group', organization: organization) } let_it_be(:outside_organization_group) { create(:group) } @@ -74,6 +75,12 @@ RSpec.describe 'getting organization information', feature_category: :cell do end end + it 'does not return ancestors of authorized groups' do + request_organization + + expect(groups.pluck('id')).not_to include(parent_group.to_global_id.to_s) + end + context 'when requesting organization user' do let(:organization_fields) do <<~FIELDS diff --git a/spec/requests/api/graphql/project/alert_management/integrations_spec.rb b/spec/requests/api/graphql/project/alert_management/integrations_spec.rb index e48db541e1f..c4d3a217027 100644 --- a/spec/requests/api/graphql/project/alert_management/integrations_spec.rb +++ b/spec/requests/api/graphql/project/alert_management/integrations_spec.rb @@ -67,7 +67,7 @@ RSpec.describe 'getting Alert Management Integrations', feature_category: :incid 'name' => 'Prometheus', 'active' => prometheus_integration.manual_configuration?, 'token' => project_alerting_setting.token, - 'url' => "http://localhost/#{project.full_path}/prometheus/alerts/notify.json", + 'url' => "http://#{Gitlab.config.gitlab.host}/#{project.full_path}/prometheus/alerts/notify.json", 'apiUrl' => prometheus_integration.api_url ) ] diff --git a/spec/requests/api/graphql/project/cluster_agents_spec.rb b/spec/requests/api/graphql/project/cluster_agents_spec.rb index 181f21001ea..104f4f41cba 100644 --- a/spec/requests/api/graphql/project/cluster_agents_spec.rb +++ b/spec/requests/api/graphql/project/cluster_agents_spec.rb @@ -22,7 +22,7 @@ RSpec.describe 'Project.cluster_agents', feature_category: :deployment_managemen end before do - allow(Gitlab::Kas::Client).to receive(:new).and_return(double(get_connected_agents: [])) + allow(Gitlab::Kas::Client).to receive(:new).and_return(double(get_connected_agents_by_agent_ids: [])) end it 'can retrieve cluster agents' do @@ -87,7 +87,7 @@ RSpec.describe 'Project.cluster_agents', feature_category: :deployment_managemen let(:cluster_agents_fields) { [:id, query_nodes(:connections, [:connection_id, :connected_at, metadata_fields])] } before do - allow(Gitlab::Kas::Client).to receive(:new).and_return(double(get_connected_agents: [connected_agent])) + allow(Gitlab::Kas::Client).to receive(:new).and_return(double(get_connected_agents_by_agent_ids: [connected_agent])) end it 'can retrieve connections and agent metadata' do diff --git a/spec/requests/api/graphql/project/value_streams_spec.rb b/spec/requests/api/graphql/project/value_streams_spec.rb new file mode 100644 index 00000000000..01e937c1e47 --- /dev/null +++ b/spec/requests/api/graphql/project/value_streams_spec.rb @@ -0,0 +1,105 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Project.value_streams', feature_category: :value_stream_management do + include GraphqlHelpers + + let_it_be(:project) { create(:project) } + let_it_be(:user) { create(:user) } + + let(:query) do + <<~QUERY + query($fullPath: ID!) { + project(fullPath: $fullPath) { + valueStreams { + nodes { + name + stages { + name + startEventIdentifier + endEventIdentifier + } + } + } + } + } + QUERY + end + + context 'when user has permissions to read value streams' do + let(:expected_value_stream) do + { + 'project' => { + 'valueStreams' => { + 'nodes' => [ + { + 'name' => 'default', + 'stages' => expected_stages + } + ] + } + } + } + end + + let(:expected_stages) do + [ + { + 'name' => 'issue', + 'startEventIdentifier' => 'ISSUE_CREATED', + 'endEventIdentifier' => 'ISSUE_STAGE_END' + }, + { + 'name' => 'plan', + 'startEventIdentifier' => 'PLAN_STAGE_START', + 'endEventIdentifier' => 'ISSUE_FIRST_MENTIONED_IN_COMMIT' + }, + { + 'name' => 'code', + 'startEventIdentifier' => 'CODE_STAGE_START', + 'endEventIdentifier' => 'MERGE_REQUEST_CREATED' + }, + { + 'name' => 'test', + 'startEventIdentifier' => 'MERGE_REQUEST_LAST_BUILD_STARTED', + 'endEventIdentifier' => 'MERGE_REQUEST_LAST_BUILD_FINISHED' + }, + { + 'name' => 'review', + 'startEventIdentifier' => 'MERGE_REQUEST_CREATED', + 'endEventIdentifier' => 'MERGE_REQUEST_MERGED' + }, + { + 'name' => 'staging', + 'startEventIdentifier' => 'MERGE_REQUEST_MERGED', + 'endEventIdentifier' => 'MERGE_REQUEST_FIRST_DEPLOYED_TO_PRODUCTION' + } + ] + end + + before_all do + project.add_guest(user) + end + + before do + post_graphql(query, current_user: user, variables: { fullPath: project.full_path }) + end + + it_behaves_like 'a working graphql query' + + it 'returns only `default` value stream' do + expect(graphql_data).to eq(expected_value_stream) + end + end + + context 'when user does not have permission to read value streams' do + before do + post_graphql(query, current_user: user, variables: { fullPath: project.full_path }) + end + + it 'returns nil' do + expect(graphql_data_at(:project, :valueStreams)).to be_nil + end + end +end diff --git a/spec/requests/api/graphql/project/work_item_state_counts_spec.rb b/spec/requests/api/graphql/project/work_item_state_counts_spec.rb new file mode 100644 index 00000000000..d13204a36b7 --- /dev/null +++ b/spec/requests/api/graphql/project/work_item_state_counts_spec.rb @@ -0,0 +1,123 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'getting Work Item counts by state', feature_category: :portfolio_management do + include GraphqlHelpers + + let_it_be(:current_user) { create(:user) } + let_it_be(:group) { create(:group, :private) } + let_it_be(:project) { create(:project, :repository, :private, group: group) } + let_it_be(:work_item_opened1) { create(:work_item, project: project, title: 'Foo') } + let_it_be(:work_item_opened2) { create(:work_item, project: project, author: current_user) } + let_it_be(:work_item_closed) { create(:work_item, :closed, project: project, description: 'Bar') } + + let(:params) { {} } + + subject(:query_counts) { post_graphql(query, current_user: current_user) } + + context 'with work items count data' do + let(:work_item_counts) { graphql_data.dig('project', 'workItemStateCounts') } + + context 'with project permissions' do + before_all do + group.add_developer(current_user) + end + + it_behaves_like 'a working graphql query' do + before do + query_counts + end + end + + it 'returns the correct counts for each state' do + query_counts + + expect(work_item_counts).to eq( + 'all' => 3, + 'opened' => 2, + 'closed' => 1 + ) + end + + context 'when other work items are present in the group' do + it 'only returns counts for work items in the current project' do + other_project = create(:project, :repository, group: group) + create(:work_item, project: other_project) + query_counts + + expect(work_item_counts).to eq( + 'all' => 3, + 'opened' => 2, + 'closed' => 1 + ) + end + end + + context 'when filters are provided' do + context 'when filtering by author username' do + let(:params) { { 'authorUsername' => current_user.username } } + + it 'returns the correct counts for each status' do + query_counts + + expect(work_item_counts).to eq( + 'all' => 1, + 'opened' => 1, + 'closed' => 0 + ) + end + end + + context 'when searching in title' do + let(:params) { { search: 'Foo', in: [:TITLE] } } + + it 'returns the correct counts for each status' do + query_counts + + expect(work_item_counts).to eq( + 'all' => 1, + 'opened' => 1, + 'closed' => 0 + ) + end + end + + context 'when searching in description' do + let(:params) { { search: 'Bar', in: [:DESCRIPTION] } } + + it 'returns the correct counts for each status' do + query_counts + + expect(work_item_counts).to eq( + 'all' => 1, + 'opened' => 0, + 'closed' => 1 + ) + end + end + end + end + + context 'without project permissions' do + it 'does not return work item counts' do + query_counts + + expect_graphql_errors_to_be_empty + expect(work_item_counts).to be_nil + end + end + end + + def query(args: params) + fields = <<~QUERY + #{all_graphql_fields_for('WorkItemStateCountsType'.classify)} + QUERY + + graphql_query_for( + 'project', + { 'fullPath' => project.full_path }, + query_graphql_field('workItemStateCounts', args, fields) + ) + end +end diff --git a/spec/requests/api/graphql/project/work_item_types_spec.rb b/spec/requests/api/graphql/project/work_item_types_spec.rb index c31a260c4b8..086db983760 100644 --- a/spec/requests/api/graphql/project/work_item_types_spec.rb +++ b/spec/requests/api/graphql/project/work_item_types_spec.rb @@ -5,56 +5,19 @@ require 'spec_helper' RSpec.describe 'getting a list of work item types for a project', feature_category: :team_planning do include GraphqlHelpers - let_it_be(:developer) { create(:user) } let_it_be(:project) { create(:project) } + let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } } - before_all do - project.add_developer(developer) - end - - let(:current_user) { developer } - - let(:fields) do - <<~GRAPHQL - workItemTypes{ - nodes { id name iconName } - } - GRAPHQL - end - - let(:query) do - graphql_query_for( - 'project', - { 'fullPath' => project.full_path }, - fields - ) - end - - context 'when user has access to the project' do - before do - post_graphql(query, current_user: current_user) - end + it_behaves_like 'graphql work item type list request spec' do + let(:current_user) { developer } + let(:parent_key) { :project } - it_behaves_like 'a working graphql query' - - it 'returns all default work item types' do - expect(graphql_data.dig('project', 'workItemTypes', 'nodes')).to match_array( - WorkItems::Type.default.map do |type| - hash_including('id' => type.to_global_id.to_s, 'name' => type.name, 'iconName' => type.icon_name) - end + let(:query) do + graphql_query_for( + 'project', + { 'fullPath' => project.full_path }, + query_nodes('WorkItemTypes', work_item_type_fields) ) 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/work_item_spec.rb b/spec/requests/api/graphql/work_item_spec.rb index 36a27abd982..fe77b7ae736 100644 --- a/spec/requests/api/graphql/work_item_spec.rb +++ b/spec/requests/api/graphql/work_item_spec.rb @@ -104,6 +104,18 @@ RSpec.describe 'Query.work_item(id)', feature_category: :team_planning do end end + context 'when querying work item type information' do + include_context 'with work item types request context' + + let(:work_item_fields) { "workItemType { #{work_item_type_fields} }" } + + it 'returns work item type information' do + expect(work_item_data['workItemType']).to match( + expected_work_item_type_response(work_item.work_item_type).first + ) + end + end + context 'when querying widgets' do describe 'description widget' do let(:work_item_fields) do diff --git a/spec/requests/api/graphql/work_items_by_reference_spec.rb b/spec/requests/api/graphql/work_items_by_reference_spec.rb new file mode 100644 index 00000000000..ad2303a81e7 --- /dev/null +++ b/spec/requests/api/graphql/work_items_by_reference_spec.rb @@ -0,0 +1,130 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'find work items by reference', feature_category: :portfolio_management do + include GraphqlHelpers + + let_it_be(:current_user) { create(:user) } + let_it_be(:project) { create(:project, :repository, :public) } + let_it_be(:group2) { create(:group, :public) } + let_it_be(:project2) { create(:project, :repository, :public, group: group2) } + let_it_be(:private_project2) { create(:project, :repository, :private, group: group2) } + let_it_be(:work_item) { create(:work_item, :task, project: project2) } + let_it_be(:private_work_item) { create(:work_item, :task, project: private_project2) } + + let(:references) { [work_item.to_reference(full: true), private_work_item.to_reference(full: true)] } + + shared_examples 'response with matching work items' do + it 'returns accessible work item' do + post_graphql(query, current_user: current_user) + + expected_items = items.map { |item| a_graphql_entity_for(item) } + expect(graphql_data_at('workItemsByReference', 'nodes')).to match(expected_items) + end + end + + context 'when user has access only to public work items' do + it_behaves_like 'a working graphql query that returns data' do + before do + post_graphql(query, current_user: current_user) + end + end + + it_behaves_like 'response with matching work items' do + let(:items) { [work_item] } + end + + it 'avoids N+1 queries', :use_sql_query_cache do + post_graphql(query, current_user: current_user) # warm up + + control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do + post_graphql(query, current_user: current_user) + end + expect(graphql_data_at('workItemsByReference', 'nodes').size).to eq(1) + + extra_work_items = create_list(:work_item, 2, :task, project: project2) + refs = references + extra_work_items.map { |item| item.to_reference(full: true) } + + expect do + post_graphql(query(refs: refs), current_user: current_user) + end.not_to exceed_all_query_limit(control_count) + expect(graphql_data_at('workItemsByReference', 'nodes').size).to eq(3) + end + end + + context 'when user has access to work items in private project' do + before_all do + private_project2.add_guest(current_user) + end + + it_behaves_like 'response with matching work items' do + let(:items) { [private_work_item, work_item] } + end + end + + context 'when refs includes links' do + let_it_be(:work_item_with_url) { create(:work_item, :task, project: project2) } + let(:references) { [work_item.to_reference(full: true), Gitlab::UrlBuilder.build(work_item_with_url)] } + + it_behaves_like 'response with matching work items' do + let(:items) { [work_item_with_url, work_item] } + end + end + + context 'when refs includes a short reference present in the context project' do + let_it_be(:same_project_work_item) { create(:work_item, :task, project: project) } + let(:references) { ["##{same_project_work_item.iid}"] } + + it_behaves_like 'response with matching work items' do + let(:items) { [same_project_work_item] } + end + end + + context 'when user cannot access context namespace' do + it 'returns error' do + post_graphql(query(namespace_path: private_project2.full_path), current_user: current_user) + + expect(graphql_data_at('workItemsByReference')).to be_nil + expect(graphql_errors).to contain_exactly(a_hash_including( + 'message' => a_string_including("you don't have permission to perform this action"), + 'path' => %w[workItemsByReference] + )) + end + end + + context 'when the context is a group' do + it 'returns empty result' do + group2.add_guest(current_user) + post_graphql(query(namespace_path: group2.full_path), current_user: current_user) + + expect_graphql_errors_to_be_empty + expect(graphql_data_at('workItemsByReference', 'nodes')).to be_empty + end + end + + context 'when there are more than the max allowed references' do + let(:references_limit) { ::Resolvers::WorkItemReferencesResolver::REFERENCES_LIMIT } + let(:references) { (0..references_limit).map { |n| "##{n}" } } + let(:error_msg) do + "Number of references exceeds the limit. " \ + "Please provide no more than #{references_limit} references at the same time." + end + + it 'returns an error message' do + post_graphql(query, current_user: current_user) + + expect_graphql_errors_to_include(error_msg) + end + end + + def query(namespace_path: project.full_path, refs: references) + fields = <<~GRAPHQL + nodes { + #{all_graphql_fields_for('WorkItem', max_depth: 2)} + } + GRAPHQL + + graphql_query_for('workItemsByReference', { contextNamespacePath: namespace_path, refs: refs }, fields) + end +end |