summaryrefslogtreecommitdiff
path: root/spec/requests/api/graphql
diff options
context:
space:
mode:
Diffstat (limited to 'spec/requests/api/graphql')
-rw-r--r--spec/requests/api/graphql/achievements/user_achievements_query_spec.rb80
-rw-r--r--spec/requests/api/graphql/ci/ci_cd_setting_spec.rb1
-rw-r--r--spec/requests/api/graphql/ci/config_variables_spec.rb4
-rw-r--r--spec/requests/api/graphql/ci/group_variables_spec.rb2
-rw-r--r--spec/requests/api/graphql/ci/inherited_ci_variables_spec.rb108
-rw-r--r--spec/requests/api/graphql/ci/instance_variables_spec.rb2
-rw-r--r--spec/requests/api/graphql/ci/job_spec.rb3
-rw-r--r--spec/requests/api/graphql/ci/jobs_spec.rb186
-rw-r--r--spec/requests/api/graphql/ci/manual_variables_spec.rb2
-rw-r--r--spec/requests/api/graphql/ci/project_variables_spec.rb2
-rw-r--r--spec/requests/api/graphql/ci/runner_spec.rb350
-rw-r--r--spec/requests/api/graphql/ci/runners_spec.rb31
-rw-r--r--spec/requests/api/graphql/current_user/todos_query_spec.rb2
-rw-r--r--spec/requests/api/graphql/current_user_query_spec.rb2
-rw-r--r--spec/requests/api/graphql/custom_emoji_query_spec.rb2
-rw-r--r--spec/requests/api/graphql/group/data_transfer_spec.rb115
-rw-r--r--spec/requests/api/graphql/group/dependency_proxy_blobs_spec.rb10
-rw-r--r--spec/requests/api/graphql/group/labels_query_spec.rb19
-rw-r--r--spec/requests/api/graphql/group/milestones_spec.rb6
-rw-r--r--spec/requests/api/graphql/issues_spec.rb34
-rw-r--r--spec/requests/api/graphql/jobs_query_spec.rb41
-rw-r--r--spec/requests/api/graphql/metrics/dashboard/annotations_spec.rb16
-rw-r--r--spec/requests/api/graphql/metrics/dashboard_query_spec.rb15
-rw-r--r--spec/requests/api/graphql/multiplexed_queries_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/achievements/award_spec.rb106
-rw-r--r--spec/requests/api/graphql/mutations/achievements/delete_spec.rb79
-rw-r--r--spec/requests/api/graphql/mutations/achievements/revoke_spec.rb91
-rw-r--r--spec/requests/api/graphql/mutations/achievements/update_spec.rb90
-rw-r--r--spec/requests/api/graphql/mutations/admin/sidekiq_queues/delete_jobs_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/award_emojis/add_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/award_emojis/remove_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/ci/job/cancel_spec.rb (renamed from spec/requests/api/graphql/mutations/ci/job_cancel_spec.rb)4
-rw-r--r--spec/requests/api/graphql/mutations/ci/job/play_spec.rb (renamed from spec/requests/api/graphql/mutations/ci/job_play_spec.rb)6
-rw-r--r--spec/requests/api/graphql/mutations/ci/job/retry_spec.rb (renamed from spec/requests/api/graphql/mutations/ci/job_retry_spec.rb)8
-rw-r--r--spec/requests/api/graphql/mutations/ci/job/unschedule_spec.rb (renamed from spec/requests/api/graphql/mutations/ci/job_unschedule_spec.rb)2
-rw-r--r--spec/requests/api/graphql/mutations/ci/job_artifact/bulk_destroy_spec.rb197
-rw-r--r--spec/requests/api/graphql/mutations/ci/job_token_scope/add_project_spec.rb19
-rw-r--r--spec/requests/api/graphql/mutations/ci/project_ci_cd_settings_update_spec.rb108
-rw-r--r--spec/requests/api/graphql/mutations/ci/runner/create_spec.rb313
-rw-r--r--spec/requests/api/graphql/mutations/clusters/agent_tokens/agent_tokens/create_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/clusters/agents/create_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/clusters/agents/delete_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/container_repository/destroy_spec.rb4
-rw-r--r--spec/requests/api/graphql/mutations/container_repository/destroy_tags_spec.rb8
-rw-r--r--spec/requests/api/graphql/mutations/custom_emoji/create_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/custom_emoji/destroy_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/design_management/update_spec.rb77
-rw-r--r--spec/requests/api/graphql/mutations/issues/bulk_update_spec.rb68
-rw-r--r--spec/requests/api/graphql/mutations/issues/create_spec.rb1
-rw-r--r--spec/requests/api/graphql/mutations/members/groups/bulk_update_spec.rb128
-rw-r--r--spec/requests/api/graphql/mutations/members/projects/bulk_update_spec.rb18
-rw-r--r--spec/requests/api/graphql/mutations/merge_requests/set_assignees_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/metrics/dashboard/annotations/create_spec.rb15
-rw-r--r--spec/requests/api/graphql/mutations/metrics/dashboard/annotations/delete_spec.rb15
-rw-r--r--spec/requests/api/graphql/mutations/notes/create/note_spec.rb17
-rw-r--r--spec/requests/api/graphql/mutations/projects/sync_fork_spec.rb153
-rw-r--r--spec/requests/api/graphql/mutations/release_asset_links/create_spec.rb4
-rw-r--r--spec/requests/api/graphql/mutations/release_asset_links/delete_spec.rb4
-rw-r--r--spec/requests/api/graphql/mutations/release_asset_links/update_spec.rb4
-rw-r--r--spec/requests/api/graphql/mutations/releases/create_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/snippets/update_spec.rb3
-rw-r--r--spec/requests/api/graphql/mutations/user_preferences/update_spec.rb12
-rw-r--r--spec/requests/api/graphql/mutations/work_items/convert_spec.rb61
-rw-r--r--spec/requests/api/graphql/mutations/work_items/create_from_task_spec.rb3
-rw-r--r--spec/requests/api/graphql/mutations/work_items/create_spec.rb152
-rw-r--r--spec/requests/api/graphql/mutations/work_items/export_spec.rb71
-rw-r--r--spec/requests/api/graphql/mutations/work_items/update_spec.rb730
-rw-r--r--spec/requests/api/graphql/mutations/work_items/update_task_spec.rb2
-rw-r--r--spec/requests/api/graphql/namespace/projects_spec.rb2
-rw-r--r--spec/requests/api/graphql/packages/package_spec.rb25
-rw-r--r--spec/requests/api/graphql/project/alert_management/alert/metrics_dashboard_url_spec.rb25
-rw-r--r--spec/requests/api/graphql/project/alert_management/alert/notes_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/base_service_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/ci_access_authorized_agents_spec.rb122
-rw-r--r--spec/requests/api/graphql/project/cluster_agents_spec.rb5
-rw-r--r--spec/requests/api/graphql/project/commit_references_spec.rb240
-rw-r--r--spec/requests/api/graphql/project/container_repositories_spec.rb4
-rw-r--r--spec/requests/api/graphql/project/data_transfer_spec.rb112
-rw-r--r--spec/requests/api/graphql/project/environments_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/flow_metrics_spec.rb23
-rw-r--r--spec/requests/api/graphql/project/fork_details_spec.rb34
-rw-r--r--spec/requests/api/graphql/project/merge_request_spec.rb27
-rw-r--r--spec/requests/api/graphql/project/merge_requests_spec.rb27
-rw-r--r--spec/requests/api/graphql/project/milestones_spec.rb29
-rw-r--r--spec/requests/api/graphql/project/project_statistics_redirect_spec.rb78
-rw-r--r--spec/requests/api/graphql/project/release_spec.rb8
-rw-r--r--spec/requests/api/graphql/project/user_access_authorized_agents_spec.rb129
-rw-r--r--spec/requests/api/graphql/project/work_items_spec.rb132
-rw-r--r--spec/requests/api/graphql/project_query_spec.rb61
-rw-r--r--spec/requests/api/graphql/query_spec.rb36
-rw-r--r--spec/requests/api/graphql/user/user_achievements_query_spec.rb95
-rw-r--r--spec/requests/api/graphql/user_spec.rb18
-rw-r--r--spec/requests/api/graphql/work_item_spec.rb186
94 files changed, 4498 insertions, 556 deletions
diff --git a/spec/requests/api/graphql/achievements/user_achievements_query_spec.rb b/spec/requests/api/graphql/achievements/user_achievements_query_spec.rb
new file mode 100644
index 00000000000..080f375245d
--- /dev/null
+++ b/spec/requests/api/graphql/achievements/user_achievements_query_spec.rb
@@ -0,0 +1,80 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'UserAchievements', feature_category: :user_profile do
+ include GraphqlHelpers
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:group) { create(:group, :public) }
+ let_it_be(:achievement) { create(:achievement, namespace: group) }
+ let_it_be(:non_revoked_achievement1) { create(:user_achievement, achievement: achievement, user: user) }
+ let_it_be(:non_revoked_achievement2) { create(:user_achievement, :revoked, achievement: achievement, user: user) }
+ let_it_be(:fields) do
+ <<~HEREDOC
+ id
+ achievements {
+ nodes {
+ userAchievements {
+ nodes {
+ id
+ achievement {
+ id
+ }
+ user {
+ id
+ }
+ awardedByUser {
+ id
+ }
+ revokedByUser {
+ id
+ }
+ }
+ }
+ }
+ }
+ HEREDOC
+ end
+
+ let_it_be(:query) do
+ graphql_query_for('namespace', { full_path: group.full_path }, fields)
+ end
+
+ before_all do
+ group.add_guest(user)
+ end
+
+ before do
+ post_graphql(query, current_user: user)
+ end
+
+ it_behaves_like 'a working graphql query'
+
+ it 'returns all non_revoked user_achievements' do
+ expect(graphql_data_at(:namespace, :achievements, :nodes, :userAchievements, :nodes))
+ .to contain_exactly(
+ a_graphql_entity_for(non_revoked_achievement1)
+ )
+ end
+
+ it 'can lookahead to eliminate N+1 queries', :use_clean_rails_memory_store_caching do
+ control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do
+ post_graphql(query, current_user: user)
+ end.count
+
+ user2 = create(:user)
+ create(:user_achievement, achievement: achievement, user: user2)
+
+ expect { post_graphql(query, current_user: user) }.not_to exceed_all_query_limit(control_count)
+ end
+
+ context 'when the achievements feature flag is disabled' do
+ before do
+ stub_feature_flags(achievements: false)
+ post_graphql(query, current_user: user)
+ end
+
+ specify { expect(graphql_data_at(:namespace, :achievements, :nodes, :userAchievements, :nodes)).to be_empty }
+ end
+end
diff --git a/spec/requests/api/graphql/ci/ci_cd_setting_spec.rb b/spec/requests/api/graphql/ci/ci_cd_setting_spec.rb
index 95cabfea2fc..0437a30eccd 100644
--- a/spec/requests/api/graphql/ci/ci_cd_setting_spec.rb
+++ b/spec/requests/api/graphql/ci/ci_cd_setting_spec.rb
@@ -50,7 +50,6 @@ RSpec.describe 'Getting Ci Cd Setting', feature_category: :continuous_integratio
expect(settings_data['jobTokenScopeEnabled']).to eql project.ci_cd_settings.job_token_scope_enabled?
expect(settings_data['inboundJobTokenScopeEnabled']).to eql(
project.ci_cd_settings.inbound_job_token_scope_enabled?)
- expect(settings_data['optInJwt']).to eql project.ci_cd_settings.opt_in_jwt?
end
end
end
diff --git a/spec/requests/api/graphql/ci/config_variables_spec.rb b/spec/requests/api/graphql/ci/config_variables_spec.rb
index f76bb8ff837..4bad5dec684 100644
--- a/spec/requests/api/graphql/ci/config_variables_spec.rb
+++ b/spec/requests/api/graphql/ci/config_variables_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Query.project(fullPath).ciConfigVariables(sha)', feature_category: :pipeline_authoring do
+RSpec.describe 'Query.project(fullPath).ciConfigVariables(ref)', feature_category: :secrets_management do
include GraphqlHelpers
include ReactiveCachingHelpers
@@ -20,7 +20,7 @@ RSpec.describe 'Query.project(fullPath).ciConfigVariables(sha)', feature_categor
%(
query {
project(fullPath: "#{project.full_path}") {
- ciConfigVariables(sha: "#{ref}") {
+ ciConfigVariables(ref: "#{ref}") {
key
value
valueOptions
diff --git a/spec/requests/api/graphql/ci/group_variables_spec.rb b/spec/requests/api/graphql/ci/group_variables_spec.rb
index d78b30787c9..3b8eeefb707 100644
--- a/spec/requests/api/graphql/ci/group_variables_spec.rb
+++ b/spec/requests/api/graphql/ci/group_variables_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Query.group(fullPath).ciVariables', feature_category: :pipeline_authoring do
+RSpec.describe 'Query.group(fullPath).ciVariables', feature_category: :secrets_management do
include GraphqlHelpers
let_it_be(:group) { create(:group) }
diff --git a/spec/requests/api/graphql/ci/inherited_ci_variables_spec.rb b/spec/requests/api/graphql/ci/inherited_ci_variables_spec.rb
new file mode 100644
index 00000000000..3b4014c178c
--- /dev/null
+++ b/spec/requests/api/graphql/ci/inherited_ci_variables_spec.rb
@@ -0,0 +1,108 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Query.project(fullPath).inheritedCiVariables', feature_category: :secrets_management do
+ include GraphqlHelpers
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:subgroup) { create(:group, parent: group) }
+ let_it_be(:project) { create(:project, group: subgroup) }
+
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ inheritedCiVariables {
+ nodes {
+ id
+ key
+ environmentScope
+ groupName
+ groupCiCdSettingsPath
+ masked
+ protected
+ raw
+ variableType
+ }
+ }
+ }
+ }
+ )
+ end
+
+ def create_variables
+ create(:ci_group_variable, group: group)
+ create(:ci_group_variable, group: subgroup)
+ end
+
+ context 'when user is not a project maintainer' do
+ before do
+ project.add_developer(user)
+ end
+
+ it 'returns nothing' do
+ post_graphql(query, current_user: user)
+
+ expect(graphql_data.dig('project', 'inheritedCiVariables')).to be_nil
+ end
+ end
+
+ context 'when user is a project maintainer' do
+ before do
+ project.add_maintainer(user)
+ end
+
+ it "returns the project's CI variables inherited from its parent group and ancestors" do
+ group_var = create(:ci_group_variable, group: group, key: 'GROUP_VAR_A',
+ environment_scope: 'production', masked: false, protected: true, raw: true)
+
+ subgroup_var = create(:ci_group_variable, group: subgroup, key: 'SUBGROUP_VAR_B',
+ masked: true, protected: false, raw: false, variable_type: 'file')
+
+ post_graphql(query, current_user: user)
+
+ expect(graphql_data.dig('project', 'inheritedCiVariables', 'nodes')).to eq([
+ {
+ 'id' => group_var.to_global_id.to_s,
+ 'key' => 'GROUP_VAR_A',
+ 'environmentScope' => 'production',
+ 'groupName' => group.name,
+ 'groupCiCdSettingsPath' => group_var.group_ci_cd_settings_path,
+ 'masked' => false,
+ 'protected' => true,
+ 'raw' => true,
+ 'variableType' => 'ENV_VAR'
+ },
+ {
+ 'id' => subgroup_var.to_global_id.to_s,
+ 'key' => 'SUBGROUP_VAR_B',
+ 'environmentScope' => '*',
+ 'groupName' => subgroup.name,
+ 'groupCiCdSettingsPath' => subgroup_var.group_ci_cd_settings_path,
+ 'masked' => true,
+ 'protected' => false,
+ 'raw' => false,
+ 'variableType' => 'FILE'
+ }
+ ])
+ end
+
+ it 'avoids N+1 database queries' do
+ create_variables
+
+ baseline = ActiveRecord::QueryRecorder.new do
+ run_with_clean_state(query, context: { current_user: user })
+ end
+
+ create_variables
+
+ multi = ActiveRecord::QueryRecorder.new do
+ run_with_clean_state(query, context: { current_user: user })
+ end
+
+ expect(multi).not_to exceed_query_limit(baseline)
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/ci/instance_variables_spec.rb b/spec/requests/api/graphql/ci/instance_variables_spec.rb
index 5b65ae88426..a612b4c91b6 100644
--- a/spec/requests/api/graphql/ci/instance_variables_spec.rb
+++ b/spec/requests/api/graphql/ci/instance_variables_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Query.ciVariables', feature_category: :pipeline_authoring do
+RSpec.describe 'Query.ciVariables', feature_category: :secrets_management do
include GraphqlHelpers
let(:query) do
diff --git a/spec/requests/api/graphql/ci/job_spec.rb b/spec/requests/api/graphql/ci/job_spec.rb
index 8121c5e5c85..960697db239 100644
--- a/spec/requests/api/graphql/ci/job_spec.rb
+++ b/spec/requests/api/graphql/ci/job_spec.rb
@@ -52,7 +52,8 @@ RSpec.describe 'Query.project(fullPath).pipelines.job(id)', feature_category: :c
'duration' => 25,
'kind' => 'BUILD',
'queuedDuration' => 2.0,
- 'status' => job_2.status.upcase
+ 'status' => job_2.status.upcase,
+ 'failureMessage' => job_2.present.failure_message
)
end
diff --git a/spec/requests/api/graphql/ci/jobs_spec.rb b/spec/requests/api/graphql/ci/jobs_spec.rb
index 674407c0a0e..0d5ac725edd 100644
--- a/spec/requests/api/graphql/ci/jobs_spec.rb
+++ b/spec/requests/api/graphql/ci/jobs_spec.rb
@@ -1,6 +1,130 @@
# frozen_string_literal: true
require 'spec_helper'
+RSpec.describe 'Query.jobs', feature_category: :continuous_integration do
+ include GraphqlHelpers
+
+ let_it_be(:admin) { create(:admin) }
+ let_it_be(:project) { create(:project, :repository, :public) }
+ let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
+ let_it_be(:runner) { create(:ci_runner) }
+ let_it_be(:build) do
+ create(:ci_build, pipeline: pipeline, name: 'my test job', ref: 'HEAD', tag_list: %w[tag1 tag2], runner: runner)
+ end
+
+ let(:query) do
+ %(
+ query {
+ jobs {
+ nodes {
+ id
+ #{fields.join(' ')}
+ }
+ }
+ }
+ )
+ end
+
+ let(:jobs_graphql_data) { graphql_data_at(:jobs, :nodes) }
+
+ let(:fields) do
+ %w[commitPath refPath webPath browseArtifactsPath playPath tags runner{id}]
+ end
+
+ it 'returns the paths in each job of a pipeline' do
+ post_graphql(query, current_user: admin)
+
+ expect(jobs_graphql_data).to contain_exactly(
+ a_graphql_entity_for(
+ build,
+ commit_path: "/#{project.full_path}/-/commit/#{build.sha}",
+ ref_path: "/#{project.full_path}/-/commits/HEAD",
+ web_path: "/#{project.full_path}/-/jobs/#{build.id}",
+ browse_artifacts_path: "/#{project.full_path}/-/jobs/#{build.id}/artifacts/browse",
+ play_path: "/#{project.full_path}/-/jobs/#{build.id}/play",
+ tags: build.tag_list,
+ runner: a_graphql_entity_for(runner)
+ )
+ )
+ end
+
+ context 'when requesting individual fields' do
+ using RSpec::Parameterized::TableSyntax
+
+ let_it_be(:admin2) { create(:admin) }
+ let_it_be(:project2) { create(:project) }
+ let_it_be(:pipeline2) { create(:ci_pipeline, project: project2) }
+
+ where(:field) { fields }
+
+ with_them do
+ let(:fields) do
+ [field]
+ end
+
+ it 'does not generate N+1 queries', :request_store, :use_sql_query_cache do
+ # warm-up cache and so on:
+ args = { current_user: admin }
+ args2 = { current_user: admin2 }
+ post_graphql(query, **args2)
+
+ control = ActiveRecord::QueryRecorder.new(skip_cached: false) do
+ post_graphql(query, **args)
+ end
+
+ create(:ci_build, pipeline: pipeline2, name: 'my test job2', ref: 'HEAD', tag_list: %w[tag3])
+ post_graphql(query, **args)
+
+ expect { post_graphql(query, **args) }.not_to exceed_all_query_limit(control)
+ end
+ end
+ end
+end
+
+RSpec.describe 'Query.jobs.runner', feature_category: :continuous_integration do
+ include GraphqlHelpers
+
+ let_it_be(:admin) { create(:admin) }
+
+ let(:jobs_runner_graphql_data) { graphql_data_at(:jobs, :nodes, :runner) }
+ let(:query) do
+ %(
+ query {
+ jobs {
+ nodes {
+ runner{
+ id
+ adminUrl
+ description
+ }
+ }
+ }
+ }
+ )
+ end
+
+ context 'when job has no runner' do
+ let_it_be(:build) { create(:ci_build) }
+
+ it 'returns nil' do
+ post_graphql(query, current_user: admin)
+
+ expect(jobs_runner_graphql_data).to eq([nil])
+ end
+ end
+
+ context 'when job has runner' do
+ let_it_be(:runner) { create(:ci_runner) }
+ let_it_be(:build_with_runner) { create(:ci_build, runner: runner) }
+
+ it 'returns runner attributes' do
+ post_graphql(query, current_user: admin)
+
+ expect(jobs_runner_graphql_data).to contain_exactly(a_graphql_entity_for(runner, :description, 'adminUrl' => "http://localhost/admin/runners/#{runner.id}"))
+ end
+ end
+end
+
RSpec.describe 'Query.project.pipeline', feature_category: :continuous_integration do
include GraphqlHelpers
@@ -260,6 +384,68 @@ RSpec.describe 'Query.project.pipeline', feature_category: :continuous_integrati
end
end
+ describe '.jobs.runnerManager' do
+ let_it_be(:admin) { create(:admin) }
+ let_it_be(:runner_manager) { create(:ci_runner_machine, created_at: Time.current, contacted_at: Time.current) }
+ let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
+ let_it_be(:build) do
+ create(:ci_build, pipeline: pipeline, name: 'my test job', runner_manager: runner_manager)
+ end
+
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ pipeline(iid: "#{pipeline.iid}") {
+ jobs {
+ nodes {
+ id
+ name
+ runnerManager {
+ #{all_graphql_fields_for('CiRunnerManager', excluded: [:runner], max_depth: 1)}
+ }
+ }
+ }
+ }
+ }
+ }
+ )
+ end
+
+ let(:jobs_graphql_data) { graphql_data_at(:project, :pipeline, :jobs, :nodes) }
+
+ it 'returns the runner manager in each job of a pipeline' do
+ post_graphql(query, current_user: admin)
+
+ expect(jobs_graphql_data).to contain_exactly(
+ a_graphql_entity_for(
+ build,
+ name: build.name,
+ runner_manager: a_graphql_entity_for(
+ runner_manager,
+ system_id: runner_manager.system_xid,
+ created_at: runner_manager.created_at.iso8601,
+ contacted_at: runner_manager.contacted_at.iso8601,
+ status: runner_manager.status.to_s.upcase
+ )
+ )
+ )
+ end
+
+ it 'does not generate N+1 queries', :request_store, :use_sql_query_cache do
+ admin2 = create(:admin)
+
+ control = ActiveRecord::QueryRecorder.new(skip_cached: false) do
+ post_graphql(query, current_user: admin)
+ end
+
+ runner_manager2 = create(:ci_runner_machine)
+ create(:ci_build, pipeline: pipeline, name: 'my test job2', runner_manager: runner_manager2)
+
+ expect { post_graphql(query, current_user: admin2) }.not_to exceed_all_query_limit(control)
+ end
+ end
+
describe '.jobs.count' do
let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
let_it_be(:successful_job) { create(:ci_build, :success, pipeline: pipeline) }
diff --git a/spec/requests/api/graphql/ci/manual_variables_spec.rb b/spec/requests/api/graphql/ci/manual_variables_spec.rb
index 921c69e535d..47dccc0deb6 100644
--- a/spec/requests/api/graphql/ci/manual_variables_spec.rb
+++ b/spec/requests/api/graphql/ci/manual_variables_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Query.project(fullPath).pipelines.jobs.manualVariables', feature_category: :pipeline_authoring do
+RSpec.describe 'Query.project(fullPath).pipelines.jobs.manualVariables', feature_category: :secrets_management do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
diff --git a/spec/requests/api/graphql/ci/project_variables_spec.rb b/spec/requests/api/graphql/ci/project_variables_spec.rb
index 0ddcac89b34..62fc2623a0f 100644
--- a/spec/requests/api/graphql/ci/project_variables_spec.rb
+++ b/spec/requests/api/graphql/ci/project_variables_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Query.project(fullPath).ciVariables', feature_category: :pipeline_authoring do
+RSpec.describe 'Query.project(fullPath).ciVariables', feature_category: :secrets_management do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
diff --git a/spec/requests/api/graphql/ci/runner_spec.rb b/spec/requests/api/graphql/ci/runner_spec.rb
index 986e3ce9e52..52b548ce8b9 100644
--- a/spec/requests/api/graphql/ci/runner_spec.rb
+++ b/spec/requests/api/graphql/ci/runner_spec.rb
@@ -6,11 +6,13 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
include GraphqlHelpers
let_it_be(:user) { create(:user, :admin) }
- let_it_be(:group) { create(:group) }
+ let_it_be(:another_admin) { create(:user, :admin) }
+ let_it_be_with_reload(:group) { create(:group) }
let_it_be(:active_instance_runner) do
- create(:ci_runner, :instance,
+ create(:ci_runner, :instance, :with_runner_manager,
description: 'Runner 1',
+ creator: user,
contacted_at: 2.hours.ago,
active: true,
version: 'adfe156',
@@ -28,6 +30,7 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
let_it_be(:inactive_instance_runner) do
create(:ci_runner, :instance,
description: 'Runner 2',
+ creator: another_admin,
contacted_at: 1.day.ago,
active: false,
version: 'adfe157',
@@ -55,7 +58,9 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
end
let_it_be(:project1) { create(:project) }
- let_it_be(:active_project_runner) { create(:ci_runner, :project, projects: [project1]) }
+ let_it_be(:active_project_runner) do
+ create(:ci_runner, :project, :with_runner_manager, projects: [project1])
+ end
shared_examples 'runner details fetch' do
let(:query) do
@@ -77,6 +82,7 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
expect(runner_data).to match a_graphql_entity_for(
runner,
description: runner.description,
+ created_by: runner.creator ? a_graphql_entity_for(runner.creator) : nil,
created_at: runner.created_at&.iso8601,
contacted_at: runner.contacted_at&.iso8601,
version: runner.version,
@@ -85,7 +91,7 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
locked: false,
active: runner.active,
paused: !runner.active,
- status: runner.status('14.5').to_s.upcase,
+ status: runner.status.to_s.upcase,
job_execution_status: runner.builds.running.any? ? 'RUNNING' : 'IDLE',
maximum_timeout: runner.maximum_timeout,
access_level: runner.access_level.to_s.upcase,
@@ -107,15 +113,39 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
),
project_count: nil,
admin_url: "http://localhost/admin/runners/#{runner.id}",
+ edit_admin_url: "http://localhost/admin/runners/#{runner.id}/edit",
+ register_admin_url: runner.registration_available? ? "http://localhost/admin/runners/#{runner.id}/register" : nil,
user_permissions: {
'readRunner' => true,
'updateRunner' => true,
'deleteRunner' => true,
'assignRunner' => true
- }
+ },
+ managers: a_hash_including(
+ "count" => runner.runner_managers.count,
+ "nodes" => an_instance_of(Array),
+ "pageInfo" => anything
+ )
)
expect(runner_data['tagList']).to match_array runner.tag_list
end
+
+ it 'does not execute more queries per runner', :use_sql_query_cache, :aggregate_failures do
+ # warm-up license cache and so on:
+ personal_access_token = create(:personal_access_token, user: user)
+ args = { current_user: user, token: { personal_access_token: personal_access_token } }
+ post_graphql(query, **args)
+ expect(graphql_data_at(:runner)).not_to be_nil
+
+ personal_access_token = create(:personal_access_token, user: another_admin)
+ args = { current_user: another_admin, token: { personal_access_token: personal_access_token } }
+ control = ActiveRecord::QueryRecorder.new(skip_cached: false) { post_graphql(query, **args) }
+
+ create(:ci_runner, :instance, version: '14.0.0', tag_list: %w[tag5 tag6], creator: another_admin)
+ create(:ci_runner, :project, version: '14.0.1', projects: [project1], tag_list: %w[tag3 tag8], creator: another_admin)
+
+ expect { post_graphql(query, **args) }.not_to exceed_all_query_limit(control)
+ end
end
shared_examples 'retrieval with no admin url' do
@@ -135,7 +165,7 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
runner_data = graphql_data_at(:runner)
expect(runner_data).not_to be_nil
- expect(runner_data).to match a_graphql_entity_for(runner, admin_url: nil)
+ expect(runner_data).to match a_graphql_entity_for(runner, admin_url: nil, edit_admin_url: nil)
expect(runner_data['tagList']).to match_array runner.tag_list
end
end
@@ -307,6 +337,24 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
it_behaves_like 'runner details fetch'
end
+ describe 'for registration type' do
+ context 'when registered with registration token' do
+ let(:runner) do
+ create(:ci_runner, registration_type: :registration_token)
+ end
+
+ it_behaves_like 'runner details fetch'
+ end
+
+ context 'when registered with authenticated user' do
+ let(:runner) do
+ create(:ci_runner, registration_type: :authenticated_user)
+ end
+
+ it_behaves_like 'runner details fetch'
+ end
+ end
+
describe 'for group runner request' do
let(:query) do
%(
@@ -330,24 +378,110 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
end
end
- describe 'for runner with status' do
- let_it_be(:stale_runner) { create(:ci_runner, description: 'Stale runner 1', created_at: 3.months.ago) }
- let_it_be(:never_contacted_instance_runner) { create(:ci_runner, description: 'Missing runner 1', created_at: 1.month.ago, contacted_at: nil) }
-
- let(:status_fragment) do
+ describe 'ephemeralRegisterUrl' do
+ let(:runner_args) { { registration_type: :authenticated_user, creator: creator } }
+ let(:query) do
%(
- status
- legacyStatusWithExplicitVersion: status(legacyMode: "14.5")
- newStatus: status(legacyMode: null)
+ query {
+ runner(id: "#{runner.to_global_id}") {
+ ephemeralRegisterUrl
+ }
+ }
)
end
+ shared_examples 'has register url' do
+ it 'retrieves register url' do
+ post_graphql(query, current_user: user)
+ expect(graphql_data_at(:runner, :ephemeral_register_url)).to eq(expected_url)
+ end
+ end
+
+ shared_examples 'has no register url' do
+ it 'retrieves no register url' do
+ post_graphql(query, current_user: user)
+ expect(graphql_data_at(:runner, :ephemeral_register_url)).to eq(nil)
+ end
+ end
+
+ context 'with an instance runner', :freeze_time do
+ let(:creator) { user }
+ let(:runner) { create(:ci_runner, **runner_args) }
+
+ context 'with valid ephemeral registration' do
+ it_behaves_like 'has register url' do
+ let(:expected_url) { "http://localhost/admin/runners/#{runner.id}/register" }
+ end
+ end
+
+ context 'when runner ephemeral registration has expired' do
+ let(:runner) do
+ create(:ci_runner, created_at: (Ci::Runner::REGISTRATION_AVAILABILITY_TIME + 1.second).ago, **runner_args)
+ end
+
+ it_behaves_like 'has no register url'
+ end
+
+ context 'when runner has already been registered' do
+ let(:runner) { create(:ci_runner, :with_runner_manager, **runner_args) }
+
+ it_behaves_like 'has no register url'
+ end
+ end
+
+ context 'with a group runner' do
+ let(:creator) { user }
+ let(:runner) { create(:ci_runner, :group, groups: [group], **runner_args) }
+
+ context 'with valid ephemeral registration' do
+ it_behaves_like 'has register url' do
+ let(:expected_url) { "http://localhost/groups/#{group.path}/-/runners/#{runner.id}/register" }
+ end
+ end
+
+ context 'when request not from creator' do
+ let(:creator) { another_admin }
+
+ before do
+ group.add_owner(another_admin)
+ end
+
+ it_behaves_like 'has no register url'
+ end
+ end
+
+ context 'with a project runner' do
+ let(:creator) { user }
+ let(:runner) { create(:ci_runner, :project, projects: [project1], **runner_args) }
+
+ context 'with valid ephemeral registration' do
+ it_behaves_like 'has register url' do
+ let(:expected_url) { "http://localhost/#{project1.full_path}/-/runners/#{runner.id}/register" }
+ end
+ end
+
+ context 'when request not from creator' do
+ let(:creator) { another_admin }
+
+ before do
+ project1.add_owner(another_admin)
+ end
+
+ it_behaves_like 'has no register url'
+ end
+ end
+ end
+
+ describe 'for runner with status' do
+ let_it_be(:stale_runner) { create(:ci_runner, description: 'Stale runner 1', created_at: 3.months.ago) }
+ let_it_be(:never_contacted_instance_runner) { create(:ci_runner, description: 'Missing runner 1', created_at: 1.month.ago, contacted_at: nil) }
+
let(:query) do
%(
query {
- staleRunner: runner(id: "#{stale_runner.to_global_id}") { #{status_fragment} }
- pausedRunner: runner(id: "#{inactive_instance_runner.to_global_id}") { #{status_fragment} }
- neverContactedInstanceRunner: runner(id: "#{never_contacted_instance_runner.to_global_id}") { #{status_fragment} }
+ staleRunner: runner(id: "#{stale_runner.to_global_id}") { status }
+ pausedRunner: runner(id: "#{inactive_instance_runner.to_global_id}") { status }
+ neverContactedInstanceRunner: runner(id: "#{never_contacted_instance_runner.to_global_id}") { status }
}
)
end
@@ -357,23 +491,17 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
stale_runner_data = graphql_data_at(:stale_runner)
expect(stale_runner_data).to match a_hash_including(
- 'status' => 'STALE',
- 'legacyStatusWithExplicitVersion' => 'STALE',
- 'newStatus' => 'STALE'
+ 'status' => 'STALE'
)
paused_runner_data = graphql_data_at(:paused_runner)
expect(paused_runner_data).to match a_hash_including(
- 'status' => 'PAUSED',
- 'legacyStatusWithExplicitVersion' => 'PAUSED',
- 'newStatus' => 'OFFLINE'
+ 'status' => 'OFFLINE'
)
never_contacted_instance_runner_data = graphql_data_at(:never_contacted_instance_runner)
expect(never_contacted_instance_runner_data).to match a_hash_including(
- 'status' => 'NEVER_CONTACTED',
- 'legacyStatusWithExplicitVersion' => 'NEVER_CONTACTED',
- 'newStatus' => 'NEVER_CONTACTED'
+ 'status' => 'NEVER_CONTACTED'
)
end
end
@@ -568,34 +696,34 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
end
end
- context 'with request made by creator' do
+ context 'with request made by creator', :frozen_time do
let(:user) { creator }
context 'with runner created in UI' do
let(:registration_type) { :authenticated_user }
- context 'with runner created in last 3 hours' do
- let(:created_at) { (3.hours - 1.second).ago }
+ context 'with runner created in last hour' do
+ let(:created_at) { (Ci::Runner::REGISTRATION_AVAILABILITY_TIME - 1.second).ago }
- context 'with no runner machine registed yet' do
+ context 'with no runner manager registered yet' do
it_behaves_like 'an ephemeral_authentication_token'
end
- context 'with first runner machine already registed' do
- let!(:runner_machine) { create(:ci_runner_machine, runner: runner) }
+ context 'with first runner manager already registered' do
+ let!(:runner_manager) { create(:ci_runner_machine, runner: runner) }
it_behaves_like 'a protected ephemeral_authentication_token'
end
end
context 'with runner created almost too long ago' do
- let(:created_at) { (3.hours - 1.second).ago }
+ let(:created_at) { (Ci::Runner::REGISTRATION_AVAILABILITY_TIME - 1.second).ago }
it_behaves_like 'an ephemeral_authentication_token'
end
context 'with runner created too long ago' do
- let(:created_at) { 3.hours.ago }
+ let(:created_at) { Ci::Runner::REGISTRATION_AVAILABILITY_TIME.ago }
it_behaves_like 'a protected ephemeral_authentication_token'
end
@@ -604,8 +732,8 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
context 'with runner registered from command line' do
let(:registration_type) { :registration_token }
- context 'with runner created in last 3 hours' do
- let(:created_at) { (3.hours - 1.second).ago }
+ context 'with runner created in last 1 hour' do
+ let(:created_at) { (Ci::Runner::REGISTRATION_AVAILABILITY_TIME - 1.second).ago }
it_behaves_like 'a protected ephemeral_authentication_token'
end
@@ -628,6 +756,12 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
<<~SINGLE
runner(id: "#{runner.to_global_id}") {
#{all_graphql_fields_for('CiRunner', excluded: excluded_fields)}
+ createdBy {
+ id
+ username
+ webPath
+ webUrl
+ }
groups {
nodes {
id
@@ -658,7 +792,7 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
let(:active_group_runner2) { create(:ci_runner, :group) }
# Exclude fields that are already hardcoded above
- let(:excluded_fields) { %w[jobs groups projects ownerProject] }
+ let(:excluded_fields) { %w[createdBy jobs groups projects ownerProject] }
let(:single_query) do
<<~QUERY
@@ -691,6 +825,8 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
control = ActiveRecord::QueryRecorder.new { post_graphql(single_query, **args) }
+ personal_access_token = create(:personal_access_token, user: another_admin)
+ args = { current_user: another_admin, token: { personal_access_token: personal_access_token } }
expect { post_graphql(double_query, **args) }.not_to exceed_query_limit(control)
expect(graphql_data.count).to eq 6
@@ -721,20 +857,20 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
end
describe 'Query limits with jobs' do
- let!(:group1) { create(:group) }
- let!(:group2) { create(:group) }
- let!(:project1) { create(:project, :repository, group: group1) }
- let!(:project2) { create(:project, :repository, group: group1) }
- let!(:project3) { create(:project, :repository, group: group2) }
+ let_it_be(:group1) { create(:group) }
+ let_it_be(:group2) { create(:group) }
+ let_it_be(:project1) { create(:project, :repository, group: group1) }
+ let_it_be(:project2) { create(:project, :repository, group: group1) }
+ let_it_be(:project3) { create(:project, :repository, group: group2) }
- let!(:merge_request1) { create(:merge_request, source_project: project1) }
- let!(:merge_request2) { create(:merge_request, source_project: project3) }
+ let_it_be(:merge_request1) { create(:merge_request, source_project: project1) }
+ let_it_be(:merge_request2) { create(:merge_request, source_project: project3) }
let(:project_runner2) { create(:ci_runner, :project, projects: [project1, project2]) }
let!(:build1) { create(:ci_build, :success, name: 'Build One', runner: project_runner2, pipeline: pipeline1) }
- let!(:pipeline1) do
+ let_it_be(:pipeline1) do
create(:ci_pipeline, project: project1, source: :merge_request_event, merge_request: merge_request1, ref: 'main',
- target_sha: 'xxx')
+ target_sha: 'xxx')
end
let(:query) do
@@ -745,24 +881,7 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
jobs {
nodes {
id
- detailedStatus {
- id
- detailsPath
- group
- icon
- text
- }
- project {
- id
- name
- webUrl
- }
- shortSha
- commitPath
- finishedAt
- duration
- queuedDuration
- tags
+ #{field}
}
}
}
@@ -770,42 +889,69 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
QUERY
end
- it 'does not execute more queries per job', :aggregate_failures do
- # warm-up license cache and so on:
- personal_access_token = create(:personal_access_token, user: user)
- args = { current_user: user, token: { personal_access_token: personal_access_token } }
- post_graphql(query, **args)
-
- control = ActiveRecord::QueryRecorder.new(query_recorder_debug: true) { post_graphql(query, **args) }
-
- # Add a new build to project_runner2
- project_runner2.runner_projects << build(:ci_runner_project, runner: project_runner2, project: project3)
- pipeline2 = create(:ci_pipeline, project: project3, source: :merge_request_event, merge_request: merge_request2,
- ref: 'main', target_sha: 'xxx')
- build2 = create(:ci_build, :success, name: 'Build Two', runner: project_runner2, pipeline: pipeline2)
+ context 'when requesting individual fields' do
+ using RSpec::Parameterized::TableSyntax
- args[:current_user] = create(:user, :admin) # do not reuse same user
- expect { post_graphql(query, **args) }.not_to exceed_all_query_limit(control)
+ where(:field) do
+ [
+ 'detailedStatus { id detailsPath group icon text }',
+ 'project { id name webUrl }'
+ ] + %w[
+ shortSha
+ browseArtifactsPath
+ commitPath
+ playPath
+ refPath
+ webPath
+ finishedAt
+ duration
+ queuedDuration
+ tags
+ ]
+ end
- expect(graphql_data.count).to eq 1
- expect(graphql_data).to match(
- a_hash_including(
- 'runner' => a_graphql_entity_for(
- project_runner2,
- jobs: { 'nodes' => containing_exactly(a_graphql_entity_for(build1), a_graphql_entity_for(build2)) }
- )
- ))
+ with_them do
+ it 'does not execute more queries per job', :use_sql_query_cache, :aggregate_failures do
+ admin2 = create(:user, :admin) # do not reuse same user
+
+ # warm-up license cache and so on:
+ personal_access_token = create(:personal_access_token, user: user)
+ personal_access_token2 = create(:personal_access_token, user: admin2)
+ args = { current_user: user, token: { personal_access_token: personal_access_token } }
+ args2 = { current_user: admin2, token: { personal_access_token: personal_access_token2 } }
+ post_graphql(query, **args2)
+
+ control = ActiveRecord::QueryRecorder.new(skip_cached: false) { post_graphql(query, **args) }
+
+ # Add a new build to project_runner2
+ project_runner2.runner_projects << build(:ci_runner_project, runner: project_runner2, project: project3)
+ pipeline2 = create(:ci_pipeline, project: project3, source: :merge_request_event, merge_request: merge_request2,
+ ref: 'main', target_sha: 'xxx')
+ build2 = create(:ci_build, :success, name: 'Build Two', runner: project_runner2, pipeline: pipeline2)
+
+ expect { post_graphql(query, **args2) }.not_to exceed_all_query_limit(control)
+
+ expect(graphql_data.count).to eq 1
+ expect(graphql_data).to match(
+ a_hash_including(
+ 'runner' => a_graphql_entity_for(
+ project_runner2,
+ jobs: { 'nodes' => containing_exactly(a_graphql_entity_for(build1), a_graphql_entity_for(build2)) }
+ )
+ ))
+ end
+ end
end
end
describe 'sorting and pagination' do
let(:query) do
<<~GQL
- query($id: CiRunnerID!, $projectSearchTerm: String, $n: Int, $cursor: String) {
- runner(id: $id) {
- #{fields}
+ query($id: CiRunnerID!, $projectSearchTerm: String, $n: Int, $cursor: String) {
+ runner(id: $id) {
+ #{fields}
+ }
}
- }
GQL
end
@@ -824,18 +970,18 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
let(:fields) do
<<~QUERY
- projects(search: $projectSearchTerm, first: $n, after: $cursor) {
- count
- nodes {
- id
- }
- pageInfo {
- hasPreviousPage
- startCursor
- endCursor
- hasNextPage
+ projects(search: $projectSearchTerm, first: $n, after: $cursor) {
+ count
+ nodes {
+ id
+ }
+ pageInfo {
+ hasPreviousPage
+ startCursor
+ endCursor
+ hasNextPage
+ }
}
- }
QUERY
end
diff --git a/spec/requests/api/graphql/ci/runners_spec.rb b/spec/requests/api/graphql/ci/runners_spec.rb
index 75d8609dc38..c8706ae9698 100644
--- a/spec/requests/api/graphql/ci/runners_spec.rb
+++ b/spec/requests/api/graphql/ci/runners_spec.rb
@@ -11,16 +11,24 @@ RSpec.describe 'Query.runners', feature_category: :runner_fleet do
let_it_be(:instance_runner) { create(:ci_runner, :instance, version: 'abc', revision: '123', description: 'Instance runner', ip_address: '127.0.0.1') }
let_it_be(:project_runner) { create(:ci_runner, :project, active: false, version: 'def', revision: '456', description: 'Project runner', projects: [project], ip_address: '127.0.0.1') }
- let(:runners_graphql_data) { graphql_data['runners'] }
+ let(:runners_graphql_data) { graphql_data_at(:runners) }
let(:params) { {} }
let(:fields) do
<<~QUERY
nodes {
- #{all_graphql_fields_for('CiRunner', excluded: %w[ownerProject])}
+ #{all_graphql_fields_for('CiRunner', excluded: %w[createdBy ownerProject])}
+ createdBy {
+ username
+ webPath
+ webUrl
+ }
ownerProject {
id
+ path
+ fullPath
+ webUrl
}
}
QUERY
@@ -50,6 +58,25 @@ RSpec.describe 'Query.runners', feature_category: :runner_fleet do
it 'returns expected runner' do
expect(runners_graphql_data['nodes']).to contain_exactly(a_graphql_entity_for(expected_runner))
end
+
+ it 'does not execute more queries per runner', :aggregate_failures do
+ # warm-up license cache and so on:
+ personal_access_token = create(:personal_access_token, user: current_user)
+ args = { current_user: current_user, token: { personal_access_token: personal_access_token } }
+ post_graphql(query, **args)
+ expect(graphql_data_at(:runners, :nodes)).not_to be_empty
+
+ admin2 = create(:admin)
+ personal_access_token = create(:personal_access_token, user: admin2)
+ args = { current_user: admin2, token: { personal_access_token: personal_access_token } }
+ control = ActiveRecord::QueryRecorder.new { post_graphql(query, **args) }
+
+ create(:ci_runner, :instance, version: '14.0.0', tag_list: %w[tag5 tag6], creator: admin2)
+ create(:ci_runner, :project, version: '14.0.1', projects: [project], tag_list: %w[tag3 tag8],
+ creator: current_user)
+
+ expect { post_graphql(query, **args) }.not_to exceed_query_limit(control)
+ end
end
context 'runner_type is INSTANCE_TYPE and status is ACTIVE' do
diff --git a/spec/requests/api/graphql/current_user/todos_query_spec.rb b/spec/requests/api/graphql/current_user/todos_query_spec.rb
index f7e23aeb241..ee019a99f8d 100644
--- a/spec/requests/api/graphql/current_user/todos_query_spec.rb
+++ b/spec/requests/api/graphql/current_user/todos_query_spec.rb
@@ -19,7 +19,7 @@ RSpec.describe 'Query current user todos', feature_category: :source_code_manage
let(:fields) do
<<~QUERY
nodes {
- #{all_graphql_fields_for('todos'.classify, max_depth: 2)}
+ #{all_graphql_fields_for('todos'.classify, max_depth: 2, excluded: ['productAnalyticsState'])}
}
QUERY
end
diff --git a/spec/requests/api/graphql/current_user_query_spec.rb b/spec/requests/api/graphql/current_user_query_spec.rb
index 53d2580caee..aceef77920d 100644
--- a/spec/requests/api/graphql/current_user_query_spec.rb
+++ b/spec/requests/api/graphql/current_user_query_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'getting project information', feature_category: :authentication_and_authorization do
+RSpec.describe 'getting project information', feature_category: :system_access do
include GraphqlHelpers
let(:fields) do
diff --git a/spec/requests/api/graphql/custom_emoji_query_spec.rb b/spec/requests/api/graphql/custom_emoji_query_spec.rb
index 7b804623e01..1858ea831dd 100644
--- a/spec/requests/api/graphql/custom_emoji_query_spec.rb
+++ b/spec/requests/api/graphql/custom_emoji_query_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'getting custom emoji within namespace', feature_category: :not_owned do
+RSpec.describe 'getting custom emoji within namespace', feature_category: :shared do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
diff --git a/spec/requests/api/graphql/group/data_transfer_spec.rb b/spec/requests/api/graphql/group/data_transfer_spec.rb
new file mode 100644
index 00000000000..b7c038afa54
--- /dev/null
+++ b/spec/requests/api/graphql/group/data_transfer_spec.rb
@@ -0,0 +1,115 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'group data transfers', feature_category: :source_code_management do
+ include GraphqlHelpers
+
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project_1) { create(:project, group: group) }
+ let_it_be(:project_2) { create(:project, group: group) }
+
+ let(:fields) do
+ <<~QUERY
+ #{all_graphql_fields_for('GroupDataTransfer'.classify)}
+ QUERY
+ end
+
+ let(:query) do
+ graphql_query_for(
+ 'group',
+ { fullPath: group.full_path },
+ query_graphql_field('DataTransfer', params, fields)
+ )
+ end
+
+ let(:from) { Date.new(2022, 1, 1) }
+ let(:to) { Date.new(2023, 1, 1) }
+ let(:params) { { from: from, to: to } }
+ let(:egress_data) do
+ graphql_data.dig('group', 'dataTransfer', 'egressNodes', 'nodes')
+ end
+
+ before do
+ create(:project_data_transfer, project: project_1, date: '2022-01-01', repository_egress: 1)
+ create(:project_data_transfer, project: project_1, date: '2022-02-01', repository_egress: 2)
+ create(:project_data_transfer, project: project_2, date: '2022-02-01', repository_egress: 4)
+ end
+
+ subject { post_graphql(query, current_user: current_user) }
+
+ context 'with anonymous access' do
+ let_it_be(:current_user) { nil }
+
+ before do
+ subject
+ end
+
+ it_behaves_like 'a working graphql query'
+
+ it 'returns no data' do
+ expect(graphql_data_at(:group, :data_transfer)).to be_nil
+ expect(graphql_errors).to be_nil
+ end
+ end
+
+ context 'with authorized user but without enough permissions' do
+ before do
+ group.add_developer(current_user)
+ subject
+ end
+
+ it_behaves_like 'a working graphql query'
+
+ it 'returns empty results' do
+ expect(graphql_data_at(:group, :data_transfer)).to be_nil
+ expect(graphql_errors).to be_nil
+ end
+ end
+
+ context 'when user has enough permissions' do
+ before do
+ group.add_owner(current_user)
+ end
+
+ context 'when data_transfer_monitoring_mock_data is NOT enabled' do
+ before do
+ stub_feature_flags(data_transfer_monitoring_mock_data: false)
+ subject
+ end
+
+ it 'returns real results' do
+ expect(response).to have_gitlab_http_status(:ok)
+
+ expect(egress_data.count).to eq(2)
+
+ expect(egress_data.first.keys).to match_array(
+ %w[date totalEgress repositoryEgress artifactsEgress packagesEgress registryEgress]
+ )
+
+ expect(egress_data.pluck('repositoryEgress')).to match_array(%w[1 6])
+ end
+
+ it_behaves_like 'a working graphql query'
+ end
+
+ context 'when data_transfer_monitoring_mock_data is enabled' do
+ before do
+ stub_feature_flags(data_transfer_monitoring_mock_data: true)
+ subject
+ end
+
+ it 'returns mock results' do
+ expect(response).to have_gitlab_http_status(:ok)
+
+ expect(egress_data.count).to eq(12)
+ expect(egress_data.first.keys).to match_array(
+ %w[date totalEgress repositoryEgress artifactsEgress packagesEgress registryEgress]
+ )
+ end
+
+ it_behaves_like 'a working graphql query'
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/group/dependency_proxy_blobs_spec.rb b/spec/requests/api/graphql/group/dependency_proxy_blobs_spec.rb
index 2c4770a31a7..a6eb114a279 100644
--- a/spec/requests/api/graphql/group/dependency_proxy_blobs_spec.rb
+++ b/spec/requests/api/graphql/group/dependency_proxy_blobs_spec.rb
@@ -26,6 +26,7 @@ RSpec.describe 'getting dependency proxy blobs in a group', feature_category: :d
#{query_graphql_field('dependency_proxy_blobs', {}, dependency_proxy_blob_fields)}
dependencyProxyBlobCount
dependencyProxyTotalSize
+ dependencyProxyTotalSizeInBytes
GQL
end
@@ -42,6 +43,7 @@ RSpec.describe 'getting dependency proxy blobs in a group', feature_category: :d
let(:dependency_proxy_blobs_response) { graphql_data.dig('group', 'dependencyProxyBlobs', 'edges') }
let(:dependency_proxy_blob_count_response) { graphql_data.dig('group', 'dependencyProxyBlobCount') }
let(:dependency_proxy_total_size_response) { graphql_data.dig('group', 'dependencyProxyTotalSize') }
+ let(:dependency_proxy_total_size_in_bytes_response) { graphql_data.dig('group', 'dependencyProxyTotalSizeInBytes') }
before do
stub_config(dependency_proxy: { enabled: true })
@@ -121,7 +123,13 @@ RSpec.describe 'getting dependency proxy blobs in a group', feature_category: :d
it 'returns the total size' do
subject
+ expected_size = ActiveSupport::NumberHelper.number_to_human_size(blobs.inject(0) { |sum, blob| sum + blob.size })
+ expect(dependency_proxy_total_size_response).to eq(expected_size)
+ end
+
+ it 'returns the total size in bytes' do
+ subject
expected_size = blobs.inject(0) { |sum, blob| sum + blob.size }
- expect(dependency_proxy_total_size_response).to eq(ActiveSupport::NumberHelper.number_to_human_size(expected_size))
+ expect(dependency_proxy_total_size_in_bytes_response).to eq(expected_size)
end
end
diff --git a/spec/requests/api/graphql/group/labels_query_spec.rb b/spec/requests/api/graphql/group/labels_query_spec.rb
deleted file mode 100644
index 28886f8d80b..00000000000
--- a/spec/requests/api/graphql/group/labels_query_spec.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe 'getting group label information', feature_category: :team_planning do
- include GraphqlHelpers
-
- let_it_be(:group) { create(:group, :public) }
- let_it_be(:label_factory) { :group_label }
- let_it_be(:label_attrs) { { group: group } }
-
- it_behaves_like 'querying a GraphQL type with labels' do
- let(:path_prefix) { ['group'] }
-
- def make_query(fields)
- graphql_query_for('group', { full_path: group.full_path }, fields)
- end
- end
-end
diff --git a/spec/requests/api/graphql/group/milestones_spec.rb b/spec/requests/api/graphql/group/milestones_spec.rb
index 28cd68493c0..209588835f2 100644
--- a/spec/requests/api/graphql/group/milestones_spec.rb
+++ b/spec/requests/api/graphql/group/milestones_spec.rb
@@ -35,12 +35,6 @@ RSpec.describe 'Milestones through GroupQuery', feature_category: :team_planning
end
context 'when filtering by timeframe' do
- it 'fetches milestones between start_date and due_date' do
- fetch_milestones(user, { start_date: now.to_s, end_date: (now + 2.days).to_s })
-
- expect_array_response(milestone_2.to_global_id.to_s, milestone_3.to_global_id.to_s)
- end
-
it 'fetches milestones between timeframe start and end arguments' do
today = Date.today
fetch_milestones(user, { timeframe: { start: today.to_s, end: (today + 2.days).to_s } })
diff --git a/spec/requests/api/graphql/issues_spec.rb b/spec/requests/api/graphql/issues_spec.rb
index e437e1bbcb0..a12049a9b2e 100644
--- a/spec/requests/api/graphql/issues_spec.rb
+++ b/spec/requests/api/graphql/issues_spec.rb
@@ -15,6 +15,7 @@ RSpec.describe 'getting an issue list at root level', feature_category: :team_pl
let_it_be(:project_b) { create(:project, :repository, :private, group: group1) }
let_it_be(:project_c) { create(:project, :repository, :public, group: group2) }
let_it_be(:project_d) { create(:project, :repository, :private, group: group2) }
+ let_it_be(:archived_project) { create(:project, :repository, :archived, group: group2) }
let_it_be(:milestone1) { create(:milestone, project: project_c, due_date: 10.days.from_now) }
let_it_be(:milestone2) { create(:milestone, project: project_d, due_date: 20.days.from_now) }
let_it_be(:milestone3) { create(:milestone, project: project_d, due_date: 30.days.from_now) }
@@ -83,6 +84,7 @@ RSpec.describe 'getting an issue list at root level', feature_category: :team_pl
)
end
+ let_it_be(:archived_issue) { create(:issue, project: archived_project) }
let_it_be(:issues, reload: true) { [issue_a, issue_b, issue_c, issue_d, issue_e] }
# we need to always provide at least one filter to the query so it doesn't fail
let_it_be(:base_params) { { iids: issues.map { |issue| issue.iid.to_s } } }
@@ -109,6 +111,38 @@ RSpec.describe 'getting an issue list at root level', feature_category: :team_pl
end
end
+ describe 'includeArchived filter' do
+ let(:base_params) { { iids: [archived_issue.iid.to_s] } }
+
+ it 'excludes issues from archived projects' do
+ post_query
+
+ issue_ids = graphql_dig_at(graphql_data_at('issues', 'nodes'), :id)
+
+ expect(issue_ids).not_to include(archived_issue.to_gid.to_s)
+ end
+
+ context 'when includeArchived is true' do
+ let(:issue_filter_params) { { include_archived: true } }
+
+ it 'includes issues from archived projects' do
+ post_query
+
+ issue_ids = graphql_dig_at(graphql_data_at('issues', 'nodes'), :id)
+
+ expect(issue_ids).to include(archived_issue.to_gid.to_s)
+ end
+ end
+ end
+
+ it 'excludes issues from archived projects' do
+ post_query
+
+ issue_ids = graphql_dig_at(graphql_data_at('issues', 'nodes'), :id)
+
+ expect(issue_ids).not_to include(archived_issue.to_gid.to_s)
+ end
+
context 'when no filters are provided' do
let(:all_query_params) { {} }
diff --git a/spec/requests/api/graphql/jobs_query_spec.rb b/spec/requests/api/graphql/jobs_query_spec.rb
index 0aea8e4c253..7607aeac6e0 100644
--- a/spec/requests/api/graphql/jobs_query_spec.rb
+++ b/spec/requests/api/graphql/jobs_query_spec.rb
@@ -5,17 +5,26 @@ require 'spec_helper'
RSpec.describe 'getting job information', feature_category: :continuous_integration do
include GraphqlHelpers
- let_it_be(:job) { create(:ci_build, :success, name: 'job1') }
-
let(:query) do
- graphql_query_for(:jobs)
+ graphql_query_for(
+ :jobs, {}, %(
+ count
+ nodes {
+ #{all_graphql_fields_for(::Types::Ci::JobType, max_depth: 1)}
+ })
+ )
end
+ let_it_be(:runner) { create(:ci_runner) }
+ let_it_be(:job) { create(:ci_build, :success, name: 'job1', runner: runner) }
+
+ subject(:request) { post_graphql(query, current_user: current_user) }
+
context 'when user is admin' do
let_it_be(:current_user) { create(:admin) }
- it 'has full access to all jobs', :aggregate_failure do
- post_graphql(query, current_user: current_user)
+ it 'has full access to all jobs', :aggregate_failures do
+ request
expect(graphql_data_at(:jobs, :count)).to eq(1)
expect(graphql_data_at(:jobs, :nodes)).to contain_exactly(a_graphql_entity_for(job))
@@ -25,14 +34,14 @@ RSpec.describe 'getting job information', feature_category: :continuous_integrat
let_it_be(:pending_job) { create(:ci_build, :pending) }
let_it_be(:failed_job) { create(:ci_build, :failed) }
- it 'gets pending jobs', :aggregate_failure do
+ it 'gets pending jobs', :aggregate_failures do
post_graphql(graphql_query_for(:jobs, { statuses: :PENDING }), current_user: current_user)
expect(graphql_data_at(:jobs, :count)).to eq(1)
expect(graphql_data_at(:jobs, :nodes)).to contain_exactly(a_graphql_entity_for(pending_job))
end
- it 'gets pending and failed jobs', :aggregate_failure do
+ it 'gets pending and failed jobs', :aggregate_failures do
post_graphql(graphql_query_for(:jobs, { statuses: [:PENDING, :FAILED] }), current_user: current_user)
expect(graphql_data_at(:jobs, :count)).to eq(2)
@@ -40,13 +49,27 @@ RSpec.describe 'getting job information', feature_category: :continuous_integrat
a_graphql_entity_for(failed_job)])
end
end
+
+ context 'when N+1 queries' do
+ it 'avoids N+1 queries successfully', :use_sql_query_cache do
+ post_graphql(query, current_user: current_user) # warmup
+
+ control = ActiveRecord::QueryRecorder.new(skip_cached: false) do
+ post_graphql(query, current_user: current_user)
+ end
+
+ create(:ci_build, :success, name: 'job2', runner: create(:ci_runner))
+
+ expect { post_graphql(query, current_user: current_user) }.not_to exceed_all_query_limit(control)
+ end
+ end
end
context 'if the user is not an admin' do
let_it_be(:current_user) { create(:user) }
- it 'has no access to the jobs', :aggregate_failure do
- post_graphql(query, current_user: current_user)
+ it 'has no access to the jobs', :aggregate_failures do
+ request
expect(graphql_data_at(:jobs, :count)).to eq(0)
expect(graphql_data_at(:jobs, :nodes)).to match_array([])
diff --git a/spec/requests/api/graphql/metrics/dashboard/annotations_spec.rb b/spec/requests/api/graphql/metrics/dashboard/annotations_spec.rb
index 4dd47142c40..143bc1672f8 100644
--- a/spec/requests/api/graphql/metrics/dashboard/annotations_spec.rb
+++ b/spec/requests/api/graphql/metrics/dashboard/annotations_spec.rb
@@ -22,6 +22,7 @@ RSpec.describe 'Getting Metrics Dashboard Annotations', feature_category: :metri
create(:metrics_dashboard_annotation, environment: environment, starting_at: to.advance(minutes: 5), dashboard_path: path)
end
+ let(:remove_monitor_metrics) { false }
let(:args) { "from: \"#{from}\", to: \"#{to}\"" }
let(:fields) do
<<~QUERY
@@ -50,6 +51,7 @@ RSpec.describe 'Getting Metrics Dashboard Annotations', feature_category: :metri
end
before do
+ stub_feature_flags(remove_monitor_metrics: remove_monitor_metrics)
project.add_developer(current_user)
post_graphql(query, current_user: current_user)
end
@@ -85,4 +87,18 @@ RSpec.describe 'Getting Metrics Dashboard Annotations', feature_category: :metri
it_behaves_like 'a working graphql query'
end
end
+
+ context 'when metrics dashboard feature is unavailable' do
+ let(:remove_monitor_metrics) { true }
+
+ it_behaves_like 'a working graphql query'
+
+ it 'returns nil' do
+ annotations = graphql_data.dig(
+ 'project', 'environments', 'nodes', 0, 'metricsDashboard', 'annotations'
+ )
+
+ expect(annotations).to be_nil
+ end
+ end
end
diff --git a/spec/requests/api/graphql/metrics/dashboard_query_spec.rb b/spec/requests/api/graphql/metrics/dashboard_query_spec.rb
index 8db0844c6d7..b7d9b59f5fe 100644
--- a/spec/requests/api/graphql/metrics/dashboard_query_spec.rb
+++ b/spec/requests/api/graphql/metrics/dashboard_query_spec.rb
@@ -45,7 +45,10 @@ RSpec.describe 'Getting Metrics Dashboard', feature_category: :metrics do
end
context 'for user with developer access' do
+ let(:remove_monitor_metrics) { false }
+
before do
+ stub_feature_flags(remove_monitor_metrics: remove_monitor_metrics)
project.add_developer(current_user)
post_graphql(query, current_user: current_user)
end
@@ -82,6 +85,18 @@ RSpec.describe 'Getting Metrics Dashboard', feature_category: :metrics do
expect(dashboard).to eql("path" => path, "schemaValidationWarnings" => ["dashboard: can't be blank", "panel_groups: should be an array of panel_groups objects"])
end
end
+
+ context 'metrics dashboard feature is unavailable' do
+ let(:remove_monitor_metrics) { true }
+
+ it_behaves_like 'a working graphql query'
+
+ it 'returns nil' do
+ dashboard = graphql_data.dig('project', 'environments', 'nodes', 0, 'metricsDashboard')
+
+ expect(dashboard).to be_nil
+ end
+ end
end
context 'requested dashboard can not be found' do
diff --git a/spec/requests/api/graphql/multiplexed_queries_spec.rb b/spec/requests/api/graphql/multiplexed_queries_spec.rb
index 4d615d3eaa4..0a5c87ebef8 100644
--- a/spec/requests/api/graphql/multiplexed_queries_spec.rb
+++ b/spec/requests/api/graphql/multiplexed_queries_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe 'Multiplexed queries', feature_category: :not_owned do
+RSpec.describe 'Multiplexed queries', feature_category: :shared do
include GraphqlHelpers
it 'returns responses for multiple queries' do
diff --git a/spec/requests/api/graphql/mutations/achievements/award_spec.rb b/spec/requests/api/graphql/mutations/achievements/award_spec.rb
new file mode 100644
index 00000000000..9bc0751e924
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/achievements/award_spec.rb
@@ -0,0 +1,106 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Mutations::Achievements::Award, feature_category: :user_profile do
+ include GraphqlHelpers
+
+ let_it_be(:developer) { create(:user) }
+ let_it_be(:maintainer) { create(:user) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:achievement) { create(:achievement, namespace: group) }
+ let_it_be(:recipient) { create(:user) }
+
+ let(:mutation) { graphql_mutation(:achievements_award, params) }
+ let(:achievement_id) { achievement&.to_global_id }
+ let(:recipient_id) { recipient&.to_global_id }
+ let(:params) do
+ {
+ achievement_id: achievement_id,
+ user_id: recipient_id
+ }
+ end
+
+ subject { post_graphql_mutation(mutation, current_user: current_user) }
+
+ def mutation_response
+ graphql_mutation_response(:achievements_create)
+ end
+
+ before_all do
+ group.add_developer(developer)
+ group.add_maintainer(maintainer)
+ end
+
+ context 'when the user does not have permission' do
+ let(:current_user) { developer }
+
+ it_behaves_like 'a mutation that returns a top-level access error'
+
+ it 'does not create an achievement' do
+ expect { subject }.not_to change { Achievements::UserAchievement.count }
+ end
+ end
+
+ context 'when the user has permission' do
+ let(:current_user) { maintainer }
+
+ context 'when the params are invalid' do
+ let(:achievement) { nil }
+
+ it 'returns the validation error' do
+ subject
+
+ expect(graphql_errors.to_s).to include('invalid value for achievementId (Expected value to not be null)')
+ end
+ end
+
+ context 'when the recipient_id is invalid' do
+ let(:recipient_id) { "gid://gitlab/User/#{non_existing_record_id}" }
+
+ it 'returns the validation error' do
+ subject
+
+ expect(graphql_data_at(:achievements_award,
+ :errors)).to include("Couldn't find User with 'id'=#{non_existing_record_id}")
+ end
+ end
+
+ context 'when the achievement_id is invalid' do
+ let(:achievement_id) { "gid://gitlab/Achievements::Achievement/#{non_existing_record_id}" }
+
+ it 'returns the validation error' do
+ subject
+
+ expect(graphql_errors.to_s)
+ .to include("The resource that you are attempting to access does not exist or you don't have permission")
+ end
+ end
+
+ context 'when the feature flag is disabled' do
+ before do
+ stub_feature_flags(achievements: false)
+ end
+
+ it 'returns the relevant error' do
+ subject
+
+ expect(graphql_errors.to_s)
+ .to include("The resource that you are attempting to access does not exist or you don't have permission")
+ end
+ end
+
+ it 'creates an achievement' do
+ expect { subject }.to change { Achievements::UserAchievement.count }.by(1)
+ end
+
+ it 'returns the new achievement' do
+ subject
+
+ expect(graphql_data_at(:achievements_award, :user_achievement, :achievement, :id))
+ .to eq(achievement.to_global_id.to_s)
+ expect(graphql_data_at(:achievements_award, :user_achievement, :user, :id))
+ .to eq(recipient.to_global_id.to_s)
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/achievements/delete_spec.rb b/spec/requests/api/graphql/mutations/achievements/delete_spec.rb
new file mode 100644
index 00000000000..276da4f46a8
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/achievements/delete_spec.rb
@@ -0,0 +1,79 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Mutations::Achievements::Delete, feature_category: :user_profile do
+ include GraphqlHelpers
+
+ let_it_be(:developer) { create(:user) }
+ let_it_be(:maintainer) { create(:user) }
+ let_it_be(:group) { create(:group) }
+
+ let!(:achievement) { create(:achievement, namespace: group) }
+ let(:mutation) { graphql_mutation(:achievements_delete, params) }
+ let(:achievement_id) { achievement&.to_global_id }
+ let(:params) { { achievement_id: achievement_id } }
+
+ subject { post_graphql_mutation(mutation, current_user: current_user) }
+
+ def mutation_response
+ graphql_mutation_response(:achievements_delete)
+ end
+
+ before_all do
+ group.add_developer(developer)
+ group.add_maintainer(maintainer)
+ end
+
+ context 'when the user does not have permission' do
+ let(:current_user) { developer }
+
+ it_behaves_like 'a mutation that returns a top-level access error'
+
+ it 'does not revoke any achievements' do
+ expect { subject }.not_to change { Achievements::Achievement.count }
+ end
+ end
+
+ context 'when the user has permission' do
+ let(:current_user) { maintainer }
+
+ context 'when the params are invalid' do
+ let(:achievement) { nil }
+
+ it 'returns the validation error' do
+ subject
+
+ expect(graphql_errors.to_s).to include('invalid value for achievementId (Expected value to not be null)')
+ end
+ end
+
+ context 'when the achievement_id is invalid' do
+ let(:achievement_id) { "gid://gitlab/Achievements::Achievement/#{non_existing_record_id}" }
+
+ it 'returns the validation error' do
+ subject
+
+ expect(graphql_errors.to_s)
+ .to include("The resource that you are attempting to access does not exist or you don't have permission")
+ end
+ end
+
+ context 'when the feature flag is disabled' do
+ before do
+ stub_feature_flags(achievements: false)
+ end
+
+ it 'returns the relevant error' do
+ subject
+
+ expect(graphql_errors.to_s)
+ .to include("The resource that you are attempting to access does not exist or you don't have permission")
+ end
+ end
+
+ it 'deletes the achievement' do
+ expect { subject }.to change { Achievements::Achievement.count }.by(-1)
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/achievements/revoke_spec.rb b/spec/requests/api/graphql/mutations/achievements/revoke_spec.rb
new file mode 100644
index 00000000000..925a1bb9fcc
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/achievements/revoke_spec.rb
@@ -0,0 +1,91 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Mutations::Achievements::Revoke, feature_category: :user_profile do
+ include GraphqlHelpers
+
+ let_it_be(:developer) { create(:user) }
+ let_it_be(:maintainer) { create(:user) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:achievement) { create(:achievement, namespace: group) }
+ let_it_be(:user_achievement) { create(:user_achievement, achievement: achievement) }
+
+ let(:mutation) { graphql_mutation(:achievements_revoke, params) }
+ let(:user_achievement_id) { user_achievement&.to_global_id }
+ let(:params) { { user_achievement_id: user_achievement_id } }
+
+ subject { post_graphql_mutation(mutation, current_user: current_user) }
+
+ def mutation_response
+ graphql_mutation_response(:achievements_create)
+ end
+
+ before_all do
+ group.add_developer(developer)
+ group.add_maintainer(maintainer)
+ end
+
+ context 'when the user does not have permission' do
+ let(:current_user) { developer }
+
+ it_behaves_like 'a mutation that returns a top-level access error'
+
+ it 'does not revoke any achievements' do
+ expect { subject }.not_to change { Achievements::UserAchievement.where(revoked_by_user_id: nil).count }
+ end
+ end
+
+ context 'when the user has permission' do
+ let(:current_user) { maintainer }
+
+ context 'when the params are invalid' do
+ let(:user_achievement) { nil }
+
+ it 'returns the validation error' do
+ subject
+
+ expect(graphql_errors.to_s).to include('invalid value for userAchievementId (Expected value to not be null)')
+ end
+ end
+
+ context 'when the user_achievement_id is invalid' do
+ let(:user_achievement_id) { "gid://gitlab/Achievements::UserAchievement/#{non_existing_record_id}" }
+
+ it 'returns the validation error' do
+ subject
+
+ expect(graphql_errors.to_s)
+ .to include("The resource that you are attempting to access does not exist or you don't have permission")
+ end
+ end
+
+ context 'when the feature flag is disabled' do
+ before do
+ stub_feature_flags(achievements: false)
+ end
+
+ it 'returns the relevant error' do
+ subject
+
+ expect(graphql_errors.to_s)
+ .to include("The resource that you are attempting to access does not exist or you don't have permission")
+ end
+ end
+
+ it 'revokes an achievement' do
+ expect { subject }.to change { Achievements::UserAchievement.where(revoked_by_user_id: nil).count }.by(-1)
+ end
+
+ it 'returns the revoked achievement' do
+ subject
+
+ expect(graphql_data_at(:achievements_revoke, :user_achievement, :achievement, :id))
+ .to eq(achievement.to_global_id.to_s)
+ expect(graphql_data_at(:achievements_revoke, :user_achievement, :revoked_by_user, :id))
+ .to eq(current_user.to_global_id.to_s)
+ expect(graphql_data_at(:achievements_revoke, :user_achievement, :revoked_at))
+ .not_to be_nil
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/achievements/update_spec.rb b/spec/requests/api/graphql/mutations/achievements/update_spec.rb
new file mode 100644
index 00000000000..b2bb01b564c
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/achievements/update_spec.rb
@@ -0,0 +1,90 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Mutations::Achievements::Update, feature_category: :user_profile do
+ include GraphqlHelpers
+ include WorkhorseHelpers
+
+ let_it_be(:developer) { create(:user) }
+ let_it_be(:maintainer) { create(:user) }
+ let_it_be(:group) { create(:group) }
+
+ let!(:achievement) { create(:achievement, namespace: group) }
+ let(:mutation) { graphql_mutation(:achievements_update, params) }
+ let(:achievement_id) { achievement&.to_global_id }
+ let(:params) { { achievement_id: achievement_id, name: 'GitLab', avatar: avatar } }
+ let(:avatar) { nil }
+
+ subject { post_graphql_mutation_with_uploads(mutation, current_user: current_user) }
+
+ def mutation_response
+ graphql_mutation_response(:achievements_update)
+ end
+
+ before_all do
+ group.add_developer(developer)
+ group.add_maintainer(maintainer)
+ end
+
+ context 'when the user does not have permission' do
+ let(:current_user) { developer }
+
+ it_behaves_like 'a mutation that returns a top-level access error'
+
+ it 'does not update the achievement' do
+ expect { subject }.not_to change { achievement.reload.name }
+ end
+ end
+
+ context 'when the user has permission' do
+ let(:current_user) { maintainer }
+
+ context 'when the params are invalid' do
+ let(:achievement) { nil }
+
+ it 'returns the validation error' do
+ subject
+
+ expect(graphql_errors.to_s).to include('invalid value for achievementId (Expected value to not be null)')
+ end
+ end
+
+ context 'when the achievement_id is invalid' do
+ let(:achievement_id) { "gid://gitlab/Achievements::Achievement/#{non_existing_record_id}" }
+
+ it 'returns the validation error' do
+ subject
+
+ expect(graphql_errors.to_s)
+ .to include("The resource that you are attempting to access does not exist or you don't have permission")
+ end
+ end
+
+ context 'when the feature flag is disabled' do
+ before do
+ stub_feature_flags(achievements: false)
+ end
+
+ it 'returns the relevant permission error' do
+ subject
+
+ expect(graphql_errors.to_s)
+ .to include("The resource that you are attempting to access does not exist or you don't have permission")
+ end
+ end
+
+ context 'with a new avatar' do
+ let(:avatar) { fixture_file_upload("spec/fixtures/dk.png") }
+
+ it 'updates the achievement' do
+ subject
+
+ achievement.reload
+
+ expect(achievement.name).to eq('GitLab')
+ expect(achievement.avatar.file).not_to be_nil
+ end
+ 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 64ea6d32f5f..b3d25155a6f 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
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Deleting Sidekiq jobs', :clean_gitlab_redis_queues, feature_category: :not_owned do
+RSpec.describe 'Deleting Sidekiq jobs', :clean_gitlab_redis_queues, feature_category: :shared do
include GraphqlHelpers
let_it_be(:admin) { create(:admin) }
diff --git a/spec/requests/api/graphql/mutations/award_emojis/add_spec.rb b/spec/requests/api/graphql/mutations/award_emojis/add_spec.rb
index fdbff0f93cd..18cc85d36e0 100644
--- a/spec/requests/api/graphql/mutations/award_emojis/add_spec.rb
+++ b/spec/requests/api/graphql/mutations/award_emojis/add_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Adding an AwardEmoji', feature_category: :not_owned do
+RSpec.describe 'Adding an AwardEmoji', feature_category: :shared do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
diff --git a/spec/requests/api/graphql/mutations/award_emojis/remove_spec.rb b/spec/requests/api/graphql/mutations/award_emojis/remove_spec.rb
index e200bfc2d18..7ec2b061a88 100644
--- a/spec/requests/api/graphql/mutations/award_emojis/remove_spec.rb
+++ b/spec/requests/api/graphql/mutations/award_emojis/remove_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Removing an AwardEmoji', feature_category: :not_owned do
+RSpec.describe 'Removing an AwardEmoji', feature_category: :shared do
include GraphqlHelpers
let(:current_user) { create(:user) }
diff --git a/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb b/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb
index 6dba2b58357..7c6a487cdd0 100644
--- a/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb
+++ b/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Toggling an AwardEmoji', feature_category: :not_owned do
+RSpec.describe 'Toggling an AwardEmoji', feature_category: :shared do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
diff --git a/spec/requests/api/graphql/mutations/ci/job_cancel_spec.rb b/spec/requests/api/graphql/mutations/ci/job/cancel_spec.rb
index 468a9e57f56..abad1ae0812 100644
--- a/spec/requests/api/graphql/mutations/ci/job_cancel_spec.rb
+++ b/spec/requests/api/graphql/mutations/ci/job/cancel_spec.rb
@@ -15,12 +15,12 @@ RSpec.describe "JobCancel", feature_category: :continuous_integration do
id: job.to_global_id.to_s
}
graphql_mutation(:job_cancel, variables,
- <<-QL
+ <<-QL
errors
job {
id
}
- QL
+ QL
)
end
diff --git a/spec/requests/api/graphql/mutations/ci/job_play_spec.rb b/spec/requests/api/graphql/mutations/ci/job/play_spec.rb
index 9ba80e51dee..0c700248f85 100644
--- a/spec/requests/api/graphql/mutations/ci/job_play_spec.rb
+++ b/spec/requests/api/graphql/mutations/ci/job/play_spec.rb
@@ -18,7 +18,7 @@ RSpec.describe 'JobPlay', feature_category: :continuous_integration do
let(:mutation) do
graphql_mutation(:job_play, variables,
- <<-QL
+ <<-QL
errors
job {
id
@@ -28,7 +28,7 @@ RSpec.describe 'JobPlay', feature_category: :continuous_integration do
}
}
}
- QL
+ QL
)
end
@@ -63,7 +63,7 @@ RSpec.describe 'JobPlay', feature_category: :continuous_integration do
}
end
- it 'provides those variables to the job', :aggregated_errors do
+ it 'provides those variables to the job', :aggregate_failures do
expect_next_instance_of(Ci::PlayBuildService) do |instance|
expect(instance).to receive(:execute).with(an_instance_of(Ci::Build), variables[:variables]).and_call_original
end
diff --git a/spec/requests/api/graphql/mutations/ci/job_retry_spec.rb b/spec/requests/api/graphql/mutations/ci/job/retry_spec.rb
index e49ee6f3163..4114c77491b 100644
--- a/spec/requests/api/graphql/mutations/ci/job_retry_spec.rb
+++ b/spec/requests/api/graphql/mutations/ci/job/retry_spec.rb
@@ -16,12 +16,12 @@ RSpec.describe 'JobRetry', feature_category: :continuous_integration do
id: job.to_global_id.to_s
}
graphql_mutation(:job_retry, variables,
- <<-QL
+ <<-QL
errors
job {
id
}
- QL
+ QL
)
end
@@ -57,12 +57,12 @@ RSpec.describe 'JobRetry', feature_category: :continuous_integration do
}
graphql_mutation(:job_retry, variables,
- <<-QL
+ <<-QL
errors
job {
id
}
- QL
+ QL
)
end
diff --git a/spec/requests/api/graphql/mutations/ci/job_unschedule_spec.rb b/spec/requests/api/graphql/mutations/ci/job/unschedule_spec.rb
index 6868b0ea279..08e155e808b 100644
--- a/spec/requests/api/graphql/mutations/ci/job_unschedule_spec.rb
+++ b/spec/requests/api/graphql/mutations/ci/job/unschedule_spec.rb
@@ -15,7 +15,7 @@ RSpec.describe 'JobUnschedule', feature_category: :continuous_integration do
id: job.to_global_id.to_s
}
graphql_mutation(:job_unschedule, variables,
- <<-QL
+ <<-QL
errors
job {
id
diff --git a/spec/requests/api/graphql/mutations/ci/job_artifact/bulk_destroy_spec.rb b/spec/requests/api/graphql/mutations/ci/job_artifact/bulk_destroy_spec.rb
new file mode 100644
index 00000000000..4e25669a0ca
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/ci/job_artifact/bulk_destroy_spec.rb
@@ -0,0 +1,197 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'BulkDestroy', feature_category: :build_artifacts do
+ include GraphqlHelpers
+
+ let(:maintainer) { create(:user) }
+ let(:developer) { create(:user) }
+ let(:first_artifact) { create(:ci_job_artifact) }
+ let(:second_artifact) { create(:ci_job_artifact, project: project) }
+ let(:second_artifact_another_project) { create(:ci_job_artifact) }
+ let(:project) { first_artifact.job.project }
+ let(:ids) { [first_artifact.to_global_id.to_s] }
+ let(:not_authorized_project_error_message) do
+ "The resource that you are attempting to access " \
+ "does not exist or you don't have permission to perform this action"
+ end
+
+ let(:mutation) do
+ variables = {
+ project_id: project.to_global_id.to_s,
+ ids: ids
+ }
+ graphql_mutation(:bulk_destroy_job_artifacts, variables, <<~FIELDS)
+ destroyedCount
+ destroyedIds
+ errors
+ FIELDS
+ end
+
+ let(:mutation_response) { graphql_mutation_response(:bulk_destroy_job_artifacts) }
+
+ it 'fails to destroy the artifact if a user not in a project' do
+ post_graphql_mutation(mutation, current_user: maintainer)
+
+ expect(graphql_errors).to include(
+ a_hash_including('message' => not_authorized_project_error_message)
+ )
+
+ expect(first_artifact.reload).to be_persisted
+ end
+
+ context 'when the `ci_job_artifact_bulk_destroy` feature flag is disabled' do
+ before do
+ stub_feature_flags(ci_job_artifact_bulk_destroy: false)
+ project.add_maintainer(maintainer)
+ end
+
+ it 'returns a resource not available error' do
+ post_graphql_mutation(mutation, current_user: maintainer)
+
+ expect(graphql_errors).to contain_exactly(
+ hash_including(
+ 'message' => '`ci_job_artifact_bulk_destroy` feature flag is disabled.'
+ )
+ )
+ end
+ end
+
+ context "when the user is a developer in a project" do
+ before do
+ project.add_developer(developer)
+ end
+
+ it 'fails to destroy the artifact' do
+ post_graphql_mutation(mutation, current_user: developer)
+
+ expect(graphql_errors).to include(
+ a_hash_including('message' => not_authorized_project_error_message)
+ )
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(first_artifact.reload).to be_persisted
+ end
+ end
+
+ context "when the user is a maintainer in a project" do
+ before do
+ project.add_maintainer(maintainer)
+ end
+
+ shared_examples 'failing mutation' do
+ it 'rejects the request' do
+ post_graphql_mutation(mutation, current_user: maintainer)
+
+ expect(graphql_errors(mutation_response)).to include(expected_error_message)
+
+ expected_not_found_artifacts.each do |artifact|
+ expect { artifact.reload }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+
+ expected_found_artifacts.each do |artifact|
+ expect(artifact.reload).to be_persisted
+ end
+ end
+ end
+
+ it 'destroys the artifact' do
+ post_graphql_mutation(mutation, current_user: maintainer)
+
+ expect(mutation_response).to include("destroyedCount" => 1, "destroyedIds" => [gid_string(first_artifact)])
+ expect(response).to have_gitlab_http_status(:success)
+ expect { first_artifact.reload }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+
+ context "and one artifact doesn't belong to the project" do
+ let(:not_owned_artifact) { create(:ci_job_artifact) }
+ let(:ids) { [first_artifact.to_global_id.to_s, not_owned_artifact.to_global_id.to_s] }
+ let(:expected_error_message) { "Not all artifacts belong to requested project" }
+ let(:expected_not_found_artifacts) { [] }
+ let(:expected_found_artifacts) { [first_artifact, not_owned_artifact] }
+
+ it_behaves_like 'failing mutation'
+ end
+
+ context "and multiple artifacts belong to the maintainer's project" do
+ let(:ids) { [first_artifact.to_global_id.to_s, second_artifact.to_global_id.to_s] }
+
+ it 'destroys all artifacts' do
+ post_graphql_mutation(mutation, current_user: maintainer)
+
+ expect(mutation_response).to include(
+ "destroyedCount" => 2,
+ "destroyedIds" => [gid_string(first_artifact), gid_string(second_artifact)]
+ )
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect { first_artifact.reload }.to raise_error(ActiveRecord::RecordNotFound)
+ expect { second_artifact.reload }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+ end
+
+ context "and one artifact belongs to a different maintainer's project" do
+ let(:ids) { [first_artifact.to_global_id.to_s, second_artifact_another_project.to_global_id.to_s] }
+ let(:expected_found_artifacts) { [first_artifact, second_artifact_another_project] }
+ let(:expected_not_found_artifacts) { [] }
+ let(:expected_error_message) { "Not all artifacts belong to requested project" }
+
+ it_behaves_like 'failing mutation'
+ end
+
+ context "and not found" do
+ let(:ids) { [first_artifact.to_global_id.to_s, second_artifact.to_global_id.to_s] }
+ let(:not_found_ids) { expected_not_found_artifacts.map(&:id).join(',') }
+ let(:expected_error_message) { "Artifacts (#{not_found_ids}) not found" }
+
+ before do
+ expected_not_found_artifacts.each(&:destroy!)
+ end
+
+ context "with one artifact" do
+ let(:expected_not_found_artifacts) { [second_artifact] }
+ let(:expected_found_artifacts) { [first_artifact] }
+
+ it_behaves_like 'failing mutation'
+ end
+
+ context "with all artifact" do
+ let(:expected_not_found_artifacts) { [first_artifact, second_artifact] }
+ let(:expected_found_artifacts) { [] }
+
+ it_behaves_like 'failing mutation'
+ end
+ end
+
+ context 'when empty request' do
+ before do
+ project.add_maintainer(maintainer)
+ end
+
+ context 'with nil value' do
+ let(:ids) { nil }
+
+ it 'does nothing and returns empty answer' do
+ post_graphql_mutation(mutation, current_user: maintainer)
+
+ expect_graphql_errors_to_include(/was provided invalid value for ids \(Expected value to not be null\)/)
+ end
+ end
+
+ context 'with empty array' do
+ let(:ids) { [] }
+
+ it 'raises argument error' do
+ post_graphql_mutation(mutation, current_user: maintainer)
+
+ expect_graphql_errors_to_include(/IDs array of job artifacts can not be empty/)
+ end
+ end
+ end
+
+ def gid_string(object)
+ Gitlab::GlobalId.build(object, id: object.id).to_s
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/ci/job_token_scope/add_project_spec.rb b/spec/requests/api/graphql/mutations/ci/job_token_scope/add_project_spec.rb
index 55e728b2141..8791d793cb4 100644
--- a/spec/requests/api/graphql/mutations/ci/job_token_scope/add_project_spec.rb
+++ b/spec/requests/api/graphql/mutations/ci/job_token_scope/add_project_spec.rb
@@ -53,14 +53,29 @@ RSpec.describe 'CiJobTokenScopeAddProject', feature_category: :continuous_integr
before do
target_project.add_developer(current_user)
+ stub_feature_flags(frozen_outbound_job_token_scopes_override: false)
end
- it 'adds the target project to the job token scope' do
+ it 'adds the target project to the inbound job token scope' do
expect do
post_graphql_mutation(mutation, current_user: current_user)
expect(response).to have_gitlab_http_status(:success)
expect(mutation_response.dig('ciJobTokenScope', 'projects', 'nodes')).not_to be_empty
- end.to change { Ci::JobToken::ProjectScopeLink.outbound.count }.by(1)
+ end.to change { Ci::JobToken::ProjectScopeLink.inbound.count }.by(1)
+ end
+
+ context 'when FF frozen_outbound_job_token_scopes is disabled' do
+ before do
+ stub_feature_flags(frozen_outbound_job_token_scopes: false)
+ end
+
+ it 'adds the target project to the outbound job token scope' do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response.dig('ciJobTokenScope', 'projects', 'nodes')).not_to be_empty
+ end.to change { Ci::JobToken::ProjectScopeLink.outbound.count }.by(1)
+ end
end
context 'when invalid target project is provided' do
diff --git a/spec/requests/api/graphql/mutations/ci/project_ci_cd_settings_update_spec.rb b/spec/requests/api/graphql/mutations/ci/project_ci_cd_settings_update_spec.rb
index 99e55c44773..aa00069b241 100644
--- a/spec/requests/api/graphql/mutations/ci/project_ci_cd_settings_update_spec.rb
+++ b/spec/requests/api/graphql/mutations/ci/project_ci_cd_settings_update_spec.rb
@@ -5,6 +5,10 @@ require 'spec_helper'
RSpec.describe 'ProjectCiCdSettingsUpdate', feature_category: :continuous_integration do
include GraphqlHelpers
+ before do
+ stub_feature_flags(frozen_outbound_job_token_scopes_override: false)
+ end
+
let_it_be(:project) do
create(:project,
keep_latest_artifact: true,
@@ -18,12 +22,11 @@ RSpec.describe 'ProjectCiCdSettingsUpdate', feature_category: :continuous_integr
full_path: project.full_path,
keep_latest_artifact: false,
job_token_scope_enabled: false,
- inbound_job_token_scope_enabled: false,
- opt_in_jwt: true
+ inbound_job_token_scope_enabled: false
}
end
- let(:mutation) { graphql_mutation(:ci_cd_settings_update, variables) }
+ let(:mutation) { graphql_mutation(:project_ci_cd_settings_update, variables) }
context 'when unauthorized' do
let(:user) { create(:user) }
@@ -61,7 +64,36 @@ RSpec.describe 'ProjectCiCdSettingsUpdate', feature_category: :continuous_integr
expect(project.keep_latest_artifact).to eq(false)
end
- it 'updates job_token_scope_enabled' do
+ describe 'ci_cd_settings_update deprecated mutation' do
+ let(:mutation) { graphql_mutation(:ci_cd_settings_update, variables) }
+
+ it 'returns error' do
+ post_graphql_mutation(mutation, current_user: user)
+
+ expect(graphql_errors).to(
+ include(
+ hash_including('message' => '`remove_cicd_settings_update` feature flag is enabled.')
+ )
+ )
+ end
+
+ context 'when remove_cicd_settings_update FF is disabled' do
+ before do
+ stub_feature_flags(remove_cicd_settings_update: false)
+ end
+
+ it 'updates ci cd settings' do
+ post_graphql_mutation(mutation, current_user: user)
+
+ project.reload
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(project.keep_latest_artifact).to eq(false)
+ end
+ end
+ end
+
+ it 'allows setting job_token_scope_enabled to false' do
post_graphql_mutation(mutation, current_user: user)
project.reload
@@ -70,6 +102,50 @@ RSpec.describe 'ProjectCiCdSettingsUpdate', feature_category: :continuous_integr
expect(project.ci_outbound_job_token_scope_enabled).to eq(false)
end
+ context 'when job_token_scope_enabled: true' do
+ let(:variables) do
+ {
+ full_path: project.full_path,
+ keep_latest_artifact: false,
+ job_token_scope_enabled: true,
+ inbound_job_token_scope_enabled: false
+ }
+ end
+
+ it 'prevents the update', :aggregate_failures do
+ project.update!(ci_outbound_job_token_scope_enabled: false)
+ post_graphql_mutation(mutation, current_user: user)
+
+ project.reload
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(graphql_errors).to(
+ include(
+ hash_including(
+ 'message' => 'job_token_scope_enabled can only be set to false'
+ )
+ )
+ )
+ expect(project.ci_outbound_job_token_scope_enabled).to eq(false)
+ end
+ end
+
+ context 'when FF frozen_outbound_job_token_scopes is disabled' do
+ before do
+ stub_feature_flags(frozen_outbound_job_token_scopes: false)
+ end
+
+ it 'allows setting job_token_scope_enabled to true' do
+ project.update!(ci_outbound_job_token_scope_enabled: true)
+ post_graphql_mutation(mutation, current_user: user)
+
+ project.reload
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(project.ci_outbound_job_token_scope_enabled).to eq(false)
+ end
+ end
+
it 'does not update job_token_scope_enabled if not specified' do
variables.except!(:job_token_scope_enabled)
@@ -101,30 +177,6 @@ RSpec.describe 'ProjectCiCdSettingsUpdate', feature_category: :continuous_integr
expect(response).to have_gitlab_http_status(:success)
expect(project.ci_inbound_job_token_scope_enabled).to eq(true)
end
-
- context 'when ci_inbound_job_token_scope disabled' do
- before do
- stub_feature_flags(ci_inbound_job_token_scope: false)
- end
-
- it 'does not update inbound_job_token_scope_enabled' do
- post_graphql_mutation(mutation, current_user: user)
-
- project.reload
-
- expect(response).to have_gitlab_http_status(:success)
- expect(project.ci_inbound_job_token_scope_enabled).to eq(true)
- end
- end
- end
-
- it 'updates ci_opt_in_jwt' do
- post_graphql_mutation(mutation, current_user: user)
-
- project.reload
-
- expect(response).to have_gitlab_http_status(:success)
- expect(project.ci_opt_in_jwt).to eq(true)
end
context 'when bad arguments are provided' do
diff --git a/spec/requests/api/graphql/mutations/ci/runner/create_spec.rb b/spec/requests/api/graphql/mutations/ci/runner/create_spec.rb
new file mode 100644
index 00000000000..1658c277ed0
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/ci/runner/create_spec.rb
@@ -0,0 +1,313 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'RunnerCreate', feature_category: :runner_fleet do
+ include GraphqlHelpers
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:group_owner) { create(:user) }
+ let_it_be(:admin) { create(:admin) }
+
+ let_it_be(:group) { create(:group) }
+ let_it_be(:other_group) { create(:group) }
+
+ let(:mutation_params) do
+ {
+ description: 'create description',
+ maintenance_note: 'create maintenance note',
+ maximum_timeout: 900,
+ access_level: 'REF_PROTECTED',
+ paused: true,
+ run_untagged: false,
+ tag_list: %w[tag1 tag2]
+ }.deep_merge(mutation_scope_params)
+ end
+
+ let(:mutation) do
+ variables = {
+ **mutation_params
+ }
+
+ graphql_mutation(
+ :runner_create,
+ variables,
+ <<-QL
+ runner {
+ ephemeralAuthenticationToken
+
+ runnerType
+ description
+ maintenanceNote
+ paused
+ tagList
+ accessLevel
+ locked
+ maximumTimeout
+ runUntagged
+ }
+ errors
+ QL
+ )
+ end
+
+ let(:mutation_response) { graphql_mutation_response(:runner_create) }
+
+ before do
+ group.add_owner(group_owner)
+ end
+
+ shared_context 'when model is invalid returns error' do
+ let(:mutation_params) do
+ {
+ description: '',
+ maintenanceNote: '',
+ paused: true,
+ accessLevel: 'NOT_PROTECTED',
+ runUntagged: false,
+ tagList: [],
+ maximumTimeout: 1
+ }.deep_merge(mutation_scope_params)
+ end
+
+ it do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(response).to have_gitlab_http_status(:success)
+
+ expect(mutation_response['errors']).to contain_exactly(
+ 'Tags list can not be empty when runner is not allowed to pick untagged jobs',
+ 'Maximum timeout needs to be at least 10 minutes'
+ )
+ end
+ end
+
+ shared_context 'when user does not have permissions' do
+ let(:current_user) { user }
+
+ it 'returns an error' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect_graphql_errors_to_include(
+ 'The resource that you are attempting to access does not exist ' \
+ "or you don't have permission to perform this action"
+ )
+ end
+ end
+
+ shared_context 'when :create_runner_workflow_for_namespace feature flag is disabled' do
+ before do
+ stub_feature_flags(create_runner_workflow_for_namespace: [other_group])
+ end
+
+ it 'returns an error' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect_graphql_errors_to_include('`create_runner_workflow_for_namespace` feature flag is disabled.')
+ end
+ end
+
+ shared_examples 'when runner is created successfully' do
+ it do
+ expected_args = { user: current_user, params: anything }
+ expect_next_instance_of(::Ci::Runners::CreateRunnerService, expected_args) do |service|
+ expect(service).to receive(:execute).and_call_original
+ end
+
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(response).to have_gitlab_http_status(:success)
+
+ expect(mutation_response['errors']).to eq([])
+ expect(mutation_response['runner']).not_to be_nil
+ mutation_params.except(:group_id, :project_id).each_key do |key|
+ expect(mutation_response['runner'][key.to_s.camelize(:lower)]).to eq mutation_params[key]
+ end
+
+ expect(mutation_response['runner']['ephemeralAuthenticationToken'])
+ .to start_with Ci::Runner::CREATED_RUNNER_TOKEN_PREFIX
+ end
+ end
+
+ context 'when runnerType is INSTANCE_TYPE' do
+ let(:mutation_scope_params) do
+ { runner_type: 'INSTANCE_TYPE' }
+ end
+
+ it_behaves_like 'when user does not have permissions'
+
+ context 'when user has permissions', :enable_admin_mode do
+ let(:current_user) { admin }
+
+ context 'when :create_runner_workflow_for_admin feature flag is disabled' do
+ before do
+ stub_feature_flags(create_runner_workflow_for_admin: false)
+ end
+
+ it 'returns an error' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect_graphql_errors_to_include('`create_runner_workflow_for_admin` feature flag is disabled.')
+ end
+ end
+
+ it_behaves_like 'when runner is created successfully'
+ it_behaves_like 'when model is invalid returns error'
+ end
+ end
+
+ context 'when runnerType is GROUP_TYPE' do
+ let(:mutation_scope_params) do
+ {
+ runner_type: 'GROUP_TYPE',
+ group_id: group.to_global_id
+ }
+ end
+
+ before do
+ stub_feature_flags(create_runner_workflow_for_namespace: [group])
+ end
+
+ it_behaves_like 'when user does not have permissions'
+
+ context 'when user has permissions' do
+ context 'when user is group owner' do
+ let(:current_user) { group_owner }
+
+ it_behaves_like 'when :create_runner_workflow_for_namespace feature flag is disabled'
+ it_behaves_like 'when runner is created successfully'
+ it_behaves_like 'when model is invalid returns error'
+
+ context 'when group_id is missing' do
+ let(:mutation_scope_params) do
+ { runner_type: 'GROUP_TYPE' }
+ end
+
+ it 'returns an error' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect_graphql_errors_to_include('`group_id` is missing')
+ end
+ end
+
+ context 'when group_id is malformed' do
+ let(:mutation_scope_params) do
+ {
+ runner_type: 'GROUP_TYPE',
+ group_id: ''
+ }
+ end
+
+ it 'returns an error' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect_graphql_errors_to_include(
+ "RunnerCreateInput! was provided invalid value for groupId"
+ )
+ end
+ end
+
+ context 'when group_id does not exist' do
+ let(:mutation_scope_params) do
+ {
+ runner_type: 'GROUP_TYPE',
+ group_id: "gid://gitlab/Group/#{non_existing_record_id}"
+ }
+ end
+
+ it 'returns an error' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(flattened_errors).not_to be_empty
+ end
+ end
+ end
+
+ context 'when user is admin in admin mode', :enable_admin_mode do
+ let(:current_user) { admin }
+
+ it_behaves_like 'when :create_runner_workflow_for_namespace feature flag is disabled'
+ it_behaves_like 'when runner is created successfully'
+ it_behaves_like 'when model is invalid returns error'
+ end
+ end
+ end
+
+ context 'when runnerType is PROJECT_TYPE' do
+ let_it_be(:project) { create(:project, namespace: group) }
+
+ let(:mutation_scope_params) do
+ {
+ runner_type: 'PROJECT_TYPE',
+ project_id: project.to_global_id
+ }
+ end
+
+ it_behaves_like 'when user does not have permissions'
+
+ context 'when user has permissions' do
+ context 'when user is group owner' do
+ let(:current_user) { group_owner }
+
+ it_behaves_like 'when :create_runner_workflow_for_namespace feature flag is disabled'
+ it_behaves_like 'when runner is created successfully'
+ it_behaves_like 'when model is invalid returns error'
+
+ context 'when project_id is missing' do
+ let(:mutation_scope_params) do
+ { runner_type: 'PROJECT_TYPE' }
+ end
+
+ it 'returns an error' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect_graphql_errors_to_include('`project_id` is missing')
+ end
+ end
+
+ context 'when project_id is malformed' do
+ let(:mutation_scope_params) do
+ {
+ runner_type: 'PROJECT_TYPE',
+ project_id: ''
+ }
+ end
+
+ it 'returns an error' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect_graphql_errors_to_include(
+ "RunnerCreateInput! was provided invalid value for projectId"
+ )
+ end
+ end
+
+ context 'when project_id does not exist' do
+ let(:mutation_scope_params) do
+ {
+ runner_type: 'PROJECT_TYPE',
+ project_id: "gid://gitlab/Project/#{non_existing_record_id}"
+ }
+ end
+
+ it 'returns an error' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect_graphql_errors_to_include(
+ 'The resource that you are attempting to access does not exist ' \
+ "or you don't have permission to perform this action"
+ )
+ end
+ end
+ end
+
+ context 'when user is admin in admin mode', :enable_admin_mode do
+ let(:current_user) { admin }
+
+ it_behaves_like 'when :create_runner_workflow_for_namespace feature flag is disabled'
+ it_behaves_like 'when runner is created successfully'
+ it_behaves_like 'when model is invalid returns error'
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/clusters/agent_tokens/agent_tokens/create_spec.rb b/spec/requests/api/graphql/mutations/clusters/agent_tokens/agent_tokens/create_spec.rb
index f544cef8864..ef0d44395bf 100644
--- a/spec/requests/api/graphql/mutations/clusters/agent_tokens/agent_tokens/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/clusters/agent_tokens/agent_tokens/create_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Create a new cluster agent token', feature_category: :kubernetes_management do
+RSpec.describe 'Create a new cluster agent token', feature_category: :deployment_management do
include GraphqlHelpers
let_it_be(:cluster_agent) { create(:cluster_agent) }
diff --git a/spec/requests/api/graphql/mutations/clusters/agents/create_spec.rb b/spec/requests/api/graphql/mutations/clusters/agents/create_spec.rb
index 66e6c5cc629..1d1e72dcff9 100644
--- a/spec/requests/api/graphql/mutations/clusters/agents/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/clusters/agents/create_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Create a new cluster agent', feature_category: :kubernetes_management do
+RSpec.describe 'Create a new cluster agent', feature_category: :deployment_management do
include GraphqlHelpers
let(:project) { create(:project, :public, :repository) }
diff --git a/spec/requests/api/graphql/mutations/clusters/agents/delete_spec.rb b/spec/requests/api/graphql/mutations/clusters/agents/delete_spec.rb
index 27a566dfb8c..b70a6282a7a 100644
--- a/spec/requests/api/graphql/mutations/clusters/agents/delete_spec.rb
+++ b/spec/requests/api/graphql/mutations/clusters/agents/delete_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Delete a cluster agent', feature_category: :kubernetes_management do
+RSpec.describe 'Delete a cluster agent', feature_category: :deployment_management do
include GraphqlHelpers
let(:cluster_agent) { create(:cluster_agent) }
diff --git a/spec/requests/api/graphql/mutations/container_repository/destroy_spec.rb b/spec/requests/api/graphql/mutations/container_repository/destroy_spec.rb
index 8b76c19cda6..ef159e41d3d 100644
--- a/spec/requests/api/graphql/mutations/container_repository/destroy_spec.rb
+++ b/spec/requests/api/graphql/mutations/container_repository/destroy_spec.rb
@@ -39,7 +39,7 @@ RSpec.describe 'Destroying a container repository', feature_category: :container
expect(DeleteContainerRepositoryWorker)
.not_to receive(:perform_async)
- expect { subject }.to change { ::Packages::Event.count }.by(1)
+ subject
expect(container_repository_mutation_response).to match_schema('graphql/container_repository')
expect(container_repository_mutation_response['status']).to eq('DELETE_SCHEDULED')
@@ -53,7 +53,7 @@ RSpec.describe 'Destroying a container repository', feature_category: :container
expect(DeleteContainerRepositoryWorker)
.not_to receive(:perform_async).with(user.id, container_repository.id)
- expect { subject }.not_to change { ::Packages::Event.count }
+ subject
expect(mutation_response).to be_nil
end
diff --git a/spec/requests/api/graphql/mutations/container_repository/destroy_tags_spec.rb b/spec/requests/api/graphql/mutations/container_repository/destroy_tags_spec.rb
index 9e07a831076..0cb607e13ec 100644
--- a/spec/requests/api/graphql/mutations/container_repository/destroy_tags_spec.rb
+++ b/spec/requests/api/graphql/mutations/container_repository/destroy_tags_spec.rb
@@ -36,7 +36,7 @@ RSpec.describe 'Destroying a container repository tags', feature_category: :cont
it 'destroys the container repository tags' do
expect(Projects::ContainerRepository::DeleteTagsService)
.to receive(:new).and_call_original
- expect { subject }.to change { ::Packages::Event.count }.by(1)
+ subject
expect(tag_names_response).to eq(tags)
expect(errors_response).to eq([])
@@ -50,7 +50,7 @@ RSpec.describe 'Destroying a container repository tags', feature_category: :cont
expect(Projects::ContainerRepository::DeleteTagsService)
.not_to receive(:new)
- expect { subject }.not_to change { ::Packages::Event.count }
+ subject
expect(mutation_response).to be_nil
end
@@ -89,7 +89,7 @@ RSpec.describe 'Destroying a container repository tags', feature_category: :cont
let(:tags) { Array.new(Mutations::ContainerRepositories::DestroyTags::LIMIT + 1, 'x') }
it 'returns too many tags error' do
- expect { subject }.not_to change { ::Packages::Event.count }
+ subject
explanation = graphql_errors.dig(0, 'message')
expect(explanation).to eq(Mutations::ContainerRepositories::DestroyTags::TOO_MANY_TAGS_ERROR_MESSAGE)
@@ -113,7 +113,7 @@ RSpec.describe 'Destroying a container repository tags', feature_category: :cont
it 'does not create a package event' do
expect(::Packages::CreateEventService).not_to receive(:new)
- expect { subject }.not_to change { ::Packages::Event.count }
+ subject
end
end
end
diff --git a/spec/requests/api/graphql/mutations/custom_emoji/create_spec.rb b/spec/requests/api/graphql/mutations/custom_emoji/create_spec.rb
index ea2ce8a13e2..19a52086f34 100644
--- a/spec/requests/api/graphql/mutations/custom_emoji/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/custom_emoji/create_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Creation of a new Custom Emoji', feature_category: :not_owned do
+RSpec.describe 'Creation of a new Custom Emoji', feature_category: :shared do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
diff --git a/spec/requests/api/graphql/mutations/custom_emoji/destroy_spec.rb b/spec/requests/api/graphql/mutations/custom_emoji/destroy_spec.rb
index ad7a043909a..2623d3d8410 100644
--- a/spec/requests/api/graphql/mutations/custom_emoji/destroy_spec.rb
+++ b/spec/requests/api/graphql/mutations/custom_emoji/destroy_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Deletion of custom emoji', feature_category: :not_owned do
+RSpec.describe 'Deletion of custom emoji', feature_category: :shared do
include GraphqlHelpers
let_it_be(:group) { create(:group) }
diff --git a/spec/requests/api/graphql/mutations/design_management/update_spec.rb b/spec/requests/api/graphql/mutations/design_management/update_spec.rb
new file mode 100644
index 00000000000..9558f2538f1
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/design_management/update_spec.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe "updating designs", feature_category: :design_management do
+ include GraphqlHelpers
+ include DesignManagementTestHelpers
+
+ let_it_be(:issue) { create(:issue) }
+ let_it_be_with_reload(:design) { create(:design, description: 'old description', issue: issue) }
+ let_it_be(:developer) { create(:user, developer_projects: [issue.project]) }
+
+ let(:user) { developer }
+ let(:description) { 'new description' }
+
+ let(:mutation) do
+ input = {
+ id: design.to_global_id.to_s,
+ description: description
+ }.compact
+
+ graphql_mutation(:design_management_update, input, <<~FIELDS)
+ errors
+ design {
+ description
+ descriptionHtml
+ }
+ FIELDS
+ end
+
+ let(:update_design) { post_graphql_mutation(mutation, current_user: user) }
+ let(:mutation_response) { graphql_mutation_response(:design_management_update) }
+
+ before do
+ enable_design_management
+ end
+
+ it 'updates design' do
+ update_design
+
+ expect(graphql_errors).not_to be_present
+ expect(mutation_response).to eq(
+ 'errors' => [],
+ 'design' => {
+ 'description' => description,
+ 'descriptionHtml' => "<p data-sourcepos=\"1:1-1:15\" dir=\"auto\">#{description}</p>"
+ }
+ )
+ end
+
+ context 'when the user is not allowed to update designs' do
+ let(:user) { create(:user) }
+
+ it 'returns an error' do
+ update_design
+
+ expect(graphql_errors).to be_present
+ end
+ end
+
+ context 'when update fails' do
+ let(:description) { 'x' * 1_000_001 }
+
+ it 'returns an error' do
+ update_design
+
+ expect(graphql_errors).not_to be_present
+ expect(mutation_response).to eq(
+ 'errors' => ["Description is too long (maximum is 1000000 characters)"],
+ 'design' => {
+ 'description' => 'old description',
+ 'descriptionHtml' => '<p data-sourcepos="1:1-1:15" dir="auto">old description</p>'
+ }
+ )
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/issues/bulk_update_spec.rb b/spec/requests/api/graphql/mutations/issues/bulk_update_spec.rb
index b9c83311908..b729585a89b 100644
--- a/spec/requests/api/graphql/mutations/issues/bulk_update_spec.rb
+++ b/spec/requests/api/graphql/mutations/issues/bulk_update_spec.rb
@@ -8,7 +8,9 @@ RSpec.describe 'Bulk update issues', feature_category: :team_planning do
let_it_be(:developer) { create(:user) }
let_it_be(:group) { create(:group).tap { |group| group.add_developer(developer) } }
let_it_be(:project) { create(:project, group: group) }
- let_it_be(:updatable_issues, reload: true) { create_list(:issue, 2, project: project) }
+ let_it_be(:label1) { create(:group_label, group: group) }
+ let_it_be(:label2) { create(:group_label, group: group) }
+ let_it_be(:updatable_issues, reload: true) { create_list(:issue, 2, project: project, label_ids: [label1.id]) }
let_it_be(:milestone) { create(:milestone, group: group) }
let(:parent) { project }
@@ -21,10 +23,36 @@ RSpec.describe 'Bulk update issues', feature_category: :team_planning do
let(:additional_arguments) do
{
assignee_ids: [current_user.to_gid.to_s],
- milestone_id: milestone.to_gid.to_s
+ milestone_id: milestone.to_gid.to_s,
+ state_event: :CLOSE,
+ add_label_ids: [label2.to_gid.to_s],
+ remove_label_ids: [label1.to_gid.to_s],
+ subscription_event: :UNSUBSCRIBE
}
end
+ before_all do
+ updatable_issues.each { |i| i.subscribe(developer, project) }
+ end
+
+ context 'when Gitlab is FOSS only' do
+ unless Gitlab.ee?
+ context 'when parent is a group' do
+ let(:parent) { group }
+
+ it 'does not allow bulk updating issues at the group level' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(graphql_errors).to contain_exactly(
+ hash_including(
+ 'message' => match(/does not represent an instance of IssueParent/)
+ )
+ )
+ end
+ end
+ end
+ end
+
context 'when the `bulk_update_issues_mutation` feature flag is disabled' do
before do
stub_feature_flags(bulk_update_issues_mutation: false)
@@ -67,6 +95,11 @@ RSpec.describe 'Bulk update issues', feature_category: :team_planning do
updatable_issues.each(&:reload)
end.to change { updatable_issues.flat_map(&:assignee_ids) }.from([]).to([current_user.id] * 2)
.and(change { updatable_issues.map(&:milestone_id) }.from([nil] * 2).to([milestone.id] * 2))
+ .and(change { updatable_issues.map(&:state) }.from(['opened'] * 2).to(['closed'] * 2))
+ .and(change { updatable_issues.flat_map(&:label_ids) }.from([label1.id] * 2).to([label2.id] * 2))
+ .and(
+ change { updatable_issues.map { |i| i.subscribed?(developer, project) } }.from([true] * 2).to([false] * 2)
+ )
expect(mutation_response).to include(
'updatedIssueCount' => updatable_issues.count
@@ -88,37 +121,6 @@ RSpec.describe 'Bulk update issues', feature_category: :team_planning do
end
end
- context 'when scoping to a parent group' do
- let(:parent) { group }
-
- it 'updates all issues' do
- expect do
- post_graphql_mutation(mutation, current_user: current_user)
- updatable_issues.each(&:reload)
- end.to change { updatable_issues.flat_map(&:assignee_ids) }.from([]).to([current_user.id] * 2)
- .and(change { updatable_issues.map(&:milestone_id) }.from([nil] * 2).to([milestone.id] * 2))
-
- expect(mutation_response).to include(
- 'updatedIssueCount' => updatable_issues.count
- )
- end
-
- context 'when current user cannot read the specified group' do
- let(:parent) { create(:group, :private) }
-
- it 'returns a resource not found error' do
- post_graphql_mutation(mutation, current_user: current_user)
-
- expect(graphql_errors).to contain_exactly(
- hash_including(
- 'message' => "The resource that you are attempting to access does not exist or you don't have " \
- 'permission to perform this action'
- )
- )
- end
- end
- end
-
context 'when setting arguments to null or none' do
let(:additional_arguments) { { assignee_ids: [], milestone_id: nil } }
diff --git a/spec/requests/api/graphql/mutations/issues/create_spec.rb b/spec/requests/api/graphql/mutations/issues/create_spec.rb
index d2d2f0014d6..b5a9c549045 100644
--- a/spec/requests/api/graphql/mutations/issues/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/issues/create_spec.rb
@@ -66,7 +66,6 @@ RSpec.describe 'Create an issue', feature_category: :team_planning do
created_issue = Issue.last
expect(created_issue.work_item_type.base_type).to eq('task')
- expect(created_issue.issue_type).to eq('task')
end
end
diff --git a/spec/requests/api/graphql/mutations/members/groups/bulk_update_spec.rb b/spec/requests/api/graphql/mutations/members/groups/bulk_update_spec.rb
index ad70129a7bc..f15b52f53a3 100644
--- a/spec/requests/api/graphql/mutations/members/groups/bulk_update_spec.rb
+++ b/spec/requests/api/graphql/mutations/members/groups/bulk_update_spec.rb
@@ -5,126 +5,14 @@ require 'spec_helper'
RSpec.describe 'GroupMemberBulkUpdate', feature_category: :subgroups do
include GraphqlHelpers
- let_it_be(:current_user) { create(:user) }
- let_it_be(:user1) { create(:user) }
- let_it_be(:user2) { create(:user) }
- let_it_be(:group) { create(:group) }
- let_it_be(:group_member1) { create(:group_member, group: group, user: user1) }
- let_it_be(:group_member2) { create(:group_member, group: group, user: user2) }
+ let_it_be(:parent_group) { create(:group) }
+ let_it_be(:parent_group_member) { create(:group_member, group: parent_group) }
+ let_it_be(:group) { create(:group, parent: parent_group) }
+ let_it_be(:source) { group }
+ let_it_be(:member_type) { :group_member }
let_it_be(:mutation_name) { :group_member_bulk_update }
+ let_it_be(:source_id_key) { 'group_id' }
+ let_it_be(:response_member_field) { 'groupMembers' }
- let(:input) do
- {
- 'group_id' => group.to_global_id.to_s,
- 'user_ids' => [user1.to_global_id.to_s, user2.to_global_id.to_s],
- 'access_level' => 'GUEST'
- }
- end
-
- let(:extra_params) { { expires_at: 10.days.from_now } }
- let(:input_params) { input.merge(extra_params) }
- let(:mutation) { graphql_mutation(mutation_name, input_params) }
- let(:mutation_response) { graphql_mutation_response(mutation_name) }
-
- context 'when user is not logged-in' do
- it_behaves_like 'a mutation that returns a top-level access error'
- end
-
- context 'when user is not an owner' do
- before do
- group.add_maintainer(current_user)
- end
-
- it_behaves_like 'a mutation that returns a top-level access error'
- end
-
- context 'when user is an owner' do
- before do
- group.add_owner(current_user)
- end
-
- shared_examples 'updates the user access role' do
- specify do
- post_graphql_mutation(mutation, current_user: current_user)
-
- new_access_levels = mutation_response['groupMembers'].map { |member| member['accessLevel']['integerValue'] }
- expect(response).to have_gitlab_http_status(:success)
- expect(mutation_response['errors']).to be_empty
- expect(new_access_levels).to all(be Gitlab::Access::GUEST)
- end
- end
-
- it_behaves_like 'updates the user access role'
-
- context 'when inherited members are passed' do
- let_it_be(:subgroup) { create(:group, parent: group) }
- let_it_be(:subgroup_member) { create(:group_member, group: subgroup) }
-
- let(:input) do
- {
- 'group_id' => group.to_global_id.to_s,
- 'user_ids' => [user1.to_global_id.to_s, user2.to_global_id.to_s, subgroup_member.user.to_global_id.to_s],
- 'access_level' => 'GUEST'
- }
- end
-
- it 'does not update the members' do
- post_graphql_mutation(mutation, current_user: current_user)
-
- error = Mutations::Members::Groups::BulkUpdate::INVALID_MEMBERS_ERROR
- expect(json_response['errors'].first['message']).to include(error)
- end
- end
-
- context 'when members count is more than the allowed limit' do
- let(:max_members_update_limit) { 1 }
-
- before do
- stub_const('Mutations::Members::Groups::BulkUpdate::MAX_MEMBERS_UPDATE_LIMIT', max_members_update_limit)
- end
-
- it 'does not update the members' do
- post_graphql_mutation(mutation, current_user: current_user)
-
- error = Mutations::Members::Groups::BulkUpdate::MAX_MEMBERS_UPDATE_ERROR
- expect(json_response['errors'].first['message']).to include(error)
- end
- end
-
- context 'when the update service raises access denied error' do
- before do
- allow_next_instance_of(Members::UpdateService) do |instance|
- allow(instance).to receive(:execute).and_raise(Gitlab::Access::AccessDeniedError)
- end
- end
-
- it 'does not update the members' do
- post_graphql_mutation(mutation, current_user: current_user)
-
- expect(mutation_response['groupMembers']).to be_nil
- expect(mutation_response['errors'])
- .to contain_exactly("Unable to update members, please check user permissions.")
- end
- end
-
- context 'when the update service returns an error message' do
- before do
- allow_next_instance_of(Members::UpdateService) do |instance|
- error_result = {
- message: 'Expires at cannot be a date in the past',
- status: :error,
- members: [group_member1]
- }
- allow(instance).to receive(:execute).and_return(error_result)
- end
- end
-
- it 'will pass through the error' do
- post_graphql_mutation(mutation, current_user: current_user)
-
- expect(mutation_response['groupMembers'].first['id']).to eq(group_member1.to_global_id.to_s)
- expect(mutation_response['errors']).to contain_exactly('Expires at cannot be a date in the past')
- end
- end
- end
+ it_behaves_like 'members bulk update mutation'
end
diff --git a/spec/requests/api/graphql/mutations/members/projects/bulk_update_spec.rb b/spec/requests/api/graphql/mutations/members/projects/bulk_update_spec.rb
new file mode 100644
index 00000000000..cbef9715cbe
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/members/projects/bulk_update_spec.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'ProjectMemberBulkUpdate', feature_category: :projects do
+ include GraphqlHelpers
+
+ let_it_be(:parent_group) { create(:group) }
+ let_it_be(:parent_group_member) { create(:group_member, group: parent_group) }
+ let_it_be(:project) { create(:project, group: parent_group) }
+ let_it_be(:source) { project }
+ let_it_be(:member_type) { :project_member }
+ let_it_be(:mutation_name) { :project_member_bulk_update }
+ let_it_be(:source_id_key) { 'project_id' }
+ let_it_be(:response_member_field) { 'projectMembers' }
+
+ it_behaves_like 'members bulk update mutation'
+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 b5f2042c42a..d41628704a1 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
@@ -106,7 +106,7 @@ RSpec.describe 'Setting assignees of a merge request', :assume_throttled, featur
end
context 'when passing an empty list of assignees' do
- let(:db_query_limit) { 31 }
+ let(:db_query_limit) { 35 }
let(:input) { { assignee_usernames: [] } }
before do
diff --git a/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/create_spec.rb b/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/create_spec.rb
index bce57b47aab..d81744abe1b 100644
--- a/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/create_spec.rb
@@ -19,7 +19,11 @@ RSpec.describe Mutations::Metrics::Dashboard::Annotations::Create, feature_categ
graphql_mutation_response(:create_annotation)
end
- specify { expect(described_class).to require_graphql_authorizations(:create_metrics_dashboard_annotation) }
+ before do
+ stub_feature_flags(remove_monitor_metrics: false)
+ end
+
+ specify { expect(described_class).to require_graphql_authorizations(:admin_metrics_dashboard_annotation) }
context 'when annotation source is environment' do
let(:mutation) do
@@ -103,6 +107,15 @@ RSpec.describe Mutations::Metrics::Dashboard::Annotations::Create, feature_categ
it_behaves_like 'an invalid argument to the mutation', argument_name: :environment_id
end
+
+ context 'when metrics dashboard feature is unavailable' do
+ before do
+ stub_feature_flags(remove_monitor_metrics: true)
+ end
+
+ 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/metrics/dashboard/annotations/delete_spec.rb b/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/delete_spec.rb
index f505dc25dc0..09977cd19d7 100644
--- a/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/delete_spec.rb
+++ b/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/delete_spec.rb
@@ -17,7 +17,11 @@ RSpec.describe Mutations::Metrics::Dashboard::Annotations::Delete, feature_categ
graphql_mutation_response(:delete_annotation)
end
- specify { expect(described_class).to require_graphql_authorizations(:delete_metrics_dashboard_annotation) }
+ before do
+ stub_feature_flags(remove_monitor_metrics: false)
+ end
+
+ specify { expect(described_class).to require_graphql_authorizations(:admin_metrics_dashboard_annotation) }
context 'when the user has permission to delete the annotation' do
before do
@@ -54,6 +58,15 @@ RSpec.describe Mutations::Metrics::Dashboard::Annotations::Delete, feature_categ
expect(mutation_response['errors']).to eq([service_response[:message]])
end
end
+
+ context 'when metrics dashboard feature is unavailable' do
+ before do
+ stub_feature_flags(remove_monitor_metrics: true)
+ end
+
+ it_behaves_like 'a mutation that returns top-level errors',
+ errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
+ end
end
context 'when the user does not have permission to delete the annotation' do
diff --git a/spec/requests/api/graphql/mutations/notes/create/note_spec.rb b/spec/requests/api/graphql/mutations/notes/create/note_spec.rb
index a6253ba424b..e6feba059c4 100644
--- a/spec/requests/api/graphql/mutations/notes/create/note_spec.rb
+++ b/spec/requests/api/graphql/mutations/notes/create/note_spec.rb
@@ -104,7 +104,8 @@ RSpec.describe 'Adding a Note', feature_category: :team_planning do
end
context 'as work item' do
- let(:noteable) { create(:work_item, :issue, project: project) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:noteable) { create(:work_item, :issue, project: project) }
context 'when using internal param' do
let(:variables_extra) { { internal: true } }
@@ -130,6 +131,20 @@ RSpec.describe 'Adding a Note', feature_category: :team_planning do
it_behaves_like 'a mutation that returns top-level errors',
errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
end
+
+ context 'when body contains quick actions' do
+ let_it_be(:noteable) { create(:work_item, :task, project: project) }
+
+ let(:variables_extra) { {} }
+
+ it_behaves_like 'work item supports labels widget updates via quick actions'
+ it_behaves_like 'work item does not support labels widget updates via quick actions'
+ it_behaves_like 'work item supports assignee widget updates via quick actions'
+ it_behaves_like 'work item does not support assignee widget updates via quick actions'
+ it_behaves_like 'work item supports start and due date widget updates via quick actions'
+ it_behaves_like 'work item does not support start and due date widget updates via quick actions'
+ it_behaves_like 'work item supports type change via quick actions'
+ end
end
end
diff --git a/spec/requests/api/graphql/mutations/projects/sync_fork_spec.rb b/spec/requests/api/graphql/mutations/projects/sync_fork_spec.rb
new file mode 100644
index 00000000000..c5dc6f390d9
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/projects/sync_fork_spec.rb
@@ -0,0 +1,153 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe "Sync project fork", feature_category: :source_code_management do
+ include GraphqlHelpers
+ include ProjectForksHelper
+ include ExclusiveLeaseHelpers
+
+ let_it_be(:source_project) { create(:project, :repository, :public) }
+ let_it_be(:current_user) { create(:user, maintainer_projects: [source_project]) }
+ let_it_be(:project, refind: true) { fork_project(source_project, current_user, { repository: true }) }
+ let_it_be(:target_branch) { project.default_branch }
+
+ let(:mutation) do
+ params = { project_path: project.full_path, target_branch: target_branch }
+
+ graphql_mutation(:project_sync_fork, params) do
+ <<-QL.strip_heredoc
+ details {
+ ahead
+ behind
+ isSyncing
+ hasConflicts
+ }
+ errors
+ QL
+ end
+ end
+
+ before do
+ source_project.change_head('feature')
+ end
+
+ context 'when synchronize_fork feature flag is disabled' do
+ before do
+ stub_feature_flags(synchronize_fork: false)
+ end
+
+ it 'does not call the sync service' do
+ expect(::Projects::Forks::SyncWorker).not_to receive(:perform_async)
+
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(graphql_mutation_response(:project_sync_fork)).to eq(
+ {
+ 'details' => nil,
+ 'errors' => ['Feature flag is disabled']
+ })
+ end
+ end
+
+ context 'when the branch is protected', :use_clean_rails_redis_caching do
+ let_it_be(:protected_branch) do
+ create(:protected_branch, :no_one_can_push, project: project, name: target_branch)
+ end
+
+ it_behaves_like 'a mutation that returns a top-level access error'
+
+ it 'does not call the sync service' do
+ expect(::Projects::Forks::SyncWorker).not_to receive(:perform_async)
+
+ post_graphql_mutation(mutation, current_user: current_user)
+ end
+ end
+
+ context 'when the user does not have permission' do
+ let_it_be(:current_user) { create(:user) }
+
+ it_behaves_like 'a mutation that returns a top-level access error'
+
+ it 'does not call the sync service' do
+ expect(::Projects::Forks::SyncWorker).not_to receive(:perform_async)
+
+ post_graphql_mutation(mutation, current_user: current_user)
+ end
+ end
+
+ context 'when the user has permission' do
+ context 'and the sync service executes successfully', :sidekiq_inline do
+ it 'calls the sync service' do
+ expect(::Projects::Forks::SyncWorker).to receive(:perform_async).and_call_original
+
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(graphql_mutation_response(:project_sync_fork)).to eq(
+ {
+ 'details' => { 'ahead' => 30, 'behind' => 0, "hasConflicts" => false, "isSyncing" => false },
+ 'errors' => []
+ })
+ end
+ end
+
+ context 'and the sync service fails to execute' do
+ let(:target_branch) { 'markdown' }
+
+ def expect_error_response(message)
+ expect(::Projects::Forks::SyncWorker).not_to receive(:perform_async)
+
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(graphql_mutation_response(:project_sync_fork)['errors']).to eq([message])
+ end
+
+ context 'when fork details cannot be resolved' do
+ let_it_be(:project) { source_project }
+
+ it 'returns an error' do
+ expect_error_response('This branch of this project cannot be updated from the upstream')
+ end
+ end
+
+ context 'when the specified branch does not exist' do
+ let(:target_branch) { 'non-existent-branch' }
+
+ it 'returns an error' do
+ expect_error_response('Target branch does not exist')
+ end
+ end
+
+ context 'when the previous execution resulted in a conflict' do
+ it 'returns an error' do
+ expect_next_instance_of(::Projects::Forks::Details) do |instance|
+ expect(instance).to receive(:has_conflicts?).twice.and_return(true)
+ end
+
+ expect_error_response('The synchronization cannot happen due to the merge conflict')
+ expect(graphql_mutation_response(:project_sync_fork)['details']['hasConflicts']).to eq(true)
+ end
+ end
+
+ context 'when the request is rate limited' do
+ it 'returns an error' do
+ expect(Gitlab::ApplicationRateLimiter).to receive(:throttled?).and_return(true)
+
+ expect_error_response('This service has been called too many times.')
+ end
+ end
+
+ context 'when another fork sync is in progress' do
+ it 'returns an error' do
+ expect_next_instance_of(Projects::Forks::Details) do |instance|
+ lease = instance_double(Gitlab::ExclusiveLease, try_obtain: false, exists?: true)
+ expect(instance).to receive(:exclusive_lease).twice.and_return(lease)
+ end
+
+ expect_error_response('Another fork sync is already in progress')
+ expect(graphql_mutation_response(:project_sync_fork)['details']['isSyncing']).to eq(true)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/release_asset_links/create_spec.rb b/spec/requests/api/graphql/mutations/release_asset_links/create_spec.rb
index 418a0e47a36..311ff48a846 100644
--- a/spec/requests/api/graphql/mutations/release_asset_links/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/release_asset_links/create_spec.rb
@@ -32,7 +32,6 @@ RSpec.describe 'Creation of a new release asset link', feature_category: :releas
url
linkType
directAssetUrl
- external
}
errors
FIELDS
@@ -49,8 +48,7 @@ RSpec.describe 'Creation of a new release asset link', feature_category: :releas
name: mutation_arguments[:name],
url: mutation_arguments[:url],
linkType: mutation_arguments[:linkType],
- directAssetUrl: end_with(mutation_arguments[:directAssetPath]),
- external: true
+ directAssetUrl: end_with(mutation_arguments[:directAssetPath])
}.with_indifferent_access
expect(mutation_response[:link]).to include(expected_response)
diff --git a/spec/requests/api/graphql/mutations/release_asset_links/delete_spec.rb b/spec/requests/api/graphql/mutations/release_asset_links/delete_spec.rb
index b6d2c3f691d..cda1030c6d6 100644
--- a/spec/requests/api/graphql/mutations/release_asset_links/delete_spec.rb
+++ b/spec/requests/api/graphql/mutations/release_asset_links/delete_spec.rb
@@ -22,7 +22,6 @@ RSpec.describe 'Deletes a release asset link', feature_category: :release_orches
url
linkType
directAssetUrl
- external
}
errors
FIELDS
@@ -39,8 +38,7 @@ RSpec.describe 'Deletes a release asset link', feature_category: :release_orches
name: release_link.name,
url: release_link.url,
linkType: release_link.link_type.upcase,
- directAssetUrl: end_with(release_link.filepath),
- external: true
+ directAssetUrl: end_with(release_link.filepath)
}.with_indifferent_access
expect(mutation_response[:link]).to match(expected_response)
diff --git a/spec/requests/api/graphql/mutations/release_asset_links/update_spec.rb b/spec/requests/api/graphql/mutations/release_asset_links/update_spec.rb
index 61395cc4042..45028cba3ae 100644
--- a/spec/requests/api/graphql/mutations/release_asset_links/update_spec.rb
+++ b/spec/requests/api/graphql/mutations/release_asset_links/update_spec.rb
@@ -40,7 +40,6 @@ RSpec.describe 'Updating an existing release asset link', feature_category: :rel
url
linkType
directAssetUrl
- external
}
errors
FIELDS
@@ -57,8 +56,7 @@ RSpec.describe 'Updating an existing release asset link', feature_category: :rel
name: mutation_arguments[:name],
url: mutation_arguments[:url],
linkType: mutation_arguments[:linkType],
- directAssetUrl: end_with(mutation_arguments[:directAssetPath]),
- external: true
+ directAssetUrl: end_with(mutation_arguments[:directAssetPath])
}.with_indifferent_access
expect(mutation_response[:link]).to include(expected_response)
diff --git a/spec/requests/api/graphql/mutations/releases/create_spec.rb b/spec/requests/api/graphql/mutations/releases/create_spec.rb
index 295b8c0e97e..7cb421f17a3 100644
--- a/spec/requests/api/graphql/mutations/releases/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/releases/create_spec.rb
@@ -59,7 +59,6 @@ RSpec.describe 'Creation of a new release', feature_category: :release_orchestra
name
url
linkType
- external
directAssetUrl
}
}
@@ -135,7 +134,6 @@ RSpec.describe 'Creation of a new release', feature_category: :release_orchestra
name: asset_link[:name],
url: asset_link[:url],
linkType: asset_link[:linkType],
- external: true,
directAssetUrl: expected_direct_asset_url
}]
}
diff --git a/spec/requests/api/graphql/mutations/snippets/update_spec.rb b/spec/requests/api/graphql/mutations/snippets/update_spec.rb
index fa087e6773c..3b98ee3c2e9 100644
--- a/spec/requests/api/graphql/mutations/snippets/update_spec.rb
+++ b/spec/requests/api/graphql/mutations/snippets/update_spec.rb
@@ -193,7 +193,6 @@ RSpec.describe 'Updating a Snippet', feature_category: :source_code_management d
end
it_behaves_like 'Snowplow event tracking with RedisHLL context' do
- let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
let(:user) { current_user }
let(:property) { 'g_edit_by_snippet_ide' }
let(:namespace) { project.namespace }
@@ -203,8 +202,6 @@ RSpec.describe 'Updating a Snippet', feature_category: :source_code_management d
let(:context) do
[Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll, event: event_name).to_context]
end
-
- let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
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 967ad75c906..65b8083c74f 100644
--- a/spec/requests/api/graphql/mutations/user_preferences/update_spec.rb
+++ b/spec/requests/api/graphql/mutations/user_preferences/update_spec.rb
@@ -11,7 +11,8 @@ RSpec.describe Mutations::UserPreferences::Update, feature_category: :user_profi
let(:input) do
{
- 'issuesSort' => sort_value
+ 'issuesSort' => sort_value,
+ 'visibilityPipelineIdType' => 'IID'
}
end
@@ -24,15 +25,20 @@ 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(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')
end
end
context 'when user has existing preference' do
before do
- current_user.create_user_preference!(issues_sort: Types::IssueSortEnum.values['TITLE_DESC'].value)
+ current_user.create_user_preference!(
+ issues_sort: Types::IssueSortEnum.values['TITLE_DESC'].value,
+ visibility_pipeline_id_type: 'id'
+ )
end
it 'updates the existing value' do
@@ -42,8 +48,10 @@ 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(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
end
end
diff --git a/spec/requests/api/graphql/mutations/work_items/convert_spec.rb b/spec/requests/api/graphql/mutations/work_items/convert_spec.rb
new file mode 100644
index 00000000000..97289597331
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/work_items/convert_spec.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe "Converts a work item to a new type", 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(:new_type) { create(:work_item_type, :incident, :default) }
+ let_it_be(:work_item, refind: true) do
+ create(:work_item, :task, project: project, milestone: create(:milestone, project: project))
+ end
+
+ let(:work_item_type_id) { new_type.to_global_id.to_s }
+ let(:mutation) { graphql_mutation(:workItemConvert, input) }
+ let(:mutation_response) { graphql_mutation_response(:work_item_convert) }
+ let(:input) do
+ {
+ 'id' => work_item.to_global_id.to_s,
+ 'work_item_type_id' => work_item_type_id
+ }
+ end
+
+ context 'when 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 has permissions to convert the work item type' do
+ let(:current_user) { developer }
+
+ context 'when work item type does not exist' do
+ let(:work_item_type_id) { "gid://gitlab/WorkItems::Type/#{non_existing_record_id}" }
+
+ it 'returns an error' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(graphql_errors).to include(
+ a_hash_including('message' => "Work Item type with id #{non_existing_record_id} was not found")
+ )
+ end
+ end
+
+ it 'converts the work item', :aggregate_failures do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ end.to change { work_item.reload.work_item_type }.to(new_type)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(work_item.reload.work_item_type.base_type).to eq('incident')
+ expect(mutation_response['workItem']).to include('id' => work_item.to_global_id.to_s)
+ expect(work_item.reload.milestone).to be_nil
+ end
+
+ it_behaves_like 'has spam protection' do
+ let(:mutation_class) { ::Mutations::WorkItems::Convert }
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/work_items/create_from_task_spec.rb b/spec/requests/api/graphql/mutations/work_items/create_from_task_spec.rb
index 97bf060356a..6a6ad1b14fd 100644
--- a/spec/requests/api/graphql/mutations/work_items/create_from_task_spec.rb
+++ b/spec/requests/api/graphql/mutations/work_items/create_from_task_spec.rb
@@ -23,7 +23,7 @@ RSpec.describe "Create a work item from a task in a work item's description", fe
}
end
- let(:mutation) { graphql_mutation(:workItemCreateFromTask, input) }
+ let(:mutation) { graphql_mutation(:workItemCreateFromTask, input, nil, ['productAnalyticsState']) }
let(:mutation_response) { graphql_mutation_response(:work_item_create_from_task) }
context 'the user is not allowed to update a work item' do
@@ -45,7 +45,6 @@ RSpec.describe "Create a work item from a task in a work item's description", fe
expect(response).to have_gitlab_http_status(:success)
expect(work_item.description).to eq("- [ ] #{created_work_item.to_reference}+")
- expect(created_work_item.issue_type).to eq('task')
expect(created_work_item.work_item_type.base_type).to eq('task')
expect(created_work_item.work_item_parent).to eq(work_item)
expect(created_work_item).to be_confidential
diff --git a/spec/requests/api/graphql/mutations/work_items/create_spec.rb b/spec/requests/api/graphql/mutations/work_items/create_spec.rb
index 16f78b67b5c..fca3c84e534 100644
--- a/spec/requests/api/graphql/mutations/work_items/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/work_items/create_spec.rb
@@ -5,52 +5,43 @@ require 'spec_helper'
RSpec.describe 'Create a work item', 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(:group) { create(:group) }
+ let_it_be(:project) { create(:project, group: group) }
+ let_it_be(:developer) { create(:user).tap { |user| group.add_developer(user) } }
let(:input) do
{
'title' => 'new title',
'description' => 'new description',
'confidential' => true,
- 'workItemTypeId' => WorkItems::Type.default_by_type(:task).to_global_id.to_s
+ 'workItemTypeId' => WorkItems::Type.default_by_type(:task).to_gid.to_s
}
end
- let(:mutation) { graphql_mutation(:workItemCreate, input.merge('projectPath' => project.full_path)) }
-
+ let(:fields) { nil }
let(:mutation_response) { graphql_mutation_response(:work_item_create) }
+ let(:current_user) { developer }
- context 'the user is not allowed to create 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 has permissions to create a work item' do
- let(:current_user) { developer }
-
+ RSpec.shared_examples 'creates work item' do
it 'creates the work item' do
expect do
post_graphql_mutation(mutation, current_user: current_user)
end.to change(WorkItem, :count).by(1)
created_work_item = WorkItem.last
-
expect(response).to have_gitlab_http_status(:success)
- expect(created_work_item.issue_type).to eq('task')
expect(created_work_item).to be_confidential
expect(created_work_item.work_item_type.base_type).to eq('task')
expect(mutation_response['workItem']).to include(
input.except('workItemTypeId').merge(
- 'id' => created_work_item.to_global_id.to_s,
+ 'id' => created_work_item.to_gid.to_s,
'workItemType' => hash_including('name' => 'Task')
)
)
end
context 'when input is invalid' do
- let(:input) { { 'title' => '', 'workItemTypeId' => WorkItems::Type.default_by_type(:task).to_global_id.to_s } }
+ let(:input) { { 'title' => '', 'workItemTypeId' => WorkItems::Type.default_by_type(:task).to_gid.to_s } }
it 'does not create and returns validation errors' do
expect do
@@ -90,16 +81,14 @@ RSpec.describe 'Create a work item', feature_category: :team_planning do
FIELDS
end
- let(:mutation) { graphql_mutation(:workItemCreate, input.merge('projectPath' => project.full_path), fields) }
-
context 'when setting parent' do
- let_it_be(:parent) { create(:work_item, project: project) }
+ let_it_be(:parent) { create(:work_item, **container_params) }
let(:input) do
{
title: 'item1',
- workItemTypeId: WorkItems::Type.default_by_type(:task).to_global_id.to_s,
- hierarchyWidget: { 'parentId' => parent.to_global_id.to_s }
+ workItemTypeId: WorkItems::Type.default_by_type(:task).to_gid.to_s,
+ hierarchyWidget: { 'parentId' => parent.to_gid.to_s }
}
end
@@ -110,14 +99,14 @@ RSpec.describe 'Create a work item', feature_category: :team_planning do
expect(widgets_response).to include(
{
'children' => { 'edges' => [] },
- 'parent' => { 'id' => parent.to_global_id.to_s },
+ 'parent' => { 'id' => parent.to_gid.to_s },
'type' => 'HIERARCHY'
}
)
end
context 'when parent work item type is invalid' do
- let_it_be(:parent) { create(:work_item, :task, project: project) }
+ let_it_be(:parent) { create(:work_item, :task, **container_params) }
it 'returns error' do
post_graphql_mutation(mutation, current_user: current_user)
@@ -137,6 +126,40 @@ RSpec.describe 'Create a work item', feature_category: :team_planning do
expect(graphql_errors.first['message']).to include('No object found for `parentId')
end
end
+
+ context 'when adjacent is already in place' do
+ let_it_be(:adjacent) { create(:work_item, :task, **container_params) }
+
+ let(:work_item) { WorkItem.last }
+
+ let(:input) do
+ {
+ title: 'item1',
+ workItemTypeId: WorkItems::Type.default_by_type(:task).to_gid.to_s,
+ hierarchyWidget: { 'parentId' => parent.to_gid.to_s }
+ }
+ end
+
+ before(:all) do
+ create(:parent_link, work_item_parent: parent, work_item: adjacent, relative_position: 0)
+ end
+
+ it 'creates work item and sets the relative position to be AFTER adjacent' do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ end.to change(WorkItem, :count).by(1)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(widgets_response).to include(
+ {
+ 'children' => { 'edges' => [] },
+ 'parent' => { 'id' => parent.to_gid.to_s },
+ 'type' => 'HIERARCHY'
+ }
+ )
+ expect(work_item.parent_link.relative_position).to be > adjacent.parent_link.relative_position
+ end
+ end
end
context 'when unsupported widget input is sent' do
@@ -144,7 +167,7 @@ RSpec.describe 'Create a work item', feature_category: :team_planning do
{
'title' => 'new title',
'description' => 'new description',
- 'workItemTypeId' => WorkItems::Type.default_by_type(:test_case).to_global_id.to_s,
+ 'workItemTypeId' => WorkItems::Type.default_by_type(:test_case).to_gid.to_s,
'hierarchyWidget' => {}
}
end
@@ -172,17 +195,15 @@ RSpec.describe 'Create a work item', feature_category: :team_planning do
FIELDS
end
- let(:mutation) { graphql_mutation(:workItemCreate, input.merge('projectPath' => project.full_path), fields) }
-
context 'when setting milestone on work item creation' do
let_it_be(:project_milestone) { create(:milestone, project: project) }
- let_it_be(:group_milestone) { create(:milestone, project: project) }
+ let_it_be(:group_milestone) { create(:milestone, group: group) }
let(:input) do
{
title: 'some WI',
- workItemTypeId: WorkItems::Type.default_by_type(:task).to_global_id.to_s,
- milestoneWidget: { 'milestoneId' => milestone.to_global_id.to_s }
+ workItemTypeId: WorkItems::Type.default_by_type(:task).to_gid.to_s,
+ milestoneWidget: { 'milestoneId' => milestone.to_gid.to_s }
}
end
@@ -196,13 +217,18 @@ RSpec.describe 'Create a work item', feature_category: :team_planning do
expect(widgets_response).to include(
{
'type' => 'MILESTONE',
- 'milestone' => { 'id' => milestone.to_global_id.to_s }
+ 'milestone' => { 'id' => milestone.to_gid.to_s }
}
)
end
end
context 'when assigning a project milestone' do
+ before do
+ group_work_item = container_params[:namespace].present?
+ skip('cannot set a project level milestone to a group level work item') if group_work_item
+ end
+
it_behaves_like "work item's milestone is set" do
let(:milestone) { project_milestone }
end
@@ -216,4 +242,66 @@ RSpec.describe 'Create a work item', feature_category: :team_planning do
end
end
end
+
+ context 'the user is not allowed to create a work item' do
+ let(:current_user) { create(:user) }
+ let(:mutation) { graphql_mutation(:workItemCreate, input.merge('projectPath' => project.full_path), fields) }
+
+ it_behaves_like 'a mutation that returns a top-level access error'
+ end
+
+ context 'when user has permissions to create a work item' do
+ context 'when creating work items in a project' do
+ context 'with projectPath' do
+ let_it_be(:container_params) { { project: project } }
+ let(:mutation) { graphql_mutation(:workItemCreate, input.merge('projectPath' => project.full_path), fields) }
+
+ it_behaves_like 'creates work item'
+ end
+
+ context 'with namespacePath' do
+ let_it_be(:container_params) { { project: project } }
+ let(:mutation) { graphql_mutation(:workItemCreate, input.merge('namespacePath' => project.full_path), fields) }
+
+ it_behaves_like 'creates work item'
+ end
+ end
+
+ context 'when creating work items in a group' do
+ let_it_be(:container_params) { { namespace: group } }
+ let(:mutation) { graphql_mutation(:workItemCreate, input.merge(namespacePath: group.full_path), fields) }
+
+ it_behaves_like 'creates work item'
+ end
+
+ context 'when both projectPath and namespacePath are passed' do
+ let_it_be(:container_params) { { project: project } }
+ let(:mutation) do
+ graphql_mutation(
+ :workItemCreate,
+ input.merge('projectPath' => project.full_path, 'namespacePath' => project.full_path),
+ fields
+ )
+ end
+
+ it_behaves_like 'a mutation that returns top-level errors', errors: [
+ Mutations::WorkItems::Create::MUTUALLY_EXCLUSIVE_ARGUMENTS_ERROR
+ ]
+ end
+
+ context 'when neither of projectPath nor namespacePath are passed' do
+ let_it_be(:container_params) { { project: project } }
+ let(:mutation) do
+ graphql_mutation(
+ :workItemCreate,
+ input,
+ fields
+ )
+ end
+
+ it_behaves_like 'a mutation that returns top-level errors', errors: [
+ Mutations::WorkItems::Create::MUTUALLY_EXCLUSIVE_ARGUMENTS_ERROR
+ ]
+ end
+ end
end
diff --git a/spec/requests/api/graphql/mutations/work_items/export_spec.rb b/spec/requests/api/graphql/mutations/work_items/export_spec.rb
new file mode 100644
index 00000000000..d5d07ea65f8
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/work_items/export_spec.rb
@@ -0,0 +1,71 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Export work items', feature_category: :team_planning do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project) }
+ let_it_be(:reporter) { create(:user).tap { |user| project.add_reporter(user) } }
+ let_it_be(:guest) { create(:user).tap { |user| project.add_guest(user) } }
+ let_it_be(:work_item) { create(:work_item, project: project) }
+
+ let(:input) { { 'projectPath' => project.full_path } }
+ let(:mutation) { graphql_mutation(:workItemExport, input) }
+ let(:mutation_response) { graphql_mutation_response(:work_item_export) }
+
+ context 'when user is not allowed to export work items' do
+ let(:current_user) { guest }
+
+ it_behaves_like 'a mutation that returns a top-level access error'
+ end
+
+ context 'when import_export_work_items_csv feature flag is disabled' do
+ let(:current_user) { reporter }
+
+ before do
+ stub_feature_flags(import_export_work_items_csv: false)
+ end
+
+ it_behaves_like 'a mutation that returns top-level errors',
+ errors: ['`import_export_work_items_csv` feature flag is disabled.']
+ end
+
+ context 'when user has permissions to export work items' do
+ let(:current_user) { reporter }
+ let(:input) do
+ super().merge(
+ 'selectedFields' => %w[TITLE DESCRIPTION AUTHOR TYPE AUTHOR_USERNAME CREATED_AT],
+ 'authorUsername' => 'admin',
+ 'iids' => [work_item.iid.to_s],
+ 'state' => 'opened',
+ 'types' => 'TASK',
+ 'search' => 'any',
+ 'in' => 'TITLE'
+ )
+ end
+
+ it 'schedules export job with given arguments', :aggregate_failures do
+ expected_arguments = {
+ selected_fields: ['title', 'description', 'author', 'type', 'author username', 'created_at'],
+ author_username: 'admin',
+ iids: [work_item.iid.to_s],
+ state: 'opened',
+ issue_types: ['task'],
+ search: 'any',
+ in: ['title']
+ }
+
+ expect(IssuableExportCsvWorker)
+ .to receive(:perform_async).with(:work_item, current_user.id, project.id, expected_arguments)
+
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(mutation_response['message']).to eq(
+ 'Your CSV export request has succeeded. The result will be emailed to ' \
+ "#{reporter.notification_email_or_default}."
+ )
+ expect(mutation_response['errors']).to be_empty
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/work_items/update_spec.rb b/spec/requests/api/graphql/mutations/work_items/update_spec.rb
index ddd294e8f82..ce1c2c01faa 100644
--- a/spec/requests/api/graphql/mutations/work_items/update_spec.rb
+++ b/spec/requests/api/graphql/mutations/work_items/update_spec.rb
@@ -7,20 +7,21 @@ RSpec.describe 'Update a work item', feature_category: :team_planning do
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
+ let_it_be(:author) { create(:user).tap { |user| project.add_reporter(user) } }
let_it_be(:developer) { create(:user).tap { |user| project.add_developer(user) } }
let_it_be(:reporter) { create(:user).tap { |user| project.add_reporter(user) } }
let_it_be(:guest) { create(:user).tap { |user| project.add_guest(user) } }
- let_it_be(:work_item, refind: true) { create(:work_item, project: project) }
+ let_it_be(:work_item, refind: true) { create(:work_item, project: project, author: author) }
let(:work_item_event) { 'CLOSE' }
let(:input) { { 'stateEvent' => work_item_event, 'title' => 'updated title' } }
let(:fields) do
<<~FIELDS
- workItem {
- state
- title
- }
- errors
+ workItem {
+ state
+ title
+ }
+ errors
FIELDS
end
@@ -81,10 +82,10 @@ RSpec.describe 'Update a work item', feature_category: :team_planning do
context 'when updating confidentiality' do
let(:fields) do
<<~FIELDS
- workItem {
- confidential
- }
- errors
+ workItem {
+ confidential
+ }
+ errors
FIELDS
end
@@ -126,18 +127,18 @@ RSpec.describe 'Update a work item', feature_category: :team_planning do
context 'with description widget input' do
let(:fields) do
<<~FIELDS
- workItem {
- title
- description
- state
- widgets {
- type
- ... on WorkItemWidgetDescription {
- description
+ workItem {
+ title
+ description
+ state
+ widgets {
+ type
+ ... on WorkItemWidgetDescription {
+ description
+ }
}
}
- }
- errors
+ errors
FIELDS
end
@@ -445,31 +446,84 @@ RSpec.describe 'Update a work item', feature_category: :team_planning do
let(:widgets_response) { mutation_response['workItem']['widgets'] }
let(:fields) do
<<~FIELDS
- workItem {
- description
- widgets {
- type
- ... on WorkItemWidgetHierarchy {
- parent {
- id
- }
- children {
- edges {
- node {
- id
+ workItem {
+ description
+ widgets {
+ type
+ ... on WorkItemWidgetHierarchy {
+ parent {
+ id
+ }
+ children {
+ edges {
+ node {
+ id
+ }
}
}
}
}
}
- }
- errors
+ errors
FIELDS
end
+ let_it_be(:valid_parent) { create(:work_item, project: project) }
+ let_it_be(:valid_child1) { create(:work_item, :task, project: project, created_at: 5.minutes.ago) }
+ let_it_be(:valid_child2) { create(:work_item, :task, project: project, created_at: 5.minutes.from_now) }
+ let(:input_base) { { parentId: valid_parent.to_gid.to_s } }
+ let(:child1_ref) { { adjacentWorkItemId: valid_child1.to_global_id.to_s } }
+ let(:child2_ref) { { adjacentWorkItemId: valid_child2.to_global_id.to_s } }
+ let(:relative_range) { [valid_child1, valid_child2].map(&:parent_link).map(&:relative_position) }
+
+ let(:invalid_relative_position_error) do
+ WorkItems::Widgets::HierarchyService::UpdateService::INVALID_RELATIVE_POSITION_ERROR
+ end
+
+ shared_examples 'updates work item parent and sets the relative position' do
+ it do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ work_item.reload
+ end.to change(work_item, :work_item_parent).from(nil).to(valid_parent)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(widgets_response).to include({ 'type' => 'HIERARCHY', 'children' => { 'edges' => [] },
+ 'parent' => { 'id' => valid_parent.to_global_id.to_s } })
+
+ expect(work_item.parent_link.relative_position).to be_between(*relative_range)
+ end
+ end
+
+ shared_examples 'sets the relative position and does not update work item parent' do
+ it do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ work_item.reload
+ end.to not_change(work_item, :work_item_parent)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(widgets_response).to include({ 'type' => 'HIERARCHY', 'children' => { 'edges' => [] },
+ 'parent' => { 'id' => valid_parent.to_global_id.to_s } })
+
+ expect(work_item.parent_link.relative_position).to be_between(*relative_range)
+ end
+ end
+
+ shared_examples 'returns "relative position is not valid" error message' do
+ it do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ work_item.reload
+ end.to not_change(work_item, :work_item_parent)
+
+ expect(mutation_response['workItem']).to be_nil
+ expect(mutation_response['errors']).to match_array([invalid_relative_position_error])
+ end
+ end
+
context 'when updating parent' do
let_it_be(:work_item, reload: true) { create(:work_item, :task, project: project) }
- let_it_be(:valid_parent) { create(:work_item, project: project) }
let_it_be(:invalid_parent) { create(:work_item, :task, project: project) }
context 'when parent work item type is invalid' do
@@ -492,20 +546,15 @@ RSpec.describe 'Update a work item', feature_category: :team_planning do
context 'when parent work item has a valid type' do
let(:input) { { 'hierarchyWidget' => { 'parentId' => valid_parent.to_global_id.to_s } } }
- it 'sets the parent for the work item' do
+ it 'updates work item parent' do
expect do
post_graphql_mutation(mutation, current_user: current_user)
work_item.reload
end.to change(work_item, :work_item_parent).from(nil).to(valid_parent)
expect(response).to have_gitlab_http_status(:success)
- expect(widgets_response).to include(
- {
- 'children' => { 'edges' => [] },
- 'parent' => { 'id' => valid_parent.to_global_id.to_s },
- 'type' => 'HIERARCHY'
- }
- )
+ expect(widgets_response).to include({ 'type' => 'HIERARCHY', 'children' => { 'edges' => [] },
+ 'parent' => { 'id' => valid_parent.to_global_id.to_s } })
end
context 'when a parent is already present' do
@@ -522,6 +571,31 @@ RSpec.describe 'Update a work item', feature_category: :team_planning do
end.to change(work_item, :work_item_parent).from(existing_parent).to(valid_parent)
end
end
+
+ context 'when updating relative position' do
+ before(:all) do
+ create(:parent_link, work_item_parent: valid_parent, work_item: valid_child1)
+ create(:parent_link, work_item_parent: valid_parent, work_item: valid_child2)
+ end
+
+ context "when incomplete positioning arguments are given" do
+ let(:input) { { hierarchyWidget: input_base.merge(child1_ref) } }
+
+ it_behaves_like 'returns "relative position is not valid" error message'
+ end
+
+ context 'when moving after adjacent' do
+ let(:input) { { hierarchyWidget: input_base.merge(child1_ref).merge(relativePosition: 'AFTER') } }
+
+ it_behaves_like 'updates work item parent and sets the relative position'
+ end
+
+ context 'when moving before adjacent' do
+ let(:input) { { hierarchyWidget: input_base.merge(child2_ref).merge(relativePosition: 'BEFORE') } }
+
+ it_behaves_like 'updates work item parent and sets the relative position'
+ end
+ end
end
context 'when parentId is null' do
@@ -577,9 +651,37 @@ RSpec.describe 'Update a work item', feature_category: :team_planning do
end
end
+ context 'when reordering existing child' do
+ let_it_be(:work_item, reload: true) { create(:work_item, :task, project: project) }
+
+ context "when parent is already assigned" do
+ before(:all) do
+ create(:parent_link, work_item_parent: valid_parent, work_item: work_item)
+ create(:parent_link, work_item_parent: valid_parent, work_item: valid_child1)
+ create(:parent_link, work_item_parent: valid_parent, work_item: valid_child2)
+ end
+
+ context "when incomplete positioning arguments are given" do
+ let(:input) { { hierarchyWidget: child1_ref } }
+
+ it_behaves_like 'returns "relative position is not valid" error message'
+ end
+
+ context 'when moving after adjacent' do
+ let(:input) { { hierarchyWidget: child1_ref.merge(relativePosition: 'AFTER') } }
+
+ it_behaves_like 'sets the relative position and does not update work item parent'
+ end
+
+ context 'when moving before adjacent' do
+ let(:input) { { hierarchyWidget: child2_ref.merge(relativePosition: 'BEFORE') } }
+
+ it_behaves_like 'sets the relative position and does not update work item parent'
+ end
+ end
+ end
+
context 'when updating children' do
- let_it_be(:valid_child1) { create(:work_item, :task, project: project) }
- let_it_be(:valid_child2) { create(:work_item, :task, project: project) }
let_it_be(:invalid_child) { create(:work_item, project: project) }
let(:input) { { 'hierarchyWidget' => { 'childrenIds' => children_ids } } }
@@ -639,23 +741,29 @@ RSpec.describe 'Update a work item', feature_category: :team_planning do
context 'when updating assignees' do
let(:fields) do
<<~FIELDS
- workItem {
- widgets {
- type
- ... on WorkItemWidgetAssignees {
- assignees {
- nodes {
- id
- username
+ workItem {
+ title
+ workItemType { name }
+ widgets {
+ type
+ ... on WorkItemWidgetAssignees {
+ assignees {
+ nodes {
+ id
+ username
+ }
}
}
- }
- ... on WorkItemWidgetDescription {
- description
+ ... on WorkItemWidgetDescription {
+ description
+ }
+ ... on WorkItemWidgetStartAndDueDate {
+ startDate
+ dueDate
+ }
}
}
- }
- errors
+ errors
FIELDS
end
@@ -728,6 +836,79 @@ RSpec.describe 'Update a work item', feature_category: :team_planning do
)
end
end
+
+ context 'when changing work item type' do
+ let_it_be(:work_item) { create(:work_item, :task, project: project) }
+ let(:description) { "/type Issue" }
+
+ let(:input) { { 'descriptionWidget' => { 'description' => description } } }
+
+ context 'with multiple commands' do
+ let_it_be(:work_item) { create(:work_item, :task, project: project) }
+
+ let(:description) { "Updating work item\n/type Issue\n/due tomorrow\n/title Foo" }
+
+ it 'updates the work item type and other attributes' do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ work_item.reload
+ end.to change { work_item.work_item_type.base_type }.from('task').to('issue')
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['workItem']['workItemType']['name']).to eq('Issue')
+ expect(mutation_response['workItem']['title']).to eq('Foo')
+ expect(mutation_response['workItem']['widgets']).to include(
+ 'type' => 'START_AND_DUE_DATE',
+ 'dueDate' => Date.tomorrow.strftime('%Y-%m-%d'),
+ 'startDate' => nil
+ )
+ end
+ end
+
+ context 'when conversion is not permitted' do
+ let_it_be(:issue) { create(:work_item, project: project) }
+ let_it_be(:link) { create(:parent_link, work_item_parent: issue, work_item: work_item) }
+
+ let(:error_msg) { 'Work item type cannot be changed to Issue with Issue as parent type.' }
+
+ it 'does not update the work item type' do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ work_item.reload
+ end.not_to change { work_item.work_item_type.base_type }
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['errors']).to include(error_msg)
+ end
+ end
+
+ context 'when new type does not support a widget' do
+ before do
+ work_item.update!(start_date: Date.current, due_date: Date.tomorrow)
+ WorkItems::Type.default_by_type(:issue).widget_definitions
+ .find_by_widget_type(:start_and_due_date).update!(disabled: true)
+ end
+
+ it 'updates the work item type and clear widget attributes' do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ work_item.reload
+ end.to change { work_item.work_item_type.base_type }.from('task').to('issue')
+ .and change { work_item.start_date }.to(nil)
+ .and change { work_item.start_date }.to(nil)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['workItem']['workItemType']['name']).to eq('Issue')
+ expect(mutation_response['workItem']['widgets']).to include(
+ {
+ 'type' => 'START_AND_DUE_DATE',
+ 'startDate' => nil,
+ 'dueDate' => nil
+ }
+ )
+ end
+ end
+ end
end
context 'when the work item type does not support the assignees widget' do
@@ -766,17 +947,17 @@ RSpec.describe 'Update a work item', feature_category: :team_planning do
let(:fields) do
<<~FIELDS
- workItem {
- widgets {
- type
- ... on WorkItemWidgetMilestone {
- milestone {
- id
+ workItem {
+ widgets {
+ type
+ ... on WorkItemWidgetMilestone {
+ milestone {
+ id
+ }
}
}
}
- }
- errors
+ errors
FIELDS
end
@@ -843,18 +1024,427 @@ RSpec.describe 'Update a work item', feature_category: :team_planning do
end
end
+ context 'when updating notifications subscription' do
+ let_it_be(:current_user) { reporter }
+ let(:input) { { 'notificationsWidget' => { 'subscribed' => desired_state } } }
+
+ let(:fields) do
+ <<~FIELDS
+ workItem {
+ widgets {
+ type
+ ... on WorkItemWidgetNotifications {
+ subscribed
+ }
+ }
+ }
+ errors
+ FIELDS
+ end
+
+ subject(:update_work_item) { post_graphql_mutation(mutation, current_user: current_user) }
+
+ shared_examples 'subscription updated successfully' do
+ let_it_be(:subscription) do
+ create(
+ :subscription, project: project,
+ user: current_user,
+ subscribable: work_item,
+ subscribed: !desired_state
+ )
+ end
+
+ it "updates existing work item's subscription state" do
+ expect do
+ update_work_item
+ subscription.reload
+ end.to change(subscription, :subscribed).to(desired_state)
+ .and(change { work_item.reload.subscribed?(reporter, project) }.to(desired_state))
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['workItem']['widgets']).to include(
+ {
+ 'subscribed' => desired_state,
+ 'type' => 'NOTIFICATIONS'
+ }
+ )
+ end
+ end
+
+ shared_examples 'subscription update ignored' do
+ context 'when user is subscribed with a subscription record' do
+ let_it_be(:subscription) do
+ create(
+ :subscription, project: project,
+ user: current_user,
+ subscribable: work_item,
+ subscribed: !desired_state
+ )
+ end
+
+ it 'ignores the update request' do
+ expect do
+ update_work_item
+ subscription.reload
+ end.to not_change(subscription, :subscribed)
+ .and(not_change { work_item.subscribed?(current_user, project) })
+
+ expect(response).to have_gitlab_http_status(:success)
+ end
+ end
+
+ context 'when user is subscribed by being a participant' do
+ let_it_be(:current_user) { author }
+
+ it 'ignores the update request' do
+ expect do
+ update_work_item
+ end.to not_change(Subscription, :count)
+ .and(not_change { work_item.subscribed?(current_user, project) })
+
+ expect(response).to have_gitlab_http_status(:success)
+ end
+ end
+ end
+
+ context 'when work item update fails' do
+ let_it_be(:desired_state) { false }
+ let(:input) { { 'title' => nil, 'notificationsWidget' => { 'subscribed' => desired_state } } }
+
+ it_behaves_like 'subscription update ignored'
+ end
+
+ context 'when user cannot update work item' do
+ let_it_be(:desired_state) { false }
+
+ before do
+ allow(Ability).to receive(:allowed?).and_call_original
+ allow(Ability).to receive(:allowed?)
+ .with(current_user, :update_subscription, work_item).and_return(false)
+ end
+
+ it_behaves_like 'subscription update ignored'
+ end
+
+ context 'when user can update work item' do
+ context 'when subscribing to notifications' do
+ let_it_be(:desired_state) { true }
+
+ it_behaves_like 'subscription updated successfully'
+ end
+
+ context 'when unsubscribing from notifications' do
+ let_it_be(:desired_state) { false }
+
+ it_behaves_like 'subscription updated successfully'
+
+ context 'when user is subscribed by being a participant' do
+ let_it_be(:current_user) { author }
+
+ it 'creates a subscription with desired state' do
+ expect { update_work_item }.to change(Subscription, :count).by(1)
+ .and(change { work_item.reload.subscribed?(author, project) }.to(desired_state))
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['workItem']['widgets']).to include(
+ {
+ 'subscribed' => desired_state,
+ 'type' => 'NOTIFICATIONS'
+ }
+ )
+ end
+ end
+ end
+ end
+ end
+
+ context 'when updating currentUserTodos' do
+ let_it_be(:current_user) { reporter }
+
+ let(:fields) do
+ <<~FIELDS
+ workItem {
+ widgets {
+ type
+ ... on WorkItemWidgetCurrentUserTodos {
+ currentUserTodos {
+ nodes {
+ id
+ state
+ }
+ }
+ }
+ }
+ }
+ errors
+ FIELDS
+ end
+
+ subject(:update_work_item) { post_graphql_mutation(mutation, current_user: current_user) }
+
+ context 'when adding a new todo' do
+ let(:input) { { 'currentUserTodosWidget' => { 'action' => 'ADD' } } }
+
+ context 'when user has access to the work item' do
+ it 'adds a new todo for the user on the work item' do
+ expect { update_work_item }.to change { current_user.todos.count }.by(1)
+
+ created_todo = current_user.todos.last
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['workItem']['widgets']).to include(
+ {
+ 'type' => 'CURRENT_USER_TODOS',
+ 'currentUserTodos' => {
+ 'nodes' => [
+ { 'id' => created_todo.to_global_id.to_s, 'state' => 'pending' }
+ ]
+ }
+ }
+ )
+ end
+ end
+
+ context 'when user has no access' do
+ let_it_be(:current_user) { create(:user) }
+
+ it 'does not create a new todo' do
+ expect { update_work_item }.to change { Todo.count }.by(0)
+
+ expect(response).to have_gitlab_http_status(:success)
+ end
+ end
+ end
+
+ context 'when marking all todos of the work item as done' do
+ let_it_be(:pending_todo1) do
+ create(:todo, target: work_item, target_type: 'WorkItem', user: current_user, state: :pending)
+ end
+
+ let_it_be(:pending_todo2) do
+ create(:todo, target: work_item, target_type: 'WorkItem', user: current_user, state: :pending)
+ end
+
+ let(:input) { { 'currentUserTodosWidget' => { 'action' => 'MARK_AS_DONE' } } }
+
+ context 'when user has access' do
+ it 'marks all todos of the user on the work item as done' do
+ expect { update_work_item }.to change { current_user.todos.done.count }.by(2)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['workItem']['widgets']).to include(
+ {
+ 'type' => 'CURRENT_USER_TODOS',
+ 'currentUserTodos' => {
+ 'nodes' => match_array([
+ { 'id' => pending_todo1.to_global_id.to_s, 'state' => 'done' },
+ { 'id' => pending_todo2.to_global_id.to_s, 'state' => 'done' }
+ ])
+ }
+ }
+ )
+ end
+ end
+
+ context 'when user has no access' do
+ let_it_be(:current_user) { create(:user) }
+
+ it 'does not mark todos as done' do
+ expect { update_work_item }.to change { Todo.done.count }.by(0)
+
+ expect(response).to have_gitlab_http_status(:success)
+ end
+ end
+ end
+
+ context 'when marking one todo of the work item as done' do
+ let_it_be(:pending_todo1) do
+ create(:todo, target: work_item, target_type: 'WorkItem', user: current_user, state: :pending)
+ end
+
+ let_it_be(:pending_todo2) do
+ create(:todo, target: work_item, target_type: 'WorkItem', user: current_user, state: :pending)
+ end
+
+ let(:input) do
+ { 'currentUserTodosWidget' => { 'action' => 'MARK_AS_DONE', todo_id: global_id_of(pending_todo1) } }
+ end
+
+ context 'when user has access' do
+ it 'marks the todo of the work item as done' do
+ expect { update_work_item }.to change { current_user.todos.done.count }.by(1)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['workItem']['widgets']).to include(
+ {
+ 'type' => 'CURRENT_USER_TODOS',
+ 'currentUserTodos' => {
+ 'nodes' => match_array([
+ { 'id' => pending_todo1.to_global_id.to_s, 'state' => 'done' },
+ { 'id' => pending_todo2.to_global_id.to_s, 'state' => 'pending' }
+ ])
+ }
+ }
+ )
+ end
+ end
+
+ context 'when user has no access' do
+ let_it_be(:current_user) { create(:user) }
+
+ it 'does not mark the todo as done' do
+ expect { update_work_item }.to change { Todo.done.count }.by(0)
+
+ expect(response).to have_gitlab_http_status(:success)
+ end
+ end
+ end
+ end
+
+ context 'when updating awardEmoji' do
+ let_it_be(:current_user) { work_item.author }
+ let_it_be(:upvote) { create(:award_emoji, :upvote, awardable: work_item, user: current_user) }
+ let(:award_action) { 'ADD' }
+ let(:award_name) { 'star' }
+ let(:input) { { 'awardEmojiWidget' => { 'action' => award_action, 'name' => award_name } } }
+
+ let(:fields) do
+ <<~FIELDS
+ workItem {
+ widgets {
+ type
+ ... on WorkItemWidgetAwardEmoji {
+ upvotes
+ downvotes
+ awardEmoji {
+ nodes {
+ name
+ user { id }
+ }
+ }
+ }
+ }
+ }
+ errors
+ FIELDS
+ end
+
+ subject(:update_work_item) { post_graphql_mutation(mutation, current_user: current_user) }
+
+ context 'when user cannot award work item' do
+ before do
+ allow(Ability).to receive(:allowed?).and_call_original
+ allow(Ability).to receive(:allowed?)
+ .with(current_user, :award_emoji, work_item).and_return(false)
+ end
+
+ it 'ignores the update request' do
+ expect do
+ update_work_item
+ end.to not_change(AwardEmoji, :count)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['errors']).to be_empty
+ expect(graphql_errors).to be_blank
+ end
+ end
+
+ context 'when user can award work item' do
+ shared_examples 'request with error' do |message|
+ it 'ignores update and returns an error' do
+ expect do
+ update_work_item
+ end.not_to change(AwardEmoji, :count)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['workItem']).to be_nil
+ expect(mutation_response['errors'].first).to include(message)
+ end
+ end
+
+ shared_examples 'request that removes emoji' do
+ it "updates work item's award emoji" do
+ expect do
+ update_work_item
+ end.to change(AwardEmoji, :count).by(-1)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['workItem']['widgets']).to include(
+ {
+ 'upvotes' => 0,
+ 'downvotes' => 0,
+ 'awardEmoji' => { 'nodes' => [] },
+ 'type' => 'AWARD_EMOJI'
+ }
+ )
+ end
+ end
+
+ shared_examples 'request that adds emoji' do
+ it "updates work item's award emoji" do
+ expect do
+ update_work_item
+ end.to change(AwardEmoji, :count).by(1)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['workItem']['widgets']).to include(
+ {
+ 'upvotes' => 1,
+ 'downvotes' => 0,
+ 'awardEmoji' => { 'nodes' => [
+ { 'name' => 'thumbsup', 'user' => { 'id' => current_user.to_gid.to_s } },
+ { 'name' => award_name, 'user' => { 'id' => current_user.to_gid.to_s } }
+ ] },
+ 'type' => 'AWARD_EMOJI'
+ }
+ )
+ end
+ end
+
+ context 'when adding award emoji' do
+ it_behaves_like 'request that adds emoji'
+
+ context 'when the emoji name is not valid' do
+ let(:award_name) { 'xxqq' }
+
+ it_behaves_like 'request with error', 'Name is not a valid emoji name'
+ end
+ end
+
+ context 'when removing award emoji' do
+ let(:award_action) { 'REMOVE' }
+
+ context 'when emoji was awarded by current user' do
+ let(:award_name) { 'thumbsup' }
+
+ it_behaves_like 'request that removes emoji'
+ end
+
+ context 'when emoji was awarded by a different user' do
+ let(:award_name) { 'thumbsdown' }
+
+ before do
+ create(:award_emoji, :downvote, awardable: work_item)
+ end
+
+ it_behaves_like 'request with error',
+ 'User has not awarded emoji of type thumbsdown on the awardable'
+ end
+ end
+ end
+ end
+
context 'when unsupported widget input is sent' do
- let_it_be(:test_case) { create(:work_item_type, :default, :test_case) }
- let_it_be(:work_item) { create(:work_item, work_item_type: test_case, project: project) }
+ let_it_be(:work_item) { create(:work_item, :test_case, project: project) }
let(:input) do
{
- 'hierarchyWidget' => {}
+ 'assigneesWidget' => { 'assigneeIds' => [developer.to_gid.to_s] }
}
end
it_behaves_like 'a mutation that returns top-level errors',
- errors: ["Following widget keys are not supported by Test Case type: [:hierarchy_widget]"]
+ errors: ["Following widget keys are not supported by Test Case type: [:assignees_widget]"]
end
end
end
diff --git a/spec/requests/api/graphql/mutations/work_items/update_task_spec.rb b/spec/requests/api/graphql/mutations/work_items/update_task_spec.rb
index 999c685ac6a..717de983871 100644
--- a/spec/requests/api/graphql/mutations/work_items/update_task_spec.rb
+++ b/spec/requests/api/graphql/mutations/work_items/update_task_spec.rb
@@ -18,7 +18,7 @@ RSpec.describe 'Update a work item task', feature_category: :team_planning do
let(:task_params) { { 'title' => 'UPDATED' } }
let(:task_input) { { 'id' => task.to_global_id.to_s }.merge(task_params) }
let(:input) { { 'id' => work_item.to_global_id.to_s, 'taskData' => task_input } }
- let(:mutation) { graphql_mutation(:workItemUpdateTask, input) }
+ let(:mutation) { graphql_mutation(:workItemUpdateTask, input, nil, ['productAnalyticsState']) }
let(:mutation_response) { graphql_mutation_response(:work_item_update_task) }
context 'the user is not allowed to read a work item' do
diff --git a/spec/requests/api/graphql/namespace/projects_spec.rb b/spec/requests/api/graphql/namespace/projects_spec.rb
index 4e12da3e3ab..83edacaf831 100644
--- a/spec/requests/api/graphql/namespace/projects_spec.rb
+++ b/spec/requests/api/graphql/namespace/projects_spec.rb
@@ -23,7 +23,7 @@ RSpec.describe 'getting projects', feature_category: :projects do
projects(includeSubgroups: #{include_subgroups}) {
edges {
node {
- #{all_graphql_fields_for('Project', max_depth: 1)}
+ #{all_graphql_fields_for('Project', max_depth: 1, excluded: ['productAnalyticsState'])}
}
}
}
diff --git a/spec/requests/api/graphql/packages/package_spec.rb b/spec/requests/api/graphql/packages/package_spec.rb
index 82fcc5254ad..7610a4aaac1 100644
--- a/spec/requests/api/graphql/packages/package_spec.rb
+++ b/spec/requests/api/graphql/packages/package_spec.rb
@@ -270,6 +270,31 @@ RSpec.describe 'package details', feature_category: :package_registry do
it 'returns composer_config_repository_url correctly' do
expect(graphql_data_at(:package, :composer_config_repository_url)).to eq("localhost/#{group.id}")
end
+
+ context 'with access to package registry for everyone' do
+ before do
+ project.project_feature.update!(package_registry_access_level: ProjectFeature::PUBLIC)
+ subject
+ end
+
+ it 'returns pypi_url correctly' do
+ expect(graphql_data_at(:package, :pypi_url)).to eq("http://__token__:<your_personal_token>@localhost/api/v4/projects/#{project.id}/packages/pypi/simple")
+ end
+ end
+
+ context 'when project is public' do
+ let_it_be(:public_project) { create(:project, :public, group: group) }
+ let_it_be(:composer_package) { create(:composer_package, project: public_project) }
+ let(:package_global_id) { global_id_of(composer_package) }
+
+ before do
+ subject
+ end
+
+ it 'returns pypi_url correctly' do
+ expect(graphql_data_at(:package, :pypi_url)).to eq("http://localhost/api/v4/projects/#{public_project.id}/packages/pypi/simple")
+ end
+ end
end
context 'web_path' do
diff --git a/spec/requests/api/graphql/project/alert_management/alert/metrics_dashboard_url_spec.rb b/spec/requests/api/graphql/project/alert_management/alert/metrics_dashboard_url_spec.rb
index b430fdeb18f..3417f9529bd 100644
--- a/spec/requests/api/graphql/project/alert_management/alert/metrics_dashboard_url_spec.rb
+++ b/spec/requests/api/graphql/project/alert_management/alert/metrics_dashboard_url_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'getting Alert Management Alert Assignees', feature_category: :projects do
+RSpec.describe 'getting Alert Management Alert Assignees', feature_category: :incident_management do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
@@ -29,6 +29,7 @@ RSpec.describe 'getting Alert Management Alert Assignees', feature_category: :pr
let(:first_alert) { alerts.first }
before do
+ stub_feature_flags(remove_monitor_metrics: false)
project.add_developer(current_user)
end
@@ -44,6 +45,17 @@ RSpec.describe 'getting Alert Management Alert Assignees', feature_category: :pr
expect(first_alert).to include('metricsDashboardUrl' => dashboard_url_for_alert)
end
+
+ context 'when metrics dashboard feature is unavailable' do
+ before do
+ stub_feature_flags(remove_monitor_metrics: true)
+ end
+
+ it 'returns nil' do
+ post_graphql(graphql_query, current_user: current_user)
+ expect(first_alert['metricsDashboardUrl']).to be_nil
+ end
+ end
end
context 'with gitlab-managed prometheus payload' do
@@ -58,5 +70,16 @@ RSpec.describe 'getting Alert Management Alert Assignees', feature_category: :pr
expect(first_alert).to include('metricsDashboardUrl' => dashboard_url_for_alert)
end
+
+ context 'when metrics dashboard feature is unavailable' do
+ before do
+ stub_feature_flags(remove_monitor_metrics: true)
+ end
+
+ it 'returns nil' do
+ post_graphql(graphql_query, current_user: current_user)
+ expect(first_alert['metricsDashboardUrl']).to be_nil
+ end
+ end
end
end
diff --git a/spec/requests/api/graphql/project/alert_management/alert/notes_spec.rb b/spec/requests/api/graphql/project/alert_management/alert/notes_spec.rb
index 16dd0dfcfcb..c1ac0367853 100644
--- a/spec/requests/api/graphql/project/alert_management/alert/notes_spec.rb
+++ b/spec/requests/api/graphql/project/alert_management/alert/notes_spec.rb
@@ -51,7 +51,7 @@ RSpec.describe 'getting Alert Management Alert Notes', feature_category: :team_p
expect(first_notes_result.first).to include(
'id' => first_system_note.to_global_id.to_s,
- 'systemNoteIconName' => 'git-merge',
+ 'systemNoteIconName' => 'merge',
'body' => first_system_note.note
)
end
diff --git a/spec/requests/api/graphql/project/base_service_spec.rb b/spec/requests/api/graphql/project/base_service_spec.rb
index 7b1b95eaf58..b27cddea07b 100644
--- a/spec/requests/api/graphql/project/base_service_spec.rb
+++ b/spec/requests/api/graphql/project/base_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'query Jira service', feature_category: :authentication_and_authorization do
+RSpec.describe 'query Jira service', feature_category: :system_access do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
diff --git a/spec/requests/api/graphql/project/ci_access_authorized_agents_spec.rb b/spec/requests/api/graphql/project/ci_access_authorized_agents_spec.rb
new file mode 100644
index 00000000000..dd76f6425fe
--- /dev/null
+++ b/spec/requests/api/graphql/project/ci_access_authorized_agents_spec.rb
@@ -0,0 +1,122 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Project.ci_access_authorized_agents', feature_category: :deployment_management do
+ include GraphqlHelpers
+
+ let_it_be(:organization) { create(:group) }
+ let_it_be(:agent_management_project) { create(:project, :private, group: organization) }
+ let_it_be(:agent) { create(:cluster_agent, project: agent_management_project) }
+
+ let_it_be(:deployment_project) { create(:project, :private, group: organization) }
+ let_it_be(:deployment_developer) { create(:user).tap { |u| deployment_project.add_developer(u) } }
+ let_it_be(:deployment_reporter) { create(:user).tap { |u| deployment_project.add_reporter(u) } }
+
+ let(:user) { deployment_developer }
+
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{deployment_project.full_path}") {
+ ciAccessAuthorizedAgents {
+ nodes {
+ agent {
+ id
+ name
+ project {
+ name
+ }
+ }
+ config
+ }
+ }
+ }
+ }
+ )
+ end
+
+ subject { GitlabSchema.execute(query, context: { current_user: user }).as_json }
+
+ context 'with project authorization' do
+ let!(:ci_access) { create(:agent_ci_access_project_authorization, agent: agent, project: deployment_project) }
+
+ it 'returns the authorized agent' do
+ authorized_agents = subject.dig('data', 'project', 'ciAccessAuthorizedAgents', 'nodes')
+
+ expect(authorized_agents.count).to eq(1)
+
+ authorized_agent = authorized_agents.first
+
+ expect(authorized_agent['agent']['id']).to eq(agent.to_global_id.to_s)
+ expect(authorized_agent['agent']['name']).to eq(agent.name)
+ expect(authorized_agent['config']).to eq({ "default_namespace" => "production" })
+ expect(authorized_agent['agent']['project']).to be_nil # User is not authorized to read other resources.
+ end
+
+ context 'when user is developer in the agent management project' do
+ before do
+ agent_management_project.add_developer(deployment_developer)
+ end
+
+ it 'returns the project information as well' do
+ authorized_agent = subject.dig('data', 'project', 'ciAccessAuthorizedAgents', 'nodes').first
+
+ expect(authorized_agent['agent']['project']['name']).to eq(agent_management_project.name)
+ end
+ end
+
+ context 'when user is reporter' do
+ let(:user) { deployment_reporter }
+
+ it 'returns nothing' do
+ expect(subject['data']['project']['ciAccessAuthorizedAgents']).to be_nil
+ end
+ end
+ end
+
+ context 'with group authorization' do
+ let!(:ci_access) { create(:agent_ci_access_group_authorization, agent: agent, group: organization) }
+
+ it 'returns the authorized agent' do
+ authorized_agents = subject.dig('data', 'project', 'ciAccessAuthorizedAgents', 'nodes')
+
+ expect(authorized_agents.count).to eq(1)
+
+ authorized_agent = authorized_agents.first
+
+ expect(authorized_agent['agent']['id']).to eq(agent.to_global_id.to_s)
+ expect(authorized_agent['agent']['name']).to eq(agent.name)
+ expect(authorized_agent['config']).to eq({ "default_namespace" => "production" })
+ expect(authorized_agent['agent']['project']).to be_nil # User is not authorized to read other resources.
+ end
+
+ context 'when user is developer in the agent management project' do
+ before do
+ agent_management_project.add_developer(deployment_developer)
+ end
+
+ it 'returns the project information as well' do
+ authorized_agent = subject.dig('data', 'project', 'ciAccessAuthorizedAgents', 'nodes').first
+
+ expect(authorized_agent['agent']['project']['name']).to eq(agent_management_project.name)
+ end
+ end
+
+ context 'when user is reporter' do
+ let(:user) { deployment_reporter }
+
+ it 'returns nothing' do
+ expect(subject['data']['project']['ciAccessAuthorizedAgents']).to be_nil
+ end
+ end
+ end
+
+ context 'when deployment project is not authorized to ci_access to the agent' do
+ it 'returns empty' do
+ authorized_agents = subject.dig('data', 'project', 'ciAccessAuthorizedAgents', 'nodes')
+
+ expect(authorized_agents).to be_empty
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/project/cluster_agents_spec.rb b/spec/requests/api/graphql/project/cluster_agents_spec.rb
index 0881eb9cdc3..181f21001ea 100644
--- a/spec/requests/api/graphql/project/cluster_agents_spec.rb
+++ b/spec/requests/api/graphql/project/cluster_agents_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Project.cluster_agents', feature_category: :kubernetes_management do
+RSpec.describe 'Project.cluster_agents', feature_category: :deployment_management do
include GraphqlHelpers
let_it_be(:project) { create(:project, :public) }
@@ -53,10 +53,11 @@ RSpec.describe 'Project.cluster_agents', feature_category: :kubernetes_managemen
let_it_be(:token_1) { create(:cluster_agent_token, agent: agents.second) }
let_it_be(:token_2) { create(:cluster_agent_token, agent: agents.second, last_used_at: 3.days.ago) }
let_it_be(:token_3) { create(:cluster_agent_token, agent: agents.second, last_used_at: 2.days.ago) }
+ let_it_be(:revoked_token) { create(:cluster_agent_token, :revoked, agent: agents.second) }
let(:cluster_agents_fields) { [:id, query_nodes(:tokens, of: 'ClusterAgentToken')] }
- it 'can select tokens in last_used_at order' do
+ it 'can select active tokens in last_used_at order' do
post_graphql(query, current_user: current_user)
tokens = graphql_data_at(:project, :cluster_agents, :nodes, :tokens, :nodes)
diff --git a/spec/requests/api/graphql/project/commit_references_spec.rb b/spec/requests/api/graphql/project/commit_references_spec.rb
new file mode 100644
index 00000000000..4b545adee12
--- /dev/null
+++ b/spec/requests/api/graphql/project/commit_references_spec.rb
@@ -0,0 +1,240 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Query.project(fullPath).commitReferences(commitSha)', feature_category: :source_code_management do
+ include GraphqlHelpers
+ include Presentable
+
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:repository) { project.repository.raw }
+ let_it_be(:current_user) { project.first_owner }
+ let_it_be(:branches_names) { %w[master not-merged-branch v1.1.0] }
+ let_it_be(:tag_name) { 'v1.0.0' }
+ let_it_be(:commit_sha) { repository.commit.id }
+
+ let(:post_query) { post_graphql(query, current_user: current_user) }
+ let(:data) { graphql_data.dig(*path) }
+ let(:base_args) { {} }
+ let(:args) { base_args }
+
+ shared_context 'with the limit argument' do
+ context 'with limit of 2' do
+ let(:args) { { limit: 2 } }
+
+ it 'returns the right amount of refs' do
+ post_query
+ expect(data.count).to be <= 2
+ end
+ end
+
+ context 'with limit of -2' do
+ let(:args) { { limit: -2 } }
+
+ it 'casts an argument error "limit must be greater then 0"' do
+ post_query
+ expect(graphql_errors).to include(custom_graphql_error(path - ['names'],
+ 'limit must be within 1..1000'))
+ end
+ end
+
+ context 'with limit of 1001' do
+ let(:args) { { limit: 1001 } }
+
+ it 'casts an argument error "limit must be greater then 0"' do
+ post_query
+ expect(graphql_errors).to include(custom_graphql_error(path - ['names'],
+ 'limit must be within 1..1000'))
+ end
+ end
+ end
+
+ describe 'the path commitReferences should return nil' do
+ let(:path) { %w[project commitReferences] }
+
+ let(:query) do
+ graphql_query_for(:project, { fullPath: project.full_path },
+ query_graphql_field(
+ :commitReferences,
+ { commitSha: commit_sha },
+ query_graphql_field(:tippingTags, :names)
+ )
+ )
+ end
+
+ context 'when commit does not exist' do
+ let(:commit_sha) { '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff4' }
+
+ it 'commitReferences returns nil' do
+ post_query
+ expect(data).to eq(nil)
+ end
+ end
+
+ context 'when sha length is incorrect' do
+ let(:commit_sha) { 'foo' }
+
+ it 'commitReferences returns nil' do
+ post_query
+ expect(data).to eq(nil)
+ end
+ end
+
+ context 'when user is not authorized' do
+ let(:commit_sha) { repository.commit.id }
+ let(:current_user) { create(:user) }
+
+ it 'commitReferences returns nil' do
+ post_query
+ expect(data).to eq(nil)
+ end
+ end
+ end
+
+ context 'with containing refs' do
+ let(:base_args) { { excludeTipped: false } }
+ let(:excluded_tipped_args) do
+ hash = base_args.dup
+ hash[:excludeTipped] = true
+ hash
+ end
+
+ context 'with path Query.project(fullPath).commitReferences(commitSha).containingTags' do
+ let_it_be(:commit_sha) { repository.find_tag(tag_name).target_commit.sha }
+ let_it_be(:path) { %w[project commitReferences containingTags names] }
+ let(:query) do
+ graphql_query_for(
+ :project,
+ { fullPath: project.full_path },
+ query_graphql_field(
+ :commitReferences,
+ { commitSha: commit_sha },
+ query_graphql_field(:containingTags, args, :names)
+ )
+ )
+ end
+
+ context 'without excludeTipped argument' do
+ it 'returns tags names containing the commit' do
+ post_query
+ expect(data).to eq(%w[v1.0.0 v1.1.0 v1.1.1])
+ end
+ end
+
+ context 'with excludeTipped argument' do
+ let_it_be(:ref_prefix) { Gitlab::Git::TAG_REF_PREFIX }
+
+ let(:args) { excluded_tipped_args }
+
+ it 'returns tags names containing the commit without the tipped tags' do
+ excluded_refs = project.repository
+ .refs_by_oid(oid: commit_sha, ref_patterns: [ref_prefix])
+ .map { |n| n.delete_prefix(ref_prefix) }
+
+ post_query
+ expect(data).to eq(%w[v1.0.0 v1.1.0 v1.1.1] - excluded_refs)
+ end
+ end
+
+ include_context 'with the limit argument'
+ end
+
+ context 'with path Query.project(fullPath).commitReferences(commitSha).containingBranches' do
+ let_it_be(:ref_prefix) { Gitlab::Git::BRANCH_REF_PREFIX }
+ let_it_be(:path) { %w[project commitReferences containingBranches names] }
+
+ let(:query) do
+ graphql_query_for(
+ :project,
+ { fullPath: project.full_path },
+ query_graphql_field(
+ :commitReferences,
+ { commitSha: commit_sha },
+ query_graphql_field(:containingBranches, args, :names)
+ )
+ )
+ end
+
+ context 'without excludeTipped argument' do
+ it 'returns branch names containing the commit' do
+ refs = project.repository.branch_names_contains(commit_sha)
+
+ post_query
+
+ expect(data).to eq(refs)
+ end
+ end
+
+ context 'with excludeTipped argument' do
+ let(:args) { excluded_tipped_args }
+
+ it 'returns branch names containing the commit without the tipped branch' do
+ refs = project.repository.branch_names_contains(commit_sha)
+
+ excluded_refs = project.repository
+ .refs_by_oid(oid: commit_sha, ref_patterns: [ref_prefix])
+ .map { |n| n.delete_prefix(ref_prefix) }
+
+ post_query
+
+ expect(data).to eq(refs - excluded_refs)
+ end
+ end
+
+ include_context 'with the limit argument'
+ end
+ end
+
+ context 'with tipping refs' do
+ context 'with path Query.project(fullPath).commitReferences(commitSha).tippingTags' do
+ let(:commit_sha) { repository.find_tag(tag_name).dereferenced_target.sha }
+ let(:path) { %w[project commitReferences tippingTags names] }
+
+ let(:query) do
+ graphql_query_for(
+ :project,
+ { fullPath: project.full_path },
+ query_graphql_field(
+ :commitReferences,
+ { commitSha: commit_sha },
+ query_graphql_field(:tippingTags, args, :names)
+ )
+ )
+ end
+
+ context 'with authorized user' do
+ it 'returns tags names tipping the commit' do
+ post_query
+
+ expect(data).to eq([tag_name])
+ end
+ end
+
+ include_context 'with the limit argument'
+ end
+
+ context 'with path Query.project(fullPath).commitReferences(commitSha).tippingBranches' do
+ let(:path) { %w[project commitReferences tippingBranches names] }
+
+ let(:query) do
+ graphql_query_for(
+ :project,
+ { fullPath: project.full_path },
+ query_graphql_field(
+ :commitReferences,
+ { commitSha: commit_sha },
+ query_graphql_field(:tippingBranches, args, :names)
+ )
+ )
+ end
+
+ it 'returns branches names tipping the commit' do
+ post_query
+
+ expect(data).to eq(branches_names)
+ end
+
+ include_context 'with the limit argument'
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/project/container_repositories_spec.rb b/spec/requests/api/graphql/project/container_repositories_spec.rb
index 7ccf8a6f5bf..9a40a972256 100644
--- a/spec/requests/api/graphql/project/container_repositories_spec.rb
+++ b/spec/requests/api/graphql/project/container_repositories_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe 'getting container repositories in a project', feature_category:
let_it_be(:container_repositories) { [container_repository, container_repositories_delete_scheduled, container_repositories_delete_failed].flatten }
let_it_be(:container_expiration_policy) { project.container_expiration_policy }
- let(:excluded_fields) { %w[pipeline jobs] }
+ let(:excluded_fields) { %w[pipeline jobs productAnalyticsState] }
let(:container_repositories_fields) do
<<~GQL
edges {
@@ -155,7 +155,7 @@ RSpec.describe 'getting container repositories in a project', feature_category:
it_behaves_like 'handling graphql network errors with the container registry'
it_behaves_like 'not hitting graphql network errors with the container registry' do
- let(:excluded_fields) { %w[pipeline jobs tags tagsCount] }
+ let(:excluded_fields) { %w[pipeline jobs tags tagsCount productAnalyticsState] }
end
it 'returns the total count of container repositories' do
diff --git a/spec/requests/api/graphql/project/data_transfer_spec.rb b/spec/requests/api/graphql/project/data_transfer_spec.rb
new file mode 100644
index 00000000000..aafa8d65eb9
--- /dev/null
+++ b/spec/requests/api/graphql/project/data_transfer_spec.rb
@@ -0,0 +1,112 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'project data transfers', feature_category: :source_code_management do
+ include GraphqlHelpers
+
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+
+ let(:fields) do
+ <<~QUERY
+ #{all_graphql_fields_for('ProjectDataTransfer'.classify)}
+ QUERY
+ end
+
+ let(:query) do
+ graphql_query_for(
+ 'project',
+ { fullPath: project.full_path },
+ query_graphql_field('DataTransfer', params, fields)
+ )
+ end
+
+ let(:from) { Date.new(2022, 1, 1) }
+ let(:to) { Date.new(2023, 1, 1) }
+ let(:params) { { from: from, to: to } }
+ let(:egress_data) do
+ graphql_data.dig('project', 'dataTransfer', 'egressNodes', 'nodes')
+ end
+
+ before do
+ create(:project_data_transfer, project: project, date: '2022-01-01', repository_egress: 1)
+ create(:project_data_transfer, project: project, date: '2022-02-01', repository_egress: 2)
+ end
+
+ subject { post_graphql(query, current_user: current_user) }
+
+ context 'with anonymous access' do
+ let_it_be(:current_user) { nil }
+
+ before do
+ subject
+ end
+
+ it_behaves_like 'a working graphql query'
+
+ it 'returns no data' do
+ expect(graphql_data_at(:project, :data_transfer)).to be_nil
+ expect(graphql_errors).to be_nil
+ end
+ end
+
+ context 'with authorized user but without enough permissions' do
+ before do
+ project.add_developer(current_user)
+ subject
+ end
+
+ it_behaves_like 'a working graphql query'
+
+ it 'returns empty results' do
+ expect(graphql_data_at(:project, :data_transfer)).to be_nil
+ expect(graphql_errors).to be_nil
+ end
+ end
+
+ context 'when user has enough permissions' do
+ before do
+ project.add_owner(current_user)
+ end
+
+ context 'when data_transfer_monitoring_mock_data is NOT enabled' do
+ before do
+ stub_feature_flags(data_transfer_monitoring_mock_data: false)
+ subject
+ end
+
+ it 'returns real results' do
+ expect(response).to have_gitlab_http_status(:ok)
+
+ expect(egress_data.count).to eq(2)
+
+ expect(egress_data.first.keys).to match_array(
+ %w[date totalEgress repositoryEgress artifactsEgress packagesEgress registryEgress]
+ )
+
+ expect(egress_data.pluck('repositoryEgress')).to match_array(%w[1 2])
+ end
+
+ it_behaves_like 'a working graphql query'
+ end
+
+ context 'when data_transfer_monitoring_mock_data is enabled' do
+ before do
+ stub_feature_flags(data_transfer_monitoring_mock_data: true)
+ subject
+ end
+
+ it 'returns mock results' do
+ expect(response).to have_gitlab_http_status(:ok)
+
+ expect(egress_data.count).to eq(12)
+ expect(egress_data.first.keys).to match_array(
+ %w[date totalEgress repositoryEgress artifactsEgress packagesEgress registryEgress]
+ )
+ end
+
+ it_behaves_like 'a working graphql query'
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/project/environments_spec.rb b/spec/requests/api/graphql/project/environments_spec.rb
index 618f591affa..bb1763ee228 100644
--- a/spec/requests/api/graphql/project/environments_spec.rb
+++ b/spec/requests/api/graphql/project/environments_spec.rb
@@ -102,7 +102,7 @@ RSpec.describe 'Project Environments query', feature_category: :continuous_deliv
end
describe 'last deployments of environments' do
- ::Deployment.statuses.each do |status, _|
+ ::Deployment.statuses.each do |status, _| # rubocop:disable RSpec/UselessDynamicDefinition
let_it_be(:"production_#{status}_deployment") do
create(:deployment, status.to_sym, environment: production, project: project)
end
diff --git a/spec/requests/api/graphql/project/flow_metrics_spec.rb b/spec/requests/api/graphql/project/flow_metrics_spec.rb
new file mode 100644
index 00000000000..3b5758b3a2e
--- /dev/null
+++ b/spec/requests/api/graphql/project/flow_metrics_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'getting project flow metrics', feature_category: :value_stream_management do
+ include GraphqlHelpers
+
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project1) { create(:project, :repository, group: group) }
+ # This is done so we can use the same count expectations in the shared examples and
+ # reuse the shared example for the group-level test.
+ let_it_be(:project2) { project1 }
+ let_it_be(:production_environment1) { create(:environment, :production, project: project1) }
+ let_it_be(:production_environment2) { production_environment1 }
+ let_it_be(:current_user) { create(:user, maintainer_projects: [project1]) }
+
+ let(:full_path) { project1.full_path }
+ let(:context) { :project }
+
+ it_behaves_like 'value stream analytics flow metrics issueCount examples'
+
+ it_behaves_like 'value stream analytics flow metrics deploymentCount examples'
+end
diff --git a/spec/requests/api/graphql/project/fork_details_spec.rb b/spec/requests/api/graphql/project/fork_details_spec.rb
index efd48b00833..91a04dc7c50 100644
--- a/spec/requests/api/graphql/project/fork_details_spec.rb
+++ b/spec/requests/api/graphql/project/fork_details_spec.rb
@@ -10,12 +10,13 @@ RSpec.describe 'getting project fork details', feature_category: :source_code_ma
let_it_be(:current_user) { create(:user, maintainer_projects: [project]) }
let_it_be(:forked_project) { fork_project(project, current_user, repository: true) }
+ let(:ref) { 'feature' }
let(:queried_project) { forked_project }
let(:query) do
graphql_query_for(:project,
{ full_path: queried_project.full_path }, <<~QUERY
- forkDetails(ref: "feature"){
+ forkDetails(ref: "#{ref}"){
ahead
behind
}
@@ -23,12 +24,23 @@ RSpec.describe 'getting project fork details', feature_category: :source_code_ma
)
end
- it 'returns fork details' do
- post_graphql(query, current_user: current_user)
+ context 'when a ref is specified' do
+ using RSpec::Parameterized::TableSyntax
- expect(graphql_data['project']['forkDetails']).to eq(
- { 'ahead' => 1, 'behind' => 29 }
- )
+ where(:ref, :counts) do
+ 'feature' | { 'ahead' => 1, 'behind' => 29 }
+ 'v1.1.1' | { 'ahead' => 5, 'behind' => 0 }
+ '7b5160f9bb23a3d58a0accdbe89da13b96b1ece9' | { 'ahead' => 9, 'behind' => 0 }
+ 'non-existent-branch' | { 'ahead' => nil, 'behind' => nil }
+ end
+
+ with_them do
+ it 'returns fork details' do
+ post_graphql(query, current_user: current_user)
+
+ expect(graphql_data['project']['forkDetails']).to eq(counts)
+ end
+ end
end
context 'when a project is not a fork' do
@@ -41,6 +53,16 @@ RSpec.describe 'getting project fork details', feature_category: :source_code_ma
end
end
+ context 'when project source is not visible' do
+ it 'does not return fork details' do
+ project.team.truncate
+
+ post_graphql(query, current_user: current_user)
+
+ expect(graphql_data['project']['forkDetails']).to be_nil
+ end
+ end
+
context 'when a user cannot read the code' do
let_it_be(:current_user) { create(:user) }
diff --git a/spec/requests/api/graphql/project/merge_request_spec.rb b/spec/requests/api/graphql/project/merge_request_spec.rb
index 76e5d687fd1..80c7258c05d 100644
--- a/spec/requests/api/graphql/project/merge_request_spec.rb
+++ b/spec/requests/api/graphql/project/merge_request_spec.rb
@@ -480,4 +480,31 @@ RSpec.describe 'getting merge request information nested in a project', feature_
merge_request.assignees << user
end
end
+
+ context 'when selecting `awardEmoji`' do
+ let_it_be(:award_emoji) { create(:award_emoji, awardable: merge_request, user: current_user) }
+
+ let(:mr_fields) do
+ <<~QUERY
+ awardEmoji {
+ nodes {
+ user {
+ username
+ }
+ name
+ }
+ }
+ QUERY
+ end
+
+ it 'includes award emojis' do
+ post_graphql(query, current_user: current_user)
+
+ response = merge_request_graphql_data['awardEmoji']['nodes']
+
+ expect(response.length).to eq(1)
+ expect(response.first['user']['username']).to eq(current_user.username)
+ expect(response.first['name']).to eq(award_emoji.name)
+ end
+ end
end
diff --git a/spec/requests/api/graphql/project/merge_requests_spec.rb b/spec/requests/api/graphql/project/merge_requests_spec.rb
index 8407faa967e..e3c4396e7d8 100644
--- a/spec/requests/api/graphql/project/merge_requests_spec.rb
+++ b/spec/requests/api/graphql/project/merge_requests_spec.rb
@@ -226,6 +226,28 @@ RSpec.describe 'getting merge request listings nested in a project', feature_cat
it_behaves_like 'when searching with parameters'
end
+ context 'when searching by approved' do
+ let(:approved_mr) { create(:merge_request, target_project: project, source_project: project) }
+
+ before do
+ create(:approval, merge_request: approved_mr)
+ end
+
+ context 'when true' do
+ let(:search_params) { { approved: true } }
+ let(:mrs) { [approved_mr] }
+
+ it_behaves_like 'when searching with parameters'
+ end
+
+ context 'when false' do
+ let(:search_params) { { approved: false } }
+ let(:mrs) { all_merge_requests }
+
+ it_behaves_like 'when searching with parameters'
+ end
+ end
+
context 'when requesting `approved_by`' do
let(:search_params) { { iids: [merge_request_a.iid.to_s, merge_request_b.iid.to_s] } }
let(:extra_iid_for_second_query) { merge_request_c.iid.to_s }
@@ -331,7 +353,7 @@ RSpec.describe 'getting merge request listings nested in a project', feature_cat
end
context 'when award emoji votes' do
- let(:requested_fields) { [:upvotes, :downvotes] }
+ let(:requested_fields) { 'upvotes downvotes awardEmoji { nodes { name } }' }
before do
create_list(:award_emoji, 2, name: 'thumbsup', awardable: merge_request_a)
@@ -588,8 +610,9 @@ RSpec.describe 'getting merge request listings nested in a project', feature_cat
end
let(:query) do
+ # Adding a no-op `not` filter to mimic the same query as the frontend does
graphql_query_for(:project, { full_path: project.full_path }, <<~QUERY)
- mergeRequests(mergedAfter: "2020-01-01", mergedBefore: "2020-01-05", first: 0) {
+ mergeRequests(mergedAfter: "2020-01-01", mergedBefore: "2020-01-05", first: 0, not: { labels: null }) {
totalTimeToMerge
count
}
diff --git a/spec/requests/api/graphql/project/milestones_spec.rb b/spec/requests/api/graphql/project/milestones_spec.rb
index 3b31da77a75..7a79bf2184a 100644
--- a/spec/requests/api/graphql/project/milestones_spec.rb
+++ b/spec/requests/api/graphql/project/milestones_spec.rb
@@ -137,18 +137,6 @@ RSpec.describe 'getting milestone listings nested in a project', feature_categor
it_behaves_like 'searching with parameters'
end
- context 'searching by custom range' do
- let(:expected) { [no_end, fully_future] }
- let(:search_params) do
- {
- start_date: (today + 6.days).iso8601,
- end_date: (today + 7.days).iso8601
- }
- end
-
- it_behaves_like 'searching with parameters'
- end
-
context 'using timeframe argument' do
let(:expected) { [no_end, fully_future] }
let(:search_params) do
@@ -188,23 +176,6 @@ RSpec.describe 'getting milestone listings nested in a project', feature_categor
end
end
- it 'is invalid to provide timeframe and start_date/end_date' do
- query = <<~GQL
- query($path: ID!, $tstart: Date!, $tend: Date!, $start: Time!, $end: Time!) {
- project(fullPath: $path) {
- milestones(timeframe: { start: $tstart, end: $tend }, startDate: $start, endDate: $end) {
- nodes { id }
- }
- }
- }
- GQL
-
- post_graphql(query, current_user: current_user,
- variables: vars.merge(vars.transform_keys { |k| :"t#{k}" }))
-
- expect(graphql_errors).to contain_exactly(a_hash_including('message' => include('deprecated in favor of timeframe')))
- end
-
it 'is invalid to invert the timeframe arguments' do
query = <<~GQL
query($path: ID!, $start: Date!, $end: Date!) {
diff --git a/spec/requests/api/graphql/project/project_statistics_redirect_spec.rb b/spec/requests/api/graphql/project/project_statistics_redirect_spec.rb
new file mode 100644
index 00000000000..8049a75ace3
--- /dev/null
+++ b/spec/requests/api/graphql/project/project_statistics_redirect_spec.rb
@@ -0,0 +1,78 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'rendering project storage type routes', feature_category: :shared do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project, :public) }
+ let_it_be(:user) { create(:user) }
+
+ let(:query) do
+ graphql_query_for('project',
+ { 'fullPath' => project.full_path },
+ "statisticsDetailsPaths { #{all_graphql_fields_for('ProjectStatisticsRedirect')} }")
+ end
+
+ it_behaves_like 'a working graphql query' do
+ before do
+ post_graphql(query, current_user: user)
+ end
+ end
+
+ shared_examples 'valid routes for storage type' do
+ it 'contains all keys' do
+ post_graphql(query, current_user: user)
+
+ expect(graphql_data['project']['statisticsDetailsPaths'].keys).to match_array(
+ %w[repository buildArtifacts wiki packages snippets containerRegistry]
+ )
+ end
+
+ it 'contains valid paths' do
+ repository_url = Gitlab::Routing.url_helpers.project_tree_url(project, "master")
+ wiki_url = Gitlab::Routing.url_helpers.project_wikis_pages_url(project)
+ build_artifacts_url = Gitlab::Routing.url_helpers.project_artifacts_url(project)
+ packages_url = Gitlab::Routing.url_helpers.project_packages_url(project)
+ snippets_url = Gitlab::Routing.url_helpers.project_snippets_url(project)
+ container_registry_url = Gitlab::Routing.url_helpers.project_container_registry_index_url(project)
+
+ post_graphql(query, current_user: user)
+
+ expect(graphql_data['project']['statisticsDetailsPaths'].values).to match_array [repository_url,
+ wiki_url,
+ build_artifacts_url,
+ packages_url,
+ snippets_url,
+ container_registry_url]
+ end
+ end
+
+ context 'when project is public' do
+ it_behaves_like 'valid routes for storage type'
+
+ context 'when user is nil' do
+ let_it_be(:user) { nil }
+
+ it_behaves_like 'valid routes for storage type'
+ end
+ end
+
+ context 'when project is private' do
+ let_it_be(:project) { create(:project, :private) }
+
+ before do
+ project.add_reporter(user)
+ end
+
+ it_behaves_like 'valid routes for storage type'
+
+ context 'when user is nil' do
+ it 'hides statisticsDetailsPaths for nil users' do
+ post_graphql(query, current_user: nil)
+
+ expect(graphql_data['project']).to be_blank
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/project/release_spec.rb b/spec/requests/api/graphql/project/release_spec.rb
index 477388585ca..8d4a39d6b30 100644
--- a/spec/requests/api/graphql/project/release_spec.rb
+++ b/spec/requests/api/graphql/project/release_spec.rb
@@ -132,7 +132,7 @@ RSpec.describe 'Query.project(fullPath).release(tagName)', feature_category: :re
let(:release_fields) do
query_graphql_field(:assets, nil,
- query_graphql_field(:links, nil, 'nodes { id name url external, directAssetUrl }'))
+ query_graphql_field(:links, nil, 'nodes { id name url, directAssetUrl }'))
end
it 'finds all release links' do
@@ -141,7 +141,6 @@ RSpec.describe 'Query.project(fullPath).release(tagName)', feature_category: :re
expected = release.links.map do |link|
a_graphql_entity_for(
link, :name, :url,
- 'external' => link.external?,
'directAssetUrl' => link.filepath ? Gitlab::Routing.url_helpers.project_release_url(project, release) << "/downloads#{link.filepath}" : link.url
)
end
@@ -322,16 +321,15 @@ RSpec.describe 'Query.project(fullPath).release(tagName)', feature_category: :re
let(:release_fields) do
query_graphql_field(:assets, nil,
- query_graphql_field(:links, nil, 'nodes { id name url external, directAssetUrl }'))
+ query_graphql_field(:links, nil, 'nodes { id name url, directAssetUrl }'))
end
- it 'finds all non source external release links' do
+ it 'finds all non source release links' do
post_query
expected = release.links.map do |link|
a_graphql_entity_for(
link, :name, :url,
- 'external' => true,
'directAssetUrl' => link.filepath ? Gitlab::Routing.url_helpers.project_release_url(project, release) << "/downloads#{link.filepath}" : link.url
)
end
diff --git a/spec/requests/api/graphql/project/user_access_authorized_agents_spec.rb b/spec/requests/api/graphql/project/user_access_authorized_agents_spec.rb
new file mode 100644
index 00000000000..b8017171fd1
--- /dev/null
+++ b/spec/requests/api/graphql/project/user_access_authorized_agents_spec.rb
@@ -0,0 +1,129 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Project.user_access_authorized_agents', feature_category: :deployment_management do
+ include GraphqlHelpers
+
+ let_it_be(:organization) { create(:group) }
+ let_it_be(:agent_management_project) { create(:project, :private, group: organization) }
+ let_it_be(:agent) { create(:cluster_agent, project: agent_management_project) }
+
+ let_it_be(:deployment_project) { create(:project, :private, group: organization) }
+ let_it_be(:deployment_developer) { create(:user).tap { |u| deployment_project.add_developer(u) } }
+ let_it_be(:deployment_reporter) { create(:user).tap { |u| deployment_project.add_reporter(u) } }
+
+ let(:user) { deployment_developer }
+
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{deployment_project.full_path}") {
+ userAccessAuthorizedAgents {
+ nodes {
+ agent {
+ id
+ name
+ project {
+ name
+ }
+ }
+ config
+ }
+ }
+ }
+ }
+ )
+ end
+
+ subject { GitlabSchema.execute(query, context: { current_user: user }).as_json }
+
+ context 'with project authorization' do
+ let!(:user_access) { create(:agent_user_access_project_authorization, agent: agent, project: deployment_project) }
+
+ it 'returns the authorized agent' do
+ authorized_agents = subject.dig('data', 'project', 'userAccessAuthorizedAgents', 'nodes')
+
+ expect(authorized_agents.count).to eq(1)
+
+ authorized_agent = authorized_agents.first
+
+ expect(authorized_agent['agent']['id']).to eq(agent.to_global_id.to_s)
+ expect(authorized_agent['agent']['name']).to eq(agent.name)
+ expect(authorized_agent['config']).to eq({})
+ expect(authorized_agent['agent']['project']).to be_nil # User is not authorized to read other resources.
+ end
+
+ context 'when user is developer in the agent management project' do
+ before do
+ agent_management_project.add_developer(deployment_developer)
+ end
+
+ it 'returns the project information as well' do
+ authorized_agent = subject.dig('data', 'project', 'userAccessAuthorizedAgents', 'nodes').first
+
+ expect(authorized_agent['agent']['project']['name']).to eq(agent_management_project.name)
+ end
+ end
+
+ context 'when user is reporter' do
+ let(:user) { deployment_reporter }
+
+ it 'returns nothing' do
+ expect(subject['data']['project']['userAccessAuthorizedAgents']).to be_nil
+ end
+ end
+ end
+
+ context 'with group authorization' do
+ let_it_be(:deployment_group) { create(:group, :private, parent: organization) }
+
+ let!(:user_access) { create(:agent_user_access_group_authorization, agent: agent, group: deployment_group) }
+
+ before_all do
+ deployment_group.add_developer(deployment_developer)
+ deployment_group.add_reporter(deployment_reporter)
+ end
+
+ it 'returns the authorized agent' do
+ authorized_agents = subject.dig('data', 'project', 'userAccessAuthorizedAgents', 'nodes')
+
+ expect(authorized_agents.count).to eq(1)
+
+ authorized_agent = authorized_agents.first
+
+ expect(authorized_agent['agent']['id']).to eq(agent.to_global_id.to_s)
+ expect(authorized_agent['agent']['name']).to eq(agent.name)
+ expect(authorized_agent['config']).to eq({})
+ expect(authorized_agent['agent']['project']).to be_nil # User is not authorized to read other resources.
+ end
+
+ context 'when user is developer in the agent management project' do
+ before do
+ agent_management_project.add_developer(deployment_developer)
+ end
+
+ it 'returns the project information as well' do
+ authorized_agent = subject.dig('data', 'project', 'userAccessAuthorizedAgents', 'nodes').first
+
+ expect(authorized_agent['agent']['project']['name']).to eq(agent_management_project.name)
+ end
+ end
+
+ context 'when user is reporter' do
+ let(:user) { deployment_reporter }
+
+ it 'returns nothing' do
+ expect(subject['data']['project']['userAccessAuthorizedAgents']).to be_nil
+ end
+ end
+ end
+
+ context 'when deployment project is not authorized to user_access to the agent' do
+ it 'returns empty' do
+ authorized_agents = subject.dig('data', 'project', 'userAccessAuthorizedAgents', 'nodes')
+
+ expect(authorized_agents).to be_empty
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/project/work_items_spec.rb b/spec/requests/api/graphql/project/work_items_spec.rb
index f49165a88ea..628a2117e9d 100644
--- a/spec/requests/api/graphql/project/work_items_spec.rb
+++ b/spec/requests/api/graphql/project/work_items_spec.rb
@@ -120,24 +120,55 @@ RSpec.describe 'getting a work item list for a project', feature_category: :team
end
context 'when querying WorkItemWidgetHierarchy' do
- let_it_be(:children) { create_list(:work_item, 3, :task, project: project) }
+ let_it_be(:children) { create_list(:work_item, 4, :task, project: project) }
let_it_be(:child_link1) { create(:parent_link, work_item_parent: item1, work_item: children[0]) }
+ let_it_be(:child_link2) { create(:parent_link, work_item_parent: item1, work_item: children[1]) }
let(:fields) do
<<~GRAPHQL
- nodes {
- widgets {
- type
- ... on WorkItemWidgetHierarchy {
- hasChildren
- parent { id }
- children { nodes { id } }
- }
+ nodes {
+ id
+ widgets {
+ type
+ ... on WorkItemWidgetHierarchy {
+ hasChildren
+ parent { id }
+ children { nodes { id } }
}
}
+ }
GRAPHQL
end
+ context 'with ordered children' do
+ let(:items_data) { graphql_data['project']['workItems']['nodes'] }
+ let(:work_item_data) { items_data.find { |item| item['id'] == item1.to_gid.to_s } }
+ let(:work_item_widget) { work_item_data["widgets"].find { |widget| widget.key?("children") } }
+ let(:children_ids) { work_item_widget.dig("children", "nodes").pluck("id") }
+
+ let(:first_child) { children[0].to_gid.to_s }
+ let(:second_child) { children[1].to_gid.to_s }
+
+ it 'returns children ordered by created_at by default' do
+ post_graphql(query, current_user: current_user)
+
+ expect(children_ids).to eq([first_child, second_child])
+ end
+
+ context 'when ordered by relative position' do
+ before do
+ child_link1.update!(relative_position: 20)
+ child_link2.update!(relative_position: 10)
+ end
+
+ it 'returns children in correct order' do
+ post_graphql(query, current_user: current_user)
+
+ expect(children_ids).to eq([second_child, first_child])
+ end
+ end
+ end
+
it 'executes limited number of N+1 queries' do
post_graphql(query, current_user: current_user) # warm-up
@@ -146,13 +177,11 @@ RSpec.describe 'getting a work item list for a project', feature_category: :team
end
parent_work_items = create_list(:work_item, 2, project: project)
- create(:parent_link, work_item_parent: parent_work_items[0], work_item: children[1])
- create(:parent_link, work_item_parent: parent_work_items[1], work_item: children[2])
+ create(:parent_link, work_item_parent: parent_work_items[0], work_item: children[2])
+ create(:parent_link, work_item_parent: parent_work_items[1], work_item: children[3])
- # There are 2 extra queries for fetching the children field
- # See: https://gitlab.com/gitlab-org/gitlab/-/issues/363569
expect { post_graphql(query, current_user: current_user) }
- .not_to exceed_query_limit(control).with_threshold(2)
+ .not_to exceed_query_limit(control)
end
it 'avoids N+1 queries when children are added to a work item' do
@@ -162,8 +191,8 @@ RSpec.describe 'getting a work item list for a project', feature_category: :team
post_graphql(query, current_user: current_user)
end
- create(:parent_link, work_item_parent: item1, work_item: children[1])
create(:parent_link, work_item_parent: item1, work_item: children[2])
+ create(:parent_link, work_item_parent: item1, work_item: children[3])
expect { post_graphql(query, current_user: current_user) }
.not_to exceed_query_limit(control)
@@ -313,6 +342,79 @@ RSpec.describe 'getting a work item list for a project', feature_category: :team
end
end
+ context 'when fetching work item notifications widget' do
+ let(:fields) do
+ <<~GRAPHQL
+ nodes {
+ widgets {
+ type
+ ... on WorkItemWidgetNotifications {
+ subscribed
+ }
+ }
+ }
+ GRAPHQL
+ end
+
+ it 'executes limited number of N+1 queries', :use_sql_query_cache do
+ control = ActiveRecord::QueryRecorder.new(skip_cached: false) do
+ post_graphql(query, current_user: current_user)
+ end
+
+ create_list(:work_item, 3, project: project)
+
+ # Performs 1 extra query per item to fetch subscriptions
+ expect { post_graphql(query, current_user: current_user) }
+ .not_to exceed_all_query_limit(control).with_threshold(3)
+ expect_graphql_errors_to_be_empty
+ end
+ end
+
+ context 'when fetching work item award emoji widget' do
+ let(:fields) do
+ <<~GRAPHQL
+ nodes {
+ widgets {
+ type
+ ... on WorkItemWidgetAwardEmoji {
+ awardEmoji {
+ nodes {
+ name
+ emoji
+ user { id }
+ }
+ }
+ upvotes
+ downvotes
+ }
+ }
+ }
+ GRAPHQL
+ end
+
+ before do
+ create(:award_emoji, name: 'star', user: current_user, awardable: item1)
+ create(:award_emoji, :upvote, awardable: item1)
+ create(:award_emoji, :downvote, awardable: item1)
+ end
+
+ it 'executes limited number of N+1 queries', :use_sql_query_cache do
+ control = ActiveRecord::QueryRecorder.new(skip_cached: false) do
+ post_graphql(query, current_user: current_user)
+ end
+
+ create_list(:work_item, 2, project: project) do |item|
+ create(:award_emoji, name: 'rocket', awardable: item)
+ create_list(:award_emoji, 2, :upvote, awardable: item)
+ create_list(:award_emoji, 2, :downvote, awardable: item)
+ end
+
+ expect { post_graphql(query, current_user: current_user) }
+ .not_to exceed_all_query_limit(control)
+ expect_graphql_errors_to_be_empty
+ end
+ end
+
def item_ids
graphql_dig_at(items_data, :node, :id)
end
diff --git a/spec/requests/api/graphql/project_query_spec.rb b/spec/requests/api/graphql/project_query_spec.rb
index 281a08e6548..9f51258c163 100644
--- a/spec/requests/api/graphql/project_query_spec.rb
+++ b/spec/requests/api/graphql/project_query_spec.rb
@@ -120,6 +120,67 @@ RSpec.describe 'getting project information', feature_category: :projects do
end
end
+ describe 'is_catalog_resource' do
+ before do
+ project.add_owner(current_user)
+ end
+
+ let(:catalog_resource_query) do
+ <<~GRAPHQL
+ {
+ project(fullPath: "#{project.full_path}") {
+ isCatalogResource
+ }
+ }
+ GRAPHQL
+ end
+
+ context 'when the project is not a catalog resource' do
+ it 'is false' do
+ post_graphql(catalog_resource_query, current_user: current_user)
+
+ expect(graphql_data.dig('project', 'isCatalogResource')).to be(false)
+ end
+ end
+
+ context 'when the project is a catalog resource' do
+ before do
+ create(:catalog_resource, project: project)
+ end
+
+ it 'is true' do
+ post_graphql(catalog_resource_query, current_user: current_user)
+
+ expect(graphql_data.dig('project', 'isCatalogResource')).to be(true)
+ end
+ end
+
+ context 'for N+1 queries with isCatalogResource' do
+ let_it_be(:project1) { create(:project, group: group) }
+ let_it_be(:project2) { create(:project, group: group) }
+
+ it 'avoids N+1 database queries' do
+ pending('See: https://gitlab.com/gitlab-org/gitlab/-/issues/403634')
+ ctx = { current_user: current_user }
+
+ baseline_query = graphql_query_for(:project, { full_path: project1.full_path }, 'isCatalogResource')
+
+ query = <<~GQL
+ query {
+ a: #{query_graphql_field(:project, { full_path: project1.full_path }, 'isCatalogResource')}
+ b: #{query_graphql_field(:project, { full_path: project2.full_path }, 'isCatalogResource')}
+ }
+ GQL
+
+ control = ActiveRecord::QueryRecorder.new do
+ run_with_clean_state(baseline_query, context: ctx)
+ end
+
+ expect { run_with_clean_state(query, context: ctx) }.not_to exceed_query_limit(control)
+ end
+ end
+ end
+
context 'when the user has reporter access to the project' do
let(:statistics_query) do
<<~GRAPHQL
diff --git a/spec/requests/api/graphql/query_spec.rb b/spec/requests/api/graphql/query_spec.rb
index 2b9d66ec744..0602cfec149 100644
--- a/spec/requests/api/graphql/query_spec.rb
+++ b/spec/requests/api/graphql/query_spec.rb
@@ -2,10 +2,10 @@
require 'spec_helper'
-RSpec.describe 'Query', feature_category: :not_owned do
+RSpec.describe 'Query', feature_category: :shared do
include GraphqlHelpers
- let_it_be(:project) { create(:project) }
+ let_it_be(:project) { create(:project, public_builds: false) }
let_it_be(:issue) { create(:issue, project: project) }
let_it_be(:developer) { create(:user) }
@@ -116,4 +116,36 @@ RSpec.describe 'Query', feature_category: :not_owned do
end
end
end
+
+ describe '.ciPipelineStage' do
+ let_it_be(:ci_stage) { create(:ci_stage, name: 'graphql test stage', project: project) }
+
+ let(:query) do
+ <<~GRAPHQL
+ {
+ ciPipelineStage(id: "#{ci_stage.to_global_id}") {
+ name
+ }
+ }
+ GRAPHQL
+ end
+
+ context 'when the current user has access to the stage' do
+ it 'fetches the stage for the given ID' do
+ project.add_developer(developer)
+
+ post_graphql(query, current_user: developer)
+
+ expect(graphql_data.dig('ciPipelineStage', 'name')).to eq('graphql test stage')
+ end
+ end
+
+ context 'when the current user does not have access to the stage' do
+ it 'returns nil' do
+ post_graphql(query, current_user: developer)
+
+ expect(graphql_data['ciPipelineStage']).to be_nil
+ end
+ end
+ end
end
diff --git a/spec/requests/api/graphql/user/user_achievements_query_spec.rb b/spec/requests/api/graphql/user/user_achievements_query_spec.rb
new file mode 100644
index 00000000000..27d32d07372
--- /dev/null
+++ b/spec/requests/api/graphql/user/user_achievements_query_spec.rb
@@ -0,0 +1,95 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'UserAchievements', feature_category: :user_profile do
+ include GraphqlHelpers
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:group) { create(:group, :private) }
+ let_it_be(:achievement) { create(:achievement, namespace: group) }
+ let_it_be(:non_revoked_achievement) { create(:user_achievement, achievement: achievement, user: user) }
+ let_it_be(:revoked_achievement) { create(:user_achievement, :revoked, achievement: achievement, user: user) }
+ let_it_be(:fields) do
+ <<~HEREDOC
+ userAchievements {
+ nodes {
+ id
+ achievement {
+ id
+ }
+ user {
+ id
+ }
+ awardedByUser {
+ id
+ }
+ revokedByUser {
+ id
+ }
+ }
+ }
+ HEREDOC
+ end
+
+ let_it_be(:query) do
+ graphql_query_for('user', { id: user.to_global_id.to_s }, fields)
+ end
+
+ let(:current_user) { user }
+
+ before_all do
+ group.add_guest(user)
+ end
+
+ before do
+ post_graphql(query, current_user: current_user)
+ end
+
+ it_behaves_like 'a working graphql query'
+
+ it 'returns all non_revoked user_achievements' do
+ expect(graphql_data_at(:user, :userAchievements, :nodes)).to contain_exactly(
+ a_graphql_entity_for(non_revoked_achievement)
+ )
+ end
+
+ it 'can lookahead to eliminate N+1 queries', :use_clean_rails_memory_store_caching do
+ control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do
+ post_graphql(query, current_user: user)
+ end.count
+
+ achievement2 = create(:achievement, namespace: group)
+ create_list(:user_achievement, 2, achievement: achievement2, user: user)
+
+ expect { post_graphql(query, current_user: user) }.not_to exceed_all_query_limit(control_count)
+ end
+
+ context 'when the achievements feature flag is disabled for a namespace' do
+ let_it_be(:group2) { create(:group) }
+ let_it_be(:achievement2) { create(:achievement, namespace: group2) }
+ let_it_be(:user_achievement2) { create(:user_achievement, achievement: achievement2, user: user) }
+
+ before do
+ stub_feature_flags(achievements: false)
+ stub_feature_flags(achievements: group2)
+ post_graphql(query, current_user: current_user)
+ end
+
+ it 'does not return user_achievements for that namespace' do
+ expect(graphql_data_at(:user, :userAchievements, :nodes)).to contain_exactly(
+ a_graphql_entity_for(user_achievement2)
+ )
+ end
+ end
+
+ context 'when current user is not a member of the private group' do
+ let(:current_user) { create(:user) }
+
+ it 'returns all achievements' do
+ expect(graphql_data_at(:user, :userAchievements, :nodes)).to contain_exactly(
+ a_graphql_entity_for(non_revoked_achievement)
+ )
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/user_spec.rb b/spec/requests/api/graphql/user_spec.rb
index c19dfa6f3f3..41ee233dfc5 100644
--- a/spec/requests/api/graphql/user_spec.rb
+++ b/spec/requests/api/graphql/user_spec.rb
@@ -10,6 +10,12 @@ RSpec.describe 'User', feature_category: :user_profile do
shared_examples 'a working user query' do
it_behaves_like 'a working graphql query' do
before do
+ # TODO: This license stub is necessary because the remote development workspaces field
+ # defined in the EE version of UserInterface gets picked up here and thus the license
+ # check happens. This comes from the `ancestors` call in
+ # lib/graphql/schema/member/has_fields.rb#fields in the graphql library.
+ stub_licensed_features(remote_development: true)
+
post_graphql(query, current_user: current_user)
end
end
@@ -36,9 +42,17 @@ RSpec.describe 'User', feature_category: :user_profile do
end
context 'when username parameter is used' do
- let(:query) { graphql_query_for(:user, { username: current_user.username.to_s }) }
+ context 'when username is identically cased' do
+ let(:query) { graphql_query_for(:user, { username: current_user.username.to_s }) }
- it_behaves_like 'a working user query'
+ it_behaves_like 'a working user query'
+ end
+
+ context 'when username is differently cased' do
+ let(:query) { graphql_query_for(:user, { username: current_user.username.to_s.upcase }) }
+
+ it_behaves_like 'a working user query'
+ end
end
context 'when username and id parameter are used' do
diff --git a/spec/requests/api/graphql/work_item_spec.rb b/spec/requests/api/graphql/work_item_spec.rb
index 0fad4f4ff3a..dc5004a121b 100644
--- a/spec/requests/api/graphql/work_item_spec.rb
+++ b/spec/requests/api/graphql/work_item_spec.rb
@@ -36,9 +36,15 @@ RSpec.describe 'Query.work_item(id)', feature_category: :team_planning do
end
context 'when the user can read the work item' do
+ let(:incoming_email_token) { current_user.incoming_email_token }
+ let(:work_item_email) do
+ "p+#{project.full_path_slug}-#{project.project_id}-#{incoming_email_token}-issue-#{work_item.iid}@gl.ab"
+ end
+
before do
project.add_developer(developer)
project.add_guest(guest)
+ stub_incoming_email_setting(enabled: true, address: "p+%{key}@gl.ab")
post_graphql(query, current_user: current_user)
end
@@ -55,11 +61,15 @@ RSpec.describe 'Query.work_item(id)', feature_category: :team_planning do
'title' => work_item.title,
'confidential' => work_item.confidential,
'workItemType' => hash_including('id' => work_item.work_item_type.to_gid.to_s),
+ 'reference' => work_item.to_reference,
+ 'createNoteEmail' => work_item_email,
'userPermissions' => {
'readWorkItem' => true,
'updateWorkItem' => true,
'deleteWorkItem' => false,
- 'adminWorkItem' => true
+ 'adminWorkItem' => true,
+ 'adminParentLink' => true,
+ 'setWorkItemMetadata' => true
},
'project' => hash_including('id' => project.to_gid.to_s, 'fullPath' => project.full_path)
)
@@ -373,6 +383,161 @@ RSpec.describe 'Query.work_item(id)', feature_category: :team_planning do
)
end
end
+
+ describe 'notifications widget' do
+ let(:work_item_fields) do
+ <<~GRAPHQL
+ id
+ widgets {
+ type
+ ... on WorkItemWidgetNotifications {
+ subscribed
+ }
+ }
+ GRAPHQL
+ end
+
+ it 'returns widget information' do
+ expect(work_item_data).to include(
+ 'id' => work_item.to_gid.to_s,
+ 'widgets' => include(
+ hash_including(
+ 'type' => 'NOTIFICATIONS',
+ 'subscribed' => work_item.subscribed?(current_user, project)
+ )
+ )
+ )
+ end
+ end
+
+ describe 'currentUserTodos widget' do
+ let_it_be(:current_user) { developer }
+ let_it_be(:other_todo) { create(:todo, state: :pending, user: current_user) }
+
+ let_it_be(:done_todo) do
+ create(:todo, state: :done, target: work_item, target_type: work_item.class.name, user: current_user)
+ end
+
+ let_it_be(:pending_todo) do
+ create(:todo, state: :pending, target: work_item, target_type: work_item.class.name, user: current_user)
+ end
+
+ let_it_be(:other_user_todo) do
+ create(:todo, state: :pending, target: work_item, target_type: work_item.class.name, user: create(:user))
+ end
+
+ let(:work_item_fields) do
+ <<~GRAPHQL
+ id
+ widgets {
+ type
+ ... on WorkItemWidgetCurrentUserTodos {
+ currentUserTodos {
+ nodes {
+ id
+ state
+ }
+ }
+ }
+ }
+ GRAPHQL
+ end
+
+ context 'with access' do
+ it 'returns widget information' do
+ expect(work_item_data).to include(
+ 'id' => work_item.to_gid.to_s,
+ 'widgets' => include(
+ hash_including(
+ 'type' => 'CURRENT_USER_TODOS',
+ 'currentUserTodos' => {
+ 'nodes' => match_array(
+ [done_todo, pending_todo].map { |t| { 'id' => t.to_gid.to_s, 'state' => t.state } }
+ )
+ }
+ )
+ )
+ )
+ end
+ end
+
+ context 'with filter' do
+ let(:work_item_fields) do
+ <<~GRAPHQL
+ id
+ widgets {
+ type
+ ... on WorkItemWidgetCurrentUserTodos {
+ currentUserTodos(state: done) {
+ nodes {
+ id
+ state
+ }
+ }
+ }
+ }
+ GRAPHQL
+ end
+
+ it 'returns widget information' do
+ expect(work_item_data).to include(
+ 'id' => work_item.to_gid.to_s,
+ 'widgets' => include(
+ hash_including(
+ 'type' => 'CURRENT_USER_TODOS',
+ 'currentUserTodos' => {
+ 'nodes' => match_array(
+ [done_todo].map { |t| { 'id' => t.to_gid.to_s, 'state' => t.state } }
+ )
+ }
+ )
+ )
+ )
+ end
+ end
+ end
+
+ describe 'award emoji widget' do
+ let_it_be(:emoji) { create(:award_emoji, name: 'star', awardable: work_item) }
+ let_it_be(:upvote) { create(:award_emoji, :upvote, awardable: work_item) }
+ let_it_be(:downvote) { create(:award_emoji, :downvote, awardable: work_item) }
+
+ let(:work_item_fields) do
+ <<~GRAPHQL
+ id
+ widgets {
+ type
+ ... on WorkItemWidgetAwardEmoji {
+ upvotes
+ downvotes
+ awardEmoji {
+ nodes {
+ name
+ }
+ }
+ }
+ }
+ GRAPHQL
+ end
+
+ it 'returns widget information' do
+ expect(work_item_data).to include(
+ 'id' => work_item.to_gid.to_s,
+ 'widgets' => include(
+ hash_including(
+ 'type' => 'AWARD_EMOJI',
+ 'upvotes' => work_item.upvotes,
+ 'downvotes' => work_item.downvotes,
+ 'awardEmoji' => {
+ 'nodes' => match_array(
+ [emoji, upvote, downvote].map { |e| { 'name' => e.name } }
+ )
+ }
+ )
+ )
+ )
+ end
+ end
end
context 'when an Issue Global ID is provided' do
@@ -398,4 +563,23 @@ RSpec.describe 'Query.work_item(id)', feature_category: :team_planning do
)
end
end
+
+ context 'when the user cannot set work item metadata' do
+ let(:current_user) { guest }
+
+ before do
+ project.add_guest(guest)
+ post_graphql(query, current_user: current_user)
+ end
+
+ it 'returns correct user permission' do
+ expect(work_item_data).to include(
+ 'id' => work_item.to_gid.to_s,
+ 'userPermissions' =>
+ hash_including(
+ 'setWorkItemMetadata' => false
+ )
+ )
+ end
+ end
end