From e4384360a16dd9a19d4d2d25d0ef1f2b862ed2a6 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Wed, 19 Jul 2023 14:16:28 +0000 Subject: Add latest changes from gitlab-org/gitlab@16-2-stable-ee --- spec/requests/admin/users_controller_spec.rb | 20 +- spec/requests/api/admin/instance_clusters_spec.rb | 2 +- spec/requests/api/admin/plan_limits_spec.rb | 10 +- spec/requests/api/ci/job_artifacts_spec.rb | 2 +- spec/requests/api/ci/pipeline_schedules_spec.rb | 18 +- spec/requests/api/ci/variables_spec.rb | 7 +- spec/requests/api/container_repositories_spec.rb | 2 +- spec/requests/api/debian_group_packages_spec.rb | 29 -- spec/requests/api/debian_project_packages_spec.rb | 39 --- spec/requests/api/deployments_spec.rb | 2 +- spec/requests/api/discussions_spec.rb | 2 +- spec/requests/api/environments_spec.rb | 68 +++- .../api/error_tracking/project_settings_spec.rb | 60 ++-- spec/requests/api/files_spec.rb | 8 +- .../graphql/boards/board_list_issues_query_spec.rb | 2 +- .../api/graphql/boards/board_lists_query_spec.rb | 2 +- .../api/graphql/ci/inherited_ci_variables_spec.rb | 178 ++++++++-- spec/requests/api/graphql/ci/runner_spec.rb | 15 +- .../container_repository_details_spec.rb | 2 +- spec/requests/api/graphql/gitlab_schema_spec.rb | 4 +- .../graphql/metrics/dashboard/annotations_spec.rb | 104 ------ .../api/graphql/metrics/dashboard_query_spec.rb | 114 ------- .../prometheus_integration/create_spec.rb | 19 +- .../ci/job_token_scope/add_project_spec.rb | 15 - .../mutations/ci/pipeline_schedule/create_spec.rb | 162 +++++++++ .../mutations/ci/pipeline_schedule/delete_spec.rb | 82 +++++ .../mutations/ci/pipeline_schedule/play_spec.rb | 80 +++++ .../ci/pipeline_schedule/take_ownership_spec.rb | 41 +++ .../mutations/ci/pipeline_schedule/update_spec.rb | 189 ++++++++++ .../mutations/ci/pipeline_schedule_create_spec.rb | 151 -------- .../mutations/ci/pipeline_schedule_delete_spec.rb | 82 ----- .../mutations/ci/pipeline_schedule_play_spec.rb | 80 ----- .../ci/pipeline_schedule_take_ownership_spec.rb | 41 --- .../mutations/ci/pipeline_schedule_update_spec.rb | 151 -------- .../ci/project_ci_cd_settings_update_spec.rb | 20 -- .../api/graphql/mutations/ci/runner/create_spec.rb | 32 -- .../api/graphql/mutations/issues/update_spec.rb | 4 +- .../graphql/mutations/notes/create/note_spec.rb | 2 +- .../api/graphql/mutations/notes/destroy_spec.rb | 2 +- .../graphql/mutations/notes/update/note_spec.rb | 2 +- .../api/graphql/mutations/snippets/destroy_spec.rb | 2 +- .../graphql/mutations/work_items/update_spec.rb | 4 +- .../alert/metrics_dashboard_url_spec.rb | 85 ----- .../project/alert_management/alerts_spec.rb | 1 - .../incident_management/timeline_events_spec.rb | 2 +- spec/requests/api/graphql/project/jobs_spec.rb | 31 +- spec/requests/api/graphql/project/pipeline_spec.rb | 36 +- spec/requests/api/graphql/user_query_spec.rb | 47 +++ spec/requests/api/group_clusters_spec.rb | 2 +- spec/requests/api/group_export_spec.rb | 120 ++++++- spec/requests/api/group_variables_spec.rb | 7 +- spec/requests/api/groups_spec.rb | 99 +++++- spec/requests/api/helpers_spec.rb | 6 + spec/requests/api/import_github_spec.rb | 27 +- spec/requests/api/internal/kubernetes_spec.rb | 54 +-- spec/requests/api/lint_spec.rb | 10 - spec/requests/api/merge_requests_spec.rb | 73 ++-- spec/requests/api/ml_model_packages_spec.rb | 146 ++++++-- spec/requests/api/npm_group_packages_spec.rb | 30 +- spec/requests/api/npm_instance_packages_spec.rb | 27 ++ spec/requests/api/npm_project_packages_spec.rb | 99 +++++- spec/requests/api/project_attributes.yml | 3 +- spec/requests/api/project_clusters_spec.rb | 2 +- spec/requests/api/project_export_spec.rb | 124 +++++-- spec/requests/api/project_hooks_spec.rb | 1 + spec/requests/api/project_packages_spec.rb | 14 + spec/requests/api/projects_spec.rb | 78 ++++- spec/requests/api/protected_branches_spec.rb | 13 +- spec/requests/api/protected_tags_spec.rb | 13 +- spec/requests/api/search_spec.rb | 12 +- spec/requests/api/settings_spec.rb | 105 +++++- spec/requests/api/statistics_spec.rb | 2 +- spec/requests/api/usage_data_spec.rb | 55 +++ spec/requests/api/user_runners_spec.rb | 243 +++++++++++++ spec/requests/api/users_spec.rb | 165 --------- spec/requests/git_http_spec.rb | 5 - .../groups/observability_controller_spec.rb | 4 + spec/requests/lfs_http_spec.rb | 2 +- spec/requests/openid_connect_spec.rb | 11 +- .../organizations/organizations_controller_spec.rb | 16 +- .../projects/alert_management_controller_spec.rb | 69 ++++ .../requests/projects/incidents_controller_spec.rb | 116 +++++++ spec/requests/projects/issues_controller_spec.rb | 7 + .../projects/merge_requests/creations_spec.rb | 20 +- .../projects/merge_requests_controller_spec.rb | 1 + .../projects/ml/candidates_controller_spec.rb | 23 +- .../projects/ml/experiments_controller_spec.rb | 27 +- .../requests/projects/ml/models_controller_spec.rb | 67 ++++ .../packages/package_files_controller_spec.rb | 2 +- .../service_desk/custom_email_controller_spec.rb | 380 +++++++++++++++++++++ .../projects/service_desk_controller_spec.rb | 109 ++++++ spec/requests/projects/tracing_controller_spec.rb | 68 ++++ spec/requests/search_controller_spec.rb | 6 +- spec/requests/users_controller_spec.rb | 28 +- spec/requests/verifies_with_email_spec.rb | 2 +- 95 files changed, 3052 insertions(+), 1494 deletions(-) delete mode 100644 spec/requests/api/graphql/metrics/dashboard/annotations_spec.rb delete mode 100644 spec/requests/api/graphql/metrics/dashboard_query_spec.rb create mode 100644 spec/requests/api/graphql/mutations/ci/pipeline_schedule/create_spec.rb create mode 100644 spec/requests/api/graphql/mutations/ci/pipeline_schedule/delete_spec.rb create mode 100644 spec/requests/api/graphql/mutations/ci/pipeline_schedule/play_spec.rb create mode 100644 spec/requests/api/graphql/mutations/ci/pipeline_schedule/take_ownership_spec.rb create mode 100644 spec/requests/api/graphql/mutations/ci/pipeline_schedule/update_spec.rb delete mode 100644 spec/requests/api/graphql/mutations/ci/pipeline_schedule_create_spec.rb delete mode 100644 spec/requests/api/graphql/mutations/ci/pipeline_schedule_delete_spec.rb delete mode 100644 spec/requests/api/graphql/mutations/ci/pipeline_schedule_play_spec.rb delete mode 100644 spec/requests/api/graphql/mutations/ci/pipeline_schedule_take_ownership_spec.rb delete mode 100644 spec/requests/api/graphql/mutations/ci/pipeline_schedule_update_spec.rb delete mode 100644 spec/requests/api/graphql/project/alert_management/alert/metrics_dashboard_url_spec.rb create mode 100644 spec/requests/api/user_runners_spec.rb create mode 100644 spec/requests/projects/alert_management_controller_spec.rb create mode 100644 spec/requests/projects/incidents_controller_spec.rb create mode 100644 spec/requests/projects/ml/models_controller_spec.rb create mode 100644 spec/requests/projects/service_desk/custom_email_controller_spec.rb create mode 100644 spec/requests/projects/service_desk_controller_spec.rb create mode 100644 spec/requests/projects/tracing_controller_spec.rb (limited to 'spec/requests') diff --git a/spec/requests/admin/users_controller_spec.rb b/spec/requests/admin/users_controller_spec.rb index 5344a2c2bb7..21cf8ab2c79 100644 --- a/spec/requests/admin/users_controller_spec.rb +++ b/spec/requests/admin/users_controller_spec.rb @@ -6,12 +6,12 @@ RSpec.describe Admin::UsersController, :enable_admin_mode, feature_category: :us let_it_be(:admin) { create(:admin) } let_it_be(:user) { create(:user) } + before do + sign_in(admin) + end + describe 'PUT #block' do context 'when request format is :json' do - before do - sign_in(admin) - end - subject(:request) { put block_admin_user_path(user, format: :json) } context 'when user was blocked' do @@ -39,4 +39,16 @@ RSpec.describe Admin::UsersController, :enable_admin_mode, feature_category: :us end end end + + describe 'PUT #unlock' do + before do + user.lock_access! + end + + subject(:request) { put unlock_admin_user_path(user) } + + it 'unlocks the user' do + expect { request }.to change { user.reload.access_locked? }.from(true).to(false) + end + end end diff --git a/spec/requests/api/admin/instance_clusters_spec.rb b/spec/requests/api/admin/instance_clusters_spec.rb index f2e62533b78..6fad020150c 100644 --- a/spec/requests/api/admin/instance_clusters_spec.rb +++ b/spec/requests/api/admin/instance_clusters_spec.rb @@ -363,7 +363,7 @@ RSpec.describe ::API::Admin::InstanceClusters, feature_category: :deployment_man end it 'returns validation error' do - expect(json_response['message']['platform_kubernetes.base'].first).to eq(_('Cannot modify managed Kubernetes cluster')) + expect(json_response['message']['platform_kubernetes'].first).to eq(_('Cannot modify managed Kubernetes cluster')) end end diff --git a/spec/requests/api/admin/plan_limits_spec.rb b/spec/requests/api/admin/plan_limits_spec.rb index cad1111b76b..97eb8a2b13f 100644 --- a/spec/requests/api/admin/plan_limits_spec.rb +++ b/spec/requests/api/admin/plan_limits_spec.rb @@ -26,6 +26,7 @@ RSpec.describe API::Admin::PlanLimits, 'PlanLimits', feature_category: :shared d expect(json_response['conan_max_file_size']).to eq(Plan.default.actual_limits.conan_max_file_size) expect(json_response['generic_packages_max_file_size']).to eq(Plan.default.actual_limits.generic_packages_max_file_size) expect(json_response['helm_max_file_size']).to eq(Plan.default.actual_limits.helm_max_file_size) + expect(json_response['limits_history']).to eq(Plan.default.actual_limits.limits_history) expect(json_response['maven_max_file_size']).to eq(Plan.default.actual_limits.maven_max_file_size) expect(json_response['npm_max_file_size']).to eq(Plan.default.actual_limits.npm_max_file_size) expect(json_response['nuget_max_file_size']).to eq(Plan.default.actual_limits.nuget_max_file_size) @@ -86,7 +87,9 @@ RSpec.describe API::Admin::PlanLimits, 'PlanLimits', feature_category: :shared d let(:params) { { 'plan_name': 'default' } } end - context 'as an admin user' do + context 'as an admin user', :freeze_time do + let(:current_timestamp) { Time.current.utc.to_i } + context 'correct params' do it 'updates multiple plan limits', :aggregate_failures do put api(path, admin, admin_mode: true), params: { @@ -124,6 +127,11 @@ RSpec.describe API::Admin::PlanLimits, 'PlanLimits', feature_category: :shared d expect(json_response['enforcement_limit']).to eq(15) expect(json_response['generic_packages_max_file_size']).to eq(20) expect(json_response['helm_max_file_size']).to eq(25) + expect(json_response['limits_history']).to eq( + { "enforcement_limit" => [{ "user_id" => admin.id, "username" => admin.username, "timestamp" => current_timestamp, "value" => 15 }], + "notification_limit" => [{ "user_id" => admin.id, "username" => admin.username, "timestamp" => current_timestamp, "value" => 90 }], + "storage_size_limit" => [{ "user_id" => admin.id, "username" => admin.username, "timestamp" => current_timestamp, "value" => 80 }] } + ) expect(json_response['maven_max_file_size']).to eq(30) expect(json_response['notification_limit']).to eq(90) expect(json_response['npm_max_file_size']).to eq(40) diff --git a/spec/requests/api/ci/job_artifacts_spec.rb b/spec/requests/api/ci/job_artifacts_spec.rb index 7cea744cdb9..6f4e7fd66ed 100644 --- a/spec/requests/api/ci/job_artifacts_spec.rb +++ b/spec/requests/api/ci/job_artifacts_spec.rb @@ -541,7 +541,7 @@ RSpec.describe API::Ci::JobArtifacts, feature_category: :build_artifacts do let(:download_headers) do { 'Content-Transfer-Encoding' => 'binary', 'Content-Disposition' => - %Q(attachment; filename="#{job_with_artifacts.artifacts_file.filename}"; filename*=UTF-8''#{job.artifacts_file.filename}) } + %(attachment; filename="#{job_with_artifacts.artifacts_file.filename}"; filename*=UTF-8''#{job.artifacts_file.filename}) } end it { expect(response).to have_gitlab_http_status(:ok) } diff --git a/spec/requests/api/ci/pipeline_schedules_spec.rb b/spec/requests/api/ci/pipeline_schedules_spec.rb index d760e4ddf28..d5f60e62b06 100644 --- a/spec/requests/api/ci/pipeline_schedules_spec.rb +++ b/spec/requests/api/ci/pipeline_schedules_spec.rb @@ -311,7 +311,8 @@ RSpec.describe API::Ci::PipelineSchedules, feature_category: :continuous_integra end end - describe 'POST /projects/:id/pipeline_schedules' do + # Move this from `shared_context` to `describe` when `ci_refactoring_pipeline_schedule_create_service` is removed. + shared_context 'POST /projects/:id/pipeline_schedules' do # rubocop:disable RSpec/ContextWording let(:params) { attributes_for(:ci_pipeline_schedule) } context 'authenticated user with valid permissions' do @@ -368,7 +369,8 @@ RSpec.describe API::Ci::PipelineSchedules, feature_category: :continuous_integra end end - describe 'PUT /projects/:id/pipeline_schedules/:pipeline_schedule_id' do + # Move this from `shared_context` to `describe` when `ci_refactoring_pipeline_schedule_create_service` is removed. + shared_context 'PUT /projects/:id/pipeline_schedules/:pipeline_schedule_id' do let(:pipeline_schedule) do create(:ci_pipeline_schedule, project: project, owner: developer) end @@ -437,6 +439,18 @@ RSpec.describe API::Ci::PipelineSchedules, feature_category: :continuous_integra end end + it_behaves_like 'POST /projects/:id/pipeline_schedules' + it_behaves_like 'PUT /projects/:id/pipeline_schedules/:pipeline_schedule_id' + + context 'when the FF ci_refactoring_pipeline_schedule_create_service is disabled' do + before do + stub_feature_flags(ci_refactoring_pipeline_schedule_create_service: false) + end + + it_behaves_like 'POST /projects/:id/pipeline_schedules' + it_behaves_like 'PUT /projects/:id/pipeline_schedules/:pipeline_schedule_id' + end + describe 'POST /projects/:id/pipeline_schedules/:pipeline_schedule_id/take_ownership' do let(:pipeline_schedule) do create(:ci_pipeline_schedule, project: project, owner: developer) diff --git a/spec/requests/api/ci/variables_spec.rb b/spec/requests/api/ci/variables_spec.rb index e937c4c2b8f..a1446e1040e 100644 --- a/spec/requests/api/ci/variables_spec.rb +++ b/spec/requests/api/ci/variables_spec.rb @@ -48,6 +48,7 @@ RSpec.describe API::Ci::Variables, feature_category: :secrets_management do expect(json_response['masked']).to eq(variable.masked?) expect(json_response['raw']).to eq(variable.raw?) expect(json_response['variable_type']).to eq('env_var') + expect(json_response['description']).to be_nil end it 'responds with 404 Not Found if requesting non-existing variable' do @@ -140,7 +141,7 @@ RSpec.describe API::Ci::Variables, feature_category: :secrets_management do it 'creates variable with optional attributes' do expect do - post api("/projects/#{project.id}/variables", user), params: { variable_type: 'file', key: 'TEST_VARIABLE_2', value: 'VALUE_2' } + post api("/projects/#{project.id}/variables", user), params: { variable_type: 'file', key: 'TEST_VARIABLE_2', value: 'VALUE_2', description: 'description' } end.to change { project.variables.count }.by(1) expect(response).to have_gitlab_http_status(:created) @@ -150,6 +151,7 @@ RSpec.describe API::Ci::Variables, feature_category: :secrets_management do expect(json_response['masked']).to be_falsey expect(json_response['raw']).to be_falsey expect(json_response['variable_type']).to eq('file') + expect(json_response['description']).to eq('description') end it 'does not allow to duplicate variable key' do @@ -226,7 +228,7 @@ RSpec.describe API::Ci::Variables, feature_category: :secrets_management do initial_variable = project.variables.reload.first value_before = initial_variable.value - put api("/projects/#{project.id}/variables/#{variable.key}", user), params: { variable_type: 'file', value: 'VALUE_1_UP', protected: true } + put api("/projects/#{project.id}/variables/#{variable.key}", user), params: { variable_type: 'file', value: 'VALUE_1_UP', protected: true, description: 'updated' } updated_variable = project.variables.reload.first @@ -235,6 +237,7 @@ RSpec.describe API::Ci::Variables, feature_category: :secrets_management do expect(updated_variable.value).to eq('VALUE_1_UP') expect(updated_variable).to be_protected expect(updated_variable.variable_type).to eq('file') + expect(updated_variable.description).to eq('updated') end it 'masks the new value when logging' do diff --git a/spec/requests/api/container_repositories_spec.rb b/spec/requests/api/container_repositories_spec.rb index 4c1e52df4fc..605fa0d92f6 100644 --- a/spec/requests/api/container_repositories_spec.rb +++ b/spec/requests/api/container_repositories_spec.rb @@ -119,7 +119,7 @@ RSpec.describe API::ContainerRepositories, feature_category: :container_registry let(:created_at) { ::ContainerRepository::MIGRATION_PHASE_1_STARTED_AT + 3.months } before do - allow(::Gitlab).to receive(:com?).and_return(on_com) + allow(::Gitlab).to receive(:com_except_jh?).and_return(on_com) repository.update_column(:created_at, created_at) end diff --git a/spec/requests/api/debian_group_packages_spec.rb b/spec/requests/api/debian_group_packages_spec.rb index 9c726e5a5f7..25b99862100 100644 --- a/spec/requests/api/debian_group_packages_spec.rb +++ b/spec/requests/api/debian_group_packages_spec.rb @@ -6,48 +6,28 @@ RSpec.describe API::DebianGroupPackages, feature_category: :package_registry do include WorkhorseHelpers include_context 'Debian repository shared context', :group, false do - shared_examples 'a Debian package tracking event' do |action| - include_context 'Debian repository access', :public, :developer, :basic do - let(:snowplow_gitlab_standard_context) do - { project: nil, namespace: container, user: user, property: 'i_package_debian_user' } - end - - it_behaves_like 'a package tracking event', described_class.name, action - end - end - - shared_examples 'not a Debian package tracking event' do - include_context 'Debian repository access', :public, :developer, :basic do - it_behaves_like 'not a package tracking event', described_class.name, /.*/ - end - end - context 'with invalid parameter' do let(:url) { "/groups/1/-/packages/debian/dists/with+space/InRelease" } it_behaves_like 'Debian packages GET request', :bad_request, /^distribution is invalid$/ - it_behaves_like 'not a Debian package tracking event' end describe 'GET groups/:id/-/packages/debian/dists/*distribution/Release.gpg' do let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/Release.gpg" } it_behaves_like 'Debian packages read endpoint', 'GET', :success, /^-----BEGIN PGP SIGNATURE-----/ - it_behaves_like 'not a Debian package tracking event' end describe 'GET groups/:id/-/packages/debian/dists/*distribution/Release' do let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/Release" } it_behaves_like 'Debian packages read endpoint', 'GET', :success, /^Codename: fixture-distribution\n$/ - it_behaves_like 'a Debian package tracking event', 'list_package' end describe 'GET groups/:id/-/packages/debian/dists/*distribution/InRelease' do let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/InRelease" } it_behaves_like 'Debian packages read endpoint', 'GET', :success, /^-----BEGIN PGP SIGNED MESSAGE-----/ - it_behaves_like 'a Debian package tracking event', 'list_package' end describe 'GET groups/:id/-/packages/debian/dists/*distribution/:component/binary-:architecture/Packages' do @@ -56,14 +36,12 @@ RSpec.describe API::DebianGroupPackages, feature_category: :package_registry do let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{target_component_name}/binary-#{architecture.name}/Packages" } it_behaves_like 'Debian packages index endpoint', /Description: This is an incomplete Packages file/ - it_behaves_like 'a Debian package tracking event', 'list_package' end describe 'GET groups/:id/-/packages/debian/dists/*distribution/:component/binary-:architecture/Packages.gz' do let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{component.name}/binary-#{architecture.name}/Packages.gz" } it_behaves_like 'Debian packages read endpoint', 'GET', :not_found, /Format gz is not supported/ - it_behaves_like 'not a Debian package tracking event' end describe 'GET groups/:id/-/packages/debian/dists/*distribution/:component/binary-:architecture/by-hash/SHA256/:file_sha256' do @@ -73,7 +51,6 @@ RSpec.describe API::DebianGroupPackages, feature_category: :package_registry do let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{target_component_name}/binary-#{architecture.name}/by-hash/SHA256/#{target_sha256}" } it_behaves_like 'Debian packages index sha256 endpoint', /^Other SHA256$/ - it_behaves_like 'a Debian package tracking event', 'list_package' end describe 'GET groups/:id/-/packages/debian/dists/*distribution/:component/source/Sources' do @@ -82,7 +59,6 @@ RSpec.describe API::DebianGroupPackages, feature_category: :package_registry do let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{target_component_name}/source/Sources" } it_behaves_like 'Debian packages index endpoint', /^Description: This is an incomplete Sources file$/ - it_behaves_like 'a Debian package tracking event', 'list_package' end describe 'GET groups/:id/-/packages/debian/dists/*distribution/:component/source/by-hash/SHA256/:file_sha256' do @@ -92,7 +68,6 @@ RSpec.describe API::DebianGroupPackages, feature_category: :package_registry do let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{target_component_name}/source/by-hash/SHA256/#{target_sha256}" } it_behaves_like 'Debian packages index sha256 endpoint', /^Other SHA256$/ - it_behaves_like 'a Debian package tracking event', 'list_package' end describe 'GET groups/:id/-/packages/debian/dists/*distribution/:component/debian-installer/binary-:architecture/Packages' do @@ -101,14 +76,12 @@ RSpec.describe API::DebianGroupPackages, feature_category: :package_registry do let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{target_component_name}/debian-installer/binary-#{architecture.name}/Packages" } it_behaves_like 'Debian packages index endpoint', /Description: This is an incomplete D-I Packages file/ - it_behaves_like 'a Debian package tracking event', 'list_package' end describe 'GET groups/:id/-/packages/debian/dists/*distribution/:component/debian-installer/binary-:architecture/Packages.gz' do let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{component.name}/debian-installer/binary-#{architecture.name}/Packages.gz" } it_behaves_like 'Debian packages read endpoint', 'GET', :not_found, /Format gz is not supported/ - it_behaves_like 'not a Debian package tracking event' end describe 'GET groups/:id/-/packages/debian/dists/*distribution/:component/debian-installer/binary-:architecture/by-hash/SHA256/:file_sha256' do @@ -118,7 +91,6 @@ RSpec.describe API::DebianGroupPackages, feature_category: :package_registry do let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{target_component_name}/debian-installer/binary-#{architecture.name}/by-hash/SHA256/#{target_sha256}" } it_behaves_like 'Debian packages index sha256 endpoint', /^Other SHA256$/ - it_behaves_like 'a Debian package tracking event', 'list_package' end describe 'GET groups/:id/-/packages/debian/pool/:codename/:project_id/:letter/:package_name/:package_version/:file_name' do @@ -139,7 +111,6 @@ RSpec.describe API::DebianGroupPackages, feature_category: :package_registry do with_them do it_behaves_like 'Debian packages read endpoint', 'GET', :success, params[:success_body] - it_behaves_like 'a Debian package tracking event', 'pull_package' context 'for bumping last downloaded at' do include_context 'Debian repository access', :public, :developer, :basic do diff --git a/spec/requests/api/debian_project_packages_spec.rb b/spec/requests/api/debian_project_packages_spec.rb index b1566860ffc..7f3f633a35c 100644 --- a/spec/requests/api/debian_project_packages_spec.rb +++ b/spec/requests/api/debian_project_packages_spec.rb @@ -7,22 +7,6 @@ RSpec.describe API::DebianProjectPackages, feature_category: :package_registry d include WorkhorseHelpers include_context 'Debian repository shared context', :project, false do - shared_examples 'a Debian package tracking event' do |action| - include_context 'Debian repository access', :public, :developer, :basic do - let(:snowplow_gitlab_standard_context) do - { project: container, namespace: container.namespace, user: user, property: 'i_package_debian_user' } - end - - it_behaves_like 'a package tracking event', described_class.name, action - end - end - - shared_examples 'not a Debian package tracking event' do - include_context 'Debian repository access', :public, :developer, :basic do - it_behaves_like 'not a package tracking event', described_class.name, /.*/ - end - end - shared_examples 'accept GET request on private project with access to package registry for everyone' do include_context 'Debian repository access', :private, :anonymous, :basic do before do @@ -37,14 +21,12 @@ RSpec.describe API::DebianProjectPackages, feature_category: :package_registry d let(:url) { "/projects/1/packages/debian/dists/with+space/InRelease" } it_behaves_like 'Debian packages GET request', :bad_request, /^distribution is invalid$/ - it_behaves_like 'not a Debian package tracking event' end describe 'GET projects/:id/packages/debian/dists/*distribution/Release.gpg' do let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/Release.gpg" } it_behaves_like 'Debian packages read endpoint', 'GET', :success, /^-----BEGIN PGP SIGNATURE-----/ - it_behaves_like 'not a Debian package tracking event' it_behaves_like 'accept GET request on private project with access to package registry for everyone' end @@ -52,7 +34,6 @@ RSpec.describe API::DebianProjectPackages, feature_category: :package_registry d let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/Release" } it_behaves_like 'Debian packages read endpoint', 'GET', :success, /^Codename: fixture-distribution\n$/ - it_behaves_like 'a Debian package tracking event', 'list_package' it_behaves_like 'accept GET request on private project with access to package registry for everyone' end @@ -60,7 +41,6 @@ RSpec.describe API::DebianProjectPackages, feature_category: :package_registry d let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/InRelease" } it_behaves_like 'Debian packages read endpoint', 'GET', :success, /^-----BEGIN PGP SIGNED MESSAGE-----/ - it_behaves_like 'a Debian package tracking event', 'list_package' it_behaves_like 'accept GET request on private project with access to package registry for everyone' end @@ -70,7 +50,6 @@ RSpec.describe API::DebianProjectPackages, feature_category: :package_registry d let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{target_component_name}/binary-#{architecture.name}/Packages" } it_behaves_like 'Debian packages index endpoint', /Description: This is an incomplete Packages file/ - it_behaves_like 'a Debian package tracking event', 'list_package' it_behaves_like 'accept GET request on private project with access to package registry for everyone' end @@ -78,7 +57,6 @@ RSpec.describe API::DebianProjectPackages, feature_category: :package_registry d let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{component.name}/binary-#{architecture.name}/Packages.gz" } it_behaves_like 'Debian packages read endpoint', 'GET', :not_found, /Format gz is not supported/ - it_behaves_like 'not a Debian package tracking event' end describe 'GET projects/:id/packages/debian/dists/*distribution/:component/binary-:architecture/by-hash/SHA256/:file_sha256' do @@ -88,7 +66,6 @@ RSpec.describe API::DebianProjectPackages, feature_category: :package_registry d let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{target_component_name}/binary-#{architecture.name}/by-hash/SHA256/#{target_sha256}" } it_behaves_like 'Debian packages index sha256 endpoint', /^Other SHA256$/ - it_behaves_like 'a Debian package tracking event', 'list_package' it_behaves_like 'accept GET request on private project with access to package registry for everyone' end @@ -98,7 +75,6 @@ RSpec.describe API::DebianProjectPackages, feature_category: :package_registry d let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{target_component_name}/source/Sources" } it_behaves_like 'Debian packages index endpoint', /^Description: This is an incomplete Sources file$/ - it_behaves_like 'a Debian package tracking event', 'list_package' it_behaves_like 'accept GET request on private project with access to package registry for everyone' end @@ -109,7 +85,6 @@ RSpec.describe API::DebianProjectPackages, feature_category: :package_registry d let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{target_component_name}/source/by-hash/SHA256/#{target_sha256}" } it_behaves_like 'Debian packages index sha256 endpoint', /^Other SHA256$/ - it_behaves_like 'a Debian package tracking event', 'list_package' it_behaves_like 'accept GET request on private project with access to package registry for everyone' end @@ -119,7 +94,6 @@ RSpec.describe API::DebianProjectPackages, feature_category: :package_registry d let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{target_component_name}/debian-installer/binary-#{architecture.name}/Packages" } it_behaves_like 'Debian packages index endpoint', /Description: This is an incomplete D-I Packages file/ - it_behaves_like 'a Debian package tracking event', 'list_package' it_behaves_like 'accept GET request on private project with access to package registry for everyone' end @@ -127,7 +101,6 @@ RSpec.describe API::DebianProjectPackages, feature_category: :package_registry d let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{component.name}/debian-installer/binary-#{architecture.name}/Packages.gz" } it_behaves_like 'Debian packages read endpoint', 'GET', :not_found, /Format gz is not supported/ - it_behaves_like 'not a Debian package tracking event' end describe 'GET projects/:id/packages/debian/dists/*distribution/:component/debian-installer/binary-:architecture/by-hash/SHA256/:file_sha256' do @@ -137,7 +110,6 @@ RSpec.describe API::DebianProjectPackages, feature_category: :package_registry d let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{target_component_name}/debian-installer/binary-#{architecture.name}/by-hash/SHA256/#{target_sha256}" } it_behaves_like 'Debian packages index sha256 endpoint', /^Other SHA256$/ - it_behaves_like 'a Debian package tracking event', 'list_package' it_behaves_like 'accept GET request on private project with access to package registry for everyone' end @@ -159,7 +131,6 @@ RSpec.describe API::DebianProjectPackages, feature_category: :package_registry d with_them do it_behaves_like 'Debian packages read endpoint', 'GET', :success, params[:success_body] - it_behaves_like 'a Debian package tracking event', 'pull_package' context 'for bumping last downloaded at' do include_context 'Debian repository access', :public, :developer, :basic do @@ -182,13 +153,11 @@ RSpec.describe API::DebianProjectPackages, feature_category: :package_registry d it_behaves_like 'Debian packages write endpoint', 'upload', :created, nil it_behaves_like 'Debian packages endpoint catching ObjectStorage::RemoteStoreError' - it_behaves_like 'a Debian package tracking event', 'push_package' context 'with codename and component' do let(:extra_params) { { distribution: distribution.codename, component: 'main' } } it_behaves_like 'Debian packages write endpoint', 'upload', :created, nil - it_behaves_like 'a Debian package tracking event', 'push_package' end context 'with codename and without component' do @@ -197,8 +166,6 @@ RSpec.describe API::DebianProjectPackages, feature_category: :package_registry d include_context 'Debian repository access', :public, :developer, :basic do it_behaves_like 'Debian packages GET request', :bad_request, /component is missing/ end - - it_behaves_like 'not a Debian package tracking event' end end @@ -209,8 +176,6 @@ RSpec.describe API::DebianProjectPackages, feature_category: :package_registry d it_behaves_like "Debian packages upload request", :created, nil end - it_behaves_like 'a Debian package tracking event', 'push_package' - context 'with codename and component' do let(:extra_params) { { distribution: distribution.codename, component: 'main' } } @@ -218,8 +183,6 @@ RSpec.describe API::DebianProjectPackages, feature_category: :package_registry d it_behaves_like "Debian packages upload request", :bad_request, /^file_name Only debs, udebs and ddebs can be directly added to a distribution$/ end - - it_behaves_like 'not a Debian package tracking event' end end @@ -227,7 +190,6 @@ RSpec.describe API::DebianProjectPackages, feature_category: :package_registry d let(:file_name) { 'sample_1.2.3~alpha2_amd64.changes' } it_behaves_like 'Debian packages write endpoint', 'upload', :created, nil - it_behaves_like 'a Debian package tracking event', 'push_package' end end @@ -237,7 +199,6 @@ RSpec.describe API::DebianProjectPackages, feature_category: :package_registry d let(:url) { "/projects/#{container.id}/packages/debian/#{file_name}/authorize" } it_behaves_like 'Debian packages write endpoint', 'upload authorize', :created, nil - it_behaves_like 'not a Debian package tracking event' end end end diff --git a/spec/requests/api/deployments_spec.rb b/spec/requests/api/deployments_spec.rb index d7056adfcb6..82ac2eed83d 100644 --- a/spec/requests/api/deployments_spec.rb +++ b/spec/requests/api/deployments_spec.rb @@ -424,7 +424,7 @@ RSpec.describe API::Deployments, feature_category: :continuous_delivery do ) expect(response).to have_gitlab_http_status(:bad_request) - expect(json_response['message']['status']).to include(%Q{cannot transition via \"run\"}) + expect(json_response['message']['status']).to include(%{cannot transition via \"run\"}) end it 'links merge requests when the deployment status changes to success', :sidekiq_inline do diff --git a/spec/requests/api/discussions_spec.rb b/spec/requests/api/discussions_spec.rb index c5126dbd1c2..a65dc6e0175 100644 --- a/spec/requests/api/discussions_spec.rb +++ b/spec/requests/api/discussions_spec.rb @@ -30,7 +30,7 @@ RSpec.describe API::Discussions, feature_category: :team_planning do end context 'when noteable is a WorkItem' do - let!(:work_item) { create(:work_item, :issue, project: project, author: user) } + let!(:work_item) { create(:work_item, project: project, author: user) } let!(:work_item_note) { create(:discussion_note_on_issue, noteable: work_item, project: project, author: user) } let(:parent) { project } diff --git a/spec/requests/api/environments_spec.rb b/spec/requests/api/environments_spec.rb index 9a435b3bce9..498e030da0b 100644 --- a/spec/requests/api/environments_spec.rb +++ b/spec/requests/api/environments_spec.rb @@ -31,6 +31,14 @@ RSpec.describe API::Environments, feature_category: :continuous_delivery do expect(json_response.first).not_to have_key('last_deployment') end + it 'returns 200 HTTP status when using JOB-TOKEN auth' do + job = create(:ci_build, :running, project: project, user: user) + + get api("/projects/#{project.id}/environments"), params: { job_token: job.token } + + expect(response).to have_gitlab_http_status(:ok) + end + context 'when filtering' do let_it_be(:stopped_environment) { create(:environment, :stopped, project: project) } @@ -132,6 +140,14 @@ RSpec.describe API::Environments, feature_category: :continuous_delivery do expect(json_response['external']).to be nil end + it 'returns 200 HTTP status when using JOB-TOKEN auth' do + job = create(:ci_build, :running, project: project, user: user) + + post api("/projects/#{project.id}/environments"), params: { name: "mepmep", job_token: job.token } + + expect(response).to have_gitlab_http_status(:created) + end + it 'requires name to be passed' do post api("/projects/#{project.id}/environments", user), params: { external_url: 'test.gitlab.com' } @@ -173,6 +189,15 @@ RSpec.describe API::Environments, feature_category: :continuous_delivery do expect(response).to have_gitlab_http_status(:ok) end + it 'returns 200 HTTP status when using JOB-TOKEN auth' do + job = create(:ci_build, :running, project: project, user: user) + + post api("/projects/#{project.id}/environments/stop_stale"), + params: { before: 1.week.ago.to_date.to_s, job_token: job.token } + + expect(response).to have_gitlab_http_status(:ok) + end + it 'returns a 400 for bad input date' do post api("/projects/#{project.id}/environments/stop_stale", user), params: { before: 1.day.ago.to_date.to_s } @@ -229,6 +254,15 @@ RSpec.describe API::Environments, feature_category: :continuous_delivery do expect(json_response['tier']).to eq('production') end + it 'returns 200 HTTP status when using JOB-TOKEN auth' do + job = create(:ci_build, :running, project: project, user: user) + + put api("/projects/#{project.id}/environments/#{environment.id}"), + params: { tier: 'production', job_token: job.token } + + expect(response).to have_gitlab_http_status(:ok) + end + it "won't allow slug to be changed" do slug = environment.slug api_url = api("/projects/#{project.id}/environments/#{environment.id}", user) @@ -261,6 +295,17 @@ RSpec.describe API::Environments, feature_category: :continuous_delivery do expect(response).to have_gitlab_http_status(:no_content) end + it 'returns 204 HTTP status when using JOB-TOKEN auth' do + environment.stop + + job = create(:ci_build, :running, project: project, user: user) + + delete api("/projects/#{project.id}/environments/#{environment.id}"), + params: { job_token: job.token } + + expect(response).to have_gitlab_http_status(:no_content) + end + it 'returns a 404 for non existing id' do delete api("/projects/#{project.id}/environments/#{non_existing_record_id}", user) @@ -291,17 +336,23 @@ RSpec.describe API::Environments, feature_category: :continuous_delivery do context 'with a stoppable environment' do before do environment.update!(state: :available) - - post api("/projects/#{project.id}/environments/#{environment.id}/stop", user) end it 'returns a 200' do + post api("/projects/#{project.id}/environments/#{environment.id}/stop", user) + expect(response).to have_gitlab_http_status(:ok) expect(response).to match_response_schema('public_api/v4/environment') + expect(environment.reload).to be_stopped end - it 'actually stops the environment' do - expect(environment.reload).to be_stopped + it 'returns 200 HTTP status when using JOB-TOKEN auth' do + job = create(:ci_build, :running, project: project, user: user) + + post api("/projects/#{project.id}/environments/#{environment.id}/stop"), + params: { job_token: job.token } + + expect(response).to have_gitlab_http_status(:ok) end end @@ -333,6 +384,15 @@ RSpec.describe API::Environments, feature_category: :continuous_delivery do expect(response).to match_response_schema('public_api/v4/environment') expect(json_response['last_deployment']).to be_present end + + it 'returns 200 HTTP status when using JOB-TOKEN auth' do + job = create(:ci_build, :running, project: project, user: user) + + get api("/projects/#{project.id}/environments/#{environment.id}"), + params: { job_token: job.token } + + expect(response).to have_gitlab_http_status(:ok) + end end context 'as non member' do diff --git a/spec/requests/api/error_tracking/project_settings_spec.rb b/spec/requests/api/error_tracking/project_settings_spec.rb index bde90627983..93ad0233ca3 100644 --- a/spec/requests/api/error_tracking/project_settings_spec.rb +++ b/spec/requests/api/error_tracking/project_settings_spec.rb @@ -3,10 +3,20 @@ require 'spec_helper' RSpec.describe API::ErrorTracking::ProjectSettings, feature_category: :error_tracking do - let_it_be(:user) { create(:user) } let_it_be(:project) { create(:project) } let_it_be(:setting) { create(:project_error_tracking_setting, project: project) } let_it_be(:project_without_setting) { create(:project) } + let_it_be(:developer) { create(:user) } + let_it_be(:maintainer) { create(:user) } + let_it_be(:non_member) { create(:user) } + let(:user) { maintainer } + + before_all do + project.add_developer(developer) + project.add_maintainer(maintainer) + project_without_setting.add_developer(developer) + project_without_setting.add_maintainer(maintainer) + end shared_examples 'returns project settings' do it 'returns correct project settings' do @@ -108,10 +118,6 @@ RSpec.describe API::ErrorTracking::ProjectSettings, feature_category: :error_tra end context 'when authenticated as maintainer' do - before do - project.add_maintainer(user) - end - context 'with integrated_error_tracking feature enabled' do it_behaves_like 'returns project settings' end @@ -179,10 +185,6 @@ RSpec.describe API::ErrorTracking::ProjectSettings, feature_category: :error_tra context 'without a project setting' do let(:project) { project_without_setting } - before do - project.add_maintainer(user) - end - it_behaves_like 'returns no project settings' end @@ -208,14 +210,14 @@ RSpec.describe API::ErrorTracking::ProjectSettings, feature_category: :error_tra end context 'when authenticated as developer' do - before do - project.add_developer(user) - end + let(:user) { developer } it_behaves_like 'returns 403' end context 'when authenticated as non-member' do + let(:user) { non_member } + it_behaves_like 'returns 404' end @@ -232,10 +234,6 @@ RSpec.describe API::ErrorTracking::ProjectSettings, feature_category: :error_tra end context 'when authenticated as maintainer' do - before do - project.add_maintainer(user) - end - it_behaves_like 'returns project settings' context 'when integrated_error_tracking feature disabled' do @@ -250,22 +248,18 @@ RSpec.describe API::ErrorTracking::ProjectSettings, feature_category: :error_tra context 'without a project setting' do let(:project) { project_without_setting } - before do - project.add_maintainer(user) - end - it_behaves_like 'returns no project settings' end context 'when authenticated as developer' do - before do - project.add_developer(user) - end + let(:user) { developer } it_behaves_like 'returns 403' end context 'when authenticated as non-member' do + let(:user) { non_member } + it_behaves_like 'returns 404' end @@ -287,14 +281,8 @@ RSpec.describe API::ErrorTracking::ProjectSettings, feature_category: :error_tra context 'when authenticated' do context 'as maintainer' do - before do - project.add_maintainer(user) - end - context "when integrated" do context "with existing setting" do - let(:project) { setting.project } - let(:setting) { create(:project_error_tracking_setting, :integrated) } let(:active) { false } it "updates a setting" do @@ -302,13 +290,7 @@ RSpec.describe API::ErrorTracking::ProjectSettings, feature_category: :error_tra expect(response).to have_gitlab_http_status(:ok) - expect(json_response).to eq( - "active" => false, - "api_url" => nil, - "integrated" => integrated, - "project_name" => nil, - "sentry_external_url" => nil - ) + expect(json_response).to include("integrated" => true) end end @@ -366,14 +348,14 @@ RSpec.describe API::ErrorTracking::ProjectSettings, feature_category: :error_tra end context "as developer" do - before do - project.add_developer(user) - end + let(:user) { developer } it_behaves_like 'returns 403' end context 'as non-member' do + let(:user) { non_member } + it_behaves_like 'returns 404' end end diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb index ed84e3e5f48..ea341703301 100644 --- a/spec/requests/api/files_spec.rb +++ b/spec/requests/api/files_spec.rb @@ -141,11 +141,9 @@ RSpec.describe API::Files, feature_category: :source_code_management do it 'caches sha256 of the content', :use_clean_rails_redis_caching do head api(route(file_path), current_user, **options), params: params - expect(Gitlab::Cache::Client).to receive(:build_with_metadata).with( - cache_identifier: 'API::Files#content_sha', - feature_category: :source_code_management, - backing_resource: :gitaly - ).and_call_original + expect_next_instance_of(Gitlab::Cache::Client) do |instance| + expect(instance).to receive(:fetch).with(anything, nil, { cache_identifier: 'API::Files#content_sha', backing_resource: :gitaly }).and_call_original + end expect(Rails.cache.fetch("blob_content_sha256:#{project.full_path}:#{response.headers['X-Gitlab-Blob-Id']}")) .to eq(content_sha256) diff --git a/spec/requests/api/graphql/boards/board_list_issues_query_spec.rb b/spec/requests/api/graphql/boards/board_list_issues_query_spec.rb index 2775c3d4c5a..86e2b288890 100644 --- a/spec/requests/api/graphql/boards/board_list_issues_query_spec.rb +++ b/spec/requests/api/graphql/boards/board_list_issues_query_spec.rb @@ -19,7 +19,7 @@ RSpec.describe 'get board lists', feature_category: :team_planning do let(:confidential) { false } let(:board_parent_type) { board_parent.class.to_s.downcase } let(:board_data) { graphql_data[board_parent_type]['boards']['nodes'][0] } - let(:lists_data) { board_data['lists']['nodes'][0] } + let(:lists_data) { board_data['lists']['nodes'][1] } let(:issues_data) { lists_data['issues']['nodes'] } let(:issue_params) { { filters: { label_name: label2.title, confidential: confidential }, first: 3 } } diff --git a/spec/requests/api/graphql/boards/board_lists_query_spec.rb b/spec/requests/api/graphql/boards/board_lists_query_spec.rb index 2f23e93e2c6..c1ad9bc8728 100644 --- a/spec/requests/api/graphql/boards/board_lists_query_spec.rb +++ b/spec/requests/api/graphql/boards/board_lists_query_spec.rb @@ -90,7 +90,7 @@ RSpec.describe 'get board lists', feature_category: :team_planning do context 'when using default sorting' do let!(:label_list) { create(:list, board: board, label: label, position: 10) } let!(:label_list2) { create(:list, board: board, label: label2, position: 2) } - let!(:backlog_list) { create(:backlog_list, board: board) } + let(:backlog_list) { board.lists.find_by(list_type: :backlog) } let(:closed_list) { board.lists.find_by(list_type: :closed) } let(:lists) { [backlog_list, label_list2, label_list, closed_list] } diff --git a/spec/requests/api/graphql/ci/inherited_ci_variables_spec.rb b/spec/requests/api/graphql/ci/inherited_ci_variables_spec.rb index 3b4014c178c..defddc64851 100644 --- a/spec/requests/api/graphql/ci/inherited_ci_variables_spec.rb +++ b/spec/requests/api/graphql/ci/inherited_ci_variables_spec.rb @@ -12,9 +12,15 @@ RSpec.describe 'Query.project(fullPath).inheritedCiVariables', feature_category: let(:query) do %( - query { + query($limit: Int, $sort: CiGroupVariablesSort) { project(fullPath: "#{project.full_path}") { - inheritedCiVariables { + inheritedCiVariables(first: $limit, sort: $sort) { + pageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor + } nodes { id key @@ -32,6 +38,34 @@ RSpec.describe 'Query.project(fullPath).inheritedCiVariables', feature_category: ) end + let(:expected_var_b) do + { + '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 + + let(:expected_var_a) do + { + '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' + } + end + def create_variables create(:ci_group_variable, group: group) create(:ci_group_variable, group: subgroup) @@ -50,45 +84,115 @@ RSpec.describe 'Query.project(fullPath).inheritedCiVariables', feature_category: end context 'when user is a project maintainer' do + let!(:group_var) do + create(:ci_group_variable, group: group, key: 'GROUP_VAR_A', + environment_scope: 'production', masked: false, protected: true, raw: true, created_at: 1.day.ago) + end + + let!(:subgroup_var) do + create(:ci_group_variable, group: subgroup, key: 'SUBGROUP_VAR_B', + masked: true, protected: false, raw: false, variable_type: 'file') + end + 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' - } + expect(graphql_data.dig('project', 'inheritedCiVariables', 'nodes')).to match_array([ + expected_var_b, expected_var_a ]) end + context 'when limiting the number of results' do + it 'returns pagination information' do + post_graphql(query, current_user: user, variables: { limit: 1 }) + + expect(has_next_page).to be_truthy + expect(has_prev_page).to be_falsey + + expect(graphql_data.dig('project', 'inheritedCiVariables', 'nodes')).to match_array([ + expected_var_b + ]) + end + end + + describe 'sorting behaviour' do + before do + post_graphql(query, current_user: user, variables: { sort: sort }) + end + + shared_examples_for 'unexpected sort parameter' do + it 'raises a NoData exception' do + expect { graphql_data }.to raise_error(GraphqlHelpers::NoData) + end + end + + context 'with sort by created_at ascenidng' do + let(:sort) { 'CREATED_ASC' } + + it 'returns variables ordered by created_at in ascending order' do + expect(graphql_data.dig('project', 'inheritedCiVariables', 'nodes')).to eq([ + expected_var_a, expected_var_b + ]) + end + end + + context 'with not existing sort parameter' do + let(:sort) { 'WRONG' } + + it_behaves_like 'unexpected sort parameter' + end + + context 'with empty sort parameter' do + let(:sort) { '' } + + it_behaves_like 'unexpected sort parameter' + end + + context 'with no sort parameter' do + let(:sort) { nil } + + it 'returns variables by default in descending order by created_at' do + expect(graphql_data.dig('project', 'inheritedCiVariables', 'nodes')).to eq([ + expected_var_b, expected_var_a + ]) + end + end + + context 'with sort by created_at descending' do + let(:sort) { 'CREATED_DESC' } + + it 'returns variables ordered by created_at in descending order' do + expect(graphql_data.dig('project', 'inheritedCiVariables', 'nodes')).to eq([ + expected_var_b, expected_var_a + ]) + end + end + + context 'with sort by key ascending' do + let(:sort) { 'KEY_ASC' } + + it 'returns variables ordered by key in ascending order' do + expect(graphql_data.dig('project', 'inheritedCiVariables', 'nodes')).to eq([ + expected_var_a, expected_var_b + ]) + end + end + + context 'with sort by key descending' do + let(:sort) { 'KEY_DESC' } + + it 'returns variables ordered by key in descending order' do + expect(graphql_data.dig('project', 'inheritedCiVariables', 'nodes')).to eq([ + expected_var_b, expected_var_a + ]) + end + end + end + it 'avoids N+1 database queries' do create_variables @@ -105,4 +209,16 @@ RSpec.describe 'Query.project(fullPath).inheritedCiVariables', feature_category: expect(multi).not_to exceed_query_limit(baseline) end end + + def pagination_info + graphql_data_at('project', 'inheritedCiVariables', 'pageInfo') + end + + def has_next_page + pagination_info['hasNextPage'] + end + + def has_prev_page + pagination_info['hasPreviousPage'] + end end diff --git a/spec/requests/api/graphql/ci/runner_spec.rb b/spec/requests/api/graphql/ci/runner_spec.rb index 63a657f3962..6acd705c982 100644 --- a/spec/requests/api/graphql/ci/runner_spec.rb +++ b/spec/requests/api/graphql/ci/runner_spec.rb @@ -272,12 +272,13 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do let_it_be(:build1) { create(:ci_build, :running, runner: active_project_runner, pipeline: pipeline1) } let_it_be(:build2) { create(:ci_build, :running, runner: active_project_runner, pipeline: pipeline2) } - let(:runner_query_fragment) { 'id jobCount' } let(:query) do %( query { - runner1: runner(id: "#{active_project_runner.to_global_id}") { #{runner_query_fragment} } - runner2: runner(id: "#{inactive_instance_runner.to_global_id}") { #{runner_query_fragment} } + runner1: runner(id: "#{active_project_runner.to_global_id}") { id jobCount(statuses: [RUNNING]) } + runner2: runner(id: "#{active_project_runner.to_global_id}") { id jobCount(statuses: FAILED) } + runner3: runner(id: "#{active_project_runner.to_global_id}") { id jobCount } + runner4: runner(id: "#{inactive_instance_runner.to_global_id}") { id jobCount } } ) end @@ -287,7 +288,9 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do expect(graphql_data).to match a_hash_including( 'runner1' => a_graphql_entity_for(active_project_runner, job_count: 2), - 'runner2' => a_graphql_entity_for(inactive_instance_runner, job_count: 0) + 'runner2' => a_graphql_entity_for(active_project_runner, job_count: 0), + 'runner3' => a_graphql_entity_for(active_project_runner, job_count: 2), + 'runner4' => a_graphql_entity_for(inactive_instance_runner, job_count: 0) ) end @@ -301,7 +304,9 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do expect(graphql_data).to match a_hash_including( 'runner1' => a_graphql_entity_for(active_project_runner, job_count: 1), - 'runner2' => a_graphql_entity_for(inactive_instance_runner, job_count: 0) + 'runner2' => a_graphql_entity_for(active_project_runner, job_count: 0), + 'runner3' => a_graphql_entity_for(active_project_runner, job_count: 1), + 'runner4' => a_graphql_entity_for(inactive_instance_runner, job_count: 0) ) end end diff --git a/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb b/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb index 88f63fd59d7..118a11851dd 100644 --- a/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb +++ b/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb @@ -241,7 +241,7 @@ RSpec.describe 'container repository details', feature_category: :container_regi end before do - allow(::Gitlab).to receive(:com?).and_return(on_com) + allow(::Gitlab).to receive(:com_except_jh?).and_return(on_com) container_repository.update_column(:created_at, created_at) end diff --git a/spec/requests/api/graphql/gitlab_schema_spec.rb b/spec/requests/api/graphql/gitlab_schema_spec.rb index c5286b93251..ad21006f99a 100644 --- a/spec/requests/api/graphql/gitlab_schema_spec.rb +++ b/spec/requests/api/graphql/gitlab_schema_spec.rb @@ -264,8 +264,8 @@ RSpec.describe 'GitlabSchema configurations', feature_category: :integrations do let(:headers) { {} } before do - allow(GitlabSchema).to receive(:execute).and_wrap_original do |method, *args| - mock_schema.execute(*args) + allow(GitlabSchema).to receive(:execute).and_wrap_original do |method, *args, **kwargs| + mock_schema.execute(*args, **kwargs) end end diff --git a/spec/requests/api/graphql/metrics/dashboard/annotations_spec.rb b/spec/requests/api/graphql/metrics/dashboard/annotations_spec.rb deleted file mode 100644 index 143bc1672f8..00000000000 --- a/spec/requests/api/graphql/metrics/dashboard/annotations_spec.rb +++ /dev/null @@ -1,104 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe 'Getting Metrics Dashboard Annotations', feature_category: :metrics do - include GraphqlHelpers - - let_it_be(:project) { create(:project) } - let_it_be(:environment) { create(:environment, project: project) } - let_it_be(:current_user) { create(:user) } - let_it_be(:path) { 'config/prometheus/common_metrics.yml' } - let_it_be(:from) { "2020-04-01T03:29:25Z" } - let_it_be(:to) { Time.zone.now.advance(minutes: 5) } - let_it_be(:annotation) { create(:metrics_dashboard_annotation, environment: environment, dashboard_path: path) } - let_it_be(:annotation_for_different_env) { create(:metrics_dashboard_annotation, dashboard_path: path) } - let_it_be(:annotation_for_different_dashboard) { create(:metrics_dashboard_annotation, environment: environment, dashboard_path: ".gitlab/dashboards/test.yml") } - let_it_be(:to_old_annotation) do - create(:metrics_dashboard_annotation, environment: environment, starting_at: Time.parse(from).advance(minutes: -5), dashboard_path: path) - end - - let_it_be(:to_new_annotation) do - 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 - #{all_graphql_fields_for('MetricsDashboardAnnotation'.classify)} - QUERY - end - - let(:query) do - %( - query { - project(fullPath: "#{project.full_path}") { - environments(name: "#{environment.name}") { - nodes { - metricsDashboard(path: "#{path}") { - annotations(#{args}) { - nodes { - #{fields} - } - } - } - } - } - } - } - ) - 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 - - it_behaves_like 'a working graphql query' - - it 'returns annotations' do - annotations = graphql_data.dig('project', 'environments', 'nodes')[0].dig('metricsDashboard', 'annotations', 'nodes') - - expect(annotations).to match_array [{ - "description" => annotation.description, - "id" => annotation.to_global_id.to_s, - "panelId" => annotation.panel_xid, - "startingAt" => annotation.starting_at.iso8601, - "endingAt" => nil - }] - end - - context 'arguments' do - context 'from is missing' do - let(:args) { "to: \"#{from}\"" } - - it 'returns error' do - post_graphql(query, current_user: current_user) - - expect(graphql_errors[0]).to include("message" => "Field 'annotations' is missing required arguments: from") - end - end - - context 'to is missing' do - let(:args) { "from: \"#{from}\"" } - - 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 deleted file mode 100644 index b7d9b59f5fe..00000000000 --- a/spec/requests/api/graphql/metrics/dashboard_query_spec.rb +++ /dev/null @@ -1,114 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe 'Getting Metrics Dashboard', feature_category: :metrics do - include GraphqlHelpers - - let_it_be(:current_user) { create(:user) } - - let(:project) { create(:project) } - let(:environment) { create(:environment, project: project) } - - let(:query) do - graphql_query_for( - 'project', { 'fullPath' => project.full_path }, - query_graphql_field( - :environments, { 'name' => environment.name }, - query_graphql_field( - :nodes, nil, - query_graphql_field( - :metricsDashboard, { 'path' => path }, - all_graphql_fields_for('MetricsDashboard'.classify) - ) - ) - ) - ) - end - - context 'for anonymous user' do - before do - post_graphql(query, current_user: current_user) - end - - context 'requested dashboard is available' do - let(:path) { 'config/prometheus/common_metrics.yml' } - - it_behaves_like 'a working graphql query' - - it 'returns nil' do - dashboard = graphql_data.dig('project', 'environments', 'nodes') - - expect(dashboard).to be_nil - end - end - 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 - - context 'requested dashboard is available' do - let(:path) { 'config/prometheus/common_metrics.yml' } - - it_behaves_like 'a working graphql query' - - it 'returns metrics dashboard' do - dashboard = graphql_data.dig('project', 'environments', 'nodes', 0, 'metricsDashboard') - - expect(dashboard).to eql("path" => path, "schemaValidationWarnings" => nil) - end - - context 'invalid dashboard' do - let(:path) { '.gitlab/dashboards/metrics.yml' } - let(:project) { create(:project, :repository, :custom_repo, namespace: current_user.namespace, files: { path => "---\ndashboard: 'test'" }) } - - it 'returns metrics dashboard' do - dashboard = graphql_data.dig('project', 'environments', 'nodes', 0, 'metricsDashboard') - - expect(dashboard).to eql("path" => path, "schemaValidationWarnings" => ["panel_groups: should be an array of panel_groups objects"]) - end - end - - context 'empty dashboard' do - let(:path) { '.gitlab/dashboards/metrics.yml' } - let(:project) { create(:project, :repository, :custom_repo, namespace: current_user.namespace, files: { path => "" }) } - - it 'returns metrics dashboard' do - dashboard = graphql_data.dig('project', 'environments', 'nodes', 0, 'metricsDashboard') - - 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 - let(:path) { 'config/prometheus/i_am_not_here.yml' } - - 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 -end diff --git a/spec/requests/api/graphql/mutations/alert_management/prometheus_integration/create_spec.rb b/spec/requests/api/graphql/mutations/alert_management/prometheus_integration/create_spec.rb index 3dee7f50af3..ec94760e3f0 100644 --- a/spec/requests/api/graphql/mutations/alert_management/prometheus_integration/create_spec.rb +++ b/spec/requests/api/graphql/mutations/alert_management/prometheus_integration/create_spec.rb @@ -8,11 +8,13 @@ RSpec.describe 'Creating a new Prometheus Integration', feature_category: :incid let_it_be(:current_user) { create(:user) } let_it_be(:project) { create(:project) } + let(:api_url) { 'https://prometheus-url.com' } + let(:variables) do { project_path: project.full_path, active: false, - api_url: 'https://prometheus-url.com' + api_url: api_url } end @@ -56,7 +58,20 @@ RSpec.describe 'Creating a new Prometheus Integration', feature_category: :incid expect(integration_response['apiUrl']).to eq(new_integration.api_url) end - [:project_path, :active, :api_url].each do |argument| + context 'without api url' do + let(:api_url) { nil } + + it 'creates a new integration' do + post_graphql_mutation(mutation, current_user: current_user) + + integration_response = mutation_response['integration'] + + expect(response).to have_gitlab_http_status(:success) + expect(integration_response['apiUrl']).to be_nil + end + end + + [:project_path, :active].each do |argument| context "without required argument #{argument}" do before do variables.delete(argument) 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 8791d793cb4..947e7dbcb37 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,7 +53,6 @@ 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 inbound job token scope' do @@ -64,20 +63,6 @@ RSpec.describe 'CiJobTokenScopeAddProject', feature_category: :continuous_integr 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 before do variables[:target_project_path] = 'unknown/project' diff --git a/spec/requests/api/graphql/mutations/ci/pipeline_schedule/create_spec.rb b/spec/requests/api/graphql/mutations/ci/pipeline_schedule/create_spec.rb new file mode 100644 index 00000000000..0d5e5f5d2fb --- /dev/null +++ b/spec/requests/api/graphql/mutations/ci/pipeline_schedule/create_spec.rb @@ -0,0 +1,162 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'PipelineSchedulecreate', feature_category: :continuous_integration do + include GraphqlHelpers + + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project, :public, :repository) } + + let(:mutation) do + variables = { + project_path: project.full_path, + **pipeline_schedule_parameters + } + + graphql_mutation( + :pipeline_schedule_create, + variables, + <<-QL + pipelineSchedule { + id + description + cron + refForDisplay + active + cronTimezone + variables { + nodes { + key + value + } + } + owner { + id + } + } + errors + QL + ) + end + + let(:pipeline_schedule_parameters) do + { + description: 'created_desc', + cron: '0 1 * * *', + cronTimezone: 'UTC', + ref: 'patch-x', + active: true, + variables: [ + { key: 'AAA', value: "AAA123", variableType: 'ENV_VAR' } + ] + } + end + + let(:mutation_response) { graphql_mutation_response(:pipeline_schedule_create) } + + context 'when unauthorized' do + it 'returns an error' do + post_graphql_mutation(mutation, current_user: user) + + expect(graphql_errors).not_to be_empty + expect(graphql_errors[0]['message']) + .to eq( + "The resource that you are attempting to access does not exist " \ + "or you don't have permission to perform this action" + ) + end + end + + # Move this from `shared_context` to `context` when `ci_refactoring_pipeline_schedule_create_service` is removed. + shared_context 'when authorized' do # rubocop:disable RSpec/ContextWording + before_all do + project.add_developer(user) + end + + context 'when success' do + it do + post_graphql_mutation(mutation, current_user: user) + + expect(response).to have_gitlab_http_status(:success) + + expect(mutation_response['pipelineSchedule']['owner']['id']).to eq(user.to_global_id.to_s) + + %w[description cron cronTimezone active].each do |key| + expect(mutation_response['pipelineSchedule'][key]).to eq(pipeline_schedule_parameters[key.to_sym]) + end + + expect(mutation_response['pipelineSchedule']['refForDisplay']).to eq(pipeline_schedule_parameters[:ref]) + + expect(mutation_response['pipelineSchedule']['variables']['nodes'][0]['key']).to eq('AAA') + expect(mutation_response['pipelineSchedule']['variables']['nodes'][0]['value']).to eq('AAA123') + + expect(mutation_response['pipelineSchedule']['owner']['id']).to eq(user.to_global_id.to_s) + + expect(mutation_response['errors']).to eq([]) + end + end + + context 'when failure' do + context 'when params are invalid' do + let(:pipeline_schedule_parameters) do + { + description: 'some description', + cron: 'abc', + cronTimezone: 'cCc', + ref: 'asd', + active: true, + variables: [] + } + end + + it do + post_graphql_mutation(mutation, current_user: user) + + expect(response).to have_gitlab_http_status(:success) + + expect(mutation_response['errors']) + .to match_array( + ["Cron is invalid syntax", "Cron timezone is invalid syntax"] + ) + end + end + + context 'when variables have duplicate name' do + before do + pipeline_schedule_parameters.merge!( + { + variables: [ + { key: 'AAA', value: "AAA123", variableType: 'ENV_VAR' }, + { key: 'AAA', value: "AAA123", variableType: 'ENV_VAR' } + ] + } + ) + end + + it 'returns error' do + post_graphql_mutation(mutation, current_user: user) + + expect(response).to have_gitlab_http_status(:success) + + expect(mutation_response['errors']) + .to match_array( + [ + "Variables have duplicate values (AAA)" + ] + ) + end + end + end + end + + it_behaves_like 'when authorized' + + context 'when the FF ci_refactoring_pipeline_schedule_create_service is disabled' do + before do + stub_feature_flags(ci_refactoring_pipeline_schedule_create_service: false) + end + + it_behaves_like 'when authorized' + end +end diff --git a/spec/requests/api/graphql/mutations/ci/pipeline_schedule/delete_spec.rb b/spec/requests/api/graphql/mutations/ci/pipeline_schedule/delete_spec.rb new file mode 100644 index 00000000000..e79395bb52c --- /dev/null +++ b/spec/requests/api/graphql/mutations/ci/pipeline_schedule/delete_spec.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'PipelineScheduleDelete', feature_category: :continuous_integration do + include GraphqlHelpers + + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project) } + let_it_be(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project, owner: user) } + + let(:mutation) do + graphql_mutation( + :pipeline_schedule_delete, + { id: pipeline_schedule_id }, + <<-QL + errors + QL + ) + end + + let(:pipeline_schedule_id) { pipeline_schedule.to_global_id.to_s } + let(:mutation_response) { graphql_mutation_response(:pipeline_schedule_delete) } + + context 'when unauthorized' do + it 'returns an error' do + post_graphql_mutation(mutation, current_user: create(:user)) + + expect(graphql_errors).not_to be_empty + expect(graphql_errors[0]['message']) + .to eq( + "The resource that you are attempting to access does not exist " \ + "or you don't have permission to perform this action" + ) + end + end + + context 'when authorized' do + before_all do + project.add_maintainer(user) + end + + context 'when success' do + it do + post_graphql_mutation(mutation, current_user: user) + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response['errors']).to eq([]) + end + end + + context 'when failure' do + context 'when destroy fails' do + before do + allow_next_found_instance_of(Ci::PipelineSchedule) do |pipeline_schedule| + allow(pipeline_schedule).to receive(:destroy).and_return(false) + end + end + + it do + post_graphql_mutation(mutation, current_user: user) + + expect(response).to have_gitlab_http_status(:success) + + expect(mutation_response['errors']).to match_array(['Failed to remove the pipeline schedule']) + end + end + + context 'when pipeline schedule not found' do + let(:pipeline_schedule_id) { 'gid://gitlab/Ci::PipelineSchedule/0' } + + it do + post_graphql_mutation(mutation, current_user: user) + + expect(graphql_errors).not_to be_empty + expect(graphql_errors[0]['message']) + .to eq("Internal server error: Couldn't find Ci::PipelineSchedule with 'id'=0") + end + end + end + end +end diff --git a/spec/requests/api/graphql/mutations/ci/pipeline_schedule/play_spec.rb b/spec/requests/api/graphql/mutations/ci/pipeline_schedule/play_spec.rb new file mode 100644 index 00000000000..55ecf8f287e --- /dev/null +++ b/spec/requests/api/graphql/mutations/ci/pipeline_schedule/play_spec.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'PipelineSchedulePlay', feature_category: :continuous_integration do + include GraphqlHelpers + + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project) } + let_it_be(:pipeline_schedule) do + create( + :ci_pipeline_schedule, + :every_minute, + project: project, + owner: user + ) + end + + let(:mutation) do + graphql_mutation( + :pipeline_schedule_play, + { id: pipeline_schedule.to_global_id.to_s }, + <<-QL + pipelineSchedule { id, nextRunAt } + errors + QL + ) + end + + let(:mutation_response) { graphql_mutation_response(:pipeline_schedule_play) } + + context 'when unauthorized' do + it 'returns an error' do + post_graphql_mutation(mutation, current_user: create(:user)) + + expect(graphql_errors).not_to be_empty + expect(graphql_errors[0]['message']) + .to eq( + "The resource that you are attempting to access does not exist " \ + "or you don't have permission to perform this action" + ) + end + end + + context 'when authorized', :sidekiq_inline do + before_all do + project.add_maintainer(user) + pipeline_schedule.update_columns(next_run_at: 2.hours.ago) + end + + context 'when mutation succeeds' do + let(:service_response) { instance_double('ServiceResponse', payload: new_pipeline) } + let(:new_pipeline) { instance_double('Ci::Pipeline', persisted?: true) } + + it do + expect(Ci::CreatePipelineService).to receive_message_chain(:new, :execute).and_return(service_response) + post_graphql_mutation(mutation, current_user: user) + + expect(mutation_response['pipelineSchedule']['id']).to include(pipeline_schedule.id.to_s) + new_next_run_at = DateTime.parse(mutation_response['pipelineSchedule']['nextRunAt']) + expect(new_next_run_at).not_to eq(pipeline_schedule.next_run_at) + expect(new_next_run_at).to eq(pipeline_schedule.reset.next_run_at) + expect(mutation_response['errors']).to eq([]) + end + end + + context 'when mutation fails' do + it do + expect(RunPipelineScheduleWorker) + .to receive(:perform_async) + .with(pipeline_schedule.id, user.id).and_return(nil) + + post_graphql_mutation(mutation, current_user: user) + + expect(mutation_response['pipelineSchedule']).to be_nil + expect(mutation_response['errors']).to match_array(['Unable to schedule a pipeline to run immediately.']) + end + end + end +end diff --git a/spec/requests/api/graphql/mutations/ci/pipeline_schedule/take_ownership_spec.rb b/spec/requests/api/graphql/mutations/ci/pipeline_schedule/take_ownership_spec.rb new file mode 100644 index 00000000000..2d1f1565a73 --- /dev/null +++ b/spec/requests/api/graphql/mutations/ci/pipeline_schedule/take_ownership_spec.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'PipelineScheduleTakeOwnership', feature_category: :continuous_integration do + include GraphqlHelpers + + let_it_be(:user) { create(:user) } + let_it_be(:owner) { create(:user) } + let_it_be(:project) { create(:project) } + let_it_be(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project, owner: owner) } + + let(:mutation) do + graphql_mutation( + :pipeline_schedule_take_ownership, + { id: pipeline_schedule_id }, + <<-QL + errors + QL + ) + end + + let(:pipeline_schedule_id) { pipeline_schedule.to_global_id.to_s } + + before_all do + project.add_maintainer(user) + end + + it 'returns an error if the user is not allowed to take ownership of the schedule' do + post_graphql_mutation(mutation, current_user: create(:user)) + + expect(graphql_errors).not_to be_empty + end + + it 'takes ownership of the schedule' do + post_graphql_mutation(mutation, current_user: user) + + expect(response).to have_gitlab_http_status(:success) + expect(graphql_errors).to be_nil + end +end diff --git a/spec/requests/api/graphql/mutations/ci/pipeline_schedule/update_spec.rb b/spec/requests/api/graphql/mutations/ci/pipeline_schedule/update_spec.rb new file mode 100644 index 00000000000..ec1595f393f --- /dev/null +++ b/spec/requests/api/graphql/mutations/ci/pipeline_schedule/update_spec.rb @@ -0,0 +1,189 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'PipelineScheduleUpdate', feature_category: :continuous_integration do + include GraphqlHelpers + + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project, :public, :repository) } + let_it_be(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project, owner: user) } + + let_it_be(:variable_one) do + create(:ci_pipeline_schedule_variable, key: 'foo', value: 'foovalue', pipeline_schedule: pipeline_schedule) + end + + let_it_be(:variable_two) do + create(:ci_pipeline_schedule_variable, key: 'bar', value: 'barvalue', pipeline_schedule: pipeline_schedule) + end + + let(:mutation) do + variables = { + id: pipeline_schedule.to_global_id.to_s, + **pipeline_schedule_parameters + } + + graphql_mutation( + :pipeline_schedule_update, + variables, + <<-QL + pipelineSchedule { + id + description + cron + refForDisplay + active + cronTimezone + variables { + nodes { + key + value + variableType + } + } + } + errors + QL + ) + end + + let(:pipeline_schedule_parameters) { {} } + let(:mutation_response) { graphql_mutation_response(:pipeline_schedule_update) } + + context 'when unauthorized' do + it 'returns an error' do + post_graphql_mutation(mutation, current_user: create(:user)) + + expect(graphql_errors).not_to be_empty + expect(graphql_errors[0]['message']) + .to eq( + "The resource that you are attempting to access does not exist " \ + "or you don't have permission to perform this action" + ) + end + end + + context 'when authorized' do + before_all do + project.add_developer(user) + end + + context 'when success' do + let(:pipeline_schedule_parameters) do + { + description: 'updated_desc', + cron: '0 1 * * *', + cronTimezone: 'UTC', + ref: 'patch-x', + active: true, + variables: [ + { key: 'AAA', value: "AAA123", variableType: 'ENV_VAR' } + ] + } + end + + it do + post_graphql_mutation(mutation, current_user: user) + + expect(response).to have_gitlab_http_status(:success) + + expect_graphql_errors_to_be_empty + + expect(mutation_response['pipelineSchedule']['id']).to eq(pipeline_schedule.to_global_id.to_s) + + %w[description cron cronTimezone active].each do |key| + expect(mutation_response['pipelineSchedule'][key]).to eq(pipeline_schedule_parameters[key.to_sym]) + end + + expect(mutation_response['pipelineSchedule']['refForDisplay']).to eq(pipeline_schedule_parameters[:ref]) + + expect(mutation_response['pipelineSchedule']['variables']['nodes'][2]['key']).to eq('AAA') + expect(mutation_response['pipelineSchedule']['variables']['nodes'][2]['value']).to eq('AAA123') + end + end + + context 'when updating and removing variables' do + let(:pipeline_schedule_parameters) do + { + variables: [ + { key: 'ABC', value: "ABC123", variableType: 'ENV_VAR', destroy: false }, + { id: variable_one.to_global_id.to_s, + key: 'foo', value: "foovalue", + variableType: 'ENV_VAR', + destroy: true }, + { id: variable_two.to_global_id.to_s, key: 'newbar', value: "newbarvalue", variableType: 'ENV_VAR' } + ] + } + end + + it 'processes variables correctly' do + post_graphql_mutation(mutation, current_user: user) + + expect(response).to have_gitlab_http_status(:success) + + expect(mutation_response['pipelineSchedule']['variables']['nodes']) + .to match_array( + [ + { "key" => 'newbar', "value" => 'newbarvalue', "variableType" => 'ENV_VAR' }, + { "key" => 'ABC', "value" => "ABC123", "variableType" => 'ENV_VAR' } + ] + ) + end + end + + context 'when failure' do + context 'when params are invalid' do + let(:pipeline_schedule_parameters) do + { + description: '', + cron: 'abc', + cronTimezone: 'cCc', + ref: '', + active: true, + variables: [] + } + end + + it do + post_graphql_mutation(mutation, current_user: user) + + expect(response).to have_gitlab_http_status(:success) + + expect(mutation_response['errors']) + .to match_array( + [ + "Cron is invalid syntax", + "Cron timezone is invalid syntax", + "Ref can't be blank", + "Description can't be blank" + ] + ) + end + end + + context 'when params have duplicate variables' do + let(:pipeline_schedule_parameters) do + { + variables: [ + { key: 'AAA', value: "AAA123", variableType: 'ENV_VAR' }, + { key: 'AAA', value: "AAA123", variableType: 'ENV_VAR' } + ] + } + end + + it 'returns error' do + post_graphql_mutation(mutation, current_user: user) + + expect(response).to have_gitlab_http_status(:success) + + expect(mutation_response['errors']) + .to match_array( + [ + "Variables have duplicate values (AAA)" + ] + ) + end + end + end + end +end diff --git a/spec/requests/api/graphql/mutations/ci/pipeline_schedule_create_spec.rb b/spec/requests/api/graphql/mutations/ci/pipeline_schedule_create_spec.rb deleted file mode 100644 index 4a45d255d99..00000000000 --- a/spec/requests/api/graphql/mutations/ci/pipeline_schedule_create_spec.rb +++ /dev/null @@ -1,151 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe 'PipelineSchedulecreate' do - include GraphqlHelpers - - let_it_be(:user) { create(:user) } - let_it_be(:project) { create(:project, :public, :repository) } - - let(:mutation) do - variables = { - project_path: project.full_path, - **pipeline_schedule_parameters - } - - graphql_mutation( - :pipeline_schedule_create, - variables, - <<-QL - pipelineSchedule { - id - description - cron - refForDisplay - active - cronTimezone - variables { - nodes { - key - value - } - } - owner { - id - } - } - errors - QL - ) - end - - let(:pipeline_schedule_parameters) do - { - description: 'created_desc', - cron: '0 1 * * *', - cronTimezone: 'UTC', - ref: 'patch-x', - active: true, - variables: [ - { key: 'AAA', value: "AAA123", variableType: 'ENV_VAR' } - ] - } - end - - let(:mutation_response) { graphql_mutation_response(:pipeline_schedule_create) } - - context 'when unauthorized' do - it 'returns an error' do - post_graphql_mutation(mutation, current_user: user) - - expect(graphql_errors).not_to be_empty - expect(graphql_errors[0]['message']) - .to eq( - "The resource that you are attempting to access does not exist " \ - "or you don't have permission to perform this action" - ) - end - end - - context 'when authorized' do - before do - project.add_developer(user) - end - - context 'when success' do - it do - post_graphql_mutation(mutation, current_user: user) - - expect(response).to have_gitlab_http_status(:success) - - expect(mutation_response['pipelineSchedule']['owner']['id']).to eq(user.to_global_id.to_s) - - %w[description cron cronTimezone active].each do |key| - expect(mutation_response['pipelineSchedule'][key]).to eq(pipeline_schedule_parameters[key.to_sym]) - end - - expect(mutation_response['pipelineSchedule']['refForDisplay']).to eq(pipeline_schedule_parameters[:ref]) - - expect(mutation_response['pipelineSchedule']['variables']['nodes'][0]['key']).to eq('AAA') - expect(mutation_response['pipelineSchedule']['variables']['nodes'][0]['value']).to eq('AAA123') - - expect(mutation_response['pipelineSchedule']['owner']['id']).to eq(user.to_global_id.to_s) - - expect(mutation_response['errors']).to eq([]) - end - end - - context 'when failure' do - context 'when params are invalid' do - let(:pipeline_schedule_parameters) do - { - description: 'some description', - cron: 'abc', - cronTimezone: 'cCc', - ref: 'asd', - active: true, - variables: [] - } - end - - it do - post_graphql_mutation(mutation, current_user: user) - - expect(response).to have_gitlab_http_status(:success) - - expect(mutation_response['errors']) - .to match_array( - ["Cron is invalid syntax", "Cron timezone is invalid syntax"] - ) - end - end - - context 'when variables have duplicate name' do - before do - pipeline_schedule_parameters.merge!( - { - variables: [ - { key: 'AAA', value: "AAA123", variableType: 'ENV_VAR' }, - { key: 'AAA', value: "AAA123", variableType: 'ENV_VAR' } - ] - } - ) - end - - it 'returns error' do - post_graphql_mutation(mutation, current_user: user) - - expect(response).to have_gitlab_http_status(:success) - - expect(mutation_response['errors']) - .to match_array( - [ - "Variables have duplicate values (AAA)" - ] - ) - end - end - end - end -end diff --git a/spec/requests/api/graphql/mutations/ci/pipeline_schedule_delete_spec.rb b/spec/requests/api/graphql/mutations/ci/pipeline_schedule_delete_spec.rb deleted file mode 100644 index b846ff0aec8..00000000000 --- a/spec/requests/api/graphql/mutations/ci/pipeline_schedule_delete_spec.rb +++ /dev/null @@ -1,82 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe 'PipelineScheduleDelete', feature_category: :continuous_integration do - include GraphqlHelpers - - let_it_be(:user) { create(:user) } - let_it_be(:project) { create(:project) } - let_it_be(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project, owner: user) } - - let(:mutation) do - graphql_mutation( - :pipeline_schedule_delete, - { id: pipeline_schedule_id }, - <<-QL - errors - QL - ) - end - - let(:pipeline_schedule_id) { pipeline_schedule.to_global_id.to_s } - let(:mutation_response) { graphql_mutation_response(:pipeline_schedule_delete) } - - context 'when unauthorized' do - it 'returns an error' do - post_graphql_mutation(mutation, current_user: create(:user)) - - expect(graphql_errors).not_to be_empty - expect(graphql_errors[0]['message']) - .to eq( - "The resource that you are attempting to access does not exist " \ - "or you don't have permission to perform this action" - ) - end - end - - context 'when authorized' do - before do - project.add_maintainer(user) - end - - context 'when success' do - it do - post_graphql_mutation(mutation, current_user: user) - - expect(response).to have_gitlab_http_status(:success) - expect(mutation_response['errors']).to eq([]) - end - end - - context 'when failure' do - context 'when destroy fails' do - before do - allow_next_found_instance_of(Ci::PipelineSchedule) do |pipeline_schedule| - allow(pipeline_schedule).to receive(:destroy).and_return(false) - end - end - - it do - post_graphql_mutation(mutation, current_user: user) - - expect(response).to have_gitlab_http_status(:success) - - expect(mutation_response['errors']).to match_array(['Failed to remove the pipeline schedule']) - end - end - - context 'when pipeline schedule not found' do - let(:pipeline_schedule_id) { 'gid://gitlab/Ci::PipelineSchedule/0' } - - it do - post_graphql_mutation(mutation, current_user: user) - - expect(graphql_errors).not_to be_empty - expect(graphql_errors[0]['message']) - .to eq("Internal server error: Couldn't find Ci::PipelineSchedule with 'id'=0") - end - end - end - end -end diff --git a/spec/requests/api/graphql/mutations/ci/pipeline_schedule_play_spec.rb b/spec/requests/api/graphql/mutations/ci/pipeline_schedule_play_spec.rb deleted file mode 100644 index 492c6946c99..00000000000 --- a/spec/requests/api/graphql/mutations/ci/pipeline_schedule_play_spec.rb +++ /dev/null @@ -1,80 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe 'PipelineSchedulePlay', feature_category: :continuous_integration do - include GraphqlHelpers - - let_it_be(:user) { create(:user) } - let_it_be(:project) { create(:project) } - let_it_be(:pipeline_schedule) do - create( - :ci_pipeline_schedule, - :every_minute, - project: project, - owner: user - ) - end - - let(:mutation) do - graphql_mutation( - :pipeline_schedule_play, - { id: pipeline_schedule.to_global_id.to_s }, - <<-QL - pipelineSchedule { id, nextRunAt } - errors - QL - ) - end - - let(:mutation_response) { graphql_mutation_response(:pipeline_schedule_play) } - - context 'when unauthorized' do - it 'returns an error' do - post_graphql_mutation(mutation, current_user: create(:user)) - - expect(graphql_errors).not_to be_empty - expect(graphql_errors[0]['message']) - .to eq( - "The resource that you are attempting to access does not exist " \ - "or you don't have permission to perform this action" - ) - end - end - - context 'when authorized', :sidekiq_inline do - before do - project.add_maintainer(user) - pipeline_schedule.update_columns(next_run_at: 2.hours.ago) - end - - context 'when mutation succeeds' do - let(:service_response) { instance_double('ServiceResponse', payload: new_pipeline) } - let(:new_pipeline) { instance_double('Ci::Pipeline', persisted?: true) } - - it do - expect(Ci::CreatePipelineService).to receive_message_chain(:new, :execute).and_return(service_response) - post_graphql_mutation(mutation, current_user: user) - - expect(mutation_response['pipelineSchedule']['id']).to include(pipeline_schedule.id.to_s) - new_next_run_at = DateTime.parse(mutation_response['pipelineSchedule']['nextRunAt']) - expect(new_next_run_at).not_to eq(pipeline_schedule.next_run_at) - expect(new_next_run_at).to eq(pipeline_schedule.reset.next_run_at) - expect(mutation_response['errors']).to eq([]) - end - end - - context 'when mutation fails' do - it do - expect(RunPipelineScheduleWorker) - .to receive(:perform_async) - .with(pipeline_schedule.id, user.id).and_return(nil) - - post_graphql_mutation(mutation, current_user: user) - - expect(mutation_response['pipelineSchedule']).to be_nil - expect(mutation_response['errors']).to match_array(['Unable to schedule a pipeline to run immediately.']) - end - end - end -end diff --git a/spec/requests/api/graphql/mutations/ci/pipeline_schedule_take_ownership_spec.rb b/spec/requests/api/graphql/mutations/ci/pipeline_schedule_take_ownership_spec.rb deleted file mode 100644 index 2d1f1565a73..00000000000 --- a/spec/requests/api/graphql/mutations/ci/pipeline_schedule_take_ownership_spec.rb +++ /dev/null @@ -1,41 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe 'PipelineScheduleTakeOwnership', feature_category: :continuous_integration do - include GraphqlHelpers - - let_it_be(:user) { create(:user) } - let_it_be(:owner) { create(:user) } - let_it_be(:project) { create(:project) } - let_it_be(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project, owner: owner) } - - let(:mutation) do - graphql_mutation( - :pipeline_schedule_take_ownership, - { id: pipeline_schedule_id }, - <<-QL - errors - QL - ) - end - - let(:pipeline_schedule_id) { pipeline_schedule.to_global_id.to_s } - - before_all do - project.add_maintainer(user) - end - - it 'returns an error if the user is not allowed to take ownership of the schedule' do - post_graphql_mutation(mutation, current_user: create(:user)) - - expect(graphql_errors).not_to be_empty - end - - it 'takes ownership of the schedule' do - post_graphql_mutation(mutation, current_user: user) - - expect(response).to have_gitlab_http_status(:success) - expect(graphql_errors).to be_nil - end -end diff --git a/spec/requests/api/graphql/mutations/ci/pipeline_schedule_update_spec.rb b/spec/requests/api/graphql/mutations/ci/pipeline_schedule_update_spec.rb deleted file mode 100644 index c1da231a4a6..00000000000 --- a/spec/requests/api/graphql/mutations/ci/pipeline_schedule_update_spec.rb +++ /dev/null @@ -1,151 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe 'PipelineScheduleUpdate', feature_category: :continuous_integration do - include GraphqlHelpers - - let_it_be(:user) { create(:user) } - let_it_be(:project) { create(:project, :public, :repository) } - let_it_be(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project, owner: user) } - - let(:mutation) do - variables = { - id: pipeline_schedule.to_global_id.to_s, - **pipeline_schedule_parameters - } - - graphql_mutation( - :pipeline_schedule_update, - variables, - <<-QL - pipelineSchedule { - id - description - cron - refForDisplay - active - cronTimezone - variables { - nodes { - key - value - } - } - } - errors - QL - ) - end - - let(:pipeline_schedule_parameters) { {} } - let(:mutation_response) { graphql_mutation_response(:pipeline_schedule_update) } - - context 'when unauthorized' do - it 'returns an error' do - post_graphql_mutation(mutation, current_user: create(:user)) - - expect(graphql_errors).not_to be_empty - expect(graphql_errors[0]['message']) - .to eq( - "The resource that you are attempting to access does not exist " \ - "or you don't have permission to perform this action" - ) - end - end - - context 'when authorized' do - before do - project.add_developer(user) - end - - context 'when success' do - let(:pipeline_schedule_parameters) do - { - description: 'updated_desc', - cron: '0 1 * * *', - cronTimezone: 'UTC', - ref: 'patch-x', - active: true, - variables: [ - { key: 'AAA', value: "AAA123", variableType: 'ENV_VAR' } - ] - } - end - - it do - post_graphql_mutation(mutation, current_user: user) - - expect(response).to have_gitlab_http_status(:success) - - expect_graphql_errors_to_be_empty - - expect(mutation_response['pipelineSchedule']['id']).to eq(pipeline_schedule.to_global_id.to_s) - - %w[description cron cronTimezone active].each do |key| - expect(mutation_response['pipelineSchedule'][key]).to eq(pipeline_schedule_parameters[key.to_sym]) - end - - expect(mutation_response['pipelineSchedule']['refForDisplay']).to eq(pipeline_schedule_parameters[:ref]) - - expect(mutation_response['pipelineSchedule']['variables']['nodes'][0]['key']).to eq('AAA') - expect(mutation_response['pipelineSchedule']['variables']['nodes'][0]['value']).to eq('AAA123') - end - end - - context 'when failure' do - context 'when params are invalid' do - let(:pipeline_schedule_parameters) do - { - description: '', - cron: 'abc', - cronTimezone: 'cCc', - ref: '', - active: true, - variables: [] - } - end - - it do - post_graphql_mutation(mutation, current_user: user) - - expect(response).to have_gitlab_http_status(:success) - - expect(mutation_response['errors']) - .to match_array( - [ - "Cron is invalid syntax", - "Cron timezone is invalid syntax", - "Ref can't be blank", - "Description can't be blank" - ] - ) - end - end - - context 'when params have duplicate variables' do - let(:pipeline_schedule_parameters) do - { - variables: [ - { key: 'AAA', value: "AAA123", variableType: 'ENV_VAR' }, - { key: 'AAA', value: "AAA123", variableType: 'ENV_VAR' } - ] - } - end - - it 'returns error' do - post_graphql_mutation(mutation, current_user: user) - - expect(response).to have_gitlab_http_status(:success) - - expect(mutation_response['errors']) - .to match_array( - [ - "Variables have duplicate values (AAA)" - ] - ) - end - end - end - end -end 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 fd92ed198e7..6e101d07b9f 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,10 +5,6 @@ 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, @@ -101,22 +97,6 @@ RSpec.describe 'ProjectCiCdSettingsUpdate', feature_category: :continuous_integr 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) diff --git a/spec/requests/api/graphql/mutations/ci/runner/create_spec.rb b/spec/requests/api/graphql/mutations/ci/runner/create_spec.rb index 1658c277ed0..b697b9f73b7 100644 --- a/spec/requests/api/graphql/mutations/ci/runner/create_spec.rb +++ b/spec/requests/api/graphql/mutations/ci/runner/create_spec.rb @@ -95,18 +95,6 @@ RSpec.describe 'RunnerCreate', feature_category: :runner_fleet do 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 } @@ -139,18 +127,6 @@ RSpec.describe 'RunnerCreate', feature_category: :runner_fleet do 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 @@ -164,17 +140,12 @@ RSpec.describe 'RunnerCreate', feature_category: :runner_fleet do } 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' @@ -226,7 +197,6 @@ RSpec.describe 'RunnerCreate', feature_category: :runner_fleet do 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 @@ -249,7 +219,6 @@ RSpec.describe 'RunnerCreate', feature_category: :runner_fleet 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' @@ -304,7 +273,6 @@ RSpec.describe 'RunnerCreate', feature_category: :runner_fleet do 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 diff --git a/spec/requests/api/graphql/mutations/issues/update_spec.rb b/spec/requests/api/graphql/mutations/issues/update_spec.rb index e51c057c182..97ead687a82 100644 --- a/spec/requests/api/graphql/mutations/issues/update_spec.rb +++ b/spec/requests/api/graphql/mutations/issues/update_spec.rb @@ -51,9 +51,7 @@ RSpec.describe 'Update of an existing issue', feature_category: :team_planning d expect do post_graphql_mutation(mutation, current_user: current_user) issue.reload - end.to change { issue.work_item_type.base_type }.from('issue').to('incident').and( - change(issue, :issue_type).from('issue').to('incident') - ) + end.to change { issue.work_item_type.base_type }.from('issue').to('incident') end end diff --git a/spec/requests/api/graphql/mutations/notes/create/note_spec.rb b/spec/requests/api/graphql/mutations/notes/create/note_spec.rb index e6feba059c4..37bcdf61d23 100644 --- a/spec/requests/api/graphql/mutations/notes/create/note_spec.rb +++ b/spec/requests/api/graphql/mutations/notes/create/note_spec.rb @@ -105,7 +105,7 @@ RSpec.describe 'Adding a Note', feature_category: :team_planning do context 'as work item' do let_it_be(:project) { create(:project) } - let_it_be(:noteable) { create(:work_item, :issue, project: project) } + let_it_be(:noteable) { create(:work_item, project: project) } context 'when using internal param' do let(:variables_extra) { { internal: true } } diff --git a/spec/requests/api/graphql/mutations/notes/destroy_spec.rb b/spec/requests/api/graphql/mutations/notes/destroy_spec.rb index f40518a574b..9d63eed276d 100644 --- a/spec/requests/api/graphql/mutations/notes/destroy_spec.rb +++ b/spec/requests/api/graphql/mutations/notes/destroy_spec.rb @@ -5,7 +5,7 @@ require 'spec_helper' RSpec.describe 'Destroying a Note', feature_category: :team_planning do include GraphqlHelpers - let(:noteable) { create(:work_item, :issue) } + let(:noteable) { create(:work_item) } let!(:note) { create(:note, noteable: noteable, project: noteable.project) } let(:global_note_id) { GitlabSchema.id_from_object(note).to_s } let(:variables) { { id: global_note_id } } diff --git a/spec/requests/api/graphql/mutations/notes/update/note_spec.rb b/spec/requests/api/graphql/mutations/notes/update/note_spec.rb index 7918bc860fe..7102f817d4c 100644 --- a/spec/requests/api/graphql/mutations/notes/update/note_spec.rb +++ b/spec/requests/api/graphql/mutations/notes/update/note_spec.rb @@ -41,7 +41,7 @@ RSpec.describe 'Updating a Note', feature_category: :team_planning do it_behaves_like 'a Note mutation update only with quick actions' context 'for work item' do - let(:noteable) { create(:work_item, :issue) } + let(:noteable) { create(:work_item) } let(:note) { create(:note, noteable: noteable, project: noteable.project, note: original_body) } it_behaves_like 'a Note mutation updates a note successfully' diff --git a/spec/requests/api/graphql/mutations/snippets/destroy_spec.rb b/spec/requests/api/graphql/mutations/snippets/destroy_spec.rb index 09e884d9412..c3f818b6627 100644 --- a/spec/requests/api/graphql/mutations/snippets/destroy_spec.rb +++ b/spec/requests/api/graphql/mutations/snippets/destroy_spec.rb @@ -53,7 +53,7 @@ RSpec.describe 'Destroying a Snippet', feature_category: :source_code_management let!(:snippet_gid) { project.to_gid.to_s } it 'returns an error' do - err_message = %Q["#{snippet_gid}" does not represent an instance of Snippet] + err_message = %["#{snippet_gid}" does not represent an instance of Snippet] post_graphql_mutation(mutation, current_user: current_user) diff --git a/spec/requests/api/graphql/mutations/work_items/update_spec.rb b/spec/requests/api/graphql/mutations/work_items/update_spec.rb index 60b5795ee9b..ea9516f256c 100644 --- a/spec/requests/api/graphql/mutations/work_items/update_spec.rb +++ b/spec/requests/api/graphql/mutations/work_items/update_spec.rb @@ -839,14 +839,14 @@ RSpec.describe 'Update a work item', feature_category: :team_planning do context 'when changing work item type' do let_it_be(:work_item) { create(:work_item, :task, project: project) } - let(:description) { "/type Issue" } + 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" } + 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 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 deleted file mode 100644 index 3417f9529bd..00000000000 --- a/spec/requests/api/graphql/project/alert_management/alert/metrics_dashboard_url_spec.rb +++ /dev/null @@ -1,85 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe 'getting Alert Management Alert Assignees', feature_category: :incident_management do - include GraphqlHelpers - - let_it_be(:project) { create(:project) } - let_it_be(:current_user) { create(:user) } - - let(:fields) do - <<~QUERY - nodes { - iid - metricsDashboardUrl - } - QUERY - end - - let(:graphql_query) do - graphql_query_for( - 'project', - { 'fullPath' => project.full_path }, - query_graphql_field('alertManagementAlerts', {}, fields) - ) - end - - let(:alerts) { graphql_data.dig('project', 'alertManagementAlerts', 'nodes') } - let(:first_alert) { alerts.first } - - before do - stub_feature_flags(remove_monitor_metrics: false) - project.add_developer(current_user) - end - - context 'with self-managed prometheus payload' do - include_context 'self-managed prometheus alert attributes' - - before do - create(:alert_management_alert, :prometheus, project: project, payload: payload) - end - - it 'includes the correct metrics dashboard url' do - post_graphql(graphql_query, current_user: current_user) - - 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 - include_context 'gitlab-managed prometheus alert attributes' - - before do - create(:alert_management_alert, :prometheus, project: project, payload: payload, prometheus_alert: prometheus_alert) - end - - it 'includes the correct metrics dashboard url' do - post_graphql(graphql_query, current_user: current_user) - - 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/alerts_spec.rb b/spec/requests/api/graphql/project/alert_management/alerts_spec.rb index 55d223daf27..7f586edd510 100644 --- a/spec/requests/api/graphql/project/alert_management/alerts_spec.rb +++ b/spec/requests/api/graphql/project/alert_management/alerts_spec.rb @@ -74,7 +74,6 @@ RSpec.describe 'getting Alert Management Alerts', feature_category: :incident_ma 'details' => { 'custom.alert' => 'payload', 'runbook' => 'runbook' }, 'createdAt' => triggered_alert.created_at.strftime('%Y-%m-%dT%H:%M:%SZ'), 'updatedAt' => triggered_alert.updated_at.strftime('%Y-%m-%dT%H:%M:%SZ'), - 'metricsDashboardUrl' => nil, 'detailsUrl' => triggered_alert.details_url, 'prometheusAlert' => nil, 'runbook' => 'runbook' diff --git a/spec/requests/api/graphql/project/incident_management/timeline_events_spec.rb b/spec/requests/api/graphql/project/incident_management/timeline_events_spec.rb index 7587b227d9f..dd383226e17 100644 --- a/spec/requests/api/graphql/project/incident_management/timeline_events_spec.rb +++ b/spec/requests/api/graphql/project/incident_management/timeline_events_spec.rb @@ -15,7 +15,7 @@ RSpec.describe 'getting incident timeline events', feature_category: :incident_m let_it_be(:promoted_from_note) { create(:note, project: project, noteable: incident) } let_it_be(:issue_url) { project_issue_url(private_project, issue) } let_it_be(:issue_ref) { "#{private_project.full_path}##{issue.iid}" } - let_it_be(:issue_link) { %Q(#{issue_url}) } + let_it_be(:issue_link) { %(#{issue_url}) } let_it_be(:timeline_event) do create( diff --git a/spec/requests/api/graphql/project/jobs_spec.rb b/spec/requests/api/graphql/project/jobs_spec.rb index aea6cad9e62..2c45c7e9b79 100644 --- a/spec/requests/api/graphql/project/jobs_spec.rb +++ b/spec/requests/api/graphql/project/jobs_spec.rb @@ -11,6 +11,9 @@ RSpec.describe 'Query.project.jobs', feature_category: :continuous_integration d create(:ci_pipeline, project: project, user: user) end + let!(:job1) { create(:ci_build, pipeline: pipeline, name: 'job 1') } + let!(:job2) { create(:ci_build, pipeline: pipeline, name: 'job 2') } + let(:query) do <<~QUERY { @@ -18,11 +21,6 @@ RSpec.describe 'Query.project.jobs', feature_category: :continuous_integration d jobs { nodes { name - previousStageJobsAndNeeds { - nodes { - name - } - } } } } @@ -30,27 +28,10 @@ RSpec.describe 'Query.project.jobs', feature_category: :continuous_integration d QUERY end - it 'does not generate N+1 queries', :request_store, :use_sql_query_cache do - build_stage = create(:ci_stage, position: 1, name: 'build', project: project, pipeline: pipeline) - test_stage = create(:ci_stage, position: 2, name: 'test', project: project, pipeline: pipeline) - create(:ci_build, pipeline: pipeline, name: 'docker 1 2', ci_stage: build_stage) - create(:ci_build, pipeline: pipeline, name: 'docker 2 2', ci_stage: build_stage) - create(:ci_build, pipeline: pipeline, name: 'rspec 1 2', ci_stage: test_stage) - test_job = create(:ci_build, pipeline: pipeline, name: 'rspec 2 2', ci_stage: test_stage) - create(:ci_build_need, build: test_job, name: 'docker 1 2') - + it 'fetches jobs' do post_graphql(query, current_user: user) + expect_graphql_errors_to_be_empty - control = ActiveRecord::QueryRecorder.new(skip_cached: false) do - post_graphql(query, current_user: user) - end - - create(:ci_build, name: 'test-a', ci_stage: test_stage, pipeline: pipeline) - test_b_job = create(:ci_build, name: 'test-b', ci_stage: test_stage, pipeline: pipeline) - create(:ci_build_need, build: test_b_job, name: 'docker 2 2') - - expect do - post_graphql(query, current_user: user) - end.not_to exceed_all_query_limit(control) + expect(graphql_data['project']['jobs']['nodes'].pluck('name')).to contain_exactly('job 1', 'job 2') end end diff --git a/spec/requests/api/graphql/project/pipeline_spec.rb b/spec/requests/api/graphql/project/pipeline_spec.rb index fb1489372fc..d20ee5bfdff 100644 --- a/spec/requests/api/graphql/project/pipeline_spec.rb +++ b/spec/requests/api/graphql/project/pipeline_spec.rb @@ -337,16 +337,33 @@ RSpec.describe 'getting pipeline information nested in a project', feature_categ end end - context 'N+1 queries on pipeline jobs' do + context 'N+1 queries on pipeline jobs.previousStageJobsOrNeeds' do let(:pipeline) { create(:ci_pipeline, project: project) } let(:fields) do <<~FIELDS - jobs { + stages { nodes { - previousStageJobsAndNeeds { + groups { nodes { - name + jobs { + nodes { + previousStageJobsOrNeeds { + nodes { + ... on JobNeedUnion { + ... on CiBuildNeed { + id + name + } + ... on CiJob { + id + name + } + } + } + } + } + } } } } @@ -357,13 +374,15 @@ RSpec.describe 'getting pipeline information nested in a project', feature_categ it 'does not generate N+1 queries', :request_store, :use_sql_query_cache do build_stage = create(:ci_stage, position: 1, name: 'build', project: project, pipeline: pipeline) test_stage = create(:ci_stage, position: 2, name: 'test', project: project, pipeline: pipeline) - create(:ci_build, pipeline: pipeline, name: 'docker 1 2', ci_stage: build_stage) + + docker_1_2 = create(:ci_build, pipeline: pipeline, name: 'docker 1 2', ci_stage: build_stage) create(:ci_build, pipeline: pipeline, name: 'docker 2 2', ci_stage: build_stage) create(:ci_build, pipeline: pipeline, name: 'rspec 1 2', ci_stage: test_stage) - test_job = create(:ci_build, pipeline: pipeline, name: 'rspec 2 2', ci_stage: test_stage) - create(:ci_build_need, build: test_job, name: 'docker 1 2') + create(:ci_build, :dependent, needed: docker_1_2, pipeline: pipeline, name: 'rspec 2 2', ci_stage: test_stage) + # warm up post_graphql(query, current_user: current_user) + expect_graphql_errors_to_be_empty control = ActiveRecord::QueryRecorder.new(skip_cached: false) do post_graphql(query, current_user: current_user) @@ -371,7 +390,7 @@ RSpec.describe 'getting pipeline information nested in a project', feature_categ create(:ci_build, name: 'test-a', ci_stage: test_stage, pipeline: pipeline) test_b_job = create(:ci_build, name: 'test-b', ci_stage: test_stage, pipeline: pipeline) - create(:ci_build_need, build: test_b_job, name: 'docker 2 2') + create(:ci_build, :dependent, needed: test_b_job, pipeline: pipeline, name: 'docker 2 2', ci_stage: test_stage) expect do post_graphql(query, current_user: current_user) @@ -424,6 +443,7 @@ RSpec.describe 'getting pipeline information nested in a project', feature_categ # warm up post_graphql(query, current_user: current_user) + expect_graphql_errors_to_be_empty control = ActiveRecord::QueryRecorder.new(skip_cached: false) do post_graphql(query, current_user: current_user) diff --git a/spec/requests/api/graphql/user_query_spec.rb b/spec/requests/api/graphql/user_query_spec.rb index ca319ed1b2e..440eb2f52be 100644 --- a/spec/requests/api/graphql/user_query_spec.rb +++ b/spec/requests/api/graphql/user_query_spec.rb @@ -503,4 +503,51 @@ RSpec.describe 'getting user information', feature_category: :user_management do end end end + + context 'authored merge requests' do + let_it_be(:current_user) { create(:user) } + let_it_be(:group) { create(:group) } + let_it_be(:subgroup) { create(:group, parent: group) } + let_it_be(:project) { create(:project, :public, group: group) } + let_it_be(:merge_request1) do + create(:merge_request, source_project: project, source_branch: '1', author: current_user) + end + + let_it_be(:merge_request2) do + create(:merge_request, source_project: project, source_branch: '2', author: current_user) + end + + let_it_be(:merge_request_different_user) do + create(:merge_request, source_project: project, source_branch: '3', author: create(:user)) + end + + let_it_be(:merge_request_different_group) do + create(:merge_request, source_project: create(:project, :public), author: current_user) + end + + let_it_be(:merge_request_subgroup) do + create(:merge_request, source_project: create(:project, :public, group: subgroup), author: current_user) + end + + let(:query) do + %( + query { + currentUser { + authoredMergeRequests(groupId: "#{group.to_global_id}") { + nodes { + id + } + } + } + } + ) + end + + it 'returns merge requests for the current user for the specified group' do + post_graphql(query, current_user: current_user) + + expect(graphql_data_at(:current_user, :authored_merge_requests, :nodes).pluck('id')).to contain_exactly( + merge_request1.to_global_id.to_s, merge_request2.to_global_id.to_s, merge_request_subgroup.to_global_id.to_s) + end + end end diff --git a/spec/requests/api/group_clusters_spec.rb b/spec/requests/api/group_clusters_spec.rb index 58d0e6a1eb5..7c194627f82 100644 --- a/spec/requests/api/group_clusters_spec.rb +++ b/spec/requests/api/group_clusters_spec.rb @@ -453,7 +453,7 @@ RSpec.describe API::GroupClusters, feature_category: :deployment_management do end it 'returns validation error' do - expect(json_response['message']['platform_kubernetes.base'].first).to eq(_('Cannot modify managed Kubernetes cluster')) + expect(json_response['message']['platform_kubernetes'].first).to eq(_('Cannot modify managed Kubernetes cluster')) end end diff --git a/spec/requests/api/group_export_spec.rb b/spec/requests/api/group_export_spec.rb index 9dd5fe6f7c4..b4add2494b0 100644 --- a/spec/requests/api/group_export_spec.rb +++ b/spec/requests/api/group_export_spec.rb @@ -168,8 +168,9 @@ RSpec.describe API::GroupExport, feature_category: :importers do end describe 'relations export' do + let(:relation) { 'labels' } let(:path) { "/groups/#{group.id}/export_relations" } - let(:download_path) { "/groups/#{group.id}/export_relations/download?relation=labels" } + let(:download_path) { "/groups/#{group.id}/export_relations/download?relation=#{relation}" } let(:status_path) { "/groups/#{group.id}/export_relations/status" } before do @@ -196,46 +197,131 @@ RSpec.describe API::GroupExport, feature_category: :importers do expect(response).to have_gitlab_http_status(:error) end end + + context 'when request is to export in batches' do + it 'accepts the request' do + expect(BulkImports::ExportService) + .to receive(:new) + .with(portable: group, user: user, batched: true) + .and_call_original + + post api(path, user), params: { batched: true } + + expect(response).to have_gitlab_http_status(:accepted) + end + end end describe 'GET /groups/:id/export_relations/download' do - let(:export) { create(:bulk_import_export, group: group, relation: 'labels') } - let(:upload) { create(:bulk_import_export_upload, export: export) } + context 'when export request is not batched' do + let(:export) { create(:bulk_import_export, group: group, relation: 'labels') } + let(:upload) { create(:bulk_import_export_upload, export: export) } + + context 'when export file exists' do + it 'downloads exported group archive' do + upload.update!(export_file: fixture_file_upload('spec/fixtures/bulk_imports/gz/labels.ndjson.gz')) + + get api(download_path, user) + + expect(response).to have_gitlab_http_status(:ok) + end + end + + context 'when export_file.file does not exist' do + it 'returns 404' do + allow(export).to receive(:upload).and_return(nil) + + get api(download_path, user) + + expect(response).to have_gitlab_http_status(:not_found) + expect(json_response['message']).to eq('Export file not found') + end + end + + context 'when export is batched' do + let(:relation) { 'milestones' } + + let_it_be(:export) { create(:bulk_import_export, :batched, group: group, relation: 'milestones') } + + it 'returns 400' do + export.update!(batched: true) + + get api(download_path, user) + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['message']).to eq('Export is batched') + end + end + end - context 'when export file exists' do - it 'downloads exported group archive' do + context 'when export request is batched' do + let(:export) { create(:bulk_import_export, :batched, group: group, relation: 'labels') } + let(:upload) { create(:bulk_import_export_upload) } + let!(:batch) { create(:bulk_import_export_batch, export: export, upload: upload) } + + it 'downloads exported batch' do upload.update!(export_file: fixture_file_upload('spec/fixtures/bulk_imports/gz/labels.ndjson.gz')) - get api(download_path, user) + get api(download_path, user), params: { batched: true, batch_number: batch.batch_number } expect(response).to have_gitlab_http_status(:ok) + expect(response.header['Content-Disposition']) + .to eq("attachment; filename=\"labels.ndjson.gz\"; filename*=UTF-8''labels.ndjson.gz") end - end - context 'when export_file.file does not exist' do - it 'returns 404' do - allow(export).to receive(:upload).and_return(nil) + context 'when request is to download not batched export' do + it 'returns 400' do + get api(download_path, user) - get api(download_path, user) + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['message']).to eq('Export is batched') + end + end - expect(response).to have_gitlab_http_status(:not_found) - expect(json_response['message']).to eq('404 Not found') + context 'when batch cannot be found' do + it 'returns 404' do + get api(download_path, user), params: { batched: true, batch_number: 0 } + + expect(response).to have_gitlab_http_status(:not_found) + expect(json_response['message']).to eq('Batch not found') + end + end + + context 'when batch file cannot be found' do + it 'returns 404' do + batch.upload.destroy! + + get api(download_path, user), params: { batched: true, batch_number: batch.batch_number } + + expect(response).to have_gitlab_http_status(:not_found) + expect(json_response['message']).to eq('Batch file not found') + end end end end describe 'GET /groups/:id/export_relations/status' do - it 'returns a list of relation export statuses' do - create(:bulk_import_export, :started, group: group, relation: 'labels') - create(:bulk_import_export, :finished, group: group, relation: 'milestones') - create(:bulk_import_export, :failed, group: group, relation: 'badges') + let_it_be(:started_export) { create(:bulk_import_export, :started, group: group, relation: 'labels') } + let_it_be(:finished_export) { create(:bulk_import_export, :finished, group: group, relation: 'milestones') } + let_it_be(:failed_export) { create(:bulk_import_export, :failed, group: group, relation: 'badges') } + it 'returns a list of relation export statuses' do get api(status_path, user) expect(response).to have_gitlab_http_status(:ok) expect(json_response.pluck('relation')).to contain_exactly('labels', 'milestones', 'badges') expect(json_response.pluck('status')).to contain_exactly(-1, 0, 1) end + + context 'when relation is specified' do + it 'return a single relation export status' do + get api(status_path, user), params: { relation: 'labels' } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['relation']).to eq('labels') + expect(json_response['status']).to eq(0) + end + end end context 'when bulk import is disabled' do diff --git a/spec/requests/api/group_variables_spec.rb b/spec/requests/api/group_variables_spec.rb index 6849b087211..d0edc181b65 100644 --- a/spec/requests/api/group_variables_spec.rb +++ b/spec/requests/api/group_variables_spec.rb @@ -56,6 +56,7 @@ RSpec.describe API::GroupVariables, feature_category: :secrets_management do expect(json_response['protected']).to eq(variable.protected?) expect(json_response['variable_type']).to eq(variable.variable_type) expect(json_response['environment_scope']).to eq(variable.environment_scope) + expect(json_response['description']).to be_nil end it 'responds with 404 Not Found if requesting non-existing variable' do @@ -115,7 +116,7 @@ RSpec.describe API::GroupVariables, feature_category: :secrets_management do it 'creates variable with optional attributes' do expect do - post api("/groups/#{group.id}/variables", user), params: { variable_type: 'file', key: 'TEST_VARIABLE_2', value: 'VALUE_2' } + post api("/groups/#{group.id}/variables", user), params: { variable_type: 'file', key: 'TEST_VARIABLE_2', value: 'VALUE_2', description: 'description' } end.to change { group.variables.count }.by(1) expect(response).to have_gitlab_http_status(:created) @@ -126,6 +127,7 @@ RSpec.describe API::GroupVariables, feature_category: :secrets_management do expect(json_response['raw']).to be_falsey expect(json_response['variable_type']).to eq('file') expect(json_response['environment_scope']).to eq('*') + expect(json_response['description']).to eq('description') end it 'does not allow to duplicate variable key' do @@ -182,7 +184,7 @@ RSpec.describe API::GroupVariables, feature_category: :secrets_management do initial_variable = group.variables.reload.first value_before = initial_variable.value - put api("/groups/#{group.id}/variables/#{variable.key}", user), params: { variable_type: 'file', value: 'VALUE_1_UP', protected: true, masked: true, raw: true } + put api("/groups/#{group.id}/variables/#{variable.key}", user), params: { variable_type: 'file', value: 'VALUE_1_UP', protected: true, masked: true, raw: true, description: 'updated' } updated_variable = group.variables.reload.first @@ -193,6 +195,7 @@ RSpec.describe API::GroupVariables, feature_category: :secrets_management do expect(json_response['variable_type']).to eq('file') expect(json_response['masked']).to be_truthy expect(json_response['raw']).to be_truthy + expect(json_response['description']).to eq('updated') end it 'masks the new value when logging' do diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index 2adf71f2a18..5296a8b3e93 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -270,29 +270,17 @@ RSpec.describe API::Groups, feature_category: :groups_and_projects do end it "includes statistics if requested", :aggregate_failures do - attributes = { - storage_size: 4093, - repository_size: 123, - wiki_size: 456, - lfs_objects_size: 234, - build_artifacts_size: 345, - pipeline_artifacts_size: 456, - packages_size: 567, - snippets_size: 1234, - uploads_size: 678 - }.stringify_keys - exposed_attributes = attributes.dup - exposed_attributes['job_artifacts_size'] = exposed_attributes.delete('build_artifacts_size') - - project1.statistics.update!(attributes) + stat_keys = %w[storage_size repository_size wiki_size + lfs_objects_size job_artifacts_size pipeline_artifacts_size + packages_size snippets_size uploads_size] get api("/groups", admin, admin_mode: true), params: { statistics: true } expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers expect(json_response).to be_an Array - expect(json_response) - .to satisfy_one { |group| group['statistics'] == exposed_attributes } + + expect(json_response[0]["statistics"].keys).to match_array(stat_keys) end end @@ -856,6 +844,39 @@ RSpec.describe API::Groups, feature_category: :groups_and_projects do expect(shared_with_groups).to contain_exactly(group_link_1.shared_with_group_id, group_link_2.shared_with_group_id) end end + + context "expose shared_runners_setting attribute" do + let(:group) { create(:group, shared_runners_enabled: true) } + + before do + group.add_owner(user1) + end + + it "returns the group with shared_runners_setting as 'enabled'", :aggregate_failures do + get api("/groups/#{group.id}", user1) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['shared_runners_setting']).to eq("enabled") + end + + it "returns the group with shared_runners_setting as 'disabled_and_unoverridable'", :aggregate_failures do + group.update!(shared_runners_enabled: false, allow_descendants_override_disabled_shared_runners: false) + + get api("/groups/#{group.id}", user1) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['shared_runners_setting']).to eq("disabled_and_unoverridable") + end + + it "returns the group with shared_runners_setting as 'disabled_and_overridable'", :aggregate_failures do + group.update!(shared_runners_enabled: false, allow_descendants_override_disabled_shared_runners: true) + + get api("/groups/#{group.id}", user1) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['shared_runners_setting']).to eq("disabled_and_overridable") + end + end end end @@ -1070,6 +1091,50 @@ RSpec.describe API::Groups, feature_category: :groups_and_projects do end end + context 'with owned' do + let_it_be(:group) { create(:group) } + + let_it_be(:project1) { create(:project, group: group) } + let_it_be(:project1_guest) { create(:user) } + let_it_be(:project1_owner) { create(:user) } + let_it_be(:project1_maintainer) { create(:user) } + + let_it_be(:project2) { create(:project, group: group) } + + before do + project1.add_guest(project1_guest) + project1.add_owner(project1_owner) + project1.add_maintainer(project1_maintainer) + + project2_owner = project1_owner + project2.add_owner(project2_owner) + end + + context "as a guest" do + it 'returns no projects' do + get api("/groups/#{group.id}/projects", project1_guest), params: { owned: true } + project_ids = json_response.map { |proj| proj['id'] } + expect(project_ids).to match_array([]) + end + end + + context "as a maintainer" do + it 'returns no projects' do + get api("/groups/#{group.id}/projects", project1_maintainer), params: { owned: true } + project_ids = json_response.map { |proj| proj['id'] } + expect(project_ids).to match_array([]) + end + end + + context "as an owner" do + it 'returns projects with owner access level' do + get api("/groups/#{group.id}/projects", project1_owner), params: { owned: true } + project_ids = json_response.map { |proj| proj['id'] } + expect(project_ids).to match_array([project1.id, project2.id]) + end + end + end + it "returns the group's projects", :aggregate_failures do get api("/groups/#{group1.id}/projects", user1) diff --git a/spec/requests/api/helpers_spec.rb b/spec/requests/api/helpers_spec.rb index 0be9df41e8f..7304437bc42 100644 --- a/spec/requests/api/helpers_spec.rb +++ b/spec/requests/api/helpers_spec.rb @@ -32,6 +32,9 @@ RSpec.describe API::Helpers, :enable_admin_mode, feature_category: :system_acces before do allow_any_instance_of(self.class).to receive(:options).and_return({}) + + allow(env['rack.session']).to receive(:enabled?).and_return(true) + allow(env['rack.session']).to receive(:loaded?).and_return(true) end def warden_authenticate_returns(value) @@ -567,6 +570,9 @@ RSpec.describe API::Helpers, :enable_admin_mode, feature_category: :system_acces context 'using warden authentication' do before do + allow(session).to receive(:enabled?).and_return(true) + allow(session).to receive(:loaded?).and_return(true) + warden_authenticate_returns admin env[API::Helpers::SUDO_HEADER] = user.username end diff --git a/spec/requests/api/import_github_spec.rb b/spec/requests/api/import_github_spec.rb index 9b5ae72526c..e394b92c0a2 100644 --- a/spec/requests/api/import_github_spec.rb +++ b/spec/requests/api/import_github_spec.rb @@ -5,7 +5,8 @@ require 'spec_helper' RSpec.describe API::ImportGithub, feature_category: :importers do let(:token) { "asdasd12345" } let(:provider) { :github } - let(:access_params) { { github_access_token: token } } + let(:access_params) { { github_access_token: token, additional_access_tokens: additional_access_tokens } } + let(:additional_access_tokens) { nil } let(:provider_username) { user.username } let(:provider_user) { double('provider', login: provider_username).as_null_object } let(:provider_repo) do @@ -51,7 +52,7 @@ RSpec.describe API::ImportGithub, feature_category: :importers do it 'returns 201 response when the project is imported successfully' do allow(Gitlab::LegacyGithubImport::ProjectCreator) .to receive(:new).with(provider_repo, provider_repo[:name], user.namespace, user, type: provider, **access_params) - .and_return(double(execute: project)) + .and_return(double(execute: project)) post api("/import/github", user), params: { target_namespace: user.namespace_path, @@ -120,6 +121,28 @@ RSpec.describe API::ImportGithub, feature_category: :importers do expect(response).to have_gitlab_http_status(:unauthorized) end end + + context 'when additional access tokens are provided' do + let(:additional_access_tokens) { 'token1,token2' } + + it 'returns 201' do + expected_access_params = { github_access_token: token, additional_access_tokens: %w[token1 token2] } + + expect(Gitlab::LegacyGithubImport::ProjectCreator) + .to receive(:new) + .with(provider_repo, provider_repo[:name], user.namespace, user, type: provider, **expected_access_params) + .and_return(double(execute: project)) + + post api("/import/github", user), params: { + target_namespace: user.namespace_path, + personal_access_token: token, + repo_id: non_existing_record_id, + additional_access_tokens: 'token1,token2' + } + + expect(response).to have_gitlab_http_status(:created) + end + end end describe "POST /import/github/cancel" do diff --git a/spec/requests/api/internal/kubernetes_spec.rb b/spec/requests/api/internal/kubernetes_spec.rb index 3c76fba4e2c..09170ca952f 100644 --- a/spec/requests/api/internal/kubernetes_spec.rb +++ b/spec/requests/api/internal/kubernetes_spec.rb @@ -122,14 +122,28 @@ RSpec.describe API::Internal::Kubernetes, feature_category: :deployment_manageme it 'tracks events and unique events', :aggregate_failures do request_count = 2 - counters = { gitops_sync: 10, k8s_api_proxy_request: 5, flux_git_push_notifications_total: 42 } - unique_counters = { agent_users_using_ci_tunnel: [10, 999, 777, 10] } + counters = { + gitops_sync: 10, + k8s_api_proxy_request: 5, + flux_git_push_notifications_total: 42, + k8s_api_proxy_requests_via_ci_access: 43, + k8s_api_proxy_requests_via_user_access: 44 + } + unique_counters = { + agent_users_using_ci_tunnel: [10, 999, 777, 10], + k8s_api_proxy_requests_unique_users_via_ci_access: [10, 999, 777, 10], + k8s_api_proxy_requests_unique_agents_via_ci_access: [10, 999, 777, 10], + k8s_api_proxy_requests_unique_users_via_user_access: [10, 999, 777, 10], + k8s_api_proxy_requests_unique_agents_via_user_access: [10, 999, 777, 10], + flux_git_push_notified_unique_projects: [10, 999, 777, 10] + } expected_counters = { kubernetes_agent_gitops_sync: request_count * counters[:gitops_sync], kubernetes_agent_k8s_api_proxy_request: request_count * counters[:k8s_api_proxy_request], - kubernetes_agent_flux_git_push_notifications_total: request_count * counters[:flux_git_push_notifications_total] + kubernetes_agent_flux_git_push_notifications_total: request_count * counters[:flux_git_push_notifications_total], + kubernetes_agent_k8s_api_proxy_requests_via_ci_access: request_count * counters[:k8s_api_proxy_requests_via_ci_access], + kubernetes_agent_k8s_api_proxy_requests_via_user_access: request_count * counters[:k8s_api_proxy_requests_via_user_access] } - expected_hll_count = unique_counters[:agent_users_using_ci_tunnel].uniq.count request_count.times do send_request(params: { counters: counters, unique_counters: unique_counters }) @@ -137,13 +151,15 @@ RSpec.describe API::Internal::Kubernetes, feature_category: :deployment_manageme expect(Gitlab::UsageDataCounters::KubernetesAgentCounter.totals).to eq(expected_counters) - expect( - Gitlab::UsageDataCounters::HLLRedisCounter - .unique_events( - event_names: 'agent_users_using_ci_tunnel', - start_date: Date.current, end_date: Date.current + 10 - ) - ).to eq(expected_hll_count) + unique_counters.each do |c, xs| + expect( + Gitlab::UsageDataCounters::HLLRedisCounter + .unique_events( + event_names: c.to_s, + start_date: Date.current, end_date: Date.current + 10 + ) + ).to eq(xs.uniq.count) + end end end end @@ -231,7 +247,7 @@ RSpec.describe API::Internal::Kubernetes, feature_category: :deployment_manageme 'gitaly_info' => a_hash_including( 'address' => match(/\.socket$/), 'token' => 'secret', - 'features' => {} + 'features' => Feature::Gitaly.server_feature_flags ), 'gitaly_repository' => a_hash_including( 'storage_name' => project.repository_storage, @@ -274,7 +290,7 @@ RSpec.describe API::Internal::Kubernetes, feature_category: :deployment_manageme 'gitaly_info' => a_hash_including( 'address' => match(/\.socket$/), 'token' => 'secret', - 'features' => {} + 'features' => Feature::Gitaly.server_feature_flags ), 'gitaly_repository' => a_hash_including( 'storage_name' => project.repository_storage, @@ -531,18 +547,6 @@ RSpec.describe API::Internal::Kubernetes, feature_category: :deployment_manageme expect(response).to have_gitlab_http_status(:forbidden) end - it 'returns 401 when global flag is disabled' do - stub_feature_flags(kas_user_access: false) - - deployment_project.add_member(user, :developer) - token = new_token - public_id = stub_user_session(user, token) - access_key = Gitlab::Kas::UserAccess.encrypt_public_session_id(public_id) - send_request(params: { agent_id: agent.id, access_type: 'session_cookie', access_key: access_key, csrf_token: mask_token(token) }) - - expect(response).to have_gitlab_http_status(:unauthorized) - end - it 'returns 401 when user id is not found in session' do deployment_project.add_member(user, :developer) token = new_token diff --git a/spec/requests/api/lint_spec.rb b/spec/requests/api/lint_spec.rb index 05a9d98a9d0..7fe17760220 100644 --- a/spec/requests/api/lint_spec.rb +++ b/spec/requests/api/lint_spec.rb @@ -3,16 +3,6 @@ require 'spec_helper' RSpec.describe API::Lint, feature_category: :pipeline_composition do - describe 'POST /ci/lint' do - it 'responds with a 410' do - user = create(:user) - - post api('/ci/lint', user), params: { content: "test_job:\n script: ls" } - - expect(response).to have_gitlab_http_status(:gone) - end - end - describe 'GET /projects/:id/ci/lint' do subject(:ci_lint) { get api("/projects/#{project.id}/ci/lint", api_user), params: { dry_run: dry_run, include_jobs: include_jobs } } diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 50e70a9dc0f..f4cac0854e7 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -2,7 +2,8 @@ require "spec_helper" -RSpec.describe API::MergeRequests, :aggregate_failures, feature_category: :source_code_management do +RSpec.describe API::MergeRequests, :aggregate_failures, feature_category: :source_code_management, + quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/418757' do include ProjectForksHelper let_it_be(:base_time) { Time.now } @@ -60,13 +61,14 @@ RSpec.describe API::MergeRequests, :aggregate_failures, feature_category: :sourc end context 'with merge status recheck projection' do - it 'does not enqueue a merge status recheck' do + it 'does not check mergeability', :sidekiq_inline do expect(check_service_class).not_to receive(:new) - get(api(endpoint_path), params: { with_merge_status_recheck: true }) + get(api(endpoint_path, user2), params: { with_merge_status_recheck: true }) expect_successful_response_with_paginated_array expect(mr_entity['merge_status']).to eq('unchecked') + expect(merge_request.reload.merge_status).to eq('unchecked') end end end @@ -112,16 +114,32 @@ RSpec.describe API::MergeRequests, :aggregate_failures, feature_category: :sourc end context 'with merge status recheck projection' do - it 'checks mergeability asynchronously' do - expect_next_instances_of(check_service_class, (1..2)) do |service| - expect(service).not_to receive(:execute) - expect(service).to receive(:async_execute).and_call_original + context 'with batched_api_mergeability_checks FF on' do + it 'checks mergeability asynchronously in batch', :sidekiq_inline do + get(api(endpoint_path, user2), params: { with_merge_status_recheck: true }) + + expect_successful_response_with_paginated_array + + expect(merge_request.reload.merge_status).to eq('can_be_merged') end + end - get(api(endpoint_path, user2), params: { with_merge_status_recheck: true }) + context 'with batched_api_mergeability_checks FF off' do + before do + stub_feature_flags(batched_api_mergeability_checks: false) + end - expect_successful_response_with_paginated_array - expect(mr_entity['merge_status']).to eq('checking') + it 'checks mergeability asynchronously' do + expect_next_instances_of(check_service_class, (1..2)) do |service| + expect(service).not_to receive(:execute) + expect(service).to receive(:async_execute).and_call_original + end + + get(api(endpoint_path, user2), params: { with_merge_status_recheck: true }) + + expect_successful_response_with_paginated_array + expect(mr_entity['merge_status']).to eq('checking') + end end end @@ -139,13 +157,14 @@ RSpec.describe API::MergeRequests, :aggregate_failures, feature_category: :sourc context 'with a reporter role' do context 'with merge status recheck projection' do - it 'does not enqueue a merge status recheck' do + it 'does not check mergeability', :sidekiq_inline do expect(check_service_class).not_to receive(:new) get(api(endpoint_path, user2), params: { with_merge_status_recheck: true }) expect_successful_response_with_paginated_array expect(mr_entity['merge_status']).to eq('unchecked') + expect(merge_request.reload.merge_status).to eq('unchecked') end end @@ -154,17 +173,33 @@ RSpec.describe API::MergeRequests, :aggregate_failures, feature_category: :sourc stub_feature_flags(restrict_merge_status_recheck: false) end - context 'with merge status recheck projection' do - it 'does enqueue a merge status recheck' do - expect_next_instances_of(check_service_class, (1..2)) do |service| - expect(service).not_to receive(:execute) - expect(service).to receive(:async_execute).and_call_original - end - + context 'with batched_api_mergeability_checks FF on' do + it 'checks mergeability asynchronously in batch', :sidekiq_inline do get(api(endpoint_path, user2), params: { with_merge_status_recheck: true }) expect_successful_response_with_paginated_array - expect(mr_entity['merge_status']).to eq('checking') + + expect(merge_request.reload.merge_status).to eq('can_be_merged') + end + end + + context 'with batched_api_mergeability_checks FF off' do + before do + stub_feature_flags(batched_api_mergeability_checks: false) + end + + context 'with merge status recheck projection' do + it 'does enqueue a merge status recheck' do + expect_next_instances_of(check_service_class, (1..2)) do |service| + expect(service).not_to receive(:execute) + expect(service).to receive(:async_execute).and_call_original + end + + get(api(endpoint_path, user2), params: { with_merge_status_recheck: true }) + + expect_successful_response_with_paginated_array + expect(mr_entity['merge_status']).to eq('checking') + end end end end diff --git a/spec/requests/api/ml_model_packages_spec.rb b/spec/requests/api/ml_model_packages_spec.rb index 9c19f522e46..3166298b430 100644 --- a/spec/requests/api/ml_model_packages_spec.rb +++ b/spec/requests/api/ml_model_packages_spec.rb @@ -75,6 +75,48 @@ RSpec.describe ::API::MlModelPackages, feature_category: :mlops do :private | :developer | true | :deploy_token | true | :success :private | :developer | true | :deploy_token | false | :unauthorized end + + # :visibility, :user_role, :member, :token_type, :valid_token, :expected_status + def download_permissions_tables + :public | :developer | true | :personal_access_token | true | :success + :public | :guest | true | :personal_access_token | true | :success + :public | :developer | true | :personal_access_token | false | :unauthorized + :public | :guest | true | :personal_access_token | false | :unauthorized + :public | :developer | false | :personal_access_token | true | :success + :public | :guest | false | :personal_access_token | true | :success + :public | :developer | false | :personal_access_token | false | :unauthorized + :public | :guest | false | :personal_access_token | false | :unauthorized + :public | :anonymous | false | :personal_access_token | true | :success + :private | :developer | true | :personal_access_token | true | :success + :private | :guest | true | :personal_access_token | true | :forbidden + :private | :developer | true | :personal_access_token | false | :unauthorized + :private | :guest | true | :personal_access_token | false | :unauthorized + :private | :developer | false | :personal_access_token | true | :not_found + :private | :guest | false | :personal_access_token | true | :not_found + :private | :developer | false | :personal_access_token | false | :unauthorized + :private | :guest | false | :personal_access_token | false | :unauthorized + :private | :anonymous | false | :personal_access_token | true | :not_found + :public | :developer | true | :job_token | true | :success + :public | :guest | true | :job_token | true | :success + :public | :developer | true | :job_token | false | :unauthorized + :public | :guest | true | :job_token | false | :unauthorized + :public | :developer | false | :job_token | true | :success + :public | :guest | false | :job_token | true | :success + :public | :developer | false | :job_token | false | :unauthorized + :public | :guest | false | :job_token | false | :unauthorized + :private | :developer | true | :job_token | true | :success + :private | :guest | true | :job_token | true | :forbidden + :private | :developer | true | :job_token | false | :unauthorized + :private | :guest | true | :job_token | false | :unauthorized + :private | :developer | false | :job_token | true | :not_found + :private | :guest | false | :job_token | true | :not_found + :private | :developer | false | :job_token | false | :unauthorized + :private | :guest | false | :job_token | false | :unauthorized + :public | :developer | true | :deploy_token | true | :success + :public | :developer | true | :deploy_token | false | :unauthorized + :private | :developer | true | :deploy_token | true | :success + :private | :developer | true | :deploy_token | false | :unauthorized + end # rubocop:enable Metrics/AbcSize end @@ -82,18 +124,23 @@ RSpec.describe ::API::MlModelPackages, feature_category: :mlops do project.send("add_#{user_role}", user) if member && user_role != :anonymous end - subject(:api_response) do - request - response - end - - describe 'PUT /api/v4/projects/:id/packages/ml_models/:package_name/:package_version/:file_name/authorize' do + describe 'PUT /api/v4/projects/:id/packages/ml_models/:model_name/:model_version/:file_name/authorize' do include_context 'ml model authorize permissions table' let(:token) { tokens[:personal_access_token] } let(:user_headers) { { 'HTTP_AUTHORIZATION' => token } } let(:headers) { user_headers.merge(workhorse_headers) } let(:request) { authorize_upload_file(headers) } + let(:model_name) { 'my_package' } + let(:file_name) { 'myfile.tar.gz' } + + subject(:api_response) do + url = "/projects/#{project.id}/packages/ml_models/#{model_name}/0.0.1/#{file_name}/authorize" + + put api(url), headers: headers + + response + end describe 'user access' do where(:visibility, :user_role, :member, :token_type, :valid_token, :expected_status) do @@ -115,16 +162,14 @@ RSpec.describe ::API::MlModelPackages, feature_category: :mlops do end describe 'application security' do - where(:param_name, :param_value) do - :package_name | 'my-package/../' - :package_name | 'my-package%2f%2e%2e%2f' - :file_name | '../.ssh%2fauthorized_keys' - :file_name | '%2e%2e%2f.ssh%2fauthorized_keys' + where(:model_name, :file_name) do + 'my-package/../' | 'myfile.tar.gz' + 'my-package%2f%2e%2e%2f' | 'myfile.tar.gz' + 'my_package' | '../.ssh%2fauthorized_keys' + 'my_package' | '%2e%2e%2f.ssh%2fauthorized_keys' end with_them do - let(:request) { authorize_upload_file(headers, param_name => param_value) } - it 'rejects malicious request' do is_expected.to have_gitlab_http_status(:bad_request) end @@ -132,7 +177,7 @@ RSpec.describe ::API::MlModelPackages, feature_category: :mlops do end end - describe 'PUT /api/v4/projects/:id/packages/ml_models/:package_name/:package_version/:file_name' do + describe 'PUT /api/v4/projects/:id/packages/ml_models/:model_name/:model_version/:file_name' do include_context 'ml model authorize permissions table' let_it_be(:file_name) { 'model.md5' } @@ -143,9 +188,21 @@ RSpec.describe ::API::MlModelPackages, feature_category: :mlops do let(:params) { { file: temp_file(file_name) } } let(:file_key) { :file } let(:send_rewritten_field) { true } + let(:model_name) { 'my_package' } + + subject(:api_response) do + url = "/projects/#{project.id}/packages/ml_models/#{model_name}/0.0.1/#{file_name}" - let(:request) do - upload_file(headers) + workhorse_finalize( + api(url), + method: :put, + file_key: file_key, + params: params, + headers: headers, + send_rewritten_field: send_rewritten_field + ) + + response end describe 'success' do @@ -179,22 +236,49 @@ RSpec.describe ::API::MlModelPackages, feature_category: :mlops do end end - def authorize_upload_file(request_headers, package_name: 'mypackage', file_name: 'myfile.tar.gz') - url = "/projects/#{project.id}/packages/ml_models/#{package_name}/0.0.1/#{file_name}/authorize" + describe 'GET /api/v4/projects/:project_id/packages/ml_models/:model_name/:model_version/:file_name' do + include_context 'ml model authorize permissions table' - put api(url), headers: request_headers - end + let_it_be(:package) { create(:ml_model_package, project: project, name: 'model', version: '0.0.1') } + let_it_be(:package_file) { create(:package_file, :generic, package: package, file_name: 'model.md5') } - def upload_file(request_headers, package_name: 'mypackage') - url = "/projects/#{project.id}/packages/ml_models/#{package_name}/0.0.1/#{file_name}" - - workhorse_finalize( - api(url), - method: :put, - file_key: file_key, - params: params, - headers: request_headers, - send_rewritten_field: send_rewritten_field - ) + let(:model_name) { package.name } + let(:model_version) { package.version } + let(:file_name) { package_file.file_name } + + let(:token) { tokens[:personal_access_token] } + let(:user_headers) { { 'HTTP_AUTHORIZATION' => token } } + let(:headers) { user_headers.merge(workhorse_headers) } + + subject(:api_response) do + url = "/projects/#{project.id}/packages/ml_models/#{model_name}/#{model_version}/#{file_name}" + + get api(url), headers: headers + + response + end + + describe 'user access' do + where(:visibility, :user_role, :member, :token_type, :valid_token, :expected_status) do + download_permissions_tables + end + + with_them do + let(:token) { valid_token ? tokens[token_type] : 'invalid-token123' } + let(:user_headers) { user_role == :anonymous ? {} : { 'HTTP_AUTHORIZATION' => token } } + + before do + project.update_column(:visibility_level, Gitlab::VisibilityLevel.level_value(visibility.to_s)) + end + + if params[:expected_status] == :success + it_behaves_like 'process ml model package download' + else + it { is_expected.to have_gitlab_http_status(expected_status) } + end + end + + it_behaves_like 'Endpoint not found if read_model_registry not available' + end end end diff --git a/spec/requests/api/npm_group_packages_spec.rb b/spec/requests/api/npm_group_packages_spec.rb index d97c7682b4b..431c59cf1b8 100644 --- a/spec/requests/api/npm_group_packages_spec.rb +++ b/spec/requests/api/npm_group_packages_spec.rb @@ -2,7 +2,8 @@ require 'spec_helper' -RSpec.describe API::NpmGroupPackages, feature_category: :package_registry do +RSpec.describe API::NpmGroupPackages, feature_category: :package_registry, + quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/418757' do using RSpec::Parameterized::TableSyntax include_context 'npm api setup' @@ -152,6 +153,14 @@ RSpec.describe API::NpmGroupPackages, feature_category: :package_registry do it_behaves_like 'returning response status', params[:expected_status] end end + + context 'when metadata cache exists' do + let_it_be(:npm_metadata_cache) { create(:npm_metadata_cache, package_name: package.name, project_id: project.id) } + + subject { get(url) } + + it_behaves_like 'generates metadata response "on-the-fly"' + end end describe 'GET /api/v4/packages/npm/-/package/*package_name/dist-tags' do @@ -164,12 +173,31 @@ RSpec.describe API::NpmGroupPackages, feature_category: :package_registry do it_behaves_like 'handling create dist tag requests', scope: :group do let(:url) { api("/groups/#{group.id}/-/packages/npm/-/package/#{package_name}/dist-tags/#{tag_name}") } end + + it_behaves_like 'enqueue a worker to sync a metadata cache' do + let(:tag_name) { 'test' } + let(:url) { api("/groups/#{group.id}/-/packages/npm/-/package/#{package_name}/dist-tags/#{tag_name}") } + let(:env) { { 'api.request.body': package.version } } + let(:headers) { build_token_auth_header(personal_access_token.token) } + + subject { put(url, env: env, headers: headers) } + end end describe 'DELETE /api/v4/packages/npm/-/package/*package_name/dist-tags/:tag' do it_behaves_like 'handling delete dist tag requests', scope: :group do let(:url) { api("/groups/#{group.id}/-/packages/npm/-/package/#{package_name}/dist-tags/#{tag_name}") } end + + it_behaves_like 'enqueue a worker to sync a metadata cache' do + let_it_be(:package_tag) { create(:packages_tag, package: package) } + + let(:tag_name) { package_tag.name } + let(:url) { api("/groups/#{group.id}/-/packages/npm/-/package/#{package_name}/dist-tags/#{tag_name}") } + let(:headers) { build_token_auth_header(personal_access_token.token) } + + subject { delete(url, headers: headers) } + end end describe 'POST /api/v4/groups/:id/-/packages/npm/-/npm/v1/security/advisories/bulk' do diff --git a/spec/requests/api/npm_instance_packages_spec.rb b/spec/requests/api/npm_instance_packages_spec.rb index 97de7fa9e52..4f965d86d66 100644 --- a/spec/requests/api/npm_instance_packages_spec.rb +++ b/spec/requests/api/npm_instance_packages_spec.rb @@ -45,6 +45,14 @@ RSpec.describe API::NpmInstancePackages, feature_category: :package_registry do end end end + + context 'when metadata cache exists' do + let_it_be(:npm_metadata_cache) { create(:npm_metadata_cache, package_name: package.name, project_id: project.id) } + + subject { get(url) } + + it_behaves_like 'generates metadata response "on-the-fly"' + end end describe 'GET /api/v4/packages/npm/-/package/*package_name/dist-tags' do @@ -57,12 +65,31 @@ RSpec.describe API::NpmInstancePackages, feature_category: :package_registry do it_behaves_like 'handling create dist tag requests', scope: :instance do let(:url) { api("/packages/npm/-/package/#{package_name}/dist-tags/#{tag_name}") } end + + it_behaves_like 'enqueue a worker to sync a metadata cache' do + let(:tag_name) { 'test' } + let(:url) { api("/packages/npm/-/package/#{package_name}/dist-tags/#{tag_name}") } + let(:env) { { 'api.request.body': package.version } } + let(:headers) { build_token_auth_header(personal_access_token.token) } + + subject { put(url, env: env, headers: headers) } + end end describe 'DELETE /api/v4/packages/npm/-/package/*package_name/dist-tags/:tag' do it_behaves_like 'handling delete dist tag requests', scope: :instance do let(:url) { api("/packages/npm/-/package/#{package_name}/dist-tags/#{tag_name}") } end + + it_behaves_like 'enqueue a worker to sync a metadata cache' do + let_it_be(:package_tag) { create(:packages_tag, package: package) } + + let(:tag_name) { package_tag.name } + let(:url) { api("/packages/npm/-/package/#{package_name}/dist-tags/#{tag_name}") } + let(:headers) { build_token_auth_header(personal_access_token.token) } + + subject { delete(url, headers: headers) } + end end describe 'POST /api/v4/packages/npm/-/npm/v1/security/advisories/bulk' do diff --git a/spec/requests/api/npm_project_packages_spec.rb b/spec/requests/api/npm_project_packages_spec.rb index 60d4bddc502..8c0b9572af3 100644 --- a/spec/requests/api/npm_project_packages_spec.rb +++ b/spec/requests/api/npm_project_packages_spec.rb @@ -2,7 +2,8 @@ require 'spec_helper' -RSpec.describe API::NpmProjectPackages, feature_category: :package_registry do +RSpec.describe API::NpmProjectPackages, feature_category: :package_registry, + quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/418757' do include ExclusiveLeaseHelpers include_context 'npm api setup' @@ -26,6 +27,51 @@ RSpec.describe API::NpmProjectPackages, feature_category: :package_registry do it_behaves_like 'rejects invalid package names' do subject { get(url) } end + + context 'when metadata cache exists', :aggregate_failures do + let!(:npm_metadata_cache) { create(:npm_metadata_cache, package_name: package.name, project_id: project.id) } + let(:metadata) { Gitlab::Json.parse(npm_metadata_cache.file.read.gsub('dist_tags', 'dist-tags')) } + + subject { get(url) } + + before do + project.add_developer(user) + end + + it 'returns response from metadata cache' do + expect(Packages::Npm::GenerateMetadataService).not_to receive(:new) + expect(Packages::Npm::MetadataCache).to receive(:find_by_package_name_and_project_id) + .with(package.name, project.id).and_call_original + + subject + + expect(json_response).to eq(metadata) + end + + it 'bumps last_downloaded_at of metadata cache' do + expect { subject } + .to change { npm_metadata_cache.reload.last_downloaded_at }.from(nil).to(instance_of(ActiveSupport::TimeWithZone)) + end + + it_behaves_like 'does not enqueue a worker to sync a metadata cache' + + context 'when npm_metadata_cache disabled' do + before do + stub_feature_flags(npm_metadata_cache: false) + end + + it_behaves_like 'generates metadata response "on-the-fly"' + end + + context 'when metadata cache file does not exist' do + before do + FileUtils.rm_rf(npm_metadata_cache.file.path) + end + + it_behaves_like 'generates metadata response "on-the-fly"' + it_behaves_like 'enqueue a worker to sync a metadata cache' + end + end end describe 'GET /api/v4/projects/:id/packages/npm/-/package/*package_name/dist-tags' do @@ -39,12 +85,31 @@ RSpec.describe API::NpmProjectPackages, feature_category: :package_registry do it_behaves_like 'handling create dist tag requests', scope: :project do let(:url) { api("/projects/#{project.id}/packages/npm/-/package/#{package_name}/dist-tags/#{tag_name}") } end + + it_behaves_like 'enqueue a worker to sync a metadata cache' do + let(:tag_name) { 'test' } + let(:url) { api("/projects/#{project.id}/packages/npm/-/package/#{package_name}/dist-tags/#{tag_name}") } + let(:env) { { 'api.request.body': package.version } } + let(:headers) { build_token_auth_header(personal_access_token.token) } + + subject { put(url, env: env, headers: headers) } + end end describe 'DELETE /api/v4/projects/:id/packages/npm/-/package/*package_name/dist-tags/:tag' do it_behaves_like 'handling delete dist tag requests', scope: :project do let(:url) { api("/projects/#{project.id}/packages/npm/-/package/#{package_name}/dist-tags/#{tag_name}") } end + + it_behaves_like 'enqueue a worker to sync a metadata cache' do + let_it_be(:package_tag) { create(:packages_tag, package: package) } + + let(:tag_name) { package_tag.name } + let(:url) { api("/projects/#{project.id}/packages/npm/-/package/#{package_name}/dist-tags/#{tag_name}") } + let(:headers) { build_token_auth_header(personal_access_token.token) } + + subject { delete(url, headers: headers) } + end end describe 'POST /api/v4/projects/:id/packages/npm/-/npm/v1/security/advisories/bulk' do @@ -176,12 +241,13 @@ RSpec.describe API::NpmProjectPackages, feature_category: :package_registry do subject(:upload_package_with_token) { upload_with_token(package_name, params) } - shared_examples 'handling invalid record with 400 error' do + shared_examples 'handling invalid record with 400 error' do |error_message| it 'handles an ActiveRecord::RecordInvalid exception with 400 error' do expect { upload_package_with_token } .not_to change { project.packages.count } expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['error']).to eq(error_message) end end @@ -191,7 +257,7 @@ RSpec.describe API::NpmProjectPackages, feature_category: :package_registry do let(:package_name) { "@#{group.path}/my_inv@@lid_package_name" } let(:params) { upload_params(package_name: package_name) } - it_behaves_like 'handling invalid record with 400 error' + it_behaves_like 'handling invalid record with 400 error', "Validation failed: Name is invalid, Name #{Gitlab::Regex.npm_package_name_regex_message}" it_behaves_like 'not a package tracking event' end @@ -213,7 +279,7 @@ RSpec.describe API::NpmProjectPackages, feature_category: :package_registry do with_them do let(:params) { upload_params(package_name: package_name, package_version: version) } - it_behaves_like 'handling invalid record with 400 error' + it_behaves_like 'handling invalid record with 400 error', "Validation failed: Version #{Gitlab::Regex.semver_regex_message}" it_behaves_like 'not a package tracking event' end end @@ -222,7 +288,7 @@ RSpec.describe API::NpmProjectPackages, feature_category: :package_registry do let(:package_name) { "@#{group.path}/my_package_name" } let(:params) { upload_params(package_name: package_name, file: 'npm/payload_with_empty_attachment.json') } - it_behaves_like 'handling invalid record with 400 error' + it_behaves_like 'handling invalid record with 400 error', 'Attachment data is empty.' it_behaves_like 'not a package tracking event' end end @@ -297,6 +363,10 @@ RSpec.describe API::NpmProjectPackages, feature_category: :package_registry do it_behaves_like 'handling upload with different authentications' end + it_behaves_like 'enqueue a worker to sync a metadata cache' do + let(:package_name) { "@#{group.path}/my_package_name" } + end + context 'with an existing package' do let_it_be(:second_project) { create(:project, namespace: namespace) } @@ -305,7 +375,7 @@ RSpec.describe API::NpmProjectPackages, feature_category: :package_registry do let(:package_name) { "@#{group.path}/test" } - it_behaves_like 'handling invalid record with 400 error' + it_behaves_like 'handling invalid record with 400 error', 'Validation failed: Package already exists' it_behaves_like 'not a package tracking event' context 'with a new version' do @@ -340,6 +410,11 @@ RSpec.describe API::NpmProjectPackages, feature_category: :package_registry do .not_to change { project.packages.count } expect(response).to have_gitlab_http_status(:forbidden) + expect(json_response['error']).to eq('Package already exists.') + end + + it_behaves_like 'does not enqueue a worker to sync a metadata cache' do + subject { upload_package_with_token } end end @@ -389,7 +464,8 @@ RSpec.describe API::NpmProjectPackages, feature_category: :package_registry do .not_to change { project.packages.count } expect(response).to have_gitlab_http_status(:bad_request) - expect(response.body).to include('Could not obtain package lease.') + expect(response.body).to include('Could not obtain package lease. Please try again.') + expect(json_response['error']).to eq('Could not obtain package lease. Please try again.') end end @@ -415,15 +491,8 @@ RSpec.describe API::NpmProjectPackages, feature_category: :package_registry do end end + it_behaves_like 'handling invalid record with 400 error', 'Validation failed: Package json structure is too large. Maximum size is 20000 characters' it_behaves_like 'not a package tracking event' - - it 'returns an error' do - expect { upload_package_with_token } - .not_to change { project.packages.count } - - expect(response).to have_gitlab_http_status(:bad_request) - expect(response.body).to include('Validation failed: Package json structure is too large') - end end end diff --git a/spec/requests/api/project_attributes.yml b/spec/requests/api/project_attributes.yml index e0e9c944fe4..86ff739da7e 100644 --- a/spec/requests/api/project_attributes.yml +++ b/spec/requests/api/project_attributes.yml @@ -27,6 +27,7 @@ itself: # project - mirror_overwrites_diverged_branches - mirror_trigger_builds - mirror_user_id + - mirror_branch_regex - only_mirror_protected_branches - pages_https_only - pending_delete @@ -42,7 +43,7 @@ itself: # project - runners_token_encrypted - storage_version - topic_list - - mirror_branch_regex + - verification_checksum remapped_attributes: avatar: avatar_url build_allow_git_fetch: build_git_strategy diff --git a/spec/requests/api/project_clusters_spec.rb b/spec/requests/api/project_clusters_spec.rb index c52948a4cb0..99c190757ca 100644 --- a/spec/requests/api/project_clusters_spec.rb +++ b/spec/requests/api/project_clusters_spec.rb @@ -443,7 +443,7 @@ RSpec.describe API::ProjectClusters, feature_category: :deployment_management do it 'returns validation error' do expect(response).to have_gitlab_http_status(:bad_request) - expect(json_response['message']['platform_kubernetes.base'].first) + expect(json_response['message']['platform_kubernetes'].first) .to eq(_('Cannot modify managed Kubernetes cluster')) end end diff --git a/spec/requests/api/project_export_spec.rb b/spec/requests/api/project_export_spec.rb index 434936c0ee7..3603a71151e 100644 --- a/spec/requests/api/project_export_spec.rb +++ b/spec/requests/api/project_export_spec.rb @@ -567,54 +567,138 @@ RSpec.describe API::ProjectExport, :aggregate_failures, :clean_gitlab_redis_cach expect(response).to have_gitlab_http_status(:error) end end + + context 'when request is to export in batches' do + it 'accepts the request' do + expect(BulkImports::ExportService) + .to receive(:new) + .with(portable: project, user: user, batched: true) + .and_call_original + + post api(path, user), params: { batched: true } + + expect(response).to have_gitlab_http_status(:accepted) + end + end end describe 'GET /projects/:id/export_relations/download' do - let_it_be(:export) { create(:bulk_import_export, project: project, relation: 'labels') } - let_it_be(:upload) { create(:bulk_import_export_upload, export: export) } + context 'when export request is not batched' do + let_it_be(:export) { create(:bulk_import_export, project: project, relation: 'labels') } + let_it_be(:upload) { create(:bulk_import_export_upload, export: export) } + + context 'when export file exists' do + it 'downloads exported project relation archive' do + upload.update!(export_file: fixture_file_upload('spec/fixtures/bulk_imports/gz/labels.ndjson.gz')) + + get api(download_path, user) + + expect(response).to have_gitlab_http_status(:ok) + expect(response.header['Content-Disposition']).to eq("attachment; filename=\"labels.ndjson.gz\"; filename*=UTF-8''labels.ndjson.gz") + end + end + + context 'when relation is not portable' do + let(:relation) { ::BulkImports::FileTransfer::ProjectConfig.new(project).skipped_relations.first } + + it_behaves_like '400 response' do + subject(:request) { get api(download_path, user) } + end + end + + context 'when export file does not exist' do + it 'returns 404' do + allow(upload).to receive(:export_file).and_return(nil) + + get api(download_path, user) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'when export is batched' do + let(:relation) { 'issues' } + + let_it_be(:export) { create(:bulk_import_export, :batched, project: project, relation: 'issues') } - context 'when export file exists' do - it 'downloads exported project relation archive' do + it 'returns 400' do + export.update!(batched: true) + + get api(download_path, user) + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['message']).to eq('Export is batched') + end + end + end + + context 'when export request is batched' do + let(:export) { create(:bulk_import_export, :batched, project: project, relation: 'labels') } + let(:upload) { create(:bulk_import_export_upload) } + let!(:batch) { create(:bulk_import_export_batch, export: export, upload: upload) } + + it 'downloads exported batch' do upload.update!(export_file: fixture_file_upload('spec/fixtures/bulk_imports/gz/labels.ndjson.gz')) - get api(download_path, user) + get api(download_path, user), params: { batched: true, batch_number: batch.batch_number } expect(response).to have_gitlab_http_status(:ok) expect(response.header['Content-Disposition']).to eq("attachment; filename=\"labels.ndjson.gz\"; filename*=UTF-8''labels.ndjson.gz") end - end - context 'when relation is not portable' do - let(:relation) { ::BulkImports::FileTransfer::ProjectConfig.new(project).skipped_relations.first } + context 'when request is to download not batched export' do + it 'returns 400' do + get api(download_path, user) - it_behaves_like '400 response' do - subject(:request) { get api(download_path, user) } + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['message']).to eq('Export is batched') + end end - end - context 'when export file does not exist' do - it 'returns 404' do - allow(upload).to receive(:export_file).and_return(nil) + context 'when batch cannot be found' do + it 'returns 404' do + get api(download_path, user), params: { batched: true, batch_number: 0 } - get api(download_path, user) + expect(response).to have_gitlab_http_status(:not_found) + expect(json_response['message']).to eq('Batch not found') + end + end + + context 'when batch file cannot be found' do + it 'returns 404' do + batch.upload.destroy! + + get api(download_path, user), params: { batched: true, batch_number: batch.batch_number } - expect(response).to have_gitlab_http_status(:not_found) + expect(response).to have_gitlab_http_status(:not_found) + expect(json_response['message']).to eq('Batch file not found') + end end end end describe 'GET /projects/:id/export_relations/status' do - it 'returns a list of relation export statuses' do - create(:bulk_import_export, :started, project: project, relation: 'labels') - create(:bulk_import_export, :finished, project: project, relation: 'milestones') - create(:bulk_import_export, :failed, project: project, relation: 'project_badges') + let_it_be(:started_export) { create(:bulk_import_export, :started, project: project, relation: 'labels') } + let_it_be(:finished_export) { create(:bulk_import_export, :finished, project: project, relation: 'milestones') } + let_it_be(:failed_export) { create(:bulk_import_export, :failed, project: project, relation: 'project_badges') } + it 'returns a list of relation export statuses' do get api(status_path, user) expect(response).to have_gitlab_http_status(:ok) expect(json_response.pluck('relation')).to contain_exactly('labels', 'milestones', 'project_badges') expect(json_response.pluck('status')).to contain_exactly(-1, 0, 1) end + + context 'when relation is specified' do + it 'return a single relation export status' do + get api(status_path, user), params: { relation: 'labels' } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['relation']).to eq('labels') + expect(json_response['status']).to eq(0) + end + end end context 'with bulk_import is disabled' do diff --git a/spec/requests/api/project_hooks_spec.rb b/spec/requests/api/project_hooks_spec.rb index c6bf77e5dcf..9d94b5437b7 100644 --- a/spec/requests/api/project_hooks_spec.rb +++ b/spec/requests/api/project_hooks_spec.rb @@ -57,6 +57,7 @@ RSpec.describe API::ProjectHooks, 'ProjectHooks', feature_category: :webhooks do job_events deployment_events releases_events + emoji_events ] end diff --git a/spec/requests/api/project_packages_spec.rb b/spec/requests/api/project_packages_spec.rb index b84b7e9c52d..09991be998a 100644 --- a/spec/requests/api/project_packages_spec.rb +++ b/spec/requests/api/project_packages_spec.rb @@ -660,6 +660,12 @@ RSpec.describe API::ProjectPackages, feature_category: :package_registry do expect(response).to have_gitlab_http_status(:no_content) end + it_behaves_like 'enqueue a worker to sync a metadata cache' do + let(:package_name) { package1.name } + + subject { delete api(package_url, user) } + end + context 'with JOB-TOKEN auth' do let(:job) { create(:ci_build, :running, user: user, project: project) } @@ -692,6 +698,14 @@ RSpec.describe API::ProjectPackages, feature_category: :package_registry do delete api(package_url, user) end + + it_behaves_like 'does not enqueue a worker to sync a metadata cache' do + before do + project.add_maintainer(user) + end + + subject { delete api(package_url, user) } + end end end end diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index bb96771b3d5..f5d1bbbc7e8 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -223,14 +223,6 @@ RSpec.describe API::Projects, :aggregate_failures, feature_category: :groups_and include_examples 'includes container_registry_access_level' - context 'when projects_preloader_fix is disabled' do - before do - stub_feature_flags(projects_preloader_fix: false) - end - - include_examples 'includes container_registry_access_level' - end - it 'includes various project feature fields' do get api(path, user) project_response = json_response.find { |p| p['id'] == project.id } @@ -1843,6 +1835,72 @@ RSpec.describe API::Projects, :aggregate_failures, feature_category: :groups_and end end + describe 'GET /users/:user_id/contributed_projects/' do + let(:path) { "/users/#{user3.id}/contributed_projects/" } + + let_it_be(:project1) { create(:project, :public, path: 'my-project') } + let_it_be(:project2) { create(:project, :public) } + let_it_be(:project3) { create(:project, :public) } + let_it_be(:private_project) { create(:project, :private) } + + before do + private_project.add_maintainer(user3) + + create(:push_event, project: project1, author: user3) + create(:push_event, project: project2, author: user3) + create(:push_event, project: private_project, author: user3) + end + + it 'returns error when user not found' do + get api("/users/#{non_existing_record_id}/contributed_projects/", user) + + expect(response).to have_gitlab_http_status(:not_found) + expect(json_response['message']).to eq('404 User Not Found') + end + + context 'with a public profile' do + it 'returns projects filtered by user' do + get api(path, user) + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.map { |project| project['id'] }) + .to contain_exactly(project1.id, project2.id) + end + end + + context 'with a private profile' do + before do + user3.update!(private_profile: true) + user3.reload + end + + context 'user does not have access to view the private profile' do + it 'returns no projects' do + get api(path, user) + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response).to be_empty + end + end + + context 'user has access to view the private profile as an admin' do + it 'returns projects filtered by user' do + get api(path, admin, admin_mode: true) + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.map { |project| project['id'] }) + .to contain_exactly(project1.id, project2.id, private_project.id) + end + end + end + end + describe 'POST /projects/user/:id' do let(:path) { "/projects/user/#{user.id}" } @@ -2588,12 +2646,12 @@ RSpec.describe API::Projects, :aggregate_failures, feature_category: :groups_and expect(diff).to be_empty, failure_message(diff) end - def failure_message(_diff) + def failure_message(diff) <<~MSG It looks like project's set of exposed attributes is different from the expected set. The following attributes are missing or newly added: - {diff.to_a.to_sentence} + #{diff.to_a.to_sentence} Please update #{project_attributes_file} file" MSG diff --git a/spec/requests/api/protected_branches_spec.rb b/spec/requests/api/protected_branches_spec.rb index 04d5f7ac20a..b79cff5a905 100644 --- a/spec/requests/api/protected_branches_spec.rb +++ b/spec/requests/api/protected_branches_spec.rb @@ -121,7 +121,18 @@ RSpec.describe API::ProtectedBranches, feature_category: :source_code_management get api(route, user) expect(json_response['push_access_levels']).to include( - a_hash_including('access_level_description' => 'Deploy key', 'deploy_key_id' => deploy_key.id) + a_hash_including('access_level_description' => deploy_key.title, 'deploy_key_id' => deploy_key.id) + ) + end + end + + context 'when a deploy key is not present' do + it 'returns null deploy key field' do + create(:protected_branch_push_access_level, protected_branch: protected_branch) + get api(route, user) + + expect(json_response['push_access_levels']).to include( + a_hash_including('deploy_key_id' => nil) ) end end diff --git a/spec/requests/api/protected_tags_spec.rb b/spec/requests/api/protected_tags_spec.rb index c6398e624f8..8a98c751877 100644 --- a/spec/requests/api/protected_tags_spec.rb +++ b/spec/requests/api/protected_tags_spec.rb @@ -95,7 +95,18 @@ RSpec.describe API::ProtectedTags, feature_category: :source_code_management do get api(route, user) expect(json_response['create_access_levels']).to include( - a_hash_including('access_level_description' => 'Deploy key', 'deploy_key_id' => deploy_key.id) + a_hash_including('access_level_description' => deploy_key.title, 'deploy_key_id' => deploy_key.id) + ) + end + end + + context 'when a deploy key is not present' do + it 'returns null deploy key field' do + create(:protected_tag_create_access_level, protected_tag: protected_tag) + get api(route, user) + + expect(json_response['create_access_levels']).to include( + a_hash_including('deploy_key_id' => nil) ) end end diff --git a/spec/requests/api/search_spec.rb b/spec/requests/api/search_spec.rb index 1b331e9c099..0feff90d088 100644 --- a/spec/requests/api/search_spec.rb +++ b/spec/requests/api/search_spec.rb @@ -688,6 +688,16 @@ RSpec.describe API::Search, :clean_gitlab_redis_rate_limiting, feature_category: end end + context 'when user does not have permissions for scope' do + it 'returns an empty array' do + project.project_feature.update!(issues_access_level: Gitlab::VisibilityLevel::PRIVATE) + + get api(endpoint, user), params: { scope: 'issues', search: 'awesome' } + + expect(json_response).to be_empty + end + end + context 'when project does not exist' do it 'returns 404 error' do get api('/projects/0/search', user), params: { scope: 'issues', search: 'awesome' } @@ -861,7 +871,7 @@ RSpec.describe API::Search, :clean_gitlab_redis_rate_limiting, feature_category: get api(endpoint, user), params: { scope: 'wiki_blobs', search: 'awesome' } end - it_behaves_like 'response is correct', schema: 'public_api/v4/blobs' + it_behaves_like 'response is correct', schema: 'public_api/v4/wiki_blobs' it_behaves_like 'ping counters', scope: :wiki_blobs diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb index 79e96d7ea3e..dfaba969153 100644 --- a/spec/requests/api/settings_spec.rb +++ b/spec/requests/api/settings_spec.rb @@ -80,6 +80,7 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting, featu expect(json_response['valid_runner_registrars']).to match_array(%w(project group)) expect(json_response['ci_max_includes']).to eq(150) expect(json_response['allow_account_deletion']).to eq(true) + expect(json_response['gitlab_shell_operation_limit']).to eq(600) end end @@ -190,13 +191,9 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting, featu default_syntax_highlighting_theme: 2, projects_api_rate_limit_unauthenticated: 100, silent_mode_enabled: true, - slack_app_enabled: true, - slack_app_id: 'SLACK_APP_ID', - slack_app_secret: 'SLACK_APP_SECRET', - slack_app_signing_secret: 'SLACK_APP_SIGNING_SECRET', - slack_app_verification_token: 'SLACK_APP_VERIFICATION_TOKEN', valid_runner_registrars: ['group'], - allow_account_deletion: false + allow_account_deletion: false, + gitlab_shell_operation_limit: 500 } expect(response).to have_gitlab_http_status(:ok) @@ -270,16 +267,23 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting, featu expect(json_response['default_syntax_highlighting_theme']).to eq(2) expect(json_response['projects_api_rate_limit_unauthenticated']).to be(100) expect(json_response['silent_mode_enabled']).to be(true) - expect(json_response['slack_app_enabled']).to be(true) - expect(json_response['slack_app_id']).to eq('SLACK_APP_ID') - expect(json_response['slack_app_secret']).to eq('SLACK_APP_SECRET') - expect(json_response['slack_app_signing_secret']).to eq('SLACK_APP_SIGNING_SECRET') - expect(json_response['slack_app_verification_token']).to eq('SLACK_APP_VERIFICATION_TOKEN') expect(json_response['valid_runner_registrars']).to eq(['group']) expect(json_response['allow_account_deletion']).to be(false) + expect(json_response['gitlab_shell_operation_limit']).to be(500) end end + it "updates default_branch_protection_defaults from the default_branch_protection param" do + expected_update = ::Gitlab::Access::BranchProtection.protected_against_developer_pushes.stringify_keys + + put api("/application/settings", admin), + params: { default_branch_protection: ::Gitlab::Access::PROTECTION_DEV_CAN_MERGE } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['default_branch_protection']).to eq(Gitlab::Access::PROTECTION_DEV_CAN_MERGE) + expect(ApplicationSetting.first.default_branch_protection_defaults).to eq(expected_update) + end + it "supports legacy performance_bar_allowed_group_id" do put api("/application/settings", admin), params: { performance_bar_allowed_group_id: group.full_path } @@ -550,6 +554,85 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting, featu end end + context 'GitLab for Slack app settings' do + let(:settings) do + { + slack_app_enabled: slack_app_enabled, + slack_app_id: slack_app_id, + slack_app_secret: slack_app_secret, + slack_app_signing_secret: slack_app_signing_secret, + slack_app_verification_token: slack_app_verification_token + } + end + + context 'when GitLab for Slack app is enabled' do + let(:slack_app_enabled) { true } + + context 'when other params are blank' do + let(:slack_app_id) { nil } + let(:slack_app_secret) { nil } + let(:slack_app_signing_secret) { nil } + let(:slack_app_verification_token) { nil } + + it 'does not update the settings' do + put api("/application/settings", admin), params: settings + + expect(response).to have_gitlab_http_status(:bad_request) + + expect(json_response['slack_app_enabled']).to be(nil) + expect(json_response['slack_app_id']).to be(nil) + expect(json_response['slack_app_secret']).to be(nil) + expect(json_response['slack_app_signing_secret']).to be(nil) + expect(json_response['slack_app_verification_token']).to be(nil) + + message = json_response['message'] + + expect(message['slack_app_id']).to include("can't be blank") + expect(message['slack_app_secret']).to include("can't be blank") + expect(message['slack_app_signing_secret']).to include("can't be blank") + expect(message['slack_app_verification_token']).to include("can't be blank") + end + end + + context 'when other params are present' do + let(:slack_app_id) { 'ID' } + let(:slack_app_secret) { 'SECRET' } + let(:slack_app_signing_secret) { 'SIGNING_SECRET' } + let(:slack_app_verification_token) { 'VERIFICATION_TOKEN' } + + it 'updates the settings' do + put api("/application/settings", admin), params: settings + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['slack_app_enabled']).to be(true) + expect(json_response['slack_app_id']).to eq('ID') + expect(json_response['slack_app_secret']).to eq('SECRET') + expect(json_response['slack_app_signing_secret']).to eq('SIGNING_SECRET') + expect(json_response['slack_app_verification_token']).to eq('VERIFICATION_TOKEN') + end + end + end + + context 'when GitLab for Slack app is not enabled' do + let(:slack_app_enabled) { false } + let(:slack_app_id) { nil } + let(:slack_app_secret) { nil } + let(:slack_app_signing_secret) { nil } + let(:slack_app_verification_token) { nil } + + it 'allows blank attributes' do + put api("/application/settings", admin), params: settings + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['slack_app_enabled']).to be(false) + expect(json_response['slack_app_id']).to be(nil) + expect(json_response['slack_app_secret']).to be(nil) + expect(json_response['slack_app_signing_secret']).to be(nil) + expect(json_response['slack_app_verification_token']).to be(nil) + end + end + end + context "missing plantuml_url value when plantuml_enabled is true" do it "returns a blank parameter error message" do put api("/application/settings", admin), params: { plantuml_enabled: true } diff --git a/spec/requests/api/statistics_spec.rb b/spec/requests/api/statistics_spec.rb index baac39abf2c..76190d4e272 100644 --- a/spec/requests/api/statistics_spec.rb +++ b/spec/requests/api/statistics_spec.rb @@ -59,7 +59,7 @@ RSpec.describe API::Statistics, 'Statistics', :aggregate_failures, feature_categ create_list(:note, 2, author: admin, project: projects.first, noteable: issues.first) create_list(:milestone, 3, project: projects.first) create(:key, user: admin) - create(:merge_request, source_project: projects.first) + create(:merge_request, :skip_diff_creation, source_project: projects.first) fork_project(projects.first, admin) # Make sure the reltuples have been updated diff --git a/spec/requests/api/usage_data_spec.rb b/spec/requests/api/usage_data_spec.rb index 935ddbf4764..c8f1e8d6973 100644 --- a/spec/requests/api/usage_data_spec.rb +++ b/spec/requests/api/usage_data_spec.rb @@ -164,6 +164,61 @@ RSpec.describe API::UsageData, feature_category: :service_ping do end end + describe 'POST /usage_data/track_event' do + let(:endpoint) { '/usage_data/track_event' } + let(:known_event) { 'i_compliance_dashboard' } + let(:unknown_event) { 'unknown' } + let(:namespace_id) { 123 } + let(:project_id) { 123 } + + context 'without CSRF token' do + it 'returns forbidden' do + allow(Gitlab::RequestForgeryProtection).to receive(:verified?).and_return(false) + + post api(endpoint, user), params: { event: known_event, namespace_id: namespace_id, project_id: project_id } + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + context 'usage_data_api feature not enabled' do + it 'returns not_found' do + stub_feature_flags(usage_data_api: false) + + post api(endpoint, user), params: { event: known_event, namespace_id: namespace_id, project_id: project_id } + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'without authentication' do + it 'returns 401 response' do + post api(endpoint), params: { event: known_event, namespace_id: namespace_id, project_id: project_id } + + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + + context 'with authentication' do + before do + stub_application_setting(usage_ping_enabled: true) + allow(Gitlab::RequestForgeryProtection).to receive(:verified?).and_return(true) + end + + context 'with correct params' do + it 'returns status ok' do + expect(Gitlab::InternalEvents).to receive(:track_event).with(known_event, anything) + # allow other events to also get triggered + allow(Gitlab::InternalEvents).to receive(:track_event) + + post api(endpoint, user), params: { event: known_event, namespace_id: namespace_id, project_id: project_id } + + expect(response).to have_gitlab_http_status(:ok) + end + end + end + end + describe 'GET /usage_data/metric_definitions' do let(:endpoint) { '/usage_data/metric_definitions' } let(:metric_yaml) do diff --git a/spec/requests/api/user_runners_spec.rb b/spec/requests/api/user_runners_spec.rb new file mode 100644 index 00000000000..0e40dcade19 --- /dev/null +++ b/spec/requests/api/user_runners_spec.rb @@ -0,0 +1,243 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::UserRunners, :aggregate_failures, feature_category: :runner_fleet do + let_it_be(:admin) { create(:admin) } + let_it_be(:user, reload: true) { create(:user, username: 'user.withdot') } + + describe 'POST /user/runners' do + subject(:request) { post api(path, current_user, **post_args), params: runner_attrs } + + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project, namespace: group) } + let_it_be(:group_owner) { create(:user).tap { |user| group.add_owner(user) } } + let_it_be(:group_maintainer) { create(:user).tap { |user| group.add_maintainer(user) } } + let_it_be(:project_developer) { create(:user).tap { |user| project.add_developer(user) } } + + let(:post_args) { { admin_mode: true } } + let(:runner_attrs) { { runner_type: 'instance_type' } } + let(:path) { '/user/runners' } + + shared_examples 'when runner creation fails due to authorization' do + it 'does not create a runner' do + expect do + request + + expect(response).to have_gitlab_http_status(:forbidden) + end.not_to change { Ci::Runner.count } + end + end + + shared_context 'when user does not have sufficient permissions returns forbidden' do + context 'when user is admin and admin mode is disabled' do + let(:current_user) { admin } + let(:post_args) { { admin_mode: false } } + + it_behaves_like 'when runner creation fails due to authorization' + end + + context 'when user is not an admin or a member of the namespace' do + let(:current_user) { user } + + it_behaves_like 'when runner creation fails due to authorization' + end + end + + shared_examples 'creates a runner' do + it 'creates a runner' do + expect do + request + + expect(response).to have_gitlab_http_status(:created) + end.to change { Ci::Runner.count }.by(1) + end + end + + shared_examples 'fails to create runner with expected_status_code' do + let(:expected_message) { nil } + let(:expected_error) { nil } + + it 'does not create runner' do + expect do + request + + expect(response).to have_gitlab_http_status(expected_status_code) + expect(json_response['message']).to include(expected_message) if expected_message + expect(json_response['error']).to include(expected_error) if expected_error + end.not_to change { Ci::Runner.count } + end + end + + shared_context 'with request authorized with access token' do + let(:current_user) { nil } + let(:pat) { create(:personal_access_token, user: token_user, scopes: [scope]) } + let(:path) { "/user/runners?private_token=#{pat.token}" } + + %i[create_runner api].each do |scope| + context "with #{scope} scope" do + let(:scope) { scope } + + it_behaves_like 'creates a runner' + end + end + + context 'with read_api scope' do + let(:scope) { :read_api } + + it_behaves_like 'fails to create runner with expected_status_code' do + let(:expected_status_code) { :forbidden } + let(:expected_error) { 'insufficient_scope' } + end + end + end + + context 'when runner_type is :instance_type' do + let(:runner_attrs) { { runner_type: 'instance_type' } } + + context 'when user has sufficient permissions' do + let(:current_user) { admin } + + it_behaves_like 'creates a runner' + end + + context 'with admin mode enabled', :enable_admin_mode do + let(:token_user) { admin } + + it_behaves_like 'with request authorized with access token' + end + + it_behaves_like 'when user does not have sufficient permissions returns forbidden' + + context 'when user is not an admin' do + let(:current_user) { user } + + it_behaves_like 'when runner creation fails due to authorization' + end + + context 'when model validation fails' do + let(:runner_attrs) { { runner_type: 'instance_type', run_untagged: false, tag_list: [] } } + let(:current_user) { admin } + + it_behaves_like 'fails to create runner with expected_status_code' do + let(:expected_status_code) { :bad_request } + let(:expected_message) { 'Tags list can not be empty' } + end + end + end + + context 'when runner_type is :group_type' do + let(:post_args) { {} } + + context 'when group_id is specified' do + let(:runner_attrs) { { runner_type: 'group_type', group_id: group.id } } + + context 'when user has sufficient permissions' do + let(:current_user) { group_owner } + + it_behaves_like 'creates a runner' + end + + it_behaves_like 'with request authorized with access token' do + let(:token_user) { group_owner } + end + + it_behaves_like 'when user does not have sufficient permissions returns forbidden' + + context 'when user is a maintainer' do + let(:current_user) { group_maintainer } + + it_behaves_like 'when runner creation fails due to authorization' + end + end + + context 'when group_id is not specified' do + let(:runner_attrs) { { runner_type: 'group_type' } } + let(:current_user) { group_owner } + + it 'fails to create runner with :bad_request' do + expect do + request + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['error']).to include('group_id is missing') + end.not_to change { Ci::Runner.count } + end + end + end + + context 'when runner_type is :project_type' do + let(:post_args) { {} } + + context 'when project_id is specified' do + let(:runner_attrs) { { runner_type: 'project_type', project_id: project.id } } + + context 'when user has sufficient permissions' do + let(:current_user) { group_owner } + + it_behaves_like 'creates a runner' + end + + it_behaves_like 'with request authorized with access token' do + let(:token_user) { group_owner } + end + + it_behaves_like 'when user does not have sufficient permissions returns forbidden' + + context 'when user is a developer' do + let(:current_user) { project_developer } + + it_behaves_like 'when runner creation fails due to authorization' + end + end + + context 'when project_id is not specified' do + let(:runner_attrs) { { runner_type: 'project_type' } } + let(:current_user) { group_owner } + + it 'fails to create runner with :bad_request' do + expect do + request + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['error']).to include('project_id is missing') + end.not_to change { Ci::Runner.count } + end + end + end + + context 'with missing runner_type' do + let(:runner_attrs) { {} } + let(:current_user) { admin } + + it 'fails to create runner with :bad_request' do + expect do + request + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['error']).to eq('runner_type is missing, runner_type does not have a valid value') + end.not_to change { Ci::Runner.count } + end + end + + context 'with unknown runner_type' do + let(:runner_attrs) { { runner_type: 'unknown' } } + let(:current_user) { admin } + + it 'fails to create runner with :bad_request' do + expect do + request + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['error']).to eq('runner_type does not have a valid value') + end.not_to change { Ci::Runner.count } + end + end + + it 'returns a 401 error if unauthorized' do + post api(path), params: runner_attrs + + expect(response).to have_gitlab_http_status(:unauthorized) + end + end +end diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index 3737c91adbc..2bbcf6b3f38 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -4851,169 +4851,4 @@ RSpec.describe API::Users, :aggregate_failures, feature_category: :user_profile let(:attributable) { user } let(:other_attributable) { admin } end - - describe 'POST /user/runners', feature_category: :runner_fleet do - subject(:request) { post api(path, current_user, **post_args), params: runner_attrs } - - let_it_be(:group_owner) { create(:user) } - let_it_be(:group) { create(:group) } - let_it_be(:project) { create(:project, namespace: group) } - - let(:post_args) { { admin_mode: true } } - let(:runner_attrs) { { runner_type: 'instance_type' } } - let(:path) { '/user/runners' } - - before do - group.add_owner(group_owner) - end - - shared_context 'returns forbidden when user does not have sufficient permissions' do - let(:current_user) { admin } - let(:post_args) { { admin_mode: false } } - - it 'does not create a runner' do - expect do - request - - expect(response).to have_gitlab_http_status(:forbidden) - end.not_to change { Ci::Runner.count } - end - end - - shared_examples 'creates a runner' do - it 'creates a runner' do - expect do - request - - expect(response).to have_gitlab_http_status(:created) - end.to change { Ci::Runner.count }.by(1) - end - end - - shared_examples 'fails to create runner with :bad_request' do - it 'does not create runner' do - expect do - request - - expect(response).to have_gitlab_http_status(:bad_request) - expect(json_response['message']).to include(expected_error) - end.not_to change { Ci::Runner.count } - end - end - - context 'when runner_type is :instance_type' do - let(:runner_attrs) { { runner_type: 'instance_type' } } - - context 'when user has sufficient permissions' do - let(:current_user) { admin } - - it_behaves_like 'creates a runner' - end - - it_behaves_like 'returns forbidden when user does not have sufficient permissions' - - context 'when model validation fails' do - let(:runner_attrs) { { runner_type: 'instance_type', run_untagged: false, tag_list: [] } } - let(:current_user) { admin } - - it_behaves_like 'fails to create runner with :bad_request' do - let(:expected_error) { 'Tags list can not be empty' } - end - end - end - - context 'when runner_type is :group_type' do - let(:post_args) { {} } - - context 'when group_id is specified' do - let(:runner_attrs) { { runner_type: 'group_type', group_id: group.id } } - - context 'when user has sufficient permissions' do - let(:current_user) { group_owner } - - it_behaves_like 'creates a runner' - end - - it_behaves_like 'returns forbidden when user does not have sufficient permissions' - end - - context 'when group_id is not specified' do - let(:runner_attrs) { { runner_type: 'group_type' } } - let(:current_user) { group_owner } - - it 'fails to create runner with :bad_request' do - expect do - request - - expect(response).to have_gitlab_http_status(:bad_request) - expect(json_response['error']).to include('group_id is missing') - end.not_to change { Ci::Runner.count } - end - end - end - - context 'when runner_type is :project_type' do - let(:post_args) { {} } - - context 'when project_id is specified' do - let(:runner_attrs) { { runner_type: 'project_type', project_id: project.id } } - - context 'when user has sufficient permissions' do - let(:current_user) { group_owner } - - it_behaves_like 'creates a runner' - end - - it_behaves_like 'returns forbidden when user does not have sufficient permissions' - end - - context 'when project_id is not specified' do - let(:runner_attrs) { { runner_type: 'project_type' } } - let(:current_user) { group_owner } - - it 'fails to create runner with :bad_request' do - expect do - request - - expect(response).to have_gitlab_http_status(:bad_request) - expect(json_response['error']).to include('project_id is missing') - end.not_to change { Ci::Runner.count } - end - end - end - - context 'with missing runner_type' do - let(:runner_attrs) { {} } - let(:current_user) { admin } - - it 'fails to create runner with :bad_request' do - expect do - request - - expect(response).to have_gitlab_http_status(:bad_request) - expect(json_response['error']).to eq('runner_type is missing, runner_type does not have a valid value') - end.not_to change { Ci::Runner.count } - end - end - - context 'with unknown runner_type' do - let(:runner_attrs) { { runner_type: 'unknown' } } - let(:current_user) { admin } - - it 'fails to create runner with :bad_request' do - expect do - request - - expect(response).to have_gitlab_http_status(:bad_request) - expect(json_response['error']).to eq('runner_type does not have a valid value') - end.not_to change { Ci::Runner.count } - end - end - - it 'returns a 401 error if unauthorized' do - post api(path), params: runner_attrs - - expect(response).to have_gitlab_http_status(:unauthorized) - end - end end diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb index 5b50e8a1021..d3d1a2a6cd0 100644 --- a/spec/requests/git_http_spec.rb +++ b/spec/requests/git_http_spec.rb @@ -236,11 +236,6 @@ RSpec.describe 'Git HTTP requests', feature_category: :source_code_management do allow(::Users::ActivityService).to receive(:new).and_return(activity_service) allow(activity_service).to receive(:execute) - # During project creation, we need to track the project wiki - # repository. So it is over the query limit threshold, and we - # have to adjust it. - allow(Gitlab::QueryLimiting::Transaction).to receive(:threshold).and_return(101) - expect do upload(path, user: user.username, password: user.password) do |response| expect(response).to have_gitlab_http_status(:ok) diff --git a/spec/requests/groups/observability_controller_spec.rb b/spec/requests/groups/observability_controller_spec.rb index b82cf2b0bad..247535bc990 100644 --- a/spec/requests/groups/observability_controller_spec.rb +++ b/spec/requests/groups/observability_controller_spec.rb @@ -17,6 +17,10 @@ RSpec.describe Groups::ObservabilityController, feature_category: :tracing do end it_behaves_like 'observability csp policy' do + before_all do + group.add_developer(user) + end + let(:tested_path) { path } end diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb index b07296a0df2..199138eb3a9 100644 --- a/spec/requests/lfs_http_spec.rb +++ b/spec/requests/lfs_http_spec.rb @@ -807,7 +807,7 @@ RSpec.describe 'Git LFS API and storage', feature_category: :source_code_managem end end - describe 'to one project' do + describe 'to one project', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/418757' do describe 'when user is authenticated' do describe 'when user has push access to the project' do before do diff --git a/spec/requests/openid_connect_spec.rb b/spec/requests/openid_connect_spec.rb index 82f972e7f94..217241200ff 100644 --- a/spec/requests/openid_connect_spec.rb +++ b/spec/requests/openid_connect_spec.rb @@ -270,13 +270,20 @@ RSpec.describe 'OpenID Connect requests', feature_category: :system_access do end context 'OpenID configuration information' do + let(:expected_scopes) do + %w[ + admin_mode api read_user read_api read_repository write_repository sudo openid profile email + read_observability write_observability create_runner + ] + end + it 'correctly returns the configuration' do get '/.well-known/openid-configuration' expect(response).to have_gitlab_http_status(:ok) expect(json_response['issuer']).to eq('http://localhost') expect(json_response['jwks_uri']).to eq('http://www.example.com/oauth/discovery/keys') - expect(json_response['scopes_supported']).to match_array %w[admin_mode api read_user read_api read_repository write_repository sudo openid profile email read_observability write_observability] + expect(json_response['scopes_supported']).to match_array expected_scopes end context 'with a cross-origin request' do @@ -286,7 +293,7 @@ RSpec.describe 'OpenID Connect requests', feature_category: :system_access do expect(response).to have_gitlab_http_status(:ok) expect(json_response['issuer']).to eq('http://localhost') expect(json_response['jwks_uri']).to eq('http://www.example.com/oauth/discovery/keys') - expect(json_response['scopes_supported']).to match_array %w[admin_mode api read_user read_api read_repository write_repository sudo openid profile email read_observability write_observability] + expect(json_response['scopes_supported']).to match_array expected_scopes end it_behaves_like 'cross-origin GET request' diff --git a/spec/requests/organizations/organizations_controller_spec.rb b/spec/requests/organizations/organizations_controller_spec.rb index a51a5751831..bd54b50de99 100644 --- a/spec/requests/organizations/organizations_controller_spec.rb +++ b/spec/requests/organizations/organizations_controller_spec.rb @@ -5,9 +5,7 @@ require 'spec_helper' RSpec.describe Organizations::OrganizationsController, feature_category: :cell do let_it_be(:organization) { create(:organization) } - describe 'GET #directory' do - subject(:gitlab_request) { get directory_organization_path(organization) } - + RSpec.shared_examples 'basic organization controller action' do before do sign_in(user) end @@ -42,4 +40,16 @@ RSpec.describe Organizations::OrganizationsController, feature_category: :cell d end end end + + describe 'GET #show' do + subject(:gitlab_request) { get organization_path(organization) } + + it_behaves_like 'basic organization controller action' + end + + describe 'GET #groups_and_projects' do + subject(:gitlab_request) { get groups_and_projects_organization_path(organization) } + + it_behaves_like 'basic organization controller action' + end end diff --git a/spec/requests/projects/alert_management_controller_spec.rb b/spec/requests/projects/alert_management_controller_spec.rb new file mode 100644 index 00000000000..698087bf761 --- /dev/null +++ b/spec/requests/projects/alert_management_controller_spec.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Projects::AlertManagementController, feature_category: :incident_management do + let_it_be(:project) { create(:project) } + let_it_be(:developer) { create(:user) } + let_it_be(:reporter) { create(:user) } + let_it_be(:id) { 1 } + + before_all do + project.add_developer(developer) + project.add_reporter(reporter) + end + + before do + sign_in(user) + end + + describe 'GET #index' do + context 'when user is authorized' do + let(:user) { developer } + + it 'shows the page' do + get project_alert_management_index_path(project) + + expect(response).to have_gitlab_http_status(:ok) + end + end + + context 'when user is unauthorized' do + let(:user) { reporter } + + it 'shows 404' do + get project_alert_management_index_path(project) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + + describe 'GET #details' do + context 'when user is authorized' do + let(:user) { developer } + + it 'shows the page' do + get project_alert_management_alert_path(project, id) + + expect(response).to have_gitlab_http_status(:ok) + end + + it 'sets alert id from the route' do + get project_alert_management_alert_path(project, id) + + expect(assigns(:alert_id)).to eq(id.to_s) + end + end + + context 'when user is unauthorized' do + let(:user) { reporter } + + it 'shows 404' do + get project_alert_management_alert_path(project, id) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end +end diff --git a/spec/requests/projects/incidents_controller_spec.rb b/spec/requests/projects/incidents_controller_spec.rb new file mode 100644 index 00000000000..9a0d6cdf8ce --- /dev/null +++ b/spec/requests/projects/incidents_controller_spec.rb @@ -0,0 +1,116 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Projects::IncidentsController, feature_category: :incident_management do + let_it_be_with_refind(:project) { create(:project) } + let_it_be(:developer) { create(:user) } + let_it_be(:guest) { create(:user) } + let_it_be(:anonymous) { nil } + + before_all do + project.add_guest(guest) + project.add_developer(developer) + end + + before do + sign_in(user) if user + end + + subject { make_request } + + shared_examples 'not found' do + include_examples 'returning response status', :not_found + end + + shared_examples 'login required' do + it 'redirects to the login page' do + subject + + expect(response).to redirect_to(new_user_session_path) + end + end + + describe 'GET #index' do + def make_request + get project_incidents_path(project) + end + + let(:user) { developer } + + it 'shows the page' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to render_template(:index) + end + + context 'when user is unauthorized' do + let(:user) { anonymous } + + it_behaves_like 'login required' + end + + context 'when user is a guest' do + let(:user) { guest } + + it 'shows the page' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to render_template(:index) + end + end + end + + describe 'GET #show' do + def make_request + get incident_project_issues_path(project, resource) + end + + let_it_be(:resource) { create(:incident, project: project) } + + let(:user) { developer } + + it 'renders incident page' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to render_template(:show) + + expect(assigns(:incident)).to be_present + expect(assigns(:incident).author.association(:status)).to be_loaded + expect(assigns(:issue)).to be_present + expect(assigns(:noteable)).to eq(assigns(:incident)) + end + + context 'with non existing id' do + let(:resource) { non_existing_record_id } + + it_behaves_like 'not found' + end + + context 'for issue' do + let_it_be(:resource) { create(:issue, project: project) } + + it_behaves_like 'not found' + end + + context 'when user is a guest' do + let(:user) { guest } + + it 'shows the page' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to render_template(:show) + end + end + + context 'when unauthorized' do + let(:user) { anonymous } + + it_behaves_like 'login required' + end + end +end diff --git a/spec/requests/projects/issues_controller_spec.rb b/spec/requests/projects/issues_controller_spec.rb index 583fd5f586e..1ae65939c86 100644 --- a/spec/requests/projects/issues_controller_spec.rb +++ b/spec/requests/projects/issues_controller_spec.rb @@ -17,6 +17,11 @@ RSpec.describe Projects::IssuesController, feature_category: :team_planning do describe 'GET #new' do include_context 'group project issue' + before do + group.add_developer(user) + login_as(user) + end + it_behaves_like "observability csp policy", described_class do let(:tested_path) do new_project_issue_path(project) @@ -26,11 +31,13 @@ RSpec.describe Projects::IssuesController, feature_category: :team_planning do describe 'GET #show' do before do + group.add_developer(user) login_as(user) end it_behaves_like "observability csp policy", described_class do include_context 'group project issue' + let(:tested_path) do project_issue_path(project, issue) end diff --git a/spec/requests/projects/merge_requests/creations_spec.rb b/spec/requests/projects/merge_requests/creations_spec.rb index ace6ef0f7b8..e8a073fef5f 100644 --- a/spec/requests/projects/merge_requests/creations_spec.rb +++ b/spec/requests/projects/merge_requests/creations_spec.rb @@ -6,8 +6,13 @@ RSpec.describe 'merge requests creations', feature_category: :code_review_workfl describe 'GET /:namespace/:project/merge_requests/new' do include ProjectForksHelper - let(:project) { create(:project, :repository) } - let(:user) { project.first_owner } + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project, :repository, group: group) } + let_it_be(:user) { create(:user) } + + before_all do + group.add_developer(user) + end before do login_as(user) @@ -26,16 +31,13 @@ RSpec.describe 'merge requests creations', feature_category: :code_review_workfl end it_behaves_like "observability csp policy", Projects::MergeRequests::CreationsController do - let_it_be(:group) { create(:group) } - let_it_be(:user) { create(:user) } - let_it_be(:project) { create(:project, group: group) } let(:tested_path) do project_new_merge_request_path(project, merge_request: { title: 'Some feature', - source_branch: 'fix', - target_branch: 'feature', - target_project: project, - source_project: project + source_branch: 'fix', + target_branch: 'feature', + target_project: project, + source_project: project }) end end diff --git a/spec/requests/projects/merge_requests_controller_spec.rb b/spec/requests/projects/merge_requests_controller_spec.rb index 955e6822211..955b6e53686 100644 --- a/spec/requests/projects/merge_requests_controller_spec.rb +++ b/spec/requests/projects/merge_requests_controller_spec.rb @@ -16,6 +16,7 @@ RSpec.describe Projects::MergeRequestsController, feature_category: :source_code context 'when logged in' do before do + group.add_developer(user) login_as(user) end diff --git a/spec/requests/projects/ml/candidates_controller_spec.rb b/spec/requests/projects/ml/candidates_controller_spec.rb index eec7af99063..4c7491970e1 100644 --- a/spec/requests/projects/ml/candidates_controller_spec.rb +++ b/spec/requests/projects/ml/candidates_controller_spec.rb @@ -10,13 +10,17 @@ RSpec.describe Projects::Ml::CandidatesController, feature_category: :mlops do let(:ff_value) { true } let(:candidate_iid) { candidate.iid } - let(:model_experiments_enabled) { true } + let(:read_model_experiments) { true } + let(:write_model_experiments) { true } before do allow(Ability).to receive(:allowed?).and_call_original allow(Ability).to receive(:allowed?) .with(user, :read_model_experiments, project) - .and_return(model_experiments_enabled) + .and_return(read_model_experiments) + allow(Ability).to receive(:allowed?) + .with(user, :write_model_experiments, project) + .and_return(write_model_experiments) sign_in(user) end @@ -34,9 +38,9 @@ RSpec.describe Projects::Ml::CandidatesController, feature_category: :mlops do end end - shared_examples '404 when model experiments is unavailable' do + shared_examples 'requires read_model_experiments' do context 'when user does not have access' do - let(:model_experiments_enabled) { false } + let(:read_model_experiments) { false } it_behaves_like 'renders 404' end @@ -61,7 +65,7 @@ RSpec.describe Projects::Ml::CandidatesController, feature_category: :mlops do end it_behaves_like '404 if candidate does not exist' - it_behaves_like '404 when model experiments is unavailable' + it_behaves_like 'requires read_model_experiments' end describe 'DELETE #destroy' do @@ -83,7 +87,14 @@ RSpec.describe Projects::Ml::CandidatesController, feature_category: :mlops do end it_behaves_like '404 if candidate does not exist' - it_behaves_like '404 when model experiments is unavailable' + + describe 'requires write_model_experiments' do + let(:write_model_experiments) { false } + + it 'is 404' do + expect(response).to have_gitlab_http_status(:not_found) + end + end end private diff --git a/spec/requests/projects/ml/experiments_controller_spec.rb b/spec/requests/projects/ml/experiments_controller_spec.rb index e2d26e84f75..9440c716640 100644 --- a/spec/requests/projects/ml/experiments_controller_spec.rb +++ b/spec/requests/projects/ml/experiments_controller_spec.rb @@ -15,13 +15,17 @@ RSpec.describe Projects::Ml::ExperimentsController, feature_category: :mlops do let(:ff_value) { true } let(:basic_params) { { namespace_id: project.namespace.to_param, project_id: project } } let(:experiment_iid) { experiment.iid } - let(:model_experiments_enabled) { true } + let(:read_model_experiments) { true } + let(:write_model_experiments) { true } before do allow(Ability).to receive(:allowed?).and_call_original allow(Ability).to receive(:allowed?) .with(user, :read_model_experiments, project) - .and_return(model_experiments_enabled) + .and_return(read_model_experiments) + allow(Ability).to receive(:allowed?) + .with(user, :write_model_experiments, project) + .and_return(write_model_experiments) sign_in(user) end @@ -40,9 +44,9 @@ RSpec.describe Projects::Ml::ExperimentsController, feature_category: :mlops do end end - shared_examples '404 when model experiments is unavailable' do + shared_examples 'requires read_model_experiments' do context 'when user does not have access' do - let(:model_experiments_enabled) { false } + let(:read_model_experiments) { false } it_behaves_like 'renders 404' end @@ -100,7 +104,7 @@ RSpec.describe Projects::Ml::ExperimentsController, feature_category: :mlops do end end - it_behaves_like '404 when model experiments is unavailable' do + it_behaves_like 'requires read_model_experiments' do before do list_experiments end @@ -211,7 +215,7 @@ RSpec.describe Projects::Ml::ExperimentsController, feature_category: :mlops do end it_behaves_like '404 if experiment does not exist' - it_behaves_like '404 when model experiments is unavailable' + it_behaves_like 'requires read_model_experiments' end end @@ -243,7 +247,7 @@ RSpec.describe Projects::Ml::ExperimentsController, feature_category: :mlops do end it_behaves_like '404 if experiment does not exist' - it_behaves_like '404 when model experiments is unavailable' + it_behaves_like 'requires read_model_experiments' end end end @@ -268,7 +272,14 @@ RSpec.describe Projects::Ml::ExperimentsController, feature_category: :mlops do end it_behaves_like '404 if experiment does not exist' - it_behaves_like '404 when model experiments is unavailable' + + describe 'requires write_model_experiments' do + let(:write_model_experiments) { false } + + it 'is 404' do + expect(response).to have_gitlab_http_status(:not_found) + end + end end private diff --git a/spec/requests/projects/ml/models_controller_spec.rb b/spec/requests/projects/ml/models_controller_spec.rb new file mode 100644 index 00000000000..d03748c8dff --- /dev/null +++ b/spec/requests/projects/ml/models_controller_spec.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Projects::Ml::ModelsController, feature_category: :mlops do + let_it_be(:project) { create(:project, :repository) } + let_it_be(:user) { project.first_owner } + let_it_be(:model1_a) { create(:ml_model_package, project: project) } + let_it_be(:model1_b) { create(:ml_model_package, project: project, name: model1_a.name) } + let_it_be(:model2) { create(:ml_model_package, project: project) } + + let(:model_registry_enabled) { true } + + before do + allow(Ability).to receive(:allowed?).and_call_original + allow(Ability).to receive(:allowed?) + .with(user, :read_model_registry, project) + .and_return(model_registry_enabled) + + sign_in(user) + end + + describe 'GET index' do + subject(:index_request) do + list_models + response + end + + it 'renders the template' do + expect(index_request).to render_template('projects/ml/models/index') + end + + it 'fetches the models using the finder' do + expect(::Projects::Ml::ModelFinder).to receive(:new).with(project).and_call_original + + index_request + end + + it 'prepares model view using the presenter' do + expect(::Ml::ModelsIndexPresenter).to receive(:new).and_call_original + + index_request + end + + it 'does not perform N+1 sql queries' do + control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) { list_models } + + create_list(:ml_model_package, 4, project: project) + + expect { list_models }.not_to exceed_all_query_limit(control_count) + end + + context 'when user does not have access' do + let(:model_registry_enabled) { false } + + it 'renders 404' do + is_expected.to have_gitlab_http_status(:not_found) + end + end + end + + private + + def list_models + get project_ml_models_path(project) + end +end diff --git a/spec/requests/projects/packages/package_files_controller_spec.rb b/spec/requests/projects/packages/package_files_controller_spec.rb index e5849be9f13..4f1793b831d 100644 --- a/spec/requests/projects/packages/package_files_controller_spec.rb +++ b/spec/requests/projects/packages/package_files_controller_spec.rb @@ -22,7 +22,7 @@ RSpec.describe Projects::Packages::PackageFilesController, feature_category: :pa subject expect(response.headers['Content-Disposition']) - .to eq(%Q(attachment; filename="#{filename}"; filename*=UTF-8''#{filename})) + .to eq(%(attachment; filename="#{filename}"; filename*=UTF-8''#{filename})) end it_behaves_like 'bumping the package last downloaded at field' diff --git a/spec/requests/projects/service_desk/custom_email_controller_spec.rb b/spec/requests/projects/service_desk/custom_email_controller_spec.rb new file mode 100644 index 00000000000..8ce238ab99c --- /dev/null +++ b/spec/requests/projects/service_desk/custom_email_controller_spec.rb @@ -0,0 +1,380 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Projects::ServiceDesk::CustomEmailController, feature_category: :service_desk do + let_it_be_with_reload(:project) do + create(:project, :private, service_desk_enabled: true) + end + + let_it_be(:custom_email_path) { project_service_desk_custom_email_path(project, format: :json) } + let_it_be(:user) { create(:user) } + let_it_be(:illegitimite_user) { create(:user) } + + let(:message) { instance_double(Mail::Message) } + let(:error_cannot_create_custom_email) { s_("ServiceDesk|Cannot create custom email") } + let(:error_cannot_update_custom_email) { s_("ServiceDesk|Cannot update custom email") } + let(:error_does_not_exist) { s_('ServiceDesk|Custom email does not exist') } + let(:error_custom_email_exists) { s_('ServiceDesk|Custom email already exists') } + + let(:custom_email_params) do + { + custom_email: 'user@example.com', + smtp_address: 'smtp.example.com', + smtp_port: '587', + smtp_username: 'user@example.com', + smtp_password: 'supersecret' + } + end + + let(:empty_json_response) do + { + "custom_email" => nil, + "custom_email_enabled" => false, + "custom_email_verification_state" => nil, + "custom_email_verification_error" => nil, + "custom_email_smtp_address" => nil, + "error_message" => nil + } + end + + before_all do + project.add_developer(illegitimite_user) + project.add_maintainer(user) + end + + shared_examples 'a json response with empty values' do + it 'returns json response with empty values' do + perform_request + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to include(empty_json_response) + end + end + + shared_examples 'a controller that responds with status' do |status| + it "responds with #{status} for GET custom email" do + get custom_email_path + expect(response).to have_gitlab_http_status(status) + end + + it "responds with #{status} for POST custom email" do + post custom_email_path + expect(response).to have_gitlab_http_status(status) + end + + it "responds with #{status} for PUT custom email" do + put custom_email_path + expect(response).to have_gitlab_http_status(status) + end + + it "responds with #{status} for DELETE custom email" do + delete custom_email_path + expect(response).to have_gitlab_http_status(status) + end + end + + shared_examples 'a controller with disabled feature flag with status' do |status| + context 'when feature flag service_desk_custom_email is disabled' do + before do + stub_feature_flags(service_desk_custom_email: false) + end + + it_behaves_like 'a controller that responds with status', status + end + end + + shared_examples 'a deletable resource' do + describe 'DELETE custom email' do + let(:perform_request) { delete custom_email_path } + + it_behaves_like 'a json response with empty values' + end + end + + context 'with legitimate user signed in' do + before do + sign_out(illegitimite_user) + sign_in(user) + end + + # because CustomEmailController check_feature_flag_enabled responds + it_behaves_like 'a controller with disabled feature flag with status', :not_found + + describe 'GET custom email' do + let(:perform_request) { get custom_email_path } + + it_behaves_like 'a json response with empty values' + end + + describe 'POST custom email' do + before do + # We send verification email directly + allow(message).to receive(:deliver) + allow(Notify).to receive(:service_desk_custom_email_verification_email).and_return(message) + end + + it 'adds custom email and kicks of verification' do + post custom_email_path, params: custom_email_params + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to include( + "custom_email" => custom_email_params[:custom_email], + "custom_email_enabled" => false, + "custom_email_verification_state" => "started", + "custom_email_verification_error" => nil, + "custom_email_smtp_address" => custom_email_params[:smtp_address], + "error_message" => nil + ) + end + + context 'when custom_email param is not valid' do + it 'does not add custom email' do + post custom_email_path, params: custom_email_params.merge(custom_email: 'useratexample.com') + + expect(response).to have_gitlab_http_status(:unprocessable_entity) + expect(json_response).to include( + empty_json_response.merge("error_message" => error_cannot_create_custom_email) + ) + end + end + + context 'when smtp_password param is not valid' do + it 'does not add custom email' do + post custom_email_path, params: custom_email_params.merge(smtp_password: '2short') + + expect(response).to have_gitlab_http_status(:unprocessable_entity) + expect(json_response).to include( + empty_json_response.merge("error_message" => error_cannot_create_custom_email) + ) + end + end + + context 'when the verification process fails fast' do + before do + # Could not establish connection, invalid host etc. + allow(message).to receive(:deliver).and_raise(SocketError) + end + + it 'adds custom email and kicks of verification and returns verification error state' do + post custom_email_path, params: custom_email_params + + # In terms of "custom email object creation", failing fast on the + # verification is a legit state that we don't treat as an error. + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to include( + "custom_email" => custom_email_params[:custom_email], + "custom_email_enabled" => false, + "custom_email_verification_state" => "failed", + "custom_email_verification_error" => "smtp_host_issue", + "custom_email_smtp_address" => custom_email_params[:smtp_address], + "error_message" => nil + ) + end + end + end + + describe 'PUT custom email' do + let(:custom_email_params) { { custom_email_enabled: true } } + + it 'does not update records' do + put custom_email_path, params: custom_email_params + + expect(response).to have_gitlab_http_status(:unprocessable_entity) + expect(json_response).to include( + empty_json_response.merge("error_message" => error_cannot_update_custom_email) + ) + end + end + + describe 'DELETE custom email' do + it 'does not touch any records' do + delete custom_email_path + + expect(response).to have_gitlab_http_status(:unprocessable_entity) + expect(json_response).to include( + empty_json_response.merge("error_message" => error_does_not_exist) + ) + end + end + + context 'when custom email is set up' do + let!(:settings) { create(:service_desk_setting, project: project, custom_email: 'user@example.com') } + let!(:credential) { create(:service_desk_custom_email_credential, project: project) } + + before do + project.reset + end + + context 'and verification started' do + let!(:verification) do + create(:service_desk_custom_email_verification, project: project) + end + + it_behaves_like 'a deletable resource' + + describe 'GET custom email' do + it 'returns custom email in its current state' do + get custom_email_path + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to include( + "custom_email" => "user@example.com", + "custom_email_enabled" => false, + "custom_email_verification_state" => "started", + "custom_email_verification_error" => nil, + "custom_email_smtp_address" => "smtp.example.com", + "error_message" => nil + ) + end + end + + describe 'POST custom email' do + it 'returns custom email in its current state' do + post custom_email_path, params: custom_email_params + + expect(response).to have_gitlab_http_status(:unprocessable_entity) + expect(json_response).to include( + "custom_email" => custom_email_params[:custom_email], + "custom_email_enabled" => false, + "custom_email_verification_state" => "started", + "custom_email_verification_error" => nil, + "custom_email_smtp_address" => custom_email_params[:smtp_address], + "error_message" => error_custom_email_exists + ) + end + end + + describe 'PUT custom email' do + let(:custom_email_params) { { custom_email_enabled: true } } + + it 'marks custom email as enabled' do + put custom_email_path, params: custom_email_params + + expect(response).to have_gitlab_http_status(:unprocessable_entity) + expect(json_response).to include( + "custom_email" => "user@example.com", + "custom_email_enabled" => false, + "custom_email_verification_state" => "started", + "custom_email_verification_error" => nil, + "custom_email_smtp_address" => "smtp.example.com", + "error_message" => error_cannot_update_custom_email + ) + end + end + end + + context 'and verification finished' do + let!(:verification) do + create(:service_desk_custom_email_verification, project: project, state: :finished, token: nil) + end + + it_behaves_like 'a deletable resource' + + describe 'GET custom email' do + it 'returns custom email in its current state' do + get custom_email_path + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to include( + "custom_email" => "user@example.com", + "custom_email_enabled" => false, + "custom_email_verification_state" => "finished", + "custom_email_verification_error" => nil, + "custom_email_smtp_address" => "smtp.example.com", + "error_message" => nil + ) + end + end + + describe 'PUT custom email' do + let(:custom_email_params) { { custom_email_enabled: true } } + + it 'marks custom email as enabled' do + put custom_email_path, params: custom_email_params + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to include( + "custom_email" => "user@example.com", + "custom_email_enabled" => true, + "custom_email_verification_state" => "finished", + "custom_email_verification_error" => nil, + "custom_email_smtp_address" => "smtp.example.com", + "error_message" => nil + ) + end + end + end + + context 'and verification failed' do + let!(:verification) do + create(:service_desk_custom_email_verification, + project: project, + state: :failed, + token: nil, + error: :smtp_host_issue + ) + end + + it_behaves_like 'a deletable resource' + + describe 'GET custom email' do + it 'returns custom email in its current state' do + get custom_email_path + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to include( + "custom_email" => "user@example.com", + "custom_email_enabled" => false, + "custom_email_verification_state" => "failed", + "custom_email_verification_error" => "smtp_host_issue", + "custom_email_smtp_address" => "smtp.example.com", + "error_message" => nil + ) + end + end + + describe 'PUT custom email' do + let(:custom_email_params) { { custom_email_enabled: true } } + + it 'does not mark custom email as enabled' do + put custom_email_path, params: custom_email_params + + expect(response).to have_gitlab_http_status(:unprocessable_entity) + expect(json_response).to include( + "custom_email" => "user@example.com", + "custom_email_enabled" => false, + "custom_email_verification_state" => "failed", + "custom_email_verification_error" => "smtp_host_issue", + "custom_email_smtp_address" => "smtp.example.com", + "error_message" => error_cannot_update_custom_email + ) + end + end + end + end + end + + context 'when user is anonymous' do + before do + sign_out(user) + sign_out(illegitimite_user) + end + + # because Projects::ApplicationController :authenticate_user! responds + # with redirect to login page + it_behaves_like 'a controller that responds with status', :found + it_behaves_like 'a controller with disabled feature flag with status', :found + end + + context 'with illegitimate user signed in' do + before do + sign_out(user) + sign_in(illegitimite_user) + end + + it_behaves_like 'a controller that responds with status', :not_found + # because CustomEmailController check_feature_flag_enabled responds + it_behaves_like 'a controller with disabled feature flag with status', :not_found + end +end diff --git a/spec/requests/projects/service_desk_controller_spec.rb b/spec/requests/projects/service_desk_controller_spec.rb new file mode 100644 index 00000000000..54fe176e244 --- /dev/null +++ b/spec/requests/projects/service_desk_controller_spec.rb @@ -0,0 +1,109 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Projects::ServiceDeskController, feature_category: :service_desk do + let_it_be(:project) do + create(:project, :private, :custom_repo, + service_desk_enabled: true, + files: { '.gitlab/issue_templates/service_desk.md' => 'template' }) + end + + let_it_be(:user) { create(:user) } + + before_all do + project.add_maintainer(user) + end + + before do + allow(Gitlab::Email::IncomingEmail).to receive(:enabled?).and_return(true) + allow(Gitlab::Email::IncomingEmail).to receive(:supports_wildcard?).and_return(true) + + sign_in(user) + end + + describe 'GET #show' do + it 'returns service_desk JSON data' do + get project_service_desk_path(project, format: :json) + + expect(json_response["service_desk_address"]).to match(/\A[^@]+@[^@]+\z/) + expect(json_response["service_desk_enabled"]).to be_truthy + expect(response).to have_gitlab_http_status(:ok) + end + + context 'when user is not project maintainer' do + let(:guest) { create(:user) } + + it 'renders 404' do + project.add_guest(guest) + sign_in(guest) + + get project_service_desk_path(project, format: :json) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'when issue template is present' do + it 'returns template_file_missing as false' do + create(:service_desk_setting, project: project, issue_template_key: 'service_desk') + + get project_service_desk_path(project, format: :json) + + response_hash = Gitlab::Json.parse(response.body) + expect(response_hash['template_file_missing']).to eq(false) + end + end + + context 'when issue template file becomes outdated' do + it 'returns template_file_missing as true' do + service = ServiceDeskSetting.new(project_id: project.id, issue_template_key: 'deleted') + service.save!(validate: false) + + get project_service_desk_path(project, format: :json) + + expect(json_response['template_file_missing']).to eq(true) + end + end + end + + describe 'PUT #update' do + it 'toggles services desk incoming email' do + project.update!(service_desk_enabled: false) + + put project_service_desk_refresh_path(project, format: :json), params: { service_desk_enabled: true } + + expect(json_response["service_desk_address"]).to be_present + expect(json_response["service_desk_enabled"]).to be_truthy + expect(response).to have_gitlab_http_status(:ok) + end + + it 'sets issue_template_key' do + put project_service_desk_refresh_path(project, format: :json), params: { issue_template_key: 'service_desk' } + + settings = project.service_desk_setting + expect(settings).to be_present + expect(settings.issue_template_key).to eq('service_desk') + expect(json_response['template_file_missing']).to eq(false) + expect(json_response['issue_template_key']).to eq('service_desk') + end + + it 'returns an error when update of service desk settings fails' do + put project_service_desk_refresh_path(project, format: :json), params: { issue_template_key: 'invalid key' } + + expect(response).to have_gitlab_http_status(:unprocessable_entity) + expect(json_response['message']).to eq('Issue template key is empty or does not exist') + end + + context 'when user cannot admin the project' do + let(:other_user) { create(:user) } + + it 'renders 404' do + sign_in(other_user) + put project_service_desk_refresh_path(project, format: :json), params: { service_desk_enabled: true } + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end +end diff --git a/spec/requests/projects/tracing_controller_spec.rb b/spec/requests/projects/tracing_controller_spec.rb new file mode 100644 index 00000000000..eecaa0d962a --- /dev/null +++ b/spec/requests/projects/tracing_controller_spec.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Projects::TracingController, feature_category: :tracing do + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project, group: group) } + let_it_be(:user) { create(:user) } + let(:path) { nil } + let(:observability_tracing_ff) { true } + + subject do + get path + response + end + + describe 'GET #index' do + before do + stub_feature_flags(observability_tracing: observability_tracing_ff) + sign_in(user) + end + + let(:path) { project_tracing_index_path(project) } + + it_behaves_like 'observability csp policy' do + before_all do + project.add_developer(user) + end + + let(:tested_path) { path } + end + + context 'when user does not have permissions' do + it 'returns 404' do + expect(subject).to have_gitlab_http_status(:not_found) + end + end + + context 'when user has permissions' do + before_all do + project.add_developer(user) + end + + it 'returns 200' do + expect(subject).to have_gitlab_http_status(:ok) + end + + it 'renders the js-tracing element correctly' do + element = Nokogiri::HTML.parse(subject.body).at_css('#js-tracing') + + expected_view_model = { + tracingUrl: Gitlab::Observability.tracing_url(project), + provisioningUrl: Gitlab::Observability.provisioning_url(project), + oauthUrl: Gitlab::Observability.oauth_url + }.to_json + expect(element.attributes['data-view-model'].value).to eq(expected_view_model) + end + + context 'when feature is disabled' do + let(:observability_tracing_ff) { false } + + it 'returns 404' do + expect(subject).to have_gitlab_http_status(:not_found) + end + end + end + end +end diff --git a/spec/requests/search_controller_spec.rb b/spec/requests/search_controller_spec.rb index f2d4e288ddc..365b20ad4aa 100644 --- a/spec/requests/search_controller_spec.rb +++ b/spec/requests/search_controller_spec.rb @@ -39,7 +39,8 @@ RSpec.describe SearchController, type: :request, feature_category: :global_searc context 'for issues scope' do let(:object) { :issue } - let(:creation_args) { { project: project, title: 'foo' } } + let(:labels) { create_list(:label, 3, project: project) } + let(:creation_args) { { project: project, title: 'foo', labels: labels } } let(:params) { { search: 'foo', scope: 'issues' } } # some N+1 queries still exist # each issue runs an extra query for group namespaces @@ -50,8 +51,9 @@ RSpec.describe SearchController, type: :request, feature_category: :global_searc context 'for merge_requests scope' do let(:creation_traits) { [:unique_branches] } + let(:labels) { create_list(:label, 3, project: project) } let(:object) { :merge_request } - let(:creation_args) { { source_project: project, title: 'bar' } } + let(:creation_args) { { source_project: project, title: 'bar', labels: labels } } let(:params) { { search: 'bar', scope: 'merge_requests' } } # some N+1 queries still exist # each merge request runs an extra query for project routes diff --git a/spec/requests/users_controller_spec.rb b/spec/requests/users_controller_spec.rb index c49dbb6a269..f96d7864782 100644 --- a/spec/requests/users_controller_spec.rb +++ b/spec/requests/users_controller_spec.rb @@ -598,15 +598,10 @@ RSpec.describe UsersController, feature_category: :user_management do expect(response).to have_gitlab_http_status(:ok) expect(response.body).not_to be_empty end - - it 'does not list projects aimed for deletion' do - expect(response).to have_gitlab_http_status(:ok) - expect(assigns(:contributed_projects)).to eq([project]) - end end %i(html json).each do |format| - context "format: #{format}" do + context "with format: #{format}" do let(:format) { format } context 'with public profile' do @@ -626,6 +621,13 @@ RSpec.describe UsersController, feature_category: :user_management do let(:user) { create(:admin) } it_behaves_like 'renders contributed projects' + + if format == :json + it 'does not list projects aimed for deletion' do + expect(response).to have_gitlab_http_status(:ok) + expect(response.body).not_to include aimed_for_deletion_project.name + end + end end end end @@ -652,15 +654,10 @@ RSpec.describe UsersController, feature_category: :user_management do expect(response).to have_gitlab_http_status(:ok) expect(response.body).not_to be_empty end - - it 'does not list projects aimed for deletion' do - expect(response).to have_gitlab_http_status(:ok) - expect(assigns(:starred_projects)).to eq([project]) - end end %i(html json).each do |format| - context "format: #{format}" do + context "with format: #{format}" do let(:format) { format } context 'with public profile' do @@ -680,6 +677,13 @@ RSpec.describe UsersController, feature_category: :user_management do let(:user) { create(:admin) } it_behaves_like 'renders starred projects' + + if format == :json + it 'does not list projects aimed for deletion' do + expect(response).to have_gitlab_http_status(:ok) + expect(response.body).not_to include aimed_for_deletion_project.name + end + end end end end diff --git a/spec/requests/verifies_with_email_spec.rb b/spec/requests/verifies_with_email_spec.rb index 6325ecc1184..f3f8e4a1a83 100644 --- a/spec/requests/verifies_with_email_spec.rb +++ b/spec/requests/verifies_with_email_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe 'VerifiesWithEmail', :clean_gitlab_redis_sessions, :clean_gitlab_redis_rate_limiting, -feature_category: :user_management do + feature_category: :instance_resiliency do include SessionHelpers include EmailHelpers -- cgit v1.2.3