diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-06-20 13:43:29 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-06-20 13:43:29 +0300 |
commit | 3b1af5cc7ed2666ff18b718ce5d30fa5a2756674 (patch) | |
tree | 3bc4a40e0ee51ec27eabf917c537033c0c5b14d4 /spec/requests | |
parent | 9bba14be3f2c211bf79e15769cd9b77bc73a13bc (diff) |
Add latest changes from gitlab-org/gitlab@16-1-stable-eev16.1.0-rc42
Diffstat (limited to 'spec/requests')
140 files changed, 2754 insertions, 999 deletions
diff --git a/spec/requests/abuse_reports_controller_spec.rb b/spec/requests/abuse_reports_controller_spec.rb index 0b9cf24230d..622d133158f 100644 --- a/spec/requests/abuse_reports_controller_spec.rb +++ b/spec/requests/abuse_reports_controller_spec.rb @@ -90,16 +90,16 @@ RSpec.describe AbuseReportsController, feature_category: :insider_threat do end end - context 'when the user has already been blocked' do + context 'when the user has already been banned' do let(:request_params) { { user_id: user.id, abuse_report: { category: abuse_category } } } it 'redirects the reporter to the user\'s profile' do - user.block + user.ban subject expect(response).to redirect_to user - expect(flash[:alert]).to eq(_('Cannot create the abuse report. This user has been blocked.')) + expect(flash[:alert]).to eq(_('Cannot create the abuse report. This user has been banned.')) end end end diff --git a/spec/requests/admin/abuse_reports_controller_spec.rb b/spec/requests/admin/abuse_reports_controller_spec.rb index 0b5aaabaa61..8d033a2e147 100644 --- a/spec/requests/admin/abuse_reports_controller_spec.rb +++ b/spec/requests/admin/abuse_reports_controller_spec.rb @@ -57,13 +57,46 @@ RSpec.describe Admin::AbuseReportsController, type: :request, feature_category: let(:report) { create(:abuse_report) } let(:params) { { user_action: 'block_user', close: 'true', reason: 'spam', comment: 'obvious spam' } } let(:expected_params) { ActionController::Parameters.new(params).permit! } + let(:message) { 'Service response' } + + subject(:request) { put admin_abuse_report_path(report, params) } it 'invokes the Admin::AbuseReportUpdateService' do expect_next_instance_of(Admin::AbuseReportUpdateService, report, admin, expected_params) do |service| - expect(service).to receive(:execute) + expect(service).to receive(:execute).and_call_original end - put admin_abuse_report_path(report, params) + request + end + + context 'when the service response is a success' do + before do + allow_next_instance_of(Admin::AbuseReportUpdateService, report, admin, expected_params) do |service| + allow(service).to receive(:execute).and_return(ServiceResponse.success(message: message)) + end + + request + end + + it 'returns the service response message with a success status' do + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['message']).to eq(message) + end + end + + context 'when the service response is an error' do + before do + allow_next_instance_of(Admin::AbuseReportUpdateService, report, admin, expected_params) do |service| + allow(service).to receive(:execute).and_return(ServiceResponse.error(message: message)) + end + + request + end + + it 'returns the service response message with a failed status' do + expect(response).to have_gitlab_http_status(:unprocessable_entity) + expect(json_response['message']).to eq(message) + end end end diff --git a/spec/requests/admin/hook_logs_controller_spec.rb b/spec/requests/admin/hook_logs_controller_spec.rb index fa9f317dbba..d0146927943 100644 --- a/spec/requests/admin/hook_logs_controller_spec.rb +++ b/spec/requests/admin/hook_logs_controller_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Admin::HookLogsController, :enable_admin_mode, feature_category: :integrations do +RSpec.describe Admin::HookLogsController, :enable_admin_mode, feature_category: :webhooks do let_it_be(:user) { create(:admin) } let_it_be_with_refind(:web_hook) { create(:system_hook) } let_it_be_with_refind(:web_hook_log) { create(:web_hook_log, web_hook: web_hook) } diff --git a/spec/requests/admin/projects_controller_spec.rb b/spec/requests/admin/projects_controller_spec.rb index 2462152b7c2..16e1827b912 100644 --- a/spec/requests/admin/projects_controller_spec.rb +++ b/spec/requests/admin/projects_controller_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Admin::ProjectsController, :enable_admin_mode, feature_category: :projects do +RSpec.describe Admin::ProjectsController, :enable_admin_mode, feature_category: :groups_and_projects do let_it_be(:project) { create(:project, :public, name: 'test', description: 'test') } let_it_be(:admin) { create(:admin) } diff --git a/spec/requests/api/admin/batched_background_migrations_spec.rb b/spec/requests/api/admin/batched_background_migrations_spec.rb index e88fba3fbe7..180b6c7abd6 100644 --- a/spec/requests/api/admin/batched_background_migrations_spec.rb +++ b/spec/requests/api/admin/batched_background_migrations_spec.rb @@ -50,6 +50,27 @@ RSpec.describe API::Admin::BatchedBackgroundMigrations, feature_category: :datab show_migration end + + context 'when migration has completed jobs' do + let(:migration) do + Gitlab::Database::SharedModel.using_connection(ci_model.connection) do + create(:batched_background_migration, :active, total_tuple_count: 100) + end + end + + let!(:batched_job) do + Gitlab::Database::SharedModel.using_connection(ci_model.connection) do + create(:batched_background_migration_job, :succeeded, batched_migration: migration, batch_size: 8) + end + end + + it 'calculates the progress using the CI database' do + show_migration + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['progress']).to eq(8) + end + end end context 'when the database name does not exist' do diff --git a/spec/requests/api/admin/dictionary_spec.rb b/spec/requests/api/admin/dictionary_spec.rb new file mode 100644 index 00000000000..effd3572423 --- /dev/null +++ b/spec/requests/api/admin/dictionary_spec.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::Admin::Dictionary, feature_category: :database do + let(:admin) { create(:admin) } + let(:path) { "/admin/databases/main/dictionary/tables/achievements" } + + describe 'GET admin/databases/:database_name/dictionary/tables/:table_name' do + it_behaves_like "GET request permissions for admin mode" + + subject(:show_table_dictionary) do + get api(path, admin, admin_mode: true) + end + + context 'when the database does not exist' do + it 'returns bad request' do + get api("/admin/databases/#{non_existing_record_id}/dictionary/tables/achievements", admin, admin_mode: true) + + expect(response).to have_gitlab_http_status(:bad_request) + end + end + + context 'when the table does not exist' do + it 'returns not found' do + get api("/admin/databases/main/dictionary/tables/#{non_existing_record_id}", admin, admin_mode: true) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'with a malicious table_name' do + it 'returns an error' do + get api("/admin/databases/main/dictionary/tables/%2E%2E%2Fpasswords.yml", admin, admin_mode: true) + + expect(response).to have_gitlab_http_status(:error) + end + end + + context 'when the params are correct' do + let(:dictionary_dir) { Rails.root.join('spec/fixtures') } + let(:path_file) { Rails.root.join(dictionary_dir, 'achievements.yml') } + + it 'fetches the table dictionary' do + allow(Gitlab::Database::GitlabSchema).to receive(:dictionary_paths).and_return([dictionary_dir]) + + expect(Gitlab::PathTraversal).to receive(:check_allowed_absolute_path_and_path_traversal!).twice.with( + path_file.to_s, [dictionary_dir.to_s]).and_call_original + + show_table_dictionary + + aggregate_failures "testing response" do + expect(json_response['table_name']).to eq('achievements') + expect(json_response['feature_categories']).to eq(['feature_category_example']) + end + end + end + end +end diff --git a/spec/requests/api/admin/migrations_spec.rb b/spec/requests/api/admin/migrations_spec.rb new file mode 100644 index 00000000000..fc464300b56 --- /dev/null +++ b/spec/requests/api/admin/migrations_spec.rb @@ -0,0 +1,89 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::Admin::Migrations, feature_category: :database do + let(:admin) { create(:admin) } + + describe 'POST /admin/migrations/:version/mark' do + let(:database) { :main } + let(:params) { { database: database } } + let(:connection) { ApplicationRecord.connection } + let(:path) { "/admin/migrations/#{version}/mark" } + let(:version) { 1 } + + subject(:mark) do + post api(path, admin, admin_mode: true), params: params + end + + context 'when the migration exists' do + before do + double = instance_double( + Database::MarkMigrationService, + execute: ServiceResponse.success) + + allow(Database::MarkMigrationService) + .to receive(:new) + .with(connection: connection, version: version) + .and_return(double) + end + + it_behaves_like "POST request permissions for admin mode" + + it 'marks the migration as successful' do + mark + + expect(response).to have_gitlab_http_status(:created) + end + end + + context 'when the migration does not exist' do + let(:version) { 123 } + + it 'returns 404' do + mark + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'when the migration was already executed' do + let(:version) { connection.migration_context.current_version } + + it 'returns 422' do + mark + + expect(response).to have_gitlab_http_status(:unprocessable_entity) + end + end + + context 'when multiple database is enabled' do + let(:ci_model) { Ci::ApplicationRecord } + let(:database) { :ci } + + before do + skip_if_multiple_databases_not_setup(:ci) + end + + it 'uses the correct connection' do + expect(Database::MarkMigrationService) + .to receive(:new) + .with(connection: ci_model.connection, version: version) + .and_call_original + + mark + end + + context 'when the database name does not exist' do + let(:database) { :wrong_database } + + it 'returns bad request', :aggregate_failures do + mark + + expect(response).to have_gitlab_http_status(:bad_request) + expect(response.body).to include('database does not have a valid value') + end + end + end + end +end diff --git a/spec/requests/api/admin/plan_limits_spec.rb b/spec/requests/api/admin/plan_limits_spec.rb index 6085b48c7c2..cad1111b76b 100644 --- a/spec/requests/api/admin/plan_limits_spec.rb +++ b/spec/requests/api/admin/plan_limits_spec.rb @@ -99,9 +99,11 @@ RSpec.describe API::Admin::PlanLimits, 'PlanLimits', feature_category: :shared d 'ci_registered_group_runners': 107, 'ci_registered_project_runners': 108, 'conan_max_file_size': 10, + 'enforcement_limit': 15, 'generic_packages_max_file_size': 20, 'helm_max_file_size': 25, 'maven_max_file_size': 30, + 'notification_limit': 90, 'npm_max_file_size': 40, 'nuget_max_file_size': 50, 'pypi_max_file_size': 60, @@ -119,9 +121,11 @@ RSpec.describe API::Admin::PlanLimits, 'PlanLimits', feature_category: :shared d expect(json_response['ci_registered_group_runners']).to eq(107) expect(json_response['ci_registered_project_runners']).to eq(108) expect(json_response['conan_max_file_size']).to eq(10) + 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['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) expect(json_response['nuget_max_file_size']).to eq(50) expect(json_response['pypi_max_file_size']).to eq(60) @@ -163,9 +167,11 @@ RSpec.describe API::Admin::PlanLimits, 'PlanLimits', feature_category: :shared d 'ci_registered_group_runners': 't', 'ci_registered_project_runners': 's', 'conan_max_file_size': 'a', + 'enforcement_limit': 'e', 'generic_packages_max_file_size': 'b', 'helm_max_file_size': 'h', 'maven_max_file_size': 'c', + 'notification_limit': 'n', 'npm_max_file_size': 'd', 'nuget_max_file_size': 'e', 'pypi_max_file_size': 'f', @@ -184,9 +190,11 @@ RSpec.describe API::Admin::PlanLimits, 'PlanLimits', feature_category: :shared d 'ci_registered_group_runners is invalid', 'ci_registered_project_runners is invalid', 'conan_max_file_size is invalid', + 'enforcement_limit is invalid', 'generic_packages_max_file_size is invalid', 'helm_max_file_size is invalid', 'maven_max_file_size is invalid', + 'notification_limit is invalid', 'npm_max_file_size is invalid', 'nuget_max_file_size is invalid', 'pypi_max_file_size is invalid', diff --git a/spec/requests/api/api_spec.rb b/spec/requests/api/api_spec.rb index 219c7dbdbc5..01bb8101f76 100644 --- a/spec/requests/api/api_spec.rb +++ b/spec/requests/api/api_spec.rb @@ -371,10 +371,10 @@ RSpec.describe API::API, feature_category: :system_access do ) end - it 'returns 429 status with exhausted' do + it 'returns 503 status and Retry-After header' do get api("/projects/#{project.id}/repository/commits", user) - expect(response).to have_gitlab_http_status(:too_many_requests) + expect(response).to have_gitlab_http_status(:service_unavailable) expect(response.headers['Retry-After']).to be(50) expect(json_response).to eql( 'message' => 'Upstream Gitaly has been exhausted. Try again later' diff --git a/spec/requests/api/badges_spec.rb b/spec/requests/api/badges_spec.rb index 1c09c1129a2..0b340b95b20 100644 --- a/spec/requests/api/badges_spec.rb +++ b/spec/requests/api/badges_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe API::Badges, feature_category: :projects do +RSpec.describe API::Badges, feature_category: :groups_and_projects do let(:maintainer) { create(:user, username: 'maintainer_user') } let(:developer) { create(:user) } let(:access_requester) { create(:user) } diff --git a/spec/requests/api/ci/runner/jobs_artifacts_spec.rb b/spec/requests/api/ci/runner/jobs_artifacts_spec.rb index 596af1110cc..2e0be23ba90 100644 --- a/spec/requests/api/ci/runner/jobs_artifacts_spec.rb +++ b/spec/requests/api/ci/runner/jobs_artifacts_spec.rb @@ -137,6 +137,10 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego let(:send_request) { subject } end + it_behaves_like 'runner migrations backoff' do + let(:request) { subject } + end + it "doesn't update runner info" do expect { subject }.not_to change { runner.reload.contacted_at } end @@ -177,18 +181,6 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego expect(json_response['RemoteObject']['SkipDelete']).to eq(true) expect(json_response['MaximumSize']).not_to be_nil end - - context 'when ci_artifacts_upload_to_final_location flag is disabled' do - before do - stub_feature_flags(ci_artifacts_upload_to_final_location: false) - end - - it 'does not skip delete' do - subject - - expect(json_response['RemoteObject']['SkipDelete']).to eq(false) - end - end end context 'when direct upload is disabled' do @@ -299,6 +291,10 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego end end + it_behaves_like 'runner migrations backoff' do + let(:request) { upload_artifacts(file_upload, headers_with_token) } + end + it "doesn't update runner info" do expect { upload_artifacts(file_upload, headers_with_token) }.not_to change { runner.reload.contacted_at } end @@ -901,6 +897,10 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego let(:send_request) { download_artifact } end + it_behaves_like 'runner migrations backoff' do + let(:request) { download_artifact } + end + it "doesn't update runner info" do expect { download_artifact }.not_to change { runner.reload.contacted_at } end diff --git a/spec/requests/api/ci/runner/jobs_put_spec.rb b/spec/requests/api/ci/runner/jobs_put_spec.rb index ab7ab4e74f8..65489ea7015 100644 --- a/spec/requests/api/ci/runner/jobs_put_spec.rb +++ b/spec/requests/api/ci/runner/jobs_put_spec.rb @@ -38,6 +38,10 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego let(:send_request) { update_job(state: 'success') } end + it_behaves_like 'runner migrations backoff' do + let(:request) { update_job(state: 'success') } + end + it 'updates runner info' do expect { update_job(state: 'success') }.to change { runner.reload.contacted_at } .and change { runner_manager.reload.contacted_at } diff --git a/spec/requests/api/ci/runner/jobs_request_post_spec.rb b/spec/requests/api/ci/runner/jobs_request_post_spec.rb index 0164eda7680..ca57208eb1d 100644 --- a/spec/requests/api/ci/runner/jobs_request_post_spec.rb +++ b/spec/requests/api/ci/runner/jobs_request_post_spec.rb @@ -90,6 +90,10 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego end end + it_behaves_like 'runner migrations backoff' do + let(:request) { post api('/jobs/request') } + end + context 'when no token is provided' do it 'returns 400 error' do post api('/jobs/request') diff --git a/spec/requests/api/ci/runner/jobs_trace_spec.rb b/spec/requests/api/ci/runner/jobs_trace_spec.rb index de67cec0a27..ee00fc5a793 100644 --- a/spec/requests/api/ci/runner/jobs_trace_spec.rb +++ b/spec/requests/api/ci/runner/jobs_trace_spec.rb @@ -45,6 +45,10 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_trace_chunks, feature_catego let(:send_request) { patch_the_trace } end + it_behaves_like 'runner migrations backoff' do + let(:request) { patch_the_trace } + end + it 'updates runner info' do runner.update!(contacted_at: 1.year.ago) diff --git a/spec/requests/api/ci/runner/runners_delete_spec.rb b/spec/requests/api/ci/runner/runners_delete_spec.rb index 681dd4d701e..d1488828bad 100644 --- a/spec/requests/api/ci/runner/runners_delete_spec.rb +++ b/spec/requests/api/ci/runner/runners_delete_spec.rb @@ -21,6 +21,10 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego end describe 'DELETE /api/v4/runners' do + it_behaves_like 'runner migrations backoff' do + let(:request) { delete api('/runners') } + end + context 'when no token is provided' do it 'returns 400 error' do delete api('/runners') diff --git a/spec/requests/api/ci/runner/runners_post_spec.rb b/spec/requests/api/ci/runner/runners_post_spec.rb index a36ea2115cf..c5e49e9ac54 100644 --- a/spec/requests/api/ci/runner/runners_post_spec.rb +++ b/spec/requests/api/ci/runner/runners_post_spec.rb @@ -5,6 +5,10 @@ require 'spec_helper' RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_category: :runner_fleet do describe '/api/v4/runners' do describe 'POST /api/v4/runners' do + it_behaves_like 'runner migrations backoff' do + let(:request) { post api('/runners') } + end + context 'when no token is provided' do it 'returns 400 error' do post api('/runners') diff --git a/spec/requests/api/ci/runner/runners_reset_spec.rb b/spec/requests/api/ci/runner/runners_reset_spec.rb index 2d1e366e820..03cb6238fc1 100644 --- a/spec/requests/api/ci/runner/runners_reset_spec.rb +++ b/spec/requests/api/ci/runner/runners_reset_spec.rb @@ -19,6 +19,10 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego let_it_be(:group_runner) { create(:ci_runner, :group, groups: [group], token_expires_at: 1.day.from_now) } describe 'POST /runners/reset_authentication_token', :freeze_time do + it_behaves_like 'runner migrations backoff' do + let(:request) { post api("/runners/reset_authentication_token") } + end + context 'current token provided' do it "resets authentication token when token doesn't have an expiration" do expect do diff --git a/spec/requests/api/ci/runner/runners_verify_post_spec.rb b/spec/requests/api/ci/runner/runners_verify_post_spec.rb index f1b33826f5e..e6af61ca7e0 100644 --- a/spec/requests/api/ci/runner/runners_verify_post_spec.rb +++ b/spec/requests/api/ci/runner/runners_verify_post_spec.rb @@ -24,6 +24,10 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego subject(:verify) { post api('/runners/verify'), params: params } + it_behaves_like 'runner migrations backoff' do + let(:request) { verify } + end + context 'when no token is provided' do it 'returns 400 error' do post api('/runners/verify') diff --git a/spec/requests/api/ci/secure_files_spec.rb b/spec/requests/api/ci/secure_files_spec.rb index db12576154e..4e1afd66683 100644 --- a/spec/requests/api/ci/secure_files_spec.rb +++ b/spec/requests/api/ci/secure_files_spec.rb @@ -56,6 +56,26 @@ RSpec.describe API::Ci::SecureFiles, feature_category: :mobile_devops do end end + context 'when the feature is disabled at the instance level' do + before do + stub_config(ci_secure_files: { enabled: false }) + end + + it 'returns a 403 when attempting to upload a file' do + expect do + post api("/projects/#{project.id}/secure_files", maintainer), params: file_params + end.not_to change { project.secure_files.count } + + expect(response).to have_gitlab_http_status(:forbidden) + end + + it 'returns a 403 when downloading a file' do + get api("/projects/#{project.id}/secure_files", developer) + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + context 'when the flag is disabled' do it 'returns a 201 when uploading a file when the ci_secure_files_read_only feature flag is disabled' do expect do diff --git a/spec/requests/api/clusters/agent_tokens_spec.rb b/spec/requests/api/clusters/agent_tokens_spec.rb index 2647684c9f8..c18ebf7d044 100644 --- a/spec/requests/api/clusters/agent_tokens_spec.rb +++ b/spec/requests/api/clusters/agent_tokens_spec.rb @@ -162,6 +162,28 @@ RSpec.describe API::Clusters::AgentTokens, feature_category: :deployment_managem expect(response).to have_gitlab_http_status(:forbidden) end end + + context 'when the active agent tokens limit is reached' do + before do + # create an additional agent token to make it 2 + create(:cluster_agent_token, agent: agent) + end + + it 'returns a bad request (400) error' do + params = { + name: 'test-token', + description: 'Test description' + } + post(api("/projects/#{project.id}/cluster_agents/#{agent.id}/tokens", user), params: params) + + aggregate_failures "testing response" do + expect(response).to have_gitlab_http_status(:bad_request) + + error_message = json_response['message'] + expect(error_message).to eq('400 Bad request - An agent can have only two active tokens at a time') + end + end + end end describe 'DELETE /projects/:id/cluster_agents/:agent_id/tokens/:token_id' do diff --git a/spec/requests/api/debian_project_packages_spec.rb b/spec/requests/api/debian_project_packages_spec.rb index 030962044c6..b1566860ffc 100644 --- a/spec/requests/api/debian_project_packages_spec.rb +++ b/spec/requests/api/debian_project_packages_spec.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require 'spec_helper' RSpec.describe API::DebianProjectPackages, feature_category: :package_registry do diff --git a/spec/requests/api/deploy_keys_spec.rb b/spec/requests/api/deploy_keys_spec.rb index 18a9211df3e..30c345ef458 100644 --- a/spec/requests/api/deploy_keys_spec.rb +++ b/spec/requests/api/deploy_keys_spec.rb @@ -59,6 +59,17 @@ RSpec.describe API::DeployKeys, :aggregate_failures, feature_category: :continuo expect { make_api_request }.not_to exceed_all_query_limit(control) end + it 'avoids N+1 database queries', :use_sql_query_cache, :request_store do + create(:deploy_keys_project, :readonly_access, project: project2, deploy_key: deploy_key) + + control = ActiveRecord::QueryRecorder.new(skip_cached: false) { make_api_request } + + deploy_key2 = create(:deploy_key, public: true) + create(:deploy_keys_project, :readonly_access, project: project3, deploy_key: deploy_key2) + + expect { make_api_request }.not_to exceed_all_query_limit(control) + end + context 'when `public` parameter is `true`' do it 'only returns public deploy keys' do make_api_request({ public: true }) @@ -81,6 +92,21 @@ RSpec.describe API::DeployKeys, :aggregate_failures, feature_category: :continuo expect(response_projects_with_write_access[1]['id']).to eq(project3.id) end end + + context 'projects_with_readonly_access' do + let!(:deploy_keys_project2) { create(:deploy_keys_project, :readonly_access, project: project2, deploy_key: deploy_key) } + let!(:deploy_keys_project3) { create(:deploy_keys_project, :readonly_access, project: project3, deploy_key: deploy_key) } + + it 'returns projects with readonly access' do + make_api_request + + response_projects_with_readonly_access = json_response.first['projects_with_readonly_access'] + + expect(response_projects_with_readonly_access[0]['id']).to eq(project.id) + expect(response_projects_with_readonly_access[1]['id']).to eq(project2.id) + expect(response_projects_with_readonly_access[2]['id']).to eq(project3.id) + end + end end end @@ -103,6 +129,7 @@ RSpec.describe API::DeployKeys, :aggregate_failures, feature_category: :continuo expect(json_response).to be_an Array expect(json_response.first['title']).to eq(deploy_key.title) expect(json_response.first).not_to have_key(:projects_with_write_access) + expect(json_response.first).not_to have_key(:projects_with_readonly_access) end it 'returns multiple deploy keys without N + 1' do @@ -129,6 +156,7 @@ RSpec.describe API::DeployKeys, :aggregate_failures, feature_category: :continuo expect(json_response['title']).to eq(deploy_key.title) expect(json_response).not_to have_key(:projects_with_write_access) + expect(json_response).not_to have_key(:projects_with_readonly_access) end it 'returns 404 Not Found with invalid ID' do diff --git a/spec/requests/api/deployments_spec.rb b/spec/requests/api/deployments_spec.rb index 3ca54cd40d0..d7056adfcb6 100644 --- a/spec/requests/api/deployments_spec.rb +++ b/spec/requests/api/deployments_spec.rb @@ -38,19 +38,17 @@ RSpec.describe API::Deployments, feature_category: :continuous_delivery do end context 'with updated_at filters specified' do - it 'returns projects deployments with last update in specified datetime range' do - perform_request({ updated_before: 30.minutes.ago, updated_after: 90.minutes.ago, order_by: :updated_at }) + context 'when using `order_by=updated_at`' do + it 'returns projects deployments with last update in specified datetime range' do + perform_request({ updated_before: 30.minutes.ago, updated_after: 90.minutes.ago, order_by: :updated_at }) - expect(response).to have_gitlab_http_status(:ok) - expect(response).to include_pagination_headers - expect(json_response.first['id']).to eq(deployment_3.id) - end - - context 'when forbidden order_by is specified' do - before do - stub_feature_flags(deployments_raise_updated_at_inefficient_error_override: false) + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(json_response.first['id']).to eq(deployment_3.id) end + end + context 'when not using `order_by=updated_at`' do it 'returns an error' do perform_request({ updated_before: 30.minutes.ago, updated_after: 90.minutes.ago, order_by: :id }) diff --git a/spec/requests/api/doorkeeper_access_spec.rb b/spec/requests/api/doorkeeper_access_spec.rb index 888220c2251..8a21abf02e2 100644 --- a/spec/requests/api/doorkeeper_access_spec.rb +++ b/spec/requests/api/doorkeeper_access_spec.rb @@ -14,7 +14,7 @@ RSpec.describe 'doorkeeper access', feature_category: :system_access do end include_examples 'user login request with unique ip limit' do - def request + def gitlab_request get api('/user'), params: { access_token: token.plaintext_token } end end @@ -34,7 +34,7 @@ RSpec.describe 'doorkeeper access', feature_category: :system_access do end include_examples 'user login request with unique ip limit' do - def request + def gitlab_request get api('/user', user) end end diff --git a/spec/requests/api/error_tracking/collector_spec.rb b/spec/requests/api/error_tracking/collector_spec.rb deleted file mode 100644 index 6a3e71bc859..00000000000 --- a/spec/requests/api/error_tracking/collector_spec.rb +++ /dev/null @@ -1,233 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe API::ErrorTracking::Collector, feature_category: :error_tracking do - let_it_be(:project) { create(:project, :private) } - let_it_be(:setting) { create(:project_error_tracking_setting, :integrated, project: project) } - let_it_be(:client_key) { create(:error_tracking_client_key, project: project) } - - RSpec.shared_examples 'not found' do - it 'reponds with 404' do - subject - - expect(response).to have_gitlab_http_status(:not_found) - end - end - - RSpec.shared_examples 'bad request' do - it 'responds with 400' do - subject - - expect(response).to have_gitlab_http_status(:bad_request) - end - end - - RSpec.shared_examples 'successful request' do - it 'writes to the database and returns OK', :aggregate_failures do - expect { subject }.to change { ErrorTracking::ErrorEvent.count }.by(1) - expect(response).to have_gitlab_http_status(:ok) - end - end - - describe "POST /error_tracking/collector/api/:id/envelope" do - let_it_be(:raw_event) { fixture_file('error_tracking/event.txt') } - let_it_be(:url) { "/error_tracking/collector/api/#{project.id}/envelope" } - - let(:params) { raw_event } - let(:headers) { { 'X-Sentry-Auth' => "Sentry sentry_key=#{client_key.public_key}" } } - - subject { post api(url), params: params, headers: headers } - - it_behaves_like 'successful request' - - context 'intergrated error tracking feature flag is disabled' do - before do - stub_feature_flags(integrated_error_tracking: false) - end - - it_behaves_like 'not found' - end - - context 'error tracking feature is disabled' do - before do - setting.update!(enabled: false) - end - - it_behaves_like 'not found' - end - - context 'integrated error tracking is disabled' do - before do - setting.update!(integrated: false) - end - - it_behaves_like 'not found' - end - - context 'auth headers are missing' do - let(:headers) { {} } - - it_behaves_like 'bad request' - end - - context 'public key is wrong' do - let(:headers) { { 'X-Sentry-Auth' => "Sentry sentry_key=glet_1fedb514e17f4b958435093deb02048c" } } - - it_behaves_like 'not found' - end - - context 'public key is inactive' do - let(:client_key) { create(:error_tracking_client_key, :disabled, project: project) } - - it_behaves_like 'not found' - end - - context 'empty body' do - let(:params) { '' } - - it_behaves_like 'bad request' - end - - context 'unknown request type' do - let(:params) { fixture_file('error_tracking/unknown.txt') } - - it_behaves_like 'bad request' - end - - context 'transaction request type' do - let(:params) { fixture_file('error_tracking/transaction.txt') } - - it 'does nothing and returns ok' do - expect { subject }.not_to change { ErrorTracking::ErrorEvent.count } - - expect(response).to have_gitlab_http_status(:ok) - end - end - - context 'gzip body' do - let(:standard_headers) do - { - 'X-Sentry-Auth' => "Sentry sentry_key=#{client_key.public_key}", - 'HTTP_CONTENT_ENCODING' => 'gzip' - } - end - - let(:params) { ActiveSupport::Gzip.compress(raw_event) } - - context 'with application/x-sentry-envelope Content-Type' do - let(:headers) { standard_headers.merge({ 'CONTENT_TYPE' => 'application/x-sentry-envelope' }) } - - it_behaves_like 'successful request' - end - - context 'with unexpected Content-Type' do - let(:headers) { standard_headers.merge({ 'CONTENT_TYPE' => 'application/gzip' }) } - - it 'responds with 415' do - subject - - expect(response).to have_gitlab_http_status(:unsupported_media_type) - end - end - end - end - - describe "POST /error_tracking/collector/api/:id/store" do - let_it_be(:raw_event) { fixture_file('error_tracking/parsed_event.json') } - let_it_be(:url) { "/error_tracking/collector/api/#{project.id}/store" } - - let(:params) { raw_event } - let(:headers) { { 'X-Sentry-Auth' => "Sentry sentry_key=#{client_key.public_key}" } } - - subject { post api(url), params: params, headers: headers } - - it_behaves_like 'successful request' - - context 'empty headers' do - let(:headers) { {} } - - it_behaves_like 'bad request' - end - - context 'empty body' do - let(:params) { '' } - - it_behaves_like 'bad request' - end - - context 'body with string instead of json' do - let(:params) { '"********"' } - - it_behaves_like 'bad request' - end - - context 'collector fails with validation error' do - before do - allow(::ErrorTracking::CollectErrorService) - .to receive(:new).and_raise(Gitlab::ErrorTracking::ErrorRepository::DatabaseError) - end - - it_behaves_like 'bad request' - end - - context 'with platform field too long' do - let(:params) do - event = Gitlab::Json.parse(raw_event) - event['platform'] = 'a' * 256 - Gitlab::Json.dump(event) - end - - it_behaves_like 'bad request' - end - - context 'gzip body' do - let(:headers) do - { - 'X-Sentry-Auth' => "Sentry sentry_key=#{client_key.public_key}", - 'HTTP_CONTENT_ENCODING' => 'gzip', - 'CONTENT_TYPE' => 'application/json' - } - end - - let(:params) { ActiveSupport::Gzip.compress(raw_event) } - - it_behaves_like 'successful request' - end - - context 'body contains nullbytes' do - let_it_be(:raw_event) { fixture_file('error_tracking/parsed_event_nullbytes.json') } - - it_behaves_like 'successful request' - end - - context 'when JSON key transaction is empty string' do - let_it_be(:raw_event) { fixture_file('error_tracking/php_empty_transaction.json') } - - it_behaves_like 'successful request' - end - - context 'sentry_key as param and empty headers' do - let(:url) { "/error_tracking/collector/api/#{project.id}/store?sentry_key=#{sentry_key}" } - let(:headers) { {} } - - context 'key is wrong' do - let(:sentry_key) { 'glet_1fedb514e17f4b958435093deb02048c' } - - it_behaves_like 'not found' - end - - context 'key is empty' do - let(:sentry_key) { '' } - - it_behaves_like 'bad request' - end - - context 'key is correct' do - let(:sentry_key) { client_key.public_key } - - it_behaves_like 'successful request' - end - end - end -end diff --git a/spec/requests/api/features_spec.rb b/spec/requests/api/features_spec.rb index 9f1af746080..2571e3b1e6a 100644 --- a/spec/requests/api/features_spec.rb +++ b/spec/requests/api/features_spec.rb @@ -31,6 +31,8 @@ RSpec.describe API::Features, stub_feature_flags: false, feature_category: :feat end describe 'GET /features' do + let(:path) { '/features' } + let(:expected_features) do [ { @@ -74,28 +76,28 @@ RSpec.describe API::Features, stub_feature_flags: false, feature_category: :feat Feature.enable(known_feature_flag.name) end + it_behaves_like 'GET request permissions for admin mode' + it 'returns a 401 for anonymous users' do get api('/features') expect(response).to have_gitlab_http_status(:unauthorized) end - it 'returns a 403 for users' do - get api('/features', user) - - expect(response).to have_gitlab_http_status(:forbidden) - end - it 'returns the feature list for admins' do - get api('/features', admin) + get api('/features', admin, admin_mode: true) - expect(response).to have_gitlab_http_status(:ok) expect(json_response).to match_array(expected_features) end end describe 'POST /feature' do let(:feature_name) { known_feature_flag.name } + let(:path) { "/features/#{feature_name}" } + + it_behaves_like 'POST request permissions for admin mode' do + let(:params) { { value: 'true' } } + end # TODO: remove this shared examples block when set_feature_flag_service feature flag # is removed. Then remove also any duplicate specs covered by the service class. @@ -115,7 +117,7 @@ RSpec.describe API::Features, stub_feature_flags: false, feature_category: :feat context 'when passed value=true' do it 'creates an enabled feature' do - post api("/features/#{feature_name}", admin), params: { value: 'true' } + post api("/features/#{feature_name}", admin, admin_mode: true), params: { value: 'true' } expect(response).to have_gitlab_http_status(:created) expect(json_response).to match( @@ -129,11 +131,11 @@ RSpec.describe API::Features, stub_feature_flags: false, feature_category: :feat it 'logs the event' do expect(Feature.logger).to receive(:info).once - post api("/features/#{feature_name}", admin), params: { value: 'true' } + post api("/features/#{feature_name}", admin, admin_mode: true), params: { value: 'true' } end it 'creates an enabled feature for the given Flipper group when passed feature_group=perf_team' do - post api("/features/#{feature_name}", admin), params: { value: 'true', feature_group: 'perf_team' } + post api("/features/#{feature_name}", admin, admin_mode: true), params: { value: 'true', feature_group: 'perf_team' } expect(response).to have_gitlab_http_status(:created) expect(json_response).to match( @@ -148,7 +150,7 @@ RSpec.describe API::Features, stub_feature_flags: false, feature_category: :feat end it 'creates an enabled feature for the given user when passed user=username' do - post api("/features/#{feature_name}", admin), params: { value: 'true', user: user.username } + post api("/features/#{feature_name}", admin, admin_mode: true), params: { value: 'true', user: user.username } expect(response).to have_gitlab_http_status(:created) expect(json_response).to match( @@ -163,7 +165,7 @@ RSpec.describe API::Features, stub_feature_flags: false, feature_category: :feat end it 'creates an enabled feature for the given user and feature group when passed user=username and feature_group=perf_team' do - post api("/features/#{feature_name}", admin), params: { value: 'true', user: user.username, feature_group: 'perf_team' } + post api("/features/#{feature_name}", admin, admin_mode: true), params: { value: 'true', user: user.username, feature_group: 'perf_team' } expect(response).to have_gitlab_http_status(:created) expect(json_response['name']).to eq(feature_name) @@ -181,7 +183,7 @@ RSpec.describe API::Features, stub_feature_flags: false, feature_category: :feat let(:expected_inexistent_path) { actor_path } it 'returns the current state of the flag without changes' do - post api("/features/#{feature_name}", admin), params: { value: 'true', actor_type => actor_path } + post api("/features/#{feature_name}", admin, admin_mode: true), params: { value: 'true', actor_type => actor_path } expect(response).to have_gitlab_http_status(:bad_request) expect(json_response['message']).to eq("400 Bad request - #{expected_inexistent_path} is not found!") @@ -190,7 +192,7 @@ RSpec.describe API::Features, stub_feature_flags: false, feature_category: :feat shared_examples 'enables the flag for the actor' do |actor_type| it 'sets the feature gate' do - post api("/features/#{feature_name}", admin), params: { value: 'true', actor_type => actor.full_path } + post api("/features/#{feature_name}", admin, admin_mode: true), params: { value: 'true', actor_type => actor.full_path } expect(response).to have_gitlab_http_status(:created) expect(json_response).to match( @@ -207,7 +209,7 @@ RSpec.describe API::Features, stub_feature_flags: false, feature_category: :feat shared_examples 'creates an enabled feature for the specified entries' do it do - post api("/features/#{feature_name}", admin), params: { value: 'true', **gate_params } + post api("/features/#{feature_name}", admin, admin_mode: true), params: { value: 'true', **gate_params } expect(response).to have_gitlab_http_status(:created) expect(json_response['name']).to eq(feature_name) @@ -404,7 +406,7 @@ RSpec.describe API::Features, stub_feature_flags: false, feature_category: :feat end it 'creates a feature with the given percentage of time if passed an integer' do - post api("/features/#{feature_name}", admin), params: { value: '50' } + post api("/features/#{feature_name}", admin, admin_mode: true), params: { value: '50' } expect(response).to have_gitlab_http_status(:created) expect(json_response).to match( @@ -419,7 +421,7 @@ RSpec.describe API::Features, stub_feature_flags: false, feature_category: :feat end it 'creates a feature with the given percentage of time if passed a float' do - post api("/features/#{feature_name}", admin), params: { value: '0.01' } + post api("/features/#{feature_name}", admin, admin_mode: true), params: { value: '0.01' } expect(response).to have_gitlab_http_status(:created) expect(json_response).to match( @@ -434,7 +436,7 @@ RSpec.describe API::Features, stub_feature_flags: false, feature_category: :feat end it 'creates a feature with the given percentage of actors if passed an integer' do - post api("/features/#{feature_name}", admin), params: { value: '50', key: 'percentage_of_actors' } + post api("/features/#{feature_name}", admin, admin_mode: true), params: { value: '50', key: 'percentage_of_actors' } expect(response).to have_gitlab_http_status(:created) expect(json_response).to match( @@ -449,7 +451,7 @@ RSpec.describe API::Features, stub_feature_flags: false, feature_category: :feat end it 'creates a feature with the given percentage of actors if passed a float' do - post api("/features/#{feature_name}", admin), params: { value: '0.01', key: 'percentage_of_actors' } + post api("/features/#{feature_name}", admin, admin_mode: true), params: { value: '0.01', key: 'percentage_of_actors' } expect(response).to have_gitlab_http_status(:created) expect(json_response).to match( @@ -473,7 +475,7 @@ RSpec.describe API::Features, stub_feature_flags: false, feature_category: :feat context 'when key and feature_group are provided' do before do - post api("/features/#{feature_name}", admin), params: { value: '0.01', key: 'percentage_of_actors', feature_group: 'some-value' } + post api("/features/#{feature_name}", admin, admin_mode: true), params: { value: '0.01', key: 'percentage_of_actors', feature_group: 'some-value' } end it_behaves_like 'fails to set the feature flag' @@ -481,7 +483,7 @@ RSpec.describe API::Features, stub_feature_flags: false, feature_category: :feat context 'when key and user are provided' do before do - post api("/features/#{feature_name}", admin), params: { value: '0.01', key: 'percentage_of_actors', user: 'some-user' } + post api("/features/#{feature_name}", admin, admin_mode: true), params: { value: '0.01', key: 'percentage_of_actors', user: 'some-user' } end it_behaves_like 'fails to set the feature flag' @@ -489,7 +491,7 @@ RSpec.describe API::Features, stub_feature_flags: false, feature_category: :feat context 'when key and group are provided' do before do - post api("/features/#{feature_name}", admin), params: { value: '0.01', key: 'percentage_of_actors', group: 'somepath' } + post api("/features/#{feature_name}", admin, admin_mode: true), params: { value: '0.01', key: 'percentage_of_actors', group: 'somepath' } end it_behaves_like 'fails to set the feature flag' @@ -497,7 +499,7 @@ RSpec.describe API::Features, stub_feature_flags: false, feature_category: :feat context 'when key and namespace are provided' do before do - post api("/features/#{feature_name}", admin), params: { value: '0.01', key: 'percentage_of_actors', namespace: 'somepath' } + post api("/features/#{feature_name}", admin, admin_mode: true), params: { value: '0.01', key: 'percentage_of_actors', namespace: 'somepath' } end it_behaves_like 'fails to set the feature flag' @@ -505,7 +507,7 @@ RSpec.describe API::Features, stub_feature_flags: false, feature_category: :feat context 'when key and project are provided' do before do - post api("/features/#{feature_name}", admin), params: { value: '0.01', key: 'percentage_of_actors', project: 'somepath' } + post api("/features/#{feature_name}", admin, admin_mode: true), params: { value: '0.01', key: 'percentage_of_actors', project: 'somepath' } end it_behaves_like 'fails to set the feature flag' @@ -520,7 +522,7 @@ RSpec.describe API::Features, stub_feature_flags: false, feature_category: :feat context 'when passed value=true' do it 'enables the feature' do - post api("/features/#{feature_name}", admin), params: { value: 'true' } + post api("/features/#{feature_name}", admin, admin_mode: true), params: { value: 'true' } expect(response).to have_gitlab_http_status(:created) expect(json_response).to match( @@ -532,7 +534,7 @@ RSpec.describe API::Features, stub_feature_flags: false, feature_category: :feat end it 'enables the feature for the given Flipper group when passed feature_group=perf_team' do - post api("/features/#{feature_name}", admin), params: { value: 'true', feature_group: 'perf_team' } + post api("/features/#{feature_name}", admin, admin_mode: true), params: { value: 'true', feature_group: 'perf_team' } expect(response).to have_gitlab_http_status(:created) expect(json_response).to match( @@ -547,7 +549,7 @@ RSpec.describe API::Features, stub_feature_flags: false, feature_category: :feat end it 'enables the feature for the given user when passed user=username' do - post api("/features/#{feature_name}", admin), params: { value: 'true', user: user.username } + post api("/features/#{feature_name}", admin, admin_mode: true), params: { value: 'true', user: user.username } expect(response).to have_gitlab_http_status(:created) expect(json_response).to match( @@ -567,7 +569,7 @@ RSpec.describe API::Features, stub_feature_flags: false, feature_category: :feat Feature.enable(feature_name) expect(Feature.enabled?(feature_name)).to eq(true) - post api("/features/#{feature_name}", admin), params: { value: 'false' } + post api("/features/#{feature_name}", admin, admin_mode: true), params: { value: 'false' } expect(response).to have_gitlab_http_status(:created) expect(json_response).to match( @@ -582,7 +584,7 @@ RSpec.describe API::Features, stub_feature_flags: false, feature_category: :feat Feature.enable(feature_name, Feature.group(:perf_team)) expect(Feature.enabled?(feature_name, admin)).to be_truthy - post api("/features/#{feature_name}", admin), params: { value: 'false', feature_group: 'perf_team' } + post api("/features/#{feature_name}", admin, admin_mode: true), params: { value: 'false', feature_group: 'perf_team' } expect(response).to have_gitlab_http_status(:created) expect(json_response).to match( @@ -597,7 +599,7 @@ RSpec.describe API::Features, stub_feature_flags: false, feature_category: :feat Feature.enable(feature_name, user) expect(Feature.enabled?(feature_name, user)).to be_truthy - post api("/features/#{feature_name}", admin), params: { value: 'false', user: user.username } + post api("/features/#{feature_name}", admin, admin_mode: true), params: { value: 'false', user: user.username } expect(response).to have_gitlab_http_status(:created) expect(json_response).to match( @@ -615,7 +617,7 @@ RSpec.describe API::Features, stub_feature_flags: false, feature_category: :feat end it 'updates the percentage of time if passed an integer' do - post api("/features/#{feature_name}", admin), params: { value: '30' } + post api("/features/#{feature_name}", admin, admin_mode: true), params: { value: '30' } expect(response).to have_gitlab_http_status(:created) expect(json_response).to match( @@ -636,7 +638,7 @@ RSpec.describe API::Features, stub_feature_flags: false, feature_category: :feat end it 'updates the percentage of actors if passed an integer' do - post api("/features/#{feature_name}", admin), params: { value: '74', key: 'percentage_of_actors' } + post api("/features/#{feature_name}", admin, admin_mode: true), params: { value: '74', key: 'percentage_of_actors' } expect(response).to have_gitlab_http_status(:created) expect(json_response).to match( @@ -663,7 +665,7 @@ RSpec.describe API::Features, stub_feature_flags: false, feature_category: :feat Feature.enable(feature_name) expect(Feature.enabled?(feature_name, user)).to be_truthy - post api("/features/#{feature_name}", admin), params: { value: 'opt_out', user: user.username } + post api("/features/#{feature_name}", admin, admin_mode: true), params: { value: 'opt_out', user: user.username } expect(response).to have_gitlab_http_status(:created) expect(json_response).to include( @@ -683,7 +685,7 @@ RSpec.describe API::Features, stub_feature_flags: false, feature_category: :feat end it 'refuses to enable the feature' do - post api("/features/#{feature_name}", admin), params: { value: 'true', user: user.username } + post api("/features/#{feature_name}", admin, admin_mode: true), params: { value: 'true', user: user.username } expect(Feature).not_to be_enabled(feature_name, user) @@ -702,7 +704,7 @@ RSpec.describe API::Features, stub_feature_flags: false, feature_category: :feat Feature.enable(feature_name) expect(Feature).to be_enabled(feature_name, user) - post api("/features/#{feature_name}", admin), params: { value: 'opt_out', user: user.username } + post api("/features/#{feature_name}", admin, admin_mode: true), params: { value: 'opt_out', user: user.username } expect(response).to have_gitlab_http_status(:bad_request) end @@ -711,6 +713,9 @@ RSpec.describe API::Features, stub_feature_flags: false, feature_category: :feat describe 'DELETE /feature/:name' do let(:feature_name) { 'my_feature' } + let(:path) { "/features/#{feature_name}" } + + it_behaves_like 'DELETE request permissions for admin mode' context 'when the user has no access' do it 'returns a 401 for anonymous users' do @@ -728,7 +733,7 @@ RSpec.describe API::Features, stub_feature_flags: false, feature_category: :feat context 'when the user has access' do it 'returns 204 when the value is not set' do - delete api("/features/#{feature_name}", admin) + delete api(path, admin, admin_mode: true) expect(response).to have_gitlab_http_status(:no_content) end @@ -740,7 +745,7 @@ RSpec.describe API::Features, stub_feature_flags: false, feature_category: :feat it 'deletes an enabled feature' do expect do - delete api("/features/#{feature_name}", admin) + delete api("/features/#{feature_name}", admin, admin_mode: true) Feature.reset end.to change { Feature.persisted_name?(feature_name) } .and change { Feature.enabled?(feature_name) } @@ -751,7 +756,7 @@ RSpec.describe API::Features, stub_feature_flags: false, feature_category: :feat it 'logs the event' do expect(Feature.logger).to receive(:info).once - delete api("/features/#{feature_name}", admin) + delete api("/features/#{feature_name}", admin, admin_mode: true) end end end diff --git a/spec/requests/api/generic_packages_spec.rb b/spec/requests/api/generic_packages_spec.rb index 6b3f378a4bc..9e8bfab6468 100644 --- a/spec/requests/api/generic_packages_spec.rb +++ b/spec/requests/api/generic_packages_spec.rb @@ -276,9 +276,9 @@ RSpec.describe API::GenericPackages, feature_category: :package_registry do expect(package.version).to eq('0.0.1') if should_set_build_info - expect(package.original_build_info.pipeline).to eq(ci_build.pipeline) + expect(package.last_build_info.pipeline).to eq(ci_build.pipeline) else - expect(package.original_build_info).to be_nil + expect(package.last_build_info).to be_nil end package_file = package.package_files.last diff --git a/spec/requests/api/graphql/audit_events/definitions_spec.rb b/spec/requests/api/graphql/audit_events/definitions_spec.rb new file mode 100644 index 00000000000..4e0f4dcfae1 --- /dev/null +++ b/spec/requests/api/graphql/audit_events/definitions_spec.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'getting a list of audit event definitions', feature_category: :audit_events do + include GraphqlHelpers + + let_it_be(:current_user) { create(:user) } + + let(:path) { %i[audit_event_definitions nodes] } + let(:audit_event_definition_keys) do + Gitlab::Audit::Type::Definition.definitions.keys + end + + let(:query) { graphql_query_for(:audit_event_definitions, {}, 'nodes { name }') } + + it 'returns the audit event definitions' do + post_graphql(query, current_user: current_user) + + returned_names = graphql_data_at(*path).map { |v| v['name'].to_sym } + + expect(returned_names).to all be_in(audit_event_definition_keys) + end +end diff --git a/spec/requests/api/graphql/ci/group_environment_scopes_spec.rb b/spec/requests/api/graphql/ci/group_environment_scopes_spec.rb new file mode 100644 index 00000000000..13a3a128979 --- /dev/null +++ b/spec/requests/api/graphql/ci/group_environment_scopes_spec.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Query.group(fullPath).environmentScopes', feature_category: :secrets_management do + include GraphqlHelpers + + let_it_be(:user) { create(:user) } + let_it_be(:group) { create(:group) } + let(:expected_environment_scopes) do + %w[ + group1_environment1 + group1_environment2 + group2_environment3 + group2_environment4 + group2_environment5 + group2_environment6 + ] + end + + let(:query) do + %( + query { + group(fullPath: "#{group.full_path}") { + environmentScopes#{environment_scopes_params} { + nodes { + name + } + } + } + } + ) + end + + before do + group.add_developer(user) + expected_environment_scopes.each_with_index do |env, index| + create(:ci_group_variable, group: group, key: "var#{index + 1}", environment_scope: env) + end + end + + context 'when query has no parameters' do + let(:environment_scopes_params) { "" } + + it 'returns all avaiable environment scopes' do + post_graphql(query, current_user: user) + + expect(graphql_data.dig('group', 'environmentScopes', 'nodes')).to eq( + expected_environment_scopes.map { |env_scope| { 'name' => env_scope } } + ) + end + end + + context 'when query has search parameters' do + let(:environment_scopes_params) { "(search: \"group1\")" } + + it 'returns only environment scopes with group1 prefix' do + post_graphql(query, current_user: user) + + expect(graphql_data.dig('group', 'environmentScopes', 'nodes')).to eq( + [ + { 'name' => 'group1_environment1' }, + { 'name' => 'group1_environment2' } + ] + ) + end + end +end diff --git a/spec/requests/api/graphql/ci/jobs_spec.rb b/spec/requests/api/graphql/ci/jobs_spec.rb index 0d5ac725edd..756fcd8b7cd 100644 --- a/spec/requests/api/graphql/ci/jobs_spec.rb +++ b/spec/requests/api/graphql/ci/jobs_spec.rb @@ -183,7 +183,7 @@ RSpec.describe 'Query.project.pipeline', feature_category: :continuous_integrati #{all_graphql_fields_for('CiBuildNeed')} } ... on CiJob { - #{all_graphql_fields_for('CiJob')} + #{all_graphql_fields_for('CiJob', excluded: %w[aiFailureAnalysis])} } } } @@ -433,8 +433,6 @@ RSpec.describe 'Query.project.pipeline', feature_category: :continuous_integrati end it 'does not generate N+1 queries', :request_store, :use_sql_query_cache do - admin2 = create(:admin) - control = ActiveRecord::QueryRecorder.new(skip_cached: false) do post_graphql(query, current_user: admin) end @@ -442,7 +440,7 @@ RSpec.describe 'Query.project.pipeline', feature_category: :continuous_integrati runner_manager2 = create(:ci_runner_machine) create(:ci_build, pipeline: pipeline, name: 'my test job2', runner_manager: runner_manager2) - expect { post_graphql(query, current_user: admin2) }.not_to exceed_all_query_limit(control) + expect { post_graphql(query, current_user: admin) }.not_to exceed_all_query_limit(control) end end diff --git a/spec/requests/api/graphql/ci/runner_spec.rb b/spec/requests/api/graphql/ci/runner_spec.rb index 52b548ce8b9..63a657f3962 100644 --- a/spec/requests/api/graphql/ci/runner_spec.rb +++ b/spec/requests/api/graphql/ci/runner_spec.rb @@ -74,6 +74,8 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do end it 'retrieves expected fields' do + stub_commonmark_sourcepos_disabled + post_graphql(query, current_user: user) runner_data = graphql_data_at(:runner) diff --git a/spec/requests/api/graphql/ci/stages_spec.rb b/spec/requests/api/graphql/ci/stages_spec.rb index f4e1a69d455..2d646a0e1c3 100644 --- a/spec/requests/api/graphql/ci/stages_spec.rb +++ b/spec/requests/api/graphql/ci/stages_spec.rb @@ -16,7 +16,7 @@ RSpec.describe 'Query.project.pipeline.stages', feature_category: :continuous_in let(:fields) do <<~QUERY nodes { - #{all_graphql_fields_for('CiStage')} + #{all_graphql_fields_for('CiStage', max_depth: 2)} } QUERY end @@ -37,7 +37,7 @@ RSpec.describe 'Query.project.pipeline.stages', feature_category: :continuous_in before_all do create(:ci_stage, pipeline: pipeline, name: 'deploy') - create_list(:ci_build, 2, pipeline: pipeline, stage: 'deploy') + create(:ci_build, pipeline: pipeline, stage: 'deploy') end it_behaves_like 'a working graphql query' do @@ -58,7 +58,7 @@ RSpec.describe 'Query.project.pipeline.stages', feature_category: :continuous_in it 'returns up to default limit jobs per stage' do post_query - expect(job_nodes.count).to eq(2) + expect(job_nodes.count).to eq(1) end context 'when the limit is manually set' do diff --git a/spec/requests/api/graphql/current_user/groups_query_spec.rb b/spec/requests/api/graphql/current_user/groups_query_spec.rb index 151d07ff0a7..435e5e62f69 100644 --- a/spec/requests/api/graphql/current_user/groups_query_spec.rb +++ b/spec/requests/api/graphql/current_user/groups_query_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe 'Query current user groups', feature_category: :subgroups do +RSpec.describe 'Query current user groups', feature_category: :groups_and_projects do include GraphqlHelpers let_it_be(:user) { create(:user) } diff --git a/spec/requests/api/graphql/group/dependency_proxy_blobs_spec.rb b/spec/requests/api/graphql/group/dependency_proxy_blobs_spec.rb index a6eb114a279..961de84234c 100644 --- a/spec/requests/api/graphql/group/dependency_proxy_blobs_spec.rb +++ b/spec/requests/api/graphql/group/dependency_proxy_blobs_spec.rb @@ -27,6 +27,7 @@ RSpec.describe 'getting dependency proxy blobs in a group', feature_category: :d dependencyProxyBlobCount dependencyProxyTotalSize dependencyProxyTotalSizeInBytes + dependencyProxyTotalSizeBytes GQL end @@ -132,4 +133,49 @@ RSpec.describe 'getting dependency proxy blobs in a group', feature_category: :d expected_size = blobs.inject(0) { |sum, blob| sum + blob.size } expect(dependency_proxy_total_size_in_bytes_response).to eq(expected_size) end + + context 'with a giant size blob' do + let_it_be(:owner) { create(:user) } + let_it_be_with_reload(:group) { create(:group) } + let_it_be(:blob) do + create(:dependency_proxy_blob, file_name: 'blob2.json', group: group, size: GraphQL::Types::Int::MAX + 1) + end + + let_it_be(:blobs) { [blob].flatten } + + context 'using dependencyProxyTotalSizeInBytes' do + let(:fields) do + <<~GQL + #{query_graphql_field('dependency_proxy_blobs', {}, dependency_proxy_blob_fields)} + dependencyProxyTotalSizeInBytes + GQL + end + + it 'returns an error' do + post_graphql(query, current_user: user, variables: variables) + + err_message = 'Integer out of bounds' + expect(graphql_errors).to include(a_hash_including('message' => a_string_including(err_message))) + end + end + + context 'using dependencyProxyTotalSizeBytes' do + let(:fields) do + <<~GQL + #{query_graphql_field('dependency_proxy_blobs', {}, dependency_proxy_blob_fields)} + dependencyProxyTotalSizeBytes + GQL + end + + let(:dependency_proxy_total_size_bytes_response) { graphql_data.dig('group', 'dependencyProxyTotalSizeBytes') } + + it 'returns the total size in bytes as a string' do + post_graphql(query, current_user: user, variables: variables) + + expect(graphql_errors).to be_nil + expected_size = String(blobs.inject(0) { |sum, blob| sum + blob.size }) + expect(dependency_proxy_total_size_bytes_response).to eq(expected_size) + end + end + end end diff --git a/spec/requests/api/graphql/group/dependency_proxy_group_setting_spec.rb b/spec/requests/api/graphql/group/dependency_proxy_group_setting_spec.rb index aca8527ba0a..c8745fcbb62 100644 --- a/spec/requests/api/graphql/group/dependency_proxy_group_setting_spec.rb +++ b/spec/requests/api/graphql/group/dependency_proxy_group_setting_spec.rb @@ -46,12 +46,15 @@ RSpec.describe 'getting dependency proxy settings for a group', feature_category context 'with different permissions' do where(:group_visibility, :role, :access_granted) do - :private | :maintainer | true + :private | :owner | true + :private | :maintainer | false :private | :developer | false :private | :reporter | false :private | :guest | false :private | :anonymous | false - :public | :maintainer | true + + :public | :owner | true + :public | :maintainer | false :public | :developer | false :public | :reporter | false :public | :guest | false @@ -73,6 +76,20 @@ RSpec.describe 'getting dependency proxy settings for a group', feature_category expect(dependency_proxy_group_setting_response).to be_blank end end + + context 'with disabled admin_package feature flag' do + before do + stub_feature_flags(raise_group_admin_package_permission_to_owner: false) + end + + if params[:role] == :maintainer + it 'return the proper response' do + subject + + expect(dependency_proxy_group_setting_response).to eq('enabled' => true) + end + end + end end end end diff --git a/spec/requests/api/graphql/group/dependency_proxy_image_ttl_policy_spec.rb b/spec/requests/api/graphql/group/dependency_proxy_image_ttl_policy_spec.rb index edff4dc1dae..8365cece4a3 100644 --- a/spec/requests/api/graphql/group/dependency_proxy_image_ttl_policy_spec.rb +++ b/spec/requests/api/graphql/group/dependency_proxy_image_ttl_policy_spec.rb @@ -45,12 +45,15 @@ RSpec.describe 'getting dependency proxy image ttl policy for a group', feature_ context 'with different permissions' do where(:group_visibility, :role, :access_granted) do - :private | :maintainer | true + :private | :owner | true + :private | :maintainer | false :private | :developer | false :private | :reporter | false :private | :guest | false :private | :anonymous | false - :public | :maintainer | true + + :public | :owner | true + :public | :maintainer | false :public | :developer | false :public | :reporter | false :public | :guest | false @@ -72,6 +75,20 @@ RSpec.describe 'getting dependency proxy image ttl policy for a group', feature_ expect(dependency_proxy_image_ttl_policy_response).to be_blank end end + + context 'when the feature flag is disabled' do + before do + stub_feature_flags(raise_group_admin_package_permission_to_owner: false) + end + + if params[:role] == :maintainer + it 'returns the proper response' do + subject + + expect(dependency_proxy_image_ttl_policy_response).to eq("createdAt" => nil, "enabled" => false, "ttl" => 90, "updatedAt" => nil) + end + end + end end end end diff --git a/spec/requests/api/graphql/group/group_members_spec.rb b/spec/requests/api/graphql/group/group_members_spec.rb index 26d1fb48408..e56e901466a 100644 --- a/spec/requests/api/graphql/group/group_members_spec.rb +++ b/spec/requests/api/graphql/group/group_members_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe 'getting group members information', feature_category: :subgroups do +RSpec.describe 'getting group members information', feature_category: :groups_and_projects do include GraphqlHelpers let_it_be(:parent_group) { create(:group, :public) } diff --git a/spec/requests/api/graphql/group_query_spec.rb b/spec/requests/api/graphql/group_query_spec.rb index ce5816999a6..6debe2d3d67 100644 --- a/spec/requests/api/graphql/group_query_spec.rb +++ b/spec/requests/api/graphql/group_query_spec.rb @@ -4,7 +4,7 @@ require 'spec_helper' # Based on spec/requests/api/groups_spec.rb # Should follow closely in order to ensure all situations are covered -RSpec.describe 'getting group information', :with_license, feature_category: :subgroups do +RSpec.describe 'getting group information', :with_license, feature_category: :groups_and_projects do include GraphqlHelpers include UploadHelpers diff --git a/spec/requests/api/graphql/groups_query_spec.rb b/spec/requests/api/graphql/groups_query_spec.rb index 84c8d3c3388..460cb40b68a 100644 --- a/spec/requests/api/graphql/groups_query_spec.rb +++ b/spec/requests/api/graphql/groups_query_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe 'searching groups', :with_license, feature_category: :subgroups do +RSpec.describe 'searching groups', :with_license, feature_category: :groups_and_projects do include GraphqlHelpers let_it_be(:user) { create(:user) } diff --git a/spec/requests/api/graphql/jobs_query_spec.rb b/spec/requests/api/graphql/jobs_query_spec.rb index 7607aeac6e0..4248a03fa74 100644 --- a/spec/requests/api/graphql/jobs_query_spec.rb +++ b/spec/requests/api/graphql/jobs_query_spec.rb @@ -10,7 +10,7 @@ RSpec.describe 'getting job information', feature_category: :continuous_integrat :jobs, {}, %( count nodes { - #{all_graphql_fields_for(::Types::Ci::JobType, max_depth: 1)} + #{all_graphql_fields_for(::Types::Ci::JobType, max_depth: 1, excluded: %w[aiFailureAnalysis])} }) ) end diff --git a/spec/requests/api/graphql/metadata_query_spec.rb b/spec/requests/api/graphql/metadata_query_spec.rb index 7d1850b1b93..b973e7d4d51 100644 --- a/spec/requests/api/graphql/metadata_query_spec.rb +++ b/spec/requests/api/graphql/metadata_query_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe 'getting project information', feature_category: :projects do +RSpec.describe 'getting project information', feature_category: :groups_and_projects do include GraphqlHelpers let(:query) { graphql_query_for('metadata', {}, all_graphql_fields_for('Metadata')) } diff --git a/spec/requests/api/graphql/mutations/achievements/delete_user_achievement_spec.rb b/spec/requests/api/graphql/mutations/achievements/delete_user_achievement_spec.rb new file mode 100644 index 00000000000..f759e6dce08 --- /dev/null +++ b/spec/requests/api/graphql/mutations/achievements/delete_user_achievement_spec.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Mutations::Achievements::DeleteUserAchievement, feature_category: :user_profile do + include GraphqlHelpers + + let_it_be(:maintainer) { create(:user) } + let_it_be(:owner) { create(:user) } + let_it_be(:group) { create(:group) } + let_it_be(:achievement) { create(:achievement, namespace: group) } + let_it_be(:user_achievement) { create(:user_achievement, achievement: achievement) } + + let(:mutation) { graphql_mutation(:user_achievements_delete, params) } + let(:user_achievement_id) { user_achievement&.to_global_id } + let(:params) { { user_achievement_id: user_achievement_id } } + + subject { post_graphql_mutation(mutation, current_user: current_user) } + + before_all do + group.add_maintainer(maintainer) + group.add_owner(owner) + end + + context 'when the user does not have permission' do + let(:current_user) { maintainer } + + it_behaves_like 'a mutation that returns a top-level access error' + + it 'does not delete any user achievements' do + expect { subject }.not_to change { Achievements::UserAchievement.count } + end + end + + context 'when the user has permission' do + let(:current_user) { owner } + + context 'when the params are invalid' do + let(:user_achievement) { nil } + + it 'returns the validation error' do + subject + + expect(graphql_errors.to_s).to include('invalid value for userAchievementId (Expected value to not be null)') + end + end + + context 'when the user_achievement_id is invalid' do + let(:user_achievement_id) { "gid://gitlab/Achievements::UserAchievement/#{non_existing_record_id}" } + + it 'returns the relevant error' do + subject + + expect(graphql_errors.to_s) + .to include("The resource that you are attempting to access does not exist or you don't have permission") + end + end + + context 'when the feature flag is disabled' do + before do + stub_feature_flags(achievements: false) + end + + it 'returns the relevant error' do + subject + + expect(graphql_errors.to_s) + .to include("The resource that you are attempting to access does not exist or you don't have permission") + end + end + + context 'when everything is ok' do + it 'deletes an user achievement' do + expect { subject }.to change { Achievements::UserAchievement.count }.by(-1) + end + + it 'returns the deleted user achievement' do + subject + + expect(graphql_data_at(:user_achievements_delete, :user_achievement, :achievement, :id)) + .to eq(achievement.to_global_id.to_s) + end + end + end +end diff --git a/spec/requests/api/graphql/mutations/alert_management/http_integration/create_spec.rb b/spec/requests/api/graphql/mutations/alert_management/http_integration/create_spec.rb index 187c88363c6..b0e9f59b996 100644 --- a/spec/requests/api/graphql/mutations/alert_management/http_integration/create_spec.rb +++ b/spec/requests/api/graphql/mutations/alert_management/http_integration/create_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe 'Creating a new HTTP Integration', feature_category: :integrations do +RSpec.describe 'Creating a new HTTP Integration', feature_category: :incident_management do include GraphqlHelpers let_it_be(:current_user) { create(:user) } diff --git a/spec/requests/api/graphql/mutations/alert_management/http_integration/destroy_spec.rb b/spec/requests/api/graphql/mutations/alert_management/http_integration/destroy_spec.rb index 1c77c71daba..110c65d24a0 100644 --- a/spec/requests/api/graphql/mutations/alert_management/http_integration/destroy_spec.rb +++ b/spec/requests/api/graphql/mutations/alert_management/http_integration/destroy_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe 'Removing an HTTP Integration', feature_category: :integrations do +RSpec.describe 'Removing an HTTP Integration', feature_category: :incident_management do include GraphqlHelpers let_it_be(:user) { create(:user) } diff --git a/spec/requests/api/graphql/mutations/alert_management/http_integration/reset_token_spec.rb b/spec/requests/api/graphql/mutations/alert_management/http_integration/reset_token_spec.rb index 427277dd540..049d7e8dace 100644 --- a/spec/requests/api/graphql/mutations/alert_management/http_integration/reset_token_spec.rb +++ b/spec/requests/api/graphql/mutations/alert_management/http_integration/reset_token_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe 'Resetting a token on an existing HTTP Integration', feature_category: :integrations do +RSpec.describe 'Resetting a token on an existing HTTP Integration', feature_category: :incident_management do include GraphqlHelpers let_it_be(:user) { create(:user) } diff --git a/spec/requests/api/graphql/mutations/alert_management/http_integration/update_spec.rb b/spec/requests/api/graphql/mutations/alert_management/http_integration/update_spec.rb index a9d189d564d..70adff1fdc4 100644 --- a/spec/requests/api/graphql/mutations/alert_management/http_integration/update_spec.rb +++ b/spec/requests/api/graphql/mutations/alert_management/http_integration/update_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe 'Updating an existing HTTP Integration', feature_category: :integrations do +RSpec.describe 'Updating an existing HTTP Integration', feature_category: :incident_management do include GraphqlHelpers let_it_be(:current_user) { create(:user) } diff --git a/spec/requests/api/graphql/mutations/ci/job_artifact/bulk_destroy_spec.rb b/spec/requests/api/graphql/mutations/ci/job_artifact/bulk_destroy_spec.rb index 4e25669a0ca..5cb48ec44a0 100644 --- a/spec/requests/api/graphql/mutations/ci/job_artifact/bulk_destroy_spec.rb +++ b/spec/requests/api/graphql/mutations/ci/job_artifact/bulk_destroy_spec.rb @@ -41,23 +41,6 @@ RSpec.describe 'BulkDestroy', feature_category: :build_artifacts do expect(first_artifact.reload).to be_persisted end - context 'when the `ci_job_artifact_bulk_destroy` feature flag is disabled' do - before do - stub_feature_flags(ci_job_artifact_bulk_destroy: false) - project.add_maintainer(maintainer) - end - - it 'returns a resource not available error' do - post_graphql_mutation(mutation, current_user: maintainer) - - expect(graphql_errors).to contain_exactly( - hash_including( - 'message' => '`ci_job_artifact_bulk_destroy` feature flag is disabled.' - ) - ) - end - end - context "when the user is a developer in a project" do before do project.add_developer(developer) 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 aa00069b241..fd92ed198e7 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 @@ -64,35 +64,6 @@ RSpec.describe 'ProjectCiCdSettingsUpdate', feature_category: :continuous_integr expect(project.keep_latest_artifact).to eq(false) end - describe 'ci_cd_settings_update deprecated mutation' do - let(:mutation) { graphql_mutation(:ci_cd_settings_update, variables) } - - it 'returns error' do - post_graphql_mutation(mutation, current_user: user) - - expect(graphql_errors).to( - include( - hash_including('message' => '`remove_cicd_settings_update` feature flag is enabled.') - ) - ) - end - - context 'when remove_cicd_settings_update FF is disabled' do - before do - stub_feature_flags(remove_cicd_settings_update: false) - end - - it 'updates ci cd settings' do - post_graphql_mutation(mutation, current_user: user) - - project.reload - - expect(response).to have_gitlab_http_status(:success) - expect(project.keep_latest_artifact).to eq(false) - end - end - end - it 'allows setting job_token_scope_enabled to false' do post_graphql_mutation(mutation, current_user: user) diff --git a/spec/requests/api/graphql/mutations/dependency_proxy/group_settings/update_spec.rb b/spec/requests/api/graphql/mutations/dependency_proxy/group_settings/update_spec.rb index 5d5696d3f66..86ea77a8f35 100644 --- a/spec/requests/api/graphql/mutations/dependency_proxy/group_settings/update_spec.rb +++ b/spec/requests/api/graphql/mutations/dependency_proxy/group_settings/update_spec.rb @@ -49,16 +49,21 @@ RSpec.describe 'Updating the dependency proxy group settings', feature_category: end context 'with permission' do - before do - group.add_maintainer(user) - end + %i[owner maintainer].each do |role| + context "for #{role}" do + before do + group.send("add_#{role}", user) + stub_feature_flags(raise_group_admin_package_permission_to_owner: false) + end - it 'returns the updated dependency proxy settings', :aggregate_failures do - subject + it 'returns the updated dependency proxy settings', :aggregate_failures do + subject - expect(response).to have_gitlab_http_status(:success) - expect(mutation_response['errors']).to be_empty - expect(group_settings[:enabled]).to eq(false) + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response['errors']).to be_empty + expect(group_settings[:enabled]).to eq(false) + end + end end end end diff --git a/spec/requests/api/graphql/mutations/dependency_proxy/image_ttl_group_policy/update_spec.rb b/spec/requests/api/graphql/mutations/dependency_proxy/image_ttl_group_policy/update_spec.rb index 66ee17f356c..bc8b2da84b9 100644 --- a/spec/requests/api/graphql/mutations/dependency_proxy/image_ttl_group_policy/update_spec.rb +++ b/spec/requests/api/graphql/mutations/dependency_proxy/image_ttl_group_policy/update_spec.rb @@ -51,19 +51,24 @@ RSpec.describe 'Updating the dependency proxy image ttl policy', feature_categor end context 'with permission' do - before do - group.add_maintainer(user) - end + %i[owner maintainer].each do |role| + context "for #{role}" do + before do + group.send("add_#{role}", user) + stub_feature_flags(raise_group_admin_package_permission_to_owner: false) + end - it 'returns the updated dependency proxy image ttl policy', :aggregate_failures do - subject + it 'returns the updated dependency proxy image ttl policy', :aggregate_failures do + subject - expect(response).to have_gitlab_http_status(:success) - expect(mutation_response['errors']).to be_empty - expect(ttl_policy_response).to include( - 'enabled' => params[:enabled], - 'ttl' => params[:ttl] - ) + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response['errors']).to be_empty + expect(ttl_policy_response).to include( + 'enabled' => params[:enabled], + 'ttl' => params[:ttl] + ) + end + end end end end diff --git a/spec/requests/api/graphql/mutations/environments/create_spec.rb b/spec/requests/api/graphql/mutations/environments/create_spec.rb new file mode 100644 index 00000000000..8a67f86dc4b --- /dev/null +++ b/spec/requests/api/graphql/mutations/environments/create_spec.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Create Environment', feature_category: :environment_management do + include GraphqlHelpers + + let_it_be(:project) { create(:project, :repository) } + let_it_be(:developer) { create(:user).tap { |u| project.add_maintainer(u) } } + let_it_be(:reporter) { create(:user).tap { |u| project.add_reporter(u) } } + + let(:current_user) { developer } + + let(:mutation) do + graphql_mutation(:environment_create, input) + end + + context 'when creating an environment' do + let(:input) do + { + project_path: project.full_path, + name: 'production', + external_url: 'https://gitlab.com/' + } + end + + it 'creates successfully' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(graphql_mutation_response(:environment_create)['environment']['name']).to eq('production') + expect(graphql_mutation_response(:environment_create)['environment']['externalUrl']).to eq('https://gitlab.com/') + expect(graphql_mutation_response(:environment_create)['errors']).to be_empty + end + + context 'when current user is reporter' do + let(:current_user) { reporter } + + it 'returns error' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(graphql_errors.to_s) + .to include("The resource that you are attempting to access does not exist or you don't have permission") + end + end + end + + context 'when name is missing' do + let(:input) do + { + project_path: project.full_path + } + end + + it 'returns error' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(graphql_errors.to_s).to include("Expected value to not be null") + end + end +end diff --git a/spec/requests/api/graphql/mutations/environments/delete_spec.rb b/spec/requests/api/graphql/mutations/environments/delete_spec.rb new file mode 100644 index 00000000000..1e28d0ebc0b --- /dev/null +++ b/spec/requests/api/graphql/mutations/environments/delete_spec.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Delete Environment', feature_category: :deployment_management do + include GraphqlHelpers + + let_it_be(:project) { create(:project, :repository) } + let_it_be(:environment) { create(:environment, project: project, state: :stopped) } + let_it_be(:maintainer) { create(:user).tap { |u| project.add_maintainer(u) } } + let_it_be(:developer) { create(:user).tap { |u| project.add_maintainer(u) } } + + let(:environment_id) { environment.to_global_id.to_s } + let(:current_user) { developer } + + let(:mutation) do + graphql_mutation(:environment_delete, input) + end + + context 'when delete is successful' do + let(:input) do + { id: environment_id } + end + + it 'deletes the environment' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + end.to change { project.reload.environments.include?(environment) }.from(true).to(false) + + expect(graphql_mutation_response(:environment_delete)['errors']).to be_empty + end + end +end diff --git a/spec/requests/api/graphql/mutations/environments/update_spec.rb b/spec/requests/api/graphql/mutations/environments/update_spec.rb new file mode 100644 index 00000000000..9c68b3a024c --- /dev/null +++ b/spec/requests/api/graphql/mutations/environments/update_spec.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Update Environment', feature_category: :deployment_management do + include GraphqlHelpers + + let_it_be(:project) { create(:project, :repository) } + let_it_be(:environment) { create(:environment, project: project) } + let_it_be(:maintainer) { create(:user).tap { |u| project.add_maintainer(u) } } + let_it_be(:developer) { create(:user).tap { |u| project.add_maintainer(u) } } + + let(:environment_id) { environment.to_global_id.to_s } + let(:current_user) { developer } + + let(:mutation) do + graphql_mutation(:environment_update, input) + end + + context 'when updating external URL' do + let(:input) do + { + id: environment_id, + external_url: 'https://gitlab.com/' + } + end + + it 'updates successfully' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + end.to change { environment.reload.external_url }.to('https://gitlab.com/') + + expect(graphql_mutation_response(:environment_update)['errors']).to be_empty + end + + context 'when url is invalid' do + let(:input) do + { + id: environment_id, + external_url: 'http://${URL}' + } + end + + it 'returns error' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + end.not_to change { environment.reload.external_url } + + expect(graphql_mutation_response(:environment_update)['errors'].first).to include('URI is invalid') + end + end + end + + context 'when updating tier' do + let(:input) do + { + id: environment_id, + tier: 'STAGING' + } + end + + it 'updates successfully' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + end.to change { environment.reload.tier }.to('staging') + + expect(graphql_mutation_response(:environment_update)['errors']).to be_empty + end + end +end diff --git a/spec/requests/api/graphql/mutations/groups/update_spec.rb b/spec/requests/api/graphql/mutations/groups/update_spec.rb index a9acc593229..b75b2464c22 100644 --- a/spec/requests/api/graphql/mutations/groups/update_spec.rb +++ b/spec/requests/api/graphql/mutations/groups/update_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe 'GroupUpdate', feature_category: :subgroups do +RSpec.describe 'GroupUpdate', feature_category: :groups_and_projects do include GraphqlHelpers let_it_be(:user) { create(:user) } diff --git a/spec/requests/api/graphql/mutations/jira_import/import_users_spec.rb b/spec/requests/api/graphql/mutations/jira_import/import_users_spec.rb index ab15aa97680..58659ea0824 100644 --- a/spec/requests/api/graphql/mutations/jira_import/import_users_spec.rb +++ b/spec/requests/api/graphql/mutations/jira_import/import_users_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe 'Importing Jira Users', feature_category: :integrations do +RSpec.describe 'Importing Jira Users', feature_category: :importers do include JiraIntegrationHelpers include GraphqlHelpers diff --git a/spec/requests/api/graphql/mutations/jira_import/start_spec.rb b/spec/requests/api/graphql/mutations/jira_import/start_spec.rb index a864bc88afc..fc4a1488b27 100644 --- a/spec/requests/api/graphql/mutations/jira_import/start_spec.rb +++ b/spec/requests/api/graphql/mutations/jira_import/start_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe 'Starting a Jira Import', feature_category: :integrations do +RSpec.describe 'Starting a Jira Import', feature_category: :importers do include JiraIntegrationHelpers include GraphqlHelpers diff --git a/spec/requests/api/graphql/mutations/members/groups/bulk_update_spec.rb b/spec/requests/api/graphql/mutations/members/groups/bulk_update_spec.rb index f15b52f53a3..1395f7b778f 100644 --- a/spec/requests/api/graphql/mutations/members/groups/bulk_update_spec.rb +++ b/spec/requests/api/graphql/mutations/members/groups/bulk_update_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe 'GroupMemberBulkUpdate', feature_category: :subgroups do +RSpec.describe 'GroupMemberBulkUpdate', feature_category: :groups_and_projects do include GraphqlHelpers let_it_be(:parent_group) { create(:group) } diff --git a/spec/requests/api/graphql/mutations/members/projects/bulk_update_spec.rb b/spec/requests/api/graphql/mutations/members/projects/bulk_update_spec.rb index cbef9715cbe..910e512f6d6 100644 --- a/spec/requests/api/graphql/mutations/members/projects/bulk_update_spec.rb +++ b/spec/requests/api/graphql/mutations/members/projects/bulk_update_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe 'ProjectMemberBulkUpdate', feature_category: :projects do +RSpec.describe 'ProjectMemberBulkUpdate', feature_category: :groups_and_projects do include GraphqlHelpers let_it_be(:parent_group) { create(:group) } diff --git a/spec/requests/api/graphql/mutations/namespace/package_settings/update_spec.rb b/spec/requests/api/graphql/mutations/namespace/package_settings/update_spec.rb index f4f4f34fe29..2f26a2f92b2 100644 --- a/spec/requests/api/graphql/mutations/namespace/package_settings/update_spec.rb +++ b/spec/requests/api/graphql/mutations/namespace/package_settings/update_spec.rb @@ -139,6 +139,15 @@ RSpec.describe 'Updating the package settings', feature_category: :package_regis end end + # To be removed when raise_group_admin_package_permission_to_owner FF is removed + RSpec.shared_examples 'disabling admin_package feature flag' do |action:| + before do + stub_feature_flags(raise_group_admin_package_permission_to_owner: false) + end + + it_behaves_like "accepting the mutation request #{action} the package settings" + end + describe 'post graphql mutation' do subject { post_graphql_mutation(mutation, current_user: user) } @@ -147,7 +156,8 @@ RSpec.describe 'Updating the package settings', feature_category: :package_regis let_it_be(:namespace, reload: true) { package_settings.namespace } where(:user_role, :shared_examples_name) do - :maintainer | 'accepting the mutation request updating the package settings' + :owner | 'accepting the mutation request updating the package settings' + :maintainer | 'denying the mutation request' :developer | 'denying the mutation request' :reporter | 'denying the mutation request' :guest | 'denying the mutation request' @@ -160,6 +170,7 @@ RSpec.describe 'Updating the package settings', feature_category: :package_regis end it_behaves_like params[:shared_examples_name] + it_behaves_like 'disabling admin_package feature flag', action: :updating if params[:user_role] == :maintainer end end @@ -169,7 +180,8 @@ RSpec.describe 'Updating the package settings', feature_category: :package_regis let(:package_settings) { namespace.package_settings } where(:user_role, :shared_examples_name) do - :maintainer | 'accepting the mutation request creating the package settings' + :owner | 'accepting the mutation request creating the package settings' + :maintainer | 'denying the mutation request' :developer | 'denying the mutation request' :reporter | 'denying the mutation request' :guest | 'denying the mutation request' @@ -182,6 +194,7 @@ RSpec.describe 'Updating the package settings', feature_category: :package_regis end it_behaves_like params[:shared_examples_name] + it_behaves_like 'disabling admin_package feature flag', action: :creating if params[:user_role] == :maintainer end end end diff --git a/spec/requests/api/graphql/mutations/projects/sync_fork_spec.rb b/spec/requests/api/graphql/mutations/projects/sync_fork_spec.rb index c5dc6f390d9..0745fb945bb 100644 --- a/spec/requests/api/graphql/mutations/projects/sync_fork_spec.rb +++ b/spec/requests/api/graphql/mutations/projects/sync_fork_spec.rb @@ -32,24 +32,6 @@ RSpec.describe "Sync project fork", feature_category: :source_code_management do source_project.change_head('feature') end - context 'when synchronize_fork feature flag is disabled' do - before do - stub_feature_flags(synchronize_fork: false) - end - - it 'does not call the sync service' do - expect(::Projects::Forks::SyncWorker).not_to receive(:perform_async) - - post_graphql_mutation(mutation, current_user: current_user) - - expect(graphql_mutation_response(:project_sync_fork)).to eq( - { - 'details' => nil, - 'errors' => ['Feature flag is disabled'] - }) - end - end - context 'when the branch is protected', :use_clean_rails_redis_caching do let_it_be(:protected_branch) do create(:protected_branch, :no_one_can_push, project: project, name: target_branch) diff --git a/spec/requests/api/graphql/mutations/snippets/create_spec.rb b/spec/requests/api/graphql/mutations/snippets/create_spec.rb index 0b1af2bf628..a6d727ae6d3 100644 --- a/spec/requests/api/graphql/mutations/snippets/create_spec.rb +++ b/spec/requests/api/graphql/mutations/snippets/create_spec.rb @@ -103,10 +103,6 @@ RSpec.describe 'Creating a Snippet', feature_category: :source_code_management d end it_behaves_like 'snippet edit usage data counters' - - it_behaves_like 'a mutation which can mutate a spammable' do - let(:service) { Snippets::CreateService } - end end context 'with PersonalSnippet' do @@ -165,7 +161,7 @@ RSpec.describe 'Creating a Snippet', feature_category: :source_code_management d it do expect(::Snippets::CreateService).to receive(:new) - .with(project: nil, current_user: user, params: hash_including(files: expected_value), spam_params: instance_of(::Spam::SpamParams)) + .with(project: nil, current_user: user, params: hash_including(files: expected_value)) .and_return(double(execute: creation_response)) subject @@ -182,7 +178,7 @@ RSpec.describe 'Creating a Snippet', feature_category: :source_code_management d it 'returns an error' do subject - expect(json_response['errors']).to be + expect(json_response['errors']).to be_present end end end diff --git a/spec/requests/api/graphql/mutations/snippets/update_spec.rb b/spec/requests/api/graphql/mutations/snippets/update_spec.rb index 3b98ee3c2e9..7c5ab691b51 100644 --- a/spec/requests/api/graphql/mutations/snippets/update_spec.rb +++ b/spec/requests/api/graphql/mutations/snippets/update_spec.rb @@ -110,10 +110,6 @@ RSpec.describe 'Updating a Snippet', feature_category: :source_code_management d end end - it_behaves_like 'a mutation which can mutate a spammable' do - let(:service) { Snippets::UpdateService } - end - def blob_at(filename) snippet.repository.blob_at('HEAD', filename) end diff --git a/spec/requests/api/graphql/mutations/work_items/update_spec.rb b/spec/requests/api/graphql/mutations/work_items/update_spec.rb index ce1c2c01faa..60b5795ee9b 100644 --- a/spec/requests/api/graphql/mutations/work_items/update_spec.rb +++ b/spec/requests/api/graphql/mutations/work_items/update_spec.rb @@ -869,7 +869,7 @@ RSpec.describe 'Update a work item', feature_category: :team_planning do let_it_be(:issue) { create(:work_item, project: project) } let_it_be(:link) { create(:parent_link, work_item_parent: issue, work_item: work_item) } - let(:error_msg) { 'Work item type cannot be changed to Issue with Issue as parent type.' } + let(:error_msg) { 'Work item type cannot be changed to issue when linked to a parent issue.' } it 'does not update the work item type' do expect do diff --git a/spec/requests/api/graphql/namespace/projects_spec.rb b/spec/requests/api/graphql/namespace/projects_spec.rb index 83edacaf831..a4bc94798be 100644 --- a/spec/requests/api/graphql/namespace/projects_spec.rb +++ b/spec/requests/api/graphql/namespace/projects_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe 'getting projects', feature_category: :projects do +RSpec.describe 'getting projects', feature_category: :groups_and_projects do include GraphqlHelpers let(:group) { create(:group) } diff --git a/spec/requests/api/graphql/namespace/root_storage_statistics_spec.rb b/spec/requests/api/graphql/namespace/root_storage_statistics_spec.rb index cee698d6dc5..7c48f324d24 100644 --- a/spec/requests/api/graphql/namespace/root_storage_statistics_spec.rb +++ b/spec/requests/api/graphql/namespace/root_storage_statistics_spec.rb @@ -10,9 +10,11 @@ RSpec.describe 'rendering namespace statistics', feature_category: :metrics do let(:user) { create(:user) } let(:query) do - graphql_query_for('namespace', - { 'fullPath' => namespace.full_path }, - "rootStorageStatistics { #{all_graphql_fields_for('RootStorageStatistics')} }") + graphql_query_for( + 'namespace', + { 'fullPath' => namespace.full_path }, + "rootStorageStatistics { #{all_graphql_fields_for('RootStorageStatistics')} }" + ) end shared_examples 'a working namespace with storage statistics query' do diff --git a/spec/requests/api/graphql/namespace_query_spec.rb b/spec/requests/api/graphql/namespace_query_spec.rb index d12a3875ebf..c0c7c5fee2b 100644 --- a/spec/requests/api/graphql/namespace_query_spec.rb +++ b/spec/requests/api/graphql/namespace_query_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe 'Query', feature_category: :subgroups do +RSpec.describe 'Query', feature_category: :groups_and_projects do include GraphqlHelpers let_it_be(:user) { create(:user) } diff --git a/spec/requests/api/graphql/project/alert_management/alert/assignees_spec.rb b/spec/requests/api/graphql/project/alert_management/alert/assignees_spec.rb index c4843c3cf97..0ca4ec0e363 100644 --- a/spec/requests/api/graphql/project/alert_management/alert/assignees_spec.rb +++ b/spec/requests/api/graphql/project/alert_management/alert/assignees_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe 'getting Alert Management Alert Assignees', feature_category: :projects do +RSpec.describe 'getting Alert Management Alert Assignees', feature_category: :groups_and_projects do include GraphqlHelpers let_it_be(:project) { create(:project) } diff --git a/spec/requests/api/graphql/project/alert_management/integrations_spec.rb b/spec/requests/api/graphql/project/alert_management/integrations_spec.rb index e8d19513a4e..e48db541e1f 100644 --- a/spec/requests/api/graphql/project/alert_management/integrations_spec.rb +++ b/spec/requests/api/graphql/project/alert_management/integrations_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require 'spec_helper' -RSpec.describe 'getting Alert Management Integrations', feature_category: :integrations do +RSpec.describe 'getting Alert Management Integrations', feature_category: :incident_management do include ::Gitlab::Routing include GraphqlHelpers diff --git a/spec/requests/api/graphql/project/environments_spec.rb b/spec/requests/api/graphql/project/environments_spec.rb index bb1763ee228..3a863bd3d77 100644 --- a/spec/requests/api/graphql/project/environments_spec.rb +++ b/spec/requests/api/graphql/project/environments_spec.rb @@ -47,6 +47,54 @@ RSpec.describe 'Project Environments query', feature_category: :continuous_deliv expect(environment_data['environmentType']).to eq(production.environment_type) end + context 'with cluster agent' do + let_it_be(:agent_management_project) { create(:project, :private, :repository) } + let_it_be(:cluster_agent) { create(:cluster_agent, project: agent_management_project) } + + let_it_be(:deployment_project) { create(:project, :private, :repository) } + let_it_be(:environment) { create(:environment, project: deployment_project, cluster_agent: cluster_agent) } + + let!(:authorization) do + create(:agent_user_access_project_authorization, project: deployment_project, agent: cluster_agent) + end + + let(:query) do + %( + query { + project(fullPath: "#{deployment_project.full_path}") { + environment(name: "#{environment.name}") { + clusterAgent { + name + } + } + } + } + ) + end + + before_all do + deployment_project.add_developer(developer) + end + + it 'returns the cluster agent of the environment' do + subject + + cluster_agent_data = graphql_data.dig('project', 'environment', 'clusterAgent') + expect(cluster_agent_data['name']).to eq(cluster_agent.name) + end + + context 'when the cluster is not authorized in the project' do + let!(:authorization) { nil } + + it 'does not return the cluster agent of the environment' do + subject + + cluster_agent_data = graphql_data.dig('project', 'environment', 'clusterAgent') + expect(cluster_agent_data).to be_nil + end + end + end + describe 'user permissions' do let(:query) do %( diff --git a/spec/requests/api/graphql/project/jira_import_spec.rb b/spec/requests/api/graphql/project/jira_import_spec.rb index 821357b6988..25cea0238ef 100644 --- a/spec/requests/api/graphql/project/jira_import_spec.rb +++ b/spec/requests/api/graphql/project/jira_import_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe 'query Jira import data', feature_category: :integrations do +RSpec.describe 'query Jira import data', feature_category: :importers do include GraphqlHelpers let_it_be(:current_user) { create(:user) } diff --git a/spec/requests/api/graphql/project/pipeline_spec.rb b/spec/requests/api/graphql/project/pipeline_spec.rb index abfdf07c288..fb1489372fc 100644 --- a/spec/requests/api/graphql/project/pipeline_spec.rb +++ b/spec/requests/api/graphql/project/pipeline_spec.rb @@ -15,7 +15,7 @@ RSpec.describe 'getting pipeline information nested in a project', feature_categ let(:path) { %i[project pipeline] } let(:pipeline_graphql_data) { graphql_data_at(*path) } let(:depth) { 3 } - let(:excluded) { %w[job project] } # Project is very expensive, due to the number of fields + let(:excluded) { %w[job project jobs] } # Project is very expensive, due to the number of fields let(:fields) { all_graphql_fields_for('Pipeline', excluded: excluded, max_depth: depth) } let(:query) do @@ -82,7 +82,11 @@ RSpec.describe 'getting pipeline information nested in a project', feature_categ context 'when enough data is requested' do let(:fields) do query_graphql_field(:jobs, nil, - query_graphql_field(:nodes, {}, all_graphql_fields_for('CiJob', max_depth: 3))) + query_graphql_field( + :nodes, {}, + all_graphql_fields_for('CiJob', excluded: %w[aiFailureAnalysis], max_depth: 3) + ) + ) end it 'contains jobs' do @@ -116,7 +120,12 @@ RSpec.describe 'getting pipeline information nested in a project', feature_categ let(:fields) do query_graphql_field(:jobs, { retried: retried_argument }, - query_graphql_field(:nodes, {}, all_graphql_fields_for('CiJob', max_depth: 3))) + query_graphql_field( + :nodes, + {}, + all_graphql_fields_for('CiJob', excluded: %w[aiFailureAnalysis], max_depth: 3) + ) + ) end context 'when we filter out retried jobs' do @@ -177,7 +186,7 @@ RSpec.describe 'getting pipeline information nested in a project', feature_categ pipeline(iid: $pipelineIID) { jobs(statuses: [$status]) { nodes { - #{all_graphql_fields_for('CiJob', max_depth: 1)} + #{all_graphql_fields_for('CiJob', excluded: %w[aiFailureAnalysis], max_depth: 3)} } } } diff --git a/spec/requests/api/graphql/project/project_members_spec.rb b/spec/requests/api/graphql/project/project_members_spec.rb index 1f1d8027592..faeb3ddd693 100644 --- a/spec/requests/api/graphql/project/project_members_spec.rb +++ b/spec/requests/api/graphql/project/project_members_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe 'getting project members information', feature_category: :projects do +RSpec.describe 'getting project members information', feature_category: :groups_and_projects do include GraphqlHelpers let_it_be(:parent_group) { create(:group, :public) } diff --git a/spec/requests/api/graphql/project/work_items_spec.rb b/spec/requests/api/graphql/project/work_items_spec.rb index 628a2117e9d..478112b687a 100644 --- a/spec/requests/api/graphql/project/work_items_spec.rb +++ b/spec/requests/api/graphql/project/work_items_spec.rb @@ -288,60 +288,6 @@ RSpec.describe 'getting a work item list for a project', feature_category: :team end end - describe 'fetching work item notes widget' do - let(:item_filter_params) { { iid: item2.iid.to_s } } - let(:fields) do - <<~GRAPHQL - edges { - node { - widgets { - type - ... on WorkItemWidgetNotes { - system: discussions(filter: ONLY_ACTIVITY, first: 10) { nodes { id notes { nodes { id system internal body } } } }, - comments: discussions(filter: ONLY_COMMENTS, first: 10) { nodes { id notes { nodes { id system internal body } } } }, - all_notes: discussions(filter: ALL_NOTES, first: 10) { nodes { id notes { nodes { id system internal body } } } } - } - } - } - } - GRAPHQL - end - - before_all do - create_notes(item1, "some note1") - create_notes(item2, "some note2") - end - - shared_examples 'fetches work item notes' do |user_comments_count:, system_notes_count:| - it "fetches notes" do - post_graphql(query, current_user: current_user) - - all_widgets = graphql_dig_at(items_data, :node, :widgets) - notes_widget = all_widgets.find { |x| x["type"] == "NOTES" } - - all_notes = graphql_dig_at(notes_widget["all_notes"], :nodes) - system_notes = graphql_dig_at(notes_widget["system"], :nodes) - comments = graphql_dig_at(notes_widget["comments"], :nodes) - - expect(comments.count).to eq(user_comments_count) - expect(system_notes.count).to eq(system_notes_count) - expect(all_notes.count).to eq(user_comments_count + system_notes_count) - end - end - - context 'when user has permission to view internal notes' do - before do - project.add_developer(current_user) - end - - it_behaves_like 'fetches work item notes', user_comments_count: 2, system_notes_count: 5 - end - - context 'when user cannot view internal notes' do - it_behaves_like 'fetches work item notes', user_comments_count: 1, system_notes_count: 5 - end - end - context 'when fetching work item notifications widget' do let(:fields) do <<~GRAPHQL @@ -426,26 +372,4 @@ RSpec.describe 'getting a work item list for a project', feature_category: :team query_graphql_field('workItems', params, fields) ) end - - def create_notes(work_item, note_body) - create(:note, system: true, project: work_item.project, noteable: work_item) - - disc_start = create(:discussion_note_on_issue, noteable: work_item, project: work_item.project, note: note_body) - create(:note, - discussion_id: disc_start.discussion_id, noteable: work_item, - project: work_item.project, note: "reply on #{note_body}") - - create(:resource_label_event, user: current_user, issue: work_item, label: label1, action: 'add') - create(:resource_label_event, user: current_user, issue: work_item, label: label1, action: 'remove') - - create(:resource_milestone_event, issue: work_item, milestone: milestone1, action: 'add') - create(:resource_milestone_event, issue: work_item, milestone: milestone1, action: 'remove') - - # confidential notes are currently available only on issues and epics - conf_disc_start = create(:discussion_note_on_issue, :confidential, - noteable: work_item, project: work_item.project, note: "confidential #{note_body}") - create(:note, :confidential, - discussion_id: conf_disc_start.discussion_id, noteable: work_item, - project: work_item.project, note: "reply on confidential #{note_body}") - end end diff --git a/spec/requests/api/graphql/project_query_spec.rb b/spec/requests/api/graphql/project_query_spec.rb index 9f51258c163..54f141d9401 100644 --- a/spec/requests/api/graphql/project_query_spec.rb +++ b/spec/requests/api/graphql/project_query_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe 'getting project information', feature_category: :projects do +RSpec.describe 'getting project information', feature_category: :groups_and_projects do include GraphqlHelpers let_it_be(:group) { create(:group) } diff --git a/spec/requests/api/graphql/subscriptions/work_item_updated_spec.rb b/spec/requests/api/graphql/subscriptions/work_item_updated_spec.rb new file mode 100644 index 00000000000..6c0962e7ec0 --- /dev/null +++ b/spec/requests/api/graphql/subscriptions/work_item_updated_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe Subscriptions::WorkItemUpdated, feature_category: :team_planning do + include GraphqlHelpers + include Graphql::Subscriptions::WorkItems::Helper + + let_it_be(:reporter) { create(:user) } + let_it_be(:project) { create(:project) } + let_it_be(:task) { create(:work_item, :task, project: project) } + + let(:current_user) { nil } + let(:subscribe) { work_item_subscription('workItemUpdated', task, current_user) } + let(:updated_work_item) { graphql_dig_at(graphql_data(response[:result]), :workItemUpdated) } + + before do + stub_const('GitlabSchema', Graphql::Subscriptions::ActionCable::MockGitlabSchema) + Graphql::Subscriptions::ActionCable::MockActionCable.clear_mocks + project.add_reporter(reporter) + end + + subject(:response) do + subscription_response do + GraphqlTriggers.work_item_updated(task) + end + end + + context 'when user is unauthorized' do + it 'does not receive any data' do + expect(response).to be_nil + end + end + + context 'when user is authorized' do + let(:current_user) { reporter } + + it 'receives updated work_item data' do + expect(updated_work_item['id']).to eq(task.to_gid.to_s) + expect(updated_work_item['iid']).to eq(task.iid.to_s) + end + end +end diff --git a/spec/requests/api/graphql/user/group_member_query_spec.rb b/spec/requests/api/graphql/user/group_member_query_spec.rb index d09cb319877..d317651bd8f 100644 --- a/spec/requests/api/graphql/user/group_member_query_spec.rb +++ b/spec/requests/api/graphql/user/group_member_query_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe 'GroupMember', feature_category: :subgroups do +RSpec.describe 'GroupMember', feature_category: :groups_and_projects do include GraphqlHelpers let_it_be(:member) { create(:group_member, :developer) } diff --git a/spec/requests/api/graphql/user/project_member_query_spec.rb b/spec/requests/api/graphql/user/project_member_query_spec.rb index 1baa7815793..b68e9d653ad 100644 --- a/spec/requests/api/graphql/user/project_member_query_spec.rb +++ b/spec/requests/api/graphql/user/project_member_query_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe 'ProjectMember', feature_category: :subgroups do +RSpec.describe 'ProjectMember', feature_category: :groups_and_projects do include GraphqlHelpers let_it_be(:member) { create(:project_member, :developer) } diff --git a/spec/requests/api/graphql/user/starred_projects_query_spec.rb b/spec/requests/api/graphql/user/starred_projects_query_spec.rb index 7d4284300d8..07ace6e5dca 100644 --- a/spec/requests/api/graphql/user/starred_projects_query_spec.rb +++ b/spec/requests/api/graphql/user/starred_projects_query_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe 'Getting starredProjects of the user', feature_category: :projects do +RSpec.describe 'Getting starredProjects of the user', feature_category: :groups_and_projects do include GraphqlHelpers let(:query) do diff --git a/spec/requests/api/graphql/users/set_namespace_commit_email_spec.rb b/spec/requests/api/graphql/users/set_namespace_commit_email_spec.rb new file mode 100644 index 00000000000..1db6f83ce4f --- /dev/null +++ b/spec/requests/api/graphql/users/set_namespace_commit_email_spec.rb @@ -0,0 +1,106 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Setting namespace commit email', feature_category: :user_profile do + include GraphqlHelpers + + let(:current_user) { create(:user) } + let(:group) { create(:group, :public) } + let(:email) { create(:email, :confirmed, user: current_user) } + let(:input) { {} } + let(:namespace_id) { group.to_global_id } + let(:email_id) { email.to_global_id } + + let(:resource_or_permission_error) do + "The resource that you are attempting to access does not exist or you don't have permission to perform this action" + end + + let(:mutation) do + variables = { + namespace_id: namespace_id, + email_id: email_id + } + graphql_mutation(:user_set_namespace_commit_email, variables.merge(input), + <<-QL.strip_heredoc + namespaceCommitEmail { + email { + id + } + } + errors + QL + ) + end + + def mutation_response + graphql_mutation_response(:user_set_namespace_commit_email) + end + + shared_examples 'success' do + it 'creates a namespace commit email' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(mutation_response.dig('namespaceCommitEmail', 'email', 'id')).to eq(email.to_global_id.to_s) + expect(graphql_errors).to be_nil + end + end + + before do + group.add_reporter(current_user) + end + + context 'when current_user is nil' do + it 'returns the top level error' do + post_graphql_mutation(mutation, current_user: nil) + + expect(graphql_errors.first).to match a_hash_including( + 'message' => resource_or_permission_error) + end + end + + context 'when the user cannot access the namespace' do + let(:namespace_id) { create(:group).to_global_id } + + it 'returns the top level error' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(graphql_errors).not_to be_empty + expect(graphql_errors.first).to match a_hash_including( + 'message' => resource_or_permission_error) + end + end + + context 'when the service returns an error' do + let(:email_id) { create(:email).to_global_id } + + it 'returns the error' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(mutation_response['errors']).to contain_exactly("Email must be provided.") + expect(mutation_response['namespaceCommitEmail']).to be_nil + end + end + + context 'when namespace is a group' do + it_behaves_like 'success' + end + + context 'when namespace is a user' do + let(:namespace_id) { current_user.namespace.to_global_id } + + it_behaves_like 'success' + end + + context 'when namespace is a project' do + let_it_be(:project) { create(:project) } + + let(:namespace_id) { project.project_namespace.to_global_id } + + before do + project.add_reporter(current_user) + end + + it_behaves_like 'success' + end +end diff --git a/spec/requests/api/graphql/work_item_spec.rb b/spec/requests/api/graphql/work_item_spec.rb index dc5004a121b..6702224f303 100644 --- a/spec/requests/api/graphql/work_item_spec.rb +++ b/spec/requests/api/graphql/work_item_spec.rb @@ -69,7 +69,8 @@ RSpec.describe 'Query.work_item(id)', feature_category: :team_planning do 'deleteWorkItem' => false, 'adminWorkItem' => true, 'adminParentLink' => true, - 'setWorkItemMetadata' => true + 'setWorkItemMetadata' => true, + 'createNote' => true }, 'project' => hash_including('id' => project.to_gid.to_s, 'fullPath' => project.full_path) ) @@ -540,6 +541,114 @@ RSpec.describe 'Query.work_item(id)', feature_category: :team_planning do end end + describe 'notes widget' do + let(:work_item_fields) do + <<~GRAPHQL + id + widgets { + type + ... on WorkItemWidgetNotes { + system: discussions(filter: ONLY_ACTIVITY, first: 10) { nodes { id notes { nodes { id system internal body } } } }, + comments: discussions(filter: ONLY_COMMENTS, first: 10) { nodes { id notes { nodes { id system internal body } } } }, + all_notes: discussions(filter: ALL_NOTES, first: 10) { nodes { id notes { nodes { id system internal body } } } } + } + } + GRAPHQL + end + + context 'when fetching award emoji from notes' do + let(:work_item_fields) do + <<~GRAPHQL + id + widgets { + type + ... on WorkItemWidgetNotes { + discussions(filter: ONLY_COMMENTS, first: 10) { + nodes { + id + notes { + nodes { + id + body + maxAccessLevelOfAuthor + authorIsContributor + awardEmoji { + nodes { + name + user { + name + } + } + } + } + } + } + } + } + } + GRAPHQL + end + + let_it_be(:note) { create(:note, project: work_item.project, noteable: work_item) } + + before_all do + create(:award_emoji, awardable: note, name: 'rocket', user: developer) + end + + it 'returns award emoji data' do + all_widgets = graphql_dig_at(work_item_data, :widgets) + notes_widget = all_widgets.find { |x| x['type'] == 'NOTES' } + notes = graphql_dig_at(notes_widget['discussions'], :nodes).flat_map { |d| d['notes']['nodes'] } + + note_with_emoji = notes.find { |n| n['id'] == note.to_gid.to_s } + + expect(note_with_emoji).to include( + 'awardEmoji' => { + 'nodes' => include( + hash_including( + 'name' => 'rocket', + 'user' => { + 'name' => developer.name + } + ) + ) + } + ) + end + + it 'returns author contributor status and max access level' do + all_widgets = graphql_dig_at(work_item_data, :widgets) + notes_widget = all_widgets.find { |x| x['type'] == 'NOTES' } + notes = graphql_dig_at(notes_widget['discussions'], :nodes).flat_map { |d| d['notes']['nodes'] } + + expect(notes).to contain_exactly( + hash_including('maxAccessLevelOfAuthor' => 'Owner', 'authorIsContributor' => false) + ) + end + + it 'avoids N+1 queries' do + another_user = create(:user).tap { |u| note.resource_parent.add_developer(u) } + create(:note, project: note.project, noteable: work_item, author: another_user) + + post_graphql(query, current_user: developer) + + control = ActiveRecord::QueryRecorder.new { post_graphql(query, current_user: developer) } + + expect_graphql_errors_to_be_empty + + another_note = create(:note, project: work_item.project, noteable: work_item) + create(:award_emoji, awardable: another_note, name: 'star', user: guest) + another_user = create(:user).tap { |u| note.resource_parent.add_developer(u) } + note_with_different_user = create(:note, project: note.project, noteable: work_item, author: another_user) + create(:award_emoji, awardable: note_with_different_user, name: 'star', user: developer) + + # TODO: Fix existing N+1 queries in https://gitlab.com/gitlab-org/gitlab/-/issues/414747 + expect { post_graphql(query, current_user: developer) }.not_to exceed_query_limit(control).with_threshold(3) + expect_graphql_errors_to_be_empty + end + end + end + context 'when an Issue Global ID is provided' do let(:global_id) { Issue.find(work_item.id).to_gid.to_s } diff --git a/spec/requests/api/group_avatar_spec.rb b/spec/requests/api/group_avatar_spec.rb index 9a0e79ee9f8..c8d06aa19dc 100644 --- a/spec/requests/api/group_avatar_spec.rb +++ b/spec/requests/api/group_avatar_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe API::GroupAvatar, feature_category: :subgroups do +RSpec.describe API::GroupAvatar, feature_category: :groups_and_projects do def avatar_path(group) "/groups/#{ERB::Util.url_encode(group.full_path)}/avatar" end diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index 84d48b4edb4..2adf71f2a18 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe API::Groups, feature_category: :subgroups do +RSpec.describe API::Groups, feature_category: :groups_and_projects do include GroupAPIHelpers include UploadHelpers include WorkhorseHelpers diff --git a/spec/requests/api/integrations_spec.rb b/spec/requests/api/integrations_spec.rb index 8d348dc0a54..4922a07cd6c 100644 --- a/spec/requests/api/integrations_spec.rb +++ b/spec/requests/api/integrations_spec.rb @@ -44,11 +44,17 @@ RSpec.describe API::Integrations, feature_category: :integrations do end where(:integration) do - # The API supports all integrations except the GitLab Slack Application - # integration; this integration must be installed via the UI. + # The Project Integrations API supports all integrations except: + # - The GitLab Slack Application integration, as it must be installed via the UI. + # - Shimo and ZenTao integrations, as new integrations are blocked from being created. + unavailable_integration_names = [ + Integrations::GitlabSlackApplication.to_param, + Integrations::Shimo.to_param, + Integrations::Zentao.to_param + ] + names = Integration.available_integration_names - names.delete(Integrations::GitlabSlackApplication.to_param) if Gitlab.ee? - names - %w[shimo zentao] + names.reject { |name| name.in?(unavailable_integration_names) } end with_them do @@ -62,14 +68,13 @@ RSpec.describe API::Integrations, feature_category: :integrations do let(:missing_attributes) do { datadog: %i[archive_trace_events], - discord: %i[branches_to_be_notified notify_only_broken_pipelines], hangouts_chat: %i[notify_only_broken_pipelines], jira: %i[issues_enabled project_key jira_issue_regex jira_issue_prefix vulnerabilities_enabled vulnerabilities_issuetype], - mattermost: %i[deployment_channel labels_to_be_notified], + mattermost: %i[labels_to_be_notified], mock_ci: %i[enable_ssl_verification], prometheus: %i[manual_configuration], pumble: %i[branches_to_be_notified notify_only_broken_pipelines], - slack: %i[alert_events alert_channel deployment_channel labels_to_be_notified], + slack: %i[labels_to_be_notified], unify_circuit: %i[branches_to_be_notified notify_only_broken_pipelines], webex_teams: %i[branches_to_be_notified notify_only_broken_pipelines] } diff --git a/spec/requests/api/internal/base_spec.rb b/spec/requests/api/internal/base_spec.rb index 6414b1efe6a..619ffd8d41a 100644 --- a/spec/requests/api/internal/base_spec.rb +++ b/spec/requests/api/internal/base_spec.rb @@ -217,47 +217,23 @@ RSpec.describe API::Internal::Base, feature_category: :system_access do end end - context 'when default_pat_expiration feature flag is true' do - it 'returns token with expiry as PersonalAccessToken::MAX_PERSONAL_ACCESS_TOKEN_LIFETIME_IN_DAYS' do - freeze_time do - token_size = (PersonalAccessToken.token_prefix || '').size + 20 - - post api('/internal/personal_access_token'), - params: { - key_id: key.id, - name: 'newtoken', - scopes: %w(read_api read_repository) - }, - headers: gitlab_shell_internal_api_request_header - - expect(json_response['success']).to be_truthy - expect(json_response['token']).to match(/\A\S{#{token_size}}\z/) - expect(json_response['scopes']).to match_array(%w(read_api read_repository)) - expect(json_response['expires_at']).to eq(max_pat_access_token_lifetime.iso8601) - end - end - end - - context 'when default_pat_expiration feature flag is false' do - before do - stub_feature_flags(default_pat_expiration: false) - end - - it 'uses nil expiration value' do + it 'returns token with expiry as PersonalAccessToken::MAX_PERSONAL_ACCESS_TOKEN_LIFETIME_IN_DAYS' do + freeze_time do token_size = (PersonalAccessToken.token_prefix || '').size + 20 post api('/internal/personal_access_token'), - params: { - key_id: key.id, - name: 'newtoken', - scopes: %w(read_api read_repository) - }, - headers: gitlab_shell_internal_api_request_header + params: { + key_id: key.id, + name: 'newtoken', + scopes: %w(read_api read_repository), + expires_at: 365.days.from_now + }, + headers: gitlab_shell_internal_api_request_header expect(json_response['success']).to be_truthy expect(json_response['token']).to match(/\A\S{#{token_size}}\z/) expect(json_response['scopes']).to match_array(%w(read_api read_repository)) - expect(json_response['expires_at']).to be_nil + expect(json_response['expires_at']).to eq(max_pat_access_token_lifetime.iso8601) end end end @@ -513,24 +489,63 @@ RSpec.describe API::Internal::Base, feature_category: :system_access do project.add_developer(user) end + shared_context 'with env passed as a JSON' do + let(:obj_dir_relative) { './objects' } + let(:alt_obj_dirs_relative) { ['./alt-objects-1', './alt-objects-2'] } + let(:env) do + { + GIT_OBJECT_DIRECTORY_RELATIVE: obj_dir_relative, + GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE: alt_obj_dirs_relative + } + end + end + shared_examples 'sets hook env' do - context 'with env passed as a JSON' do - let(:obj_dir_relative) { './objects' } - let(:alt_obj_dirs_relative) { ['./alt-objects-1', './alt-objects-2'] } - let(:env) do - { - GIT_OBJECT_DIRECTORY_RELATIVE: obj_dir_relative, - GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE: alt_obj_dirs_relative - } - end + include_context 'with env passed as a JSON' - it 'sets env in RequestStore' do - expect(Gitlab::Git::HookEnv).to receive(:set).with(gl_repository, env.stringify_keys) + it 'sets env in RequestStore' do + expect(Gitlab::Git::HookEnv).to receive(:set).with(gl_repository, env.stringify_keys) - subject + subject - expect(response).to have_gitlab_http_status(:ok) - end + expect(response).to have_gitlab_http_status(:ok) + end + end + + shared_examples 'sets hook env and routes to primary' do + include_context 'with env passed as a JSON' + + let(:interceptor) do + Class.new(::GRPC::ClientInterceptor) do + def route_to_primary_received? + @route_to_primary_count.to_i > 0 + end + + def request_response(request:, call:, method:, metadata:) # rubocop:disable Lint/UnusedMethodArgument + @route_to_primary_count ||= 0 + @route_to_primary_count += 1 if metadata['gitaly-route-repository-accessor-policy'] == 'primary-only' + + yield + end + end.new + end + + before do + Gitlab::GitalyClient.clear_stubs! + allow(::Gitlab::GitalyClient).to receive(:interceptors).and_return([interceptor]) + end + + after do + Gitlab::GitalyClient.clear_stubs! + end + + it 'sets env in RequestStore and routes gRPC messages to primary', :request_store do + expect(Gitlab::Git::HookEnv).to receive(:set).with(gl_repository, env.stringify_keys).and_call_original + + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(interceptor.route_to_primary_received?).to be_truthy end end @@ -549,6 +564,8 @@ RSpec.describe API::Internal::Base, feature_category: :system_access do expect(user.reload.last_activity_on).to eql(Date.today) end + # Wiki repositories don't invoke any Gitaly RPCs to check for changes, so we can only test for the + # hook environment being set. it_behaves_like 'sets hook env' do let(:gl_repository) { Gitlab::GlRepository::WIKI.identifier_for_container(project.wiki) } end @@ -588,7 +605,7 @@ RSpec.describe API::Internal::Base, feature_category: :system_access do expect(user.reload.last_activity_on).to eql(Date.today) end - it_behaves_like 'sets hook env' do + it_behaves_like 'sets hook env and routes to primary' do let(:gl_repository) { Gitlab::GlRepository::SNIPPET.identifier_for_container(personal_snippet) } end end @@ -620,7 +637,7 @@ RSpec.describe API::Internal::Base, feature_category: :system_access do expect(user.reload.last_activity_on).to eql(Date.today) end - it_behaves_like 'sets hook env' do + it_behaves_like 'sets hook env and routes to primary' do let(:gl_repository) { Gitlab::GlRepository::SNIPPET.identifier_for_container(project_snippet) } end end diff --git a/spec/requests/api/internal/error_tracking_spec.rb b/spec/requests/api/internal/error_tracking_spec.rb index 83012e26138..1906bed6007 100644 --- a/spec/requests/api/internal/error_tracking_spec.rb +++ b/spec/requests/api/internal/error_tracking_spec.rb @@ -19,7 +19,6 @@ RSpec.describe API::Internal::ErrorTracking, feature_category: :error_tracking d before do # Because the feature flag is disabled in specs we have to enable it explicitly. - stub_feature_flags(use_click_house_database_for_error_tracking: true) stub_feature_flags(gitlab_error_tracking: true) end @@ -90,9 +89,8 @@ RSpec.describe API::Internal::ErrorTracking, feature_category: :error_tracking d expect(json_response).to eq('enabled' => true) end - context 'when feature flags use_click_house_database_for_error_tracking or gitlab_error_tracking are disabled' do + context 'when feature flags gitlab_error_tracking are disabled' do before do - stub_feature_flags(use_click_house_database_for_error_tracking: false) stub_feature_flags(gitlab_error_tracking: false) end diff --git a/spec/requests/api/internal/kubernetes_spec.rb b/spec/requests/api/internal/kubernetes_spec.rb index c07382a6e04..3c76fba4e2c 100644 --- a/spec/requests/api/internal/kubernetes_spec.rb +++ b/spec/requests/api/internal/kubernetes_spec.rb @@ -122,11 +122,12 @@ 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 } + 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] } 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_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] } expected_hll_count = unique_counters[:agent_users_using_ci_tunnel].uniq.count @@ -337,6 +338,81 @@ RSpec.describe API::Internal::Kubernetes, feature_category: :deployment_manageme end end + describe 'GET /internal/kubernetes/verify_project_access' do + def send_request(headers: {}, params: {}) + get api("/internal/kubernetes/verify_project_access"), params: params, headers: headers.reverse_merge(jwt_auth_headers) + end + + include_examples 'authorization' + include_examples 'agent authentication' + include_examples 'error handling' + + shared_examples 'access is granted' do + it 'returns success response' do + send_request(params: { id: project_id }, headers: { 'Authorization' => "Bearer #{agent_token.token}" }) + + expect(response).to have_gitlab_http_status(:no_content) + end + end + + shared_examples 'access is denied' do + it 'returns 404' do + send_request(params: { id: project_id }, headers: { 'Authorization' => "Bearer #{agent_token.token}" }) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'an agent is found' do + let_it_be(:agent_token) { create(:cluster_agent_token) } + let(:project_id) { project.id } + + include_examples 'agent token tracking' + + context 'project is public' do + let(:project) { create(:project, :public) } + + it_behaves_like 'access is granted' + + context 'repository is for project members only' do + let(:project) { create(:project, :public, :repository_private) } + + it_behaves_like 'access is denied' + end + end + + context 'project is private' do + let(:project) { create(:project, :private) } + + it_behaves_like 'access is denied' + + context 'and agent belongs to project' do + let(:agent_token) { create(:cluster_agent_token, agent: create(:cluster_agent, project: project)) } + + it_behaves_like 'access is granted' + end + end + + context 'project is internal' do + let(:project) { create(:project, :internal) } + + it_behaves_like 'access is denied' + + context 'and agent belongs to project' do + let(:agent_token) { create(:cluster_agent_token, agent: create(:cluster_agent, project: project)) } + + it_behaves_like 'access is granted' + end + end + + context 'project does not exist' do + let(:project_id) { non_existing_record_id } + + it_behaves_like 'access is denied' + end + end + end + describe 'POST /internal/kubernetes/authorize_proxy_user', :clean_gitlab_redis_sessions do include SessionHelpers diff --git a/spec/requests/api/issues/post_projects_issues_spec.rb b/spec/requests/api/issues/post_projects_issues_spec.rb index 5a15a0b6dad..1cd20680afb 100644 --- a/spec/requests/api/issues/post_projects_issues_spec.rb +++ b/spec/requests/api/issues/post_projects_issues_spec.rb @@ -416,11 +416,12 @@ RSpec.describe API::Issues, :aggregate_failures, feature_category: :team_plannin end before do - expect_next_instance_of(Spam::SpamActionService) do |spam_service| - expect(spam_service).to receive_messages(check_for_spam?: true) + expect_next_instance_of(Issue) do |instance| + expect(instance).to receive(:check_for_spam).with(user: user, action: :create).and_call_original end + expect_next_instance_of(Spam::AkismetService) do |akismet_service| - expect(akismet_service).to receive_messages(spam?: true) + expect(akismet_service).to receive(:spam?).and_return(true) end end diff --git a/spec/requests/api/markdown_spec.rb b/spec/requests/api/markdown_spec.rb index db5bbd610fc..8298d0bf150 100644 --- a/spec/requests/api/markdown_spec.rb +++ b/spec/requests/api/markdown_spec.rb @@ -5,13 +5,18 @@ require "spec_helper" RSpec.describe API::Markdown, feature_category: :team_planning do describe "POST /markdown" do let(:user) {} # No-op. It gets overwritten in the contexts below. + let(:token) {} # No-op. It gets overwritten in the contexts below. let(:disable_authenticate_markdown_api) { false } before do stub_commonmark_sourcepos_disabled stub_feature_flags(authenticate_markdown_api: false) if disable_authenticate_markdown_api - post api("/markdown", user), params: params + if token + post api("/markdown", personal_access_token: token), params: params + else + post api("/markdown", user), params: params + end end shared_examples "rendered markdown text without GFM" do @@ -85,6 +90,13 @@ RSpec.describe API::Markdown, feature_category: :team_planning do let(:issue_url) { "http://#{Gitlab.config.gitlab.host}/#{issue.project.namespace.path}/#{issue.project.path}/-/issues/#{issue.iid}" } let(:text) { ":tada: Hello world! :100: #{issue.to_reference}" } + context "when personal access token has only read_api scope" do + let(:token) { create(:personal_access_token, user: user, scopes: [:read_api]) } + let(:params) { { text: text } } + + it_behaves_like "rendered markdown text without GFM" + end + context "when not using gfm" do context "without project" do let(:params) { { text: text } } diff --git a/spec/requests/api/maven_packages_spec.rb b/spec/requests/api/maven_packages_spec.rb index 60e91973b5d..4e746802500 100644 --- a/spec/requests/api/maven_packages_spec.rb +++ b/spec/requests/api/maven_packages_spec.rb @@ -1020,7 +1020,7 @@ RSpec.describe API::MavenPackages, feature_category: :package_registry do upload_file(params: params.merge(job_token: job.token)) expect(response).to have_gitlab_http_status(:ok) - expect(project.reload.packages.last.original_build_info.pipeline).to eq job.pipeline + expect(project.reload.packages.last.last_build_info.pipeline).to eq job.pipeline end it 'rejects upload without running job token' do @@ -1155,25 +1155,6 @@ RSpec.describe API::MavenPackages, feature_category: :package_registry do expect(response).to have_gitlab_http_status(:no_content) end - - context 'when the stored sha1 is not the same' do - let(:sent_sha1) { File.read(file_upload.path) } - let(:stored_sha1) { 'wrong sha1' } - - it 'logs an error and returns conflict' do - expect(Gitlab::ErrorTracking).to receive(:log_exception).with( - instance_of(ArgumentError), - message: 'maven package file sha1 conflict', - stored_sha1: stored_sha1, - received_sha256: Digest::SHA256.hexdigest(sent_sha1), - sha256_hexdigest_of_stored_sha1: Digest::SHA256.hexdigest(stored_sha1) - ) - - upload - - expect(response).to have_gitlab_http_status(:conflict) - end - end end context 'for md5 file' do diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb index 353fddcb08d..f3e5f3ab891 100644 --- a/spec/requests/api/members_spec.rb +++ b/spec/requests/api/members_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe API::Members, feature_category: :subgroups do +RSpec.describe API::Members, feature_category: :groups_and_projects do let_it_be(:maintainer) { create(:user, username: 'maintainer_user') } let_it_be(:maintainer2) { create(:user, username: 'user-with-maintainer-role') } let_it_be(:developer) { create(:user) } diff --git a/spec/requests/api/ml/mlflow/experiments_spec.rb b/spec/requests/api/ml/mlflow/experiments_spec.rb index 1a2577e69e7..fc2e814752c 100644 --- a/spec/requests/api/ml/mlflow/experiments_spec.rb +++ b/spec/requests/api/ml/mlflow/experiments_spec.rb @@ -20,7 +20,6 @@ RSpec.describe API::Ml::Mlflow::Experiments, feature_category: :mlops do end let(:current_user) { developer } - let(:ff_value) { true } let(:access_token) { tokens[:write] } let(:headers) { { 'Authorization' => "Bearer #{access_token.token}" } } let(:project_id) { project.id } @@ -52,10 +51,6 @@ RSpec.describe API::Ml::Mlflow::Experiments, feature_category: :mlops do response end - before do - stub_feature_flags(ml_experiment_tracking: ff_value) - end - describe 'GET /projects/:id/ml/mlflow/api/2.0/mlflow/experiments/get' do let(:experiment_iid) { experiment.iid.to_s } let(:route) { "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/experiments/get?experiment_id=#{experiment_iid}" } diff --git a/spec/requests/api/ml/mlflow/runs_spec.rb b/spec/requests/api/ml/mlflow/runs_spec.rb index 746372b7978..a85fe4d867a 100644 --- a/spec/requests/api/ml/mlflow/runs_spec.rb +++ b/spec/requests/api/ml/mlflow/runs_spec.rb @@ -26,7 +26,6 @@ RSpec.describe API::Ml::Mlflow::Runs, feature_category: :mlops do end let(:current_user) { developer } - let(:ff_value) { true } let(:access_token) { tokens[:write] } let(:headers) { { 'Authorization' => "Bearer #{access_token.token}" } } let(:project_id) { project.id } @@ -40,10 +39,6 @@ RSpec.describe API::Ml::Mlflow::Runs, feature_category: :mlops do response end - before do - stub_feature_flags(ml_experiment_tracking: ff_value) - end - RSpec.shared_examples 'MLflow|run_id param error cases' do context 'when run id is not passed' do let(:params) { {} } diff --git a/spec/requests/api/ml_model_packages_spec.rb b/spec/requests/api/ml_model_packages_spec.rb new file mode 100644 index 00000000000..9c19f522e46 --- /dev/null +++ b/spec/requests/api/ml_model_packages_spec.rb @@ -0,0 +1,200 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ::API::MlModelPackages, feature_category: :mlops do + include HttpBasicAuthHelpers + include PackagesManagerApiSpecHelpers + include WorkhorseHelpers + using RSpec::Parameterized::TableSyntax + + include_context 'workhorse headers' + + let_it_be(:project, reload: true) { create(:project) } + let_it_be(:personal_access_token) { create(:personal_access_token) } + let_it_be(:job) { create(:ci_build, :running, user: personal_access_token.user, project: project) } + let_it_be(:deploy_token) { create(:deploy_token, read_package_registry: true, write_package_registry: true) } + let_it_be(:project_deploy_token) { create(:project_deploy_token, deploy_token: deploy_token, project: project) } + let_it_be(:another_project, reload: true) { create(:project) } + + let_it_be(:tokens) do + { + personal_access_token: personal_access_token.token, + deploy_token: deploy_token.token, + job_token: job.token + } + end + + let(:user) { personal_access_token.user } + let(:user_role) { :developer } + let(:member) { true } + let(:ci_build) { create(:ci_build, :running, user: user, project: project) } + let(:project_to_enable_ff) { project } + let(:headers) { {} } + + shared_context 'ml model authorize permissions table' do # rubocop:disable RSpec/ContextWording + # rubocop:disable Metrics/AbcSize + # :visibility, :user_role, :member, :token_type, :valid_token, :expected_status + def authorize_permissions_table + :public | :developer | true | :personal_access_token | true | :success + :public | :guest | true | :personal_access_token | true | :forbidden + :public | :developer | true | :personal_access_token | false | :unauthorized + :public | :guest | true | :personal_access_token | false | :unauthorized + :public | :developer | false | :personal_access_token | true | :forbidden + :public | :guest | false | :personal_access_token | true | :forbidden + :public | :developer | false | :personal_access_token | false | :unauthorized + :public | :guest | false | :personal_access_token | false | :unauthorized + :public | :anonymous | false | :personal_access_token | true | :unauthorized + :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 | :unauthorized + :public | :developer | true | :job_token | true | :success + :public | :guest | true | :job_token | true | :forbidden + :public | :developer | true | :job_token | false | :unauthorized + :public | :guest | true | :job_token | false | :unauthorized + :public | :developer | false | :job_token | true | :forbidden + :public | :guest | false | :job_token | true | :forbidden + :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 + + before 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 + 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) } + + describe 'user access' do + where(:visibility, :user_role, :member, :token_type, :valid_token, :expected_status) do + authorize_permissions_table + 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 + + it { is_expected.to have_gitlab_http_status(expected_status) } + end + + it_behaves_like 'Endpoint not found if read_model_registry not available' + 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' + 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 + end + end + end + + describe 'PUT /api/v4/projects/:id/packages/ml_models/:package_name/:package_version/:file_name' do + include_context 'ml model authorize permissions table' + + let_it_be(:file_name) { 'model.md5' } + + let(:token) { tokens[:personal_access_token] } + let(:user_headers) { { 'HTTP_AUTHORIZATION' => token } } + let(:headers) { user_headers.merge(workhorse_headers) } + let(:params) { { file: temp_file(file_name) } } + let(:file_key) { :file } + let(:send_rewritten_field) { true } + + let(:request) do + upload_file(headers) + end + + describe 'success' do + it 'creates a new package' do + expect { api_response }.to change { Packages::PackageFile.count }.by(1) + expect(api_response).to have_gitlab_http_status(:created) + end + end + + describe 'user access' do + where(:visibility, :user_role, :member, :token_type, :valid_token, :expected_status) do + authorize_permissions_table + 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 upload' + 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 + + 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" + + put api(url), headers: request_headers + end + + 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 + ) + end +end diff --git a/spec/requests/api/namespaces_spec.rb b/spec/requests/api/namespaces_spec.rb index f268a092034..f796edfb20e 100644 --- a/spec/requests/api/namespaces_spec.rb +++ b/spec/requests/api/namespaces_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe API::Namespaces, :aggregate_failures, feature_category: :subgroups do +RSpec.describe API::Namespaces, :aggregate_failures, feature_category: :groups_and_projects do let_it_be(:admin) { create(:admin) } let_it_be(:user) { create(:user) } let_it_be(:group1) { create(:group, name: 'group.one') } @@ -30,7 +30,7 @@ RSpec.describe API::Namespaces, :aggregate_failures, feature_category: :subgroup expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers expect(group_kind_json_response.keys).to include('id', 'kind', 'name', 'path', 'full_path', - 'parent_id', 'members_count_with_descendants') + 'parent_id', 'members_count_with_descendants', 'root_repository_size') expect(user_kind_json_response.keys).to include('id', 'kind', 'name', 'path', 'full_path', 'parent_id') end @@ -66,7 +66,7 @@ RSpec.describe API::Namespaces, :aggregate_failures, feature_category: :subgroup owned_group_response = json_response.find { |resource| resource['id'] == group1.id } expect(owned_group_response.keys).to include('id', 'kind', 'name', 'path', 'full_path', - 'parent_id', 'members_count_with_descendants') + 'parent_id', 'members_count_with_descendants', 'root_repository_size') end it "returns correct attributes when user cannot admin group" do diff --git a/spec/requests/api/npm_group_packages_spec.rb b/spec/requests/api/npm_group_packages_spec.rb new file mode 100644 index 00000000000..d97c7682b4b --- /dev/null +++ b/spec/requests/api/npm_group_packages_spec.rb @@ -0,0 +1,186 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::NpmGroupPackages, feature_category: :package_registry do + using RSpec::Parameterized::TableSyntax + + include_context 'npm api setup' + + describe 'GET /api/v4/groups/:id/-/packages/npm/*package_name' do + let(:url) { api("/groups/#{group.id}/-/packages/npm/#{package_name}") } + + it_behaves_like 'handling get metadata requests', scope: :group + + context 'with a duplicate package name in another project' do + subject { get(url) } + + before do + group.add_developer(user) + end + + let_it_be(:project2) { create(:project, :public, namespace: namespace) } + let_it_be(:package2) do + create(:npm_package, + project: project2, + name: "@#{group.path}/scoped_package", + version: '1.2.0') + end + + it_behaves_like 'rejects invalid package names' + + it 'includes all matching package versions in the response' do + subject + + expect(json_response['versions'].keys).to match_array([package.version, package2.version]) + end + + context 'with the feature flag disabled' do + before do + stub_feature_flags(npm_allow_packages_in_multiple_projects: false) + end + + it 'returns matching package versions from only one project' do + subject + + expect(json_response['versions'].keys).to match_array([package2.version]) + end + end + end + + context 'with mixed group and project visibilities' do + subject { get(url, headers: headers) } + + where(:auth, :group_visibility, :project_visibility, :user_role, :expected_status) do + nil | :public | :public | nil | :ok + nil | :public | :internal | nil | :not_found + nil | :public | :private | nil | :not_found + nil | :internal | :internal | nil | :not_found + nil | :internal | :private | nil | :not_found + nil | :private | :private | nil | :not_found + + :oauth | :public | :public | :guest | :ok + :oauth | :public | :internal | :guest | :ok + :oauth | :public | :private | :guest | :forbidden + :oauth | :internal | :internal | :guest | :ok + :oauth | :internal | :private | :guest | :forbidden + :oauth | :private | :private | :guest | :forbidden + :oauth | :public | :public | :reporter | :ok + :oauth | :public | :internal | :reporter | :ok + :oauth | :public | :private | :reporter | :ok + :oauth | :internal | :internal | :reporter | :ok + :oauth | :internal | :private | :reporter | :ok + :oauth | :private | :private | :reporter | :ok + + :personal_access_token | :public | :public | :guest | :ok + :personal_access_token | :public | :internal | :guest | :ok + :personal_access_token | :public | :private | :guest | :forbidden + :personal_access_token | :internal | :internal | :guest | :ok + :personal_access_token | :internal | :private | :guest | :forbidden + :personal_access_token | :private | :private | :guest | :forbidden + :personal_access_token | :public | :public | :reporter | :ok + :personal_access_token | :public | :internal | :reporter | :ok + :personal_access_token | :public | :private | :reporter | :ok + :personal_access_token | :internal | :internal | :reporter | :ok + :personal_access_token | :internal | :private | :reporter | :ok + :personal_access_token | :private | :private | :reporter | :ok + + :job_token | :public | :public | :developer | :ok + :job_token | :public | :internal | :developer | :ok + :job_token | :public | :private | :developer | :ok + :job_token | :internal | :internal | :developer | :ok + :job_token | :internal | :private | :developer | :ok + :job_token | :private | :private | :developer | :ok + + :deploy_token | :public | :public | nil | :ok + :deploy_token | :public | :internal | nil | :ok + :deploy_token | :public | :private | nil | :ok + :deploy_token | :internal | :internal | nil | :ok + :deploy_token | :internal | :private | nil | :ok + :deploy_token | :private | :private | nil | :ok + end + + with_them do + let(:headers) do + case auth + when :oauth + build_token_auth_header(token.plaintext_token) + when :personal_access_token + build_token_auth_header(personal_access_token.token) + when :job_token + build_token_auth_header(job.token) + when :deploy_token + build_token_auth_header(deploy_token.token) + else + {} + end + end + + before do + project.update!(visibility: project_visibility.to_s) + project.send("add_#{user_role}", user) if user_role + group.update!(visibility: group_visibility.to_s) + group.send("add_#{user_role}", user) if user_role + end + + it_behaves_like 'returning response status', params[:expected_status] + end + end + + context 'when user is a reporter of project but is not a direct member of group' do + subject { get(url, headers: headers) } + + where(:group_visibility, :project_visibility, :expected_status) do + :public | :public | :ok + :public | :internal | :ok + :public | :private | :ok + :internal | :internal | :ok + :internal | :private | :ok + :private | :private | :ok + end + + with_them do + let(:headers) { build_token_auth_header(personal_access_token.token) } + + before do + project.update!(visibility: project_visibility.to_s) + project.add_reporter(user) + + group.update!(visibility: group_visibility.to_s) + end + + it_behaves_like 'returning response status', params[:expected_status] + end + end + end + + describe 'GET /api/v4/packages/npm/-/package/*package_name/dist-tags' do + it_behaves_like 'handling get dist tags requests', scope: :group do + let(:url) { api("/groups/#{group.id}/-/packages/npm/-/package/#{package_name}/dist-tags") } + end + end + + describe 'PUT /api/v4/packages/npm/-/package/*package_name/dist-tags/:tag' 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 + 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 + end + + describe 'POST /api/v4/groups/:id/-/packages/npm/-/npm/v1/security/advisories/bulk' do + it_behaves_like 'handling audit request', path: 'advisories/bulk', scope: :group do + let(:url) { api("/groups/#{group.id}/-/packages/npm/-/npm/v1/security/advisories/bulk") } + end + end + + describe 'POST /api/v4/groups/:id/-/packages/npm/-/npm/v1/security/audits/quick' do + it_behaves_like 'handling audit request', path: 'audits/quick', scope: :group do + let(:url) { api("/groups/#{group.id}/-/packages/npm/-/npm/v1/security/audits/quick") } + end + end +end diff --git a/spec/requests/api/npm_project_packages_spec.rb b/spec/requests/api/npm_project_packages_spec.rb index d673645c51a..60d4bddc502 100644 --- a/spec/requests/api/npm_project_packages_spec.rb +++ b/spec/requests/api/npm_project_packages_spec.rb @@ -265,7 +265,7 @@ RSpec.describe API::NpmProjectPackages, feature_category: :package_registry do upload_package_with_token expect(response).to have_gitlab_http_status(:ok) - expect(project.reload.packages.find(json_response['id']).original_build_info.pipeline).to eq job.pipeline + expect(project.reload.packages.find(json_response['id']).last_build_info.pipeline).to eq job.pipeline end end end diff --git a/spec/requests/api/nuget_group_packages_spec.rb b/spec/requests/api/nuget_group_packages_spec.rb index facbc01220d..07199119cb5 100644 --- a/spec/requests/api/nuget_group_packages_spec.rb +++ b/spec/requests/api/nuget_group_packages_spec.rb @@ -26,13 +26,7 @@ RSpec.describe API::NugetGroupPackages, feature_category: :package_registry do shared_examples 'handling all endpoints' do describe 'GET /api/v4/groups/:id/-/packages/nuget' do - it_behaves_like 'handling nuget service requests', - example_names_with_status: { - anonymous_requests_example_name: 'rejects nuget packages access', - anonymous_requests_status: :unauthorized, - guest_requests_example_name: 'process nuget service index request', - guest_requests_status: :success - } do + it_behaves_like 'handling nuget service requests' do let(:url) { "/groups/#{target.id}/-/packages/nuget/index.json" } end end diff --git a/spec/requests/api/pages_domains_spec.rb b/spec/requests/api/pages_domains_spec.rb index 9ca027c2edc..42d83ff8139 100644 --- a/spec/requests/api/pages_domains_spec.rb +++ b/spec/requests/api/pages_domains_spec.rb @@ -440,8 +440,8 @@ RSpec.describe API::PagesDomains, feature_category: :pages do expect(response).to have_gitlab_http_status(:ok) expect(response).to match_response_schema('public_api/v4/pages_domain/detail') expect(pages_domain_with_letsencrypt.auto_ssl_enabled).to be false - expect(pages_domain_with_letsencrypt.key).to be - expect(pages_domain_with_letsencrypt.certificate).to be + expect(pages_domain_with_letsencrypt.key).to be_present + expect(pages_domain_with_letsencrypt.certificate).to be_present end it 'updates pages domain with expired certificate', :aggregate_failures do diff --git a/spec/requests/api/project_attributes.yml b/spec/requests/api/project_attributes.yml index e9581265bb0..e0e9c944fe4 100644 --- a/spec/requests/api/project_attributes.yml +++ b/spec/requests/api/project_attributes.yml @@ -127,6 +127,7 @@ project_feature: - project_id - updated_at - operations_access_level + - model_experiments_access_level computed_attributes: - issues_enabled - jobs_enabled @@ -180,6 +181,9 @@ project_setting: - cube_api_key - encrypted_cube_api_key - encrypted_cube_api_key_iv + - encrypted_product_analytics_configurator_connection_string + - encrypted_product_analytics_configurator_connection_string_iv + - product_analytics_configurator_connection_string build_service_desk_setting: # service_desk_setting unexposed_attributes: diff --git a/spec/requests/api/project_export_spec.rb b/spec/requests/api/project_export_spec.rb index 22d7ea36f6c..434936c0ee7 100644 --- a/spec/requests/api/project_export_spec.rb +++ b/spec/requests/api/project_export_spec.rb @@ -284,7 +284,7 @@ RSpec.describe API::ProjectExport, :aggregate_failures, :clean_gitlab_redis_cach stub_application_setting(project_download_export_limit: 1) end - it 'throttles downloads within same namespaces' do + it 'throttles downloads within same namespaces', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/413230' do # simulate prior request to the same namespace, which increments the rate limit counter for that scope Gitlab::ApplicationRateLimiter.throttled?(:project_download_export, scope: [user, project_finished.namespace]) diff --git a/spec/requests/api/project_hooks_spec.rb b/spec/requests/api/project_hooks_spec.rb index 8e5e9d847ea..c6bf77e5dcf 100644 --- a/spec/requests/api/project_hooks_spec.rb +++ b/spec/requests/api/project_hooks_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe API::ProjectHooks, 'ProjectHooks', feature_category: :integrations do +RSpec.describe API::ProjectHooks, 'ProjectHooks', feature_category: :webhooks do let_it_be(:user) { create(:user) } let_it_be(:user3) { create(:user) } let_it_be(:project) { create(:project, creator_id: user.id, namespace: user.namespace) } diff --git a/spec/requests/api/project_job_token_scope_spec.rb b/spec/requests/api/project_job_token_scope_spec.rb index df210a00012..06e28d57ca6 100644 --- a/spec/requests/api/project_job_token_scope_spec.rb +++ b/spec/requests/api/project_job_token_scope_spec.rb @@ -73,4 +73,444 @@ RSpec.describe API::ProjectJobTokenScope, feature_category: :secrets_management end end end + + describe 'PATCH /projects/:id/job_token_scope' do + let_it_be(:project) { create(:project, :public) } + let_it_be(:user) { create(:user) } + + let(:patch_job_token_scope_path) { "/projects/#{project.id}/job_token_scope" } + let(:patch_job_token_scope_params) do + { enabled: false } + end + + subject { patch api(patch_job_token_scope_path, user), params: patch_job_token_scope_params } + + context 'when unauthenticated user (missing user)' do + context 'for public project' do + it 'does not return ci cd settings of job token' do + project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC) + + patch api(patch_job_token_scope_path), params: patch_job_token_scope_params + + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + end + + context 'when authenticated user as maintainer' do + before_all { project.add_maintainer(user) } + + it 'returns unauthorized and blank response when invalid auth credentials are given' do + invalid_personal_access_token = build(:personal_access_token, user: user) + + patch api(patch_job_token_scope_path, user, personal_access_token: invalid_personal_access_token), + params: patch_job_token_scope_params + + expect(response).to have_gitlab_http_status(:unauthorized) + end + + it 'returns no content and updates the ci cd setting `ci_inbound_job_token_scope_enabled`' do + subject + + expect(response).to have_gitlab_http_status(:no_content) + expect(response.body).to be_blank + + project.reload + + expect(project.reload.ci_inbound_job_token_scope_enabled?).to be_falsey + expect(project.reload.ci_outbound_job_token_scope_enabled?).to be_falsey + end + + it 'returns bad_request when ::Projects::UpdateService fails' do + project_update_service_result = { status: :error, message: "any_internal_error_message" } + project_update_service = instance_double(Projects::UpdateService, execute: project_update_service_result) + allow(::Projects::UpdateService).to receive(:new).and_return(project_update_service) + + subject + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response).to be_present + end + + it 'returns bad_request when invalid value for parameter is given' do + patch api(patch_job_token_scope_path, user), params: {} + + expect(response).to have_gitlab_http_status(:bad_request) + end + + it 'returns bad_request when invalid parameter given, e.g. truthy value' do + patch api(patch_job_token_scope_path, user), params: { enabled: 123 } + + expect(response).to have_gitlab_http_status(:bad_request) + end + + it 'returns bad_request when invalid parameter given, e.g. `nil`' do + patch api(patch_job_token_scope_path, user), params: { enabled: nil } + + expect(response).to have_gitlab_http_status(:bad_request) + end + + it 'returns bad_request and leaves it untouched when unpermitted parameter given' do + expect do + patch api(patch_job_token_scope_path, user), + params: { + irrelevant_parameter_boolean: true, + irrelevant_parameter_number: 12.34 + } + end.not_to change { project.reload.updated_at } + + expect(response).to have_gitlab_http_status(:bad_request) + + project_reloaded = Project.find(project.id) + expect(project_reloaded.ci_inbound_job_token_scope_enabled?).to eq project.ci_inbound_job_token_scope_enabled? + expect(project_reloaded.ci_outbound_job_token_scope_enabled?).to eq project.ci_outbound_job_token_scope_enabled? + end + + # We intend to deprecate the possibility to enable the outbound job token scope until gitlab release `v17.0` . + it 'returns bad_request when param `outbound_scope_enabled` given' do + patch api(patch_job_token_scope_path, user), params: { outbound_scope_enabled: true } + + expect(response).to have_gitlab_http_status(:bad_request) + + project.reload + + expect(project.reload.ci_inbound_job_token_scope_enabled?).to be_truthy + expect(project.reload.ci_outbound_job_token_scope_enabled?).to be_falsey + end + end + + context 'when authenticated user as developer' do + before do + project.add_developer(user) + end + + it 'returns forbidden and no ci cd settings for public project' do + project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC) + + subject + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + end + + describe "GET /projects/:id/job_token_scope/allowlist" do + let_it_be(:project) { create(:project, :public) } + + let_it_be(:user) { create(:user) } + + let(:get_job_token_scope_allowlist_path) { "/projects/#{project.id}/job_token_scope/allowlist" } + + subject { get api(get_job_token_scope_allowlist_path, user) } + + context 'when unauthenticated user (missing user)' do + context 'for public project' do + it 'does not return ci cd settings of job token' do + project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC) + + get api(get_job_token_scope_allowlist_path) + + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + end + + context 'when authenticated user as maintainer' do + before_all { project.add_maintainer(user) } + + it 'returns allowlist containing only the source projects' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(json_response).to be_present + expect(json_response).to include hash_including("id" => project.id) + end + + it 'returns allowlist of project' do + create(:ci_job_token_project_scope_link, source_project: project, direction: :inbound) + create(:ci_job_token_project_scope_link, source_project: project, direction: :outbound) + + ci_job_token_project_scope_link = + create( + :ci_job_token_project_scope_link, + source_project: project, + direction: :inbound + ) + + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response.count).to eq 3 + expect(json_response).to include( + hash_including("id" => project.id), + hash_including("id" => ci_job_token_project_scope_link.target_project.id) + ) + end + + context 'when authenticated user as developer' do + before do + project.add_developer(user) + end + + it 'returns forbidden and no ci cd settings for public project' do + project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC) + + subject + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + end + end + + describe "POST /projects/:id/job_token_scope/allowlist" do + let_it_be(:project) { create(:project, :public) } + let_it_be(:project_inbound_allowed) { create(:project, :public) } + let_it_be(:user) { create(:user) } + + let(:post_job_token_scope_allowlist_path) { "/projects/#{project.id}/job_token_scope/allowlist" } + + let(:post_job_token_scope_allowlist_params) do + { target_project_id: project_inbound_allowed.id } + end + + subject do + post api(post_job_token_scope_allowlist_path, user), params: post_job_token_scope_allowlist_params + end + + context 'when unauthenticated user (missing user)' do + context 'for public project' do + it 'does not return ci cd settings of job token' do + project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC) + + post api(post_job_token_scope_allowlist_path) + + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + end + + context 'when authenticated user as maintainer' do + before_all { project.add_maintainer(user) } + + it 'returns unauthorized and blank response when invalid auth credentials are given' do + invalid_personal_access_token = build(:personal_access_token, user: user) + + post api(post_job_token_scope_allowlist_path, user, personal_access_token: invalid_personal_access_token), + params: post_job_token_scope_allowlist_params + + expect(response).to have_gitlab_http_status(:unauthorized) + end + + it 'returns created and creates job token scope link' do + subject + + expect(response).to have_gitlab_http_status(:created) + expect(json_response).to be_present + expect(json_response).to include( + "target_project_id" => project_inbound_allowed.id, + "source_project_id" => project.id + ) + expect(json_response).not_to include "id", "direction" + end + + it 'returns bad_request and does not create an additional job token scope link' do + create( + :ci_job_token_project_scope_link, + source_project: project, + target_project: project_inbound_allowed, + direction: :inbound + ) + + subject + + expect(response).to have_gitlab_http_status(:bad_request) + end + + it 'returns bad_request when adding the source project' do + post api(post_job_token_scope_allowlist_path, user), params: { target_project_id: project.id } + + expect(response).to have_gitlab_http_status(:bad_request) + end + + it 'returns not_found when project for param `project_id` does not exist' do + post api(post_job_token_scope_allowlist_path, user), params: { target_project_id: non_existing_record_id } + + expect(response).to have_gitlab_http_status(:not_found) + end + + it 'returns :bad_request when parameter `project_id` missing' do + post api(post_job_token_scope_allowlist_path, user), params: {} + + expect(response).to have_gitlab_http_status(:bad_request) + end + + it 'returns :bad_request when parameter `project_id` is nil value' do + post api(post_job_token_scope_allowlist_path, user), params: { target_project_id: nil } + + expect(response).to have_gitlab_http_status(:bad_request) + end + + it 'returns :bad_request when parameter `project_id` is empty value' do + post api(post_job_token_scope_allowlist_path, user), params: { target_project_id: '' } + + expect(response).to have_gitlab_http_status(:bad_request) + end + + it 'returns :bad_request when parameter `project_id` is float value' do + post api(post_job_token_scope_allowlist_path, user), params: { target_project_id: 12.34 } + + expect(response).to have_gitlab_http_status(:bad_request) + end + end + + context 'when authenticated user as developer' do + before_all { project.add_developer(user) } + + context 'for private project' do + it 'returns forbidden and no ci cd settings' do + project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) + + subject + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + context 'for public project' do + it 'returns forbidden and no ci cd settings' do + project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC) + + subject + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + end + end + + describe 'DELETE /projects/:id/job_token_scope/allowlist/:target_project_id' do + let_it_be(:project) { create(:project, :public) } + let_it_be(:target_project) { create(:project, :public) } + let_it_be(:user) { create(:user) } + let_it_be(:link) do + create(:ci_job_token_project_scope_link, + source_project: project, + target_project: target_project) + end + + let(:project_id) { project.id } + let(:delete_job_token_scope_path) do + "/projects/#{project_id}/job_token_scope/allowlist/#{target_project.id}" + end + + subject { delete api(delete_job_token_scope_path, user) } + + context 'when unauthenticated user (missing user)' do + let(:user) { nil } + + context 'for public project' do + it 'does not delete requested project from allowlist' do + project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC) + + subject + + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + end + + context 'when user has no permissions to project' do + it 'responds with 401 forbidden' do + subject + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + context 'when authenticated user as a developer' do + before do + project.add_developer(user) + end + + it 'returns 403 Forbidden' do + subject + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + context 'when authenticated user as a maintainer' do + before do + project.add_maintainer(user) + end + + context 'for the target project member' do + before do + target_project.add_guest(user) + end + + it 'returns no content and deletes requested project from allowlist' do + expect_next_instance_of( + Ci::JobTokenScope::RemoveProjectService, + project, + user + ) do |service| + expect(service).to receive(:execute).with(target_project, :inbound) + .and_return(instance_double('ServiceResponse', success?: true)) + end + + subject + + expect(response).to have_gitlab_http_status(:no_content) + expect(response.body).to be_blank + end + + context 'when fails to remove target project' do + it 'returns a bad request' do + expect_next_instance_of( + Ci::JobTokenScope::RemoveProjectService, + project, + user + ) do |service| + expect(service).to receive(:execute).with(target_project, :inbound) + .and_return(instance_double('ServiceResponse', + success?: false, + reason: nil, + message: 'Failed to remove')) + end + + subject + + expect(response).to have_gitlab_http_status(:bad_request) + end + end + end + + context 'when user project does not exists' do + before do + project.destroy! + end + + it 'responds with 404 Not found' do + subject + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'when target project does not exists' do + before do + target_project.destroy! + end + + it 'responds with 404 Not found' do + subject + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + end end diff --git a/spec/requests/api/project_packages_spec.rb b/spec/requests/api/project_packages_spec.rb index c003ae9cd48..b84b7e9c52d 100644 --- a/spec/requests/api/project_packages_spec.rb +++ b/spec/requests/api/project_packages_spec.rb @@ -3,9 +3,11 @@ require 'spec_helper' RSpec.describe API::ProjectPackages, feature_category: :package_registry do - let_it_be(:project) { create(:project, :public) } + using RSpec::Parameterized::TableSyntax - let(:user) { create(:user) } + let_it_be_with_reload(:project) { create(:project, :public) } + + let_it_be(:user) { create(:user) } let!(:package1) { create(:npm_package, :last_downloaded_at, project: project, version: '3.1.0', name: "@#{project.root_namespace.path}/foo1") } let(:package_url) { "/projects/#{project.id}/packages/#{package1.id}" } let!(:package2) { create(:nuget_package, project: project, version: '2.0.4') } @@ -101,7 +103,7 @@ RSpec.describe API::ProjectPackages, feature_category: :package_registry do end context 'project is private' do - let(:project) { create(:project, :private) } + let_it_be(:project) { create(:project, :private) } context 'for unauthenticated user' do it_behaves_like 'rejects packages access', :project, :no_type, :not_found @@ -235,7 +237,7 @@ RSpec.describe API::ProjectPackages, feature_category: :package_registry do expect do get api(package_url, user) - end.not_to exceed_query_limit(control) + end.not_to exceed_query_limit(control).with_threshold(4) end end @@ -286,7 +288,7 @@ RSpec.describe API::ProjectPackages, feature_category: :package_registry do end context 'project is private' do - let(:project) { create(:project, :private) } + let_it_be(:project) { create(:project, :private) } it 'returns 404 for non authenticated user' do get api(package_url) @@ -362,6 +364,235 @@ RSpec.describe API::ProjectPackages, feature_category: :package_registry do end end + describe 'GET /projects/:id/packages/:package_id/pipelines' do + let(:package_pipelines_url) { "/projects/#{project.id}/packages/#{package1.id}/pipelines" } + + let(:tokens) do + { + personal_access_token: personal_access_token.token, + job_token: job.token + } + end + + let_it_be(:personal_access_token) { create(:personal_access_token) } + let_it_be(:user) { personal_access_token.user } + let_it_be(:job) { create(:ci_build, :running, user: user, project: project) } + let(:headers) { {} } + + subject { get api(package_pipelines_url) } + + shared_examples 'returns package pipelines' do |expected_status| + it 'returns the first page of package pipelines' do + subject + + expect(response).to have_gitlab_http_status(expected_status) + expect(response).to match_response_schema('public_api/v4/packages/pipelines') + expect(json_response.length).to eq(3) + expect(json_response.pluck('id')).to eq(pipelines.reverse.map(&:id)) + end + end + + context 'without the need for a license' do + context 'when the package does not exist' do + let(:package_pipelines_url) { "/projects/#{project.id}/packages/0/pipelines" } + + it_behaves_like 'returning response status', :not_found + end + + context 'when there are no pipelines for the package' do + let(:package_pipelines_url) { "/projects/#{project.id}/packages/#{package2.id}/pipelines" } + + it 'returns an empty response' do + subject + + expect(response).to have_gitlab_http_status(:success) + expect(response).to match_response_schema('public_api/v4/packages/pipelines') + expect(json_response.length).to eq(0) + end + end + + context 'with valid package and pipelines' do + let!(:pipelines) do + create_list(:ci_pipeline, 3, user: user, project: project).each do |pipeline| + create(:package_build_info, package: package1, pipeline: pipeline) + end + end + + where(:visibility, :user_role, :member, :token_type, :valid_token, :shared_examples_name, :expected_status) do + :public | :developer | true | :personal_access_token | true | 'returns package pipelines' | :success + :public | :guest | true | :personal_access_token | true | 'returns package pipelines' | :success + :public | :developer | true | :personal_access_token | false | 'returning response status' | :unauthorized + :public | :guest | true | :personal_access_token | false | 'returning response status' | :unauthorized + :public | :developer | false | :personal_access_token | true | 'returns package pipelines' | :success + :public | :guest | false | :personal_access_token | true | 'returns package pipelines' | :success + :public | :developer | false | :personal_access_token | false | 'returning response status' | :unauthorized + :public | :guest | false | :personal_access_token | false | 'returning response status' | :unauthorized + :public | :anonymous | false | nil | true | 'returns package pipelines' | :success + :private | :developer | true | :personal_access_token | true | 'returns package pipelines' | :success + :private | :guest | true | :personal_access_token | true | 'returning response status' | :forbidden + :private | :developer | true | :personal_access_token | false | 'returning response status' | :unauthorized + :private | :guest | true | :personal_access_token | false | 'returning response status' | :unauthorized + :private | :developer | false | :personal_access_token | true | 'returning response status' | :not_found + :private | :guest | false | :personal_access_token | true | 'returning response status' | :not_found + :private | :developer | false | :personal_access_token | false | 'returning response status' | :unauthorized + :private | :guest | false | :personal_access_token | false | 'returning response status' | :unauthorized + :private | :anonymous | false | nil | true | 'returning response status' | :not_found + :public | :developer | true | :job_token | true | 'returns package pipelines' | :success + :public | :guest | true | :job_token | true | 'returns package pipelines' | :success + :public | :developer | true | :job_token | false | 'returning response status' | :unauthorized + :public | :guest | true | :job_token | false | 'returning response status' | :unauthorized + :public | :developer | false | :job_token | true | 'returns package pipelines' | :success + :public | :guest | false | :job_token | true | 'returns package pipelines' | :success + :public | :developer | false | :job_token | false | 'returning response status' | :unauthorized + :public | :guest | false | :job_token | false | 'returning response status' | :unauthorized + :private | :developer | true | :job_token | true | 'returns package pipelines' | :success + # TODO uncomment the spec below when https://gitlab.com/gitlab-org/gitlab/-/issues/370998 is resolved + # :private | :guest | true | :job_token | true | 'returning response status' | :forbidden + :private | :developer | true | :job_token | false | 'returning response status' | :unauthorized + :private | :guest | true | :job_token | false | 'returning response status' | :unauthorized + :private | :developer | false | :job_token | true | 'returning response status' | :not_found + :private | :guest | false | :job_token | true | 'returning response status' | :not_found + :private | :developer | false | :job_token | false | 'returning response status' | :unauthorized + :private | :guest | false | :job_token | false | 'returning response status' | :unauthorized + end + + with_them do + subject { get api(package_pipelines_url), headers: headers } + + let(:invalid_token) { 'invalid-token123' } + let(:token) { valid_token ? tokens[token_type] : invalid_token } + let(:headers) do + case token_type + when :personal_access_token + { Gitlab::Auth::AuthFinders::PRIVATE_TOKEN_HEADER => token } + when :job_token + { Gitlab::Auth::AuthFinders::JOB_TOKEN_HEADER => token } + when nil + {} + end + end + + before do + project.update!(visibility: visibility.to_s) + project.send("add_#{user_role}", user) if member && user_role != :anonymous + end + + it_behaves_like params[:shared_examples_name], params[:expected_status] + end + end + + context 'pagination' do + shared_context 'setup pipeline records' do + let!(:pipelines) do + create_list(:package_build_info, 21, :with_pipeline, package: package1) + end + end + + shared_examples 'returns the default number of pipelines' do + it do + subject + + expect(json_response.size).to eq(20) + end + end + + shared_examples 'returns an error about the invalid per_page value' do + it do + subject + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['error']).to match(/per_page does not have a valid value/) + end + end + + context 'without pagination params' do + include_context 'setup pipeline records' + + it_behaves_like 'returns the default number of pipelines' + end + + context 'with valid per_page value' do + let(:per_page) { 11 } + + subject { get api(package_pipelines_url, user), params: { per_page: per_page } } + + include_context 'setup pipeline records' + + it 'returns the correct number of pipelines' do + subject + + expect(json_response.size).to eq(per_page) + end + end + + context 'with invalid pagination params' do + subject { get api(package_pipelines_url, user), params: { per_page: per_page } } + + context 'with non-positive per_page' do + let(:per_page) { -2 } + + it_behaves_like 'returns an error about the invalid per_page value' + end + + context 'with a too high value for per_page' do + let(:per_page) { 21 } + + it_behaves_like 'returns an error about the invalid per_page value' + end + end + + context 'with valid pagination params' do + let_it_be(:package1) { create(:npm_package, :last_downloaded_at, project: project) } + let_it_be(:build_info1) { create(:package_build_info, :with_pipeline, package: package1) } + let_it_be(:build_info2) { create(:package_build_info, :with_pipeline, package: package1) } + let_it_be(:build_info3) { create(:package_build_info, :with_pipeline, package: package1) } + + let(:pipeline1) { build_info1.pipeline } + let(:pipeline2) { build_info2.pipeline } + let(:pipeline3) { build_info3.pipeline } + + let(:per_page) { 2 } + + context 'with no cursor supplied' do + subject { get api(package_pipelines_url, user), params: { per_page: per_page } } + + it 'returns first 2 pipelines' do + subject + + expect(json_response.pluck('id')).to contain_exactly(pipeline3.id, pipeline2.id) + end + end + + context 'with a cursor parameter' do + let(:cursor) { Base64.urlsafe_encode64(Gitlab::Json.dump(cursor_attributes)) } + + subject { get api(package_pipelines_url, user), params: { per_page: per_page, cursor: cursor } } + + before do + subject + end + + context 'with a cursor for the next page' do + let(:cursor_attributes) { { "id" => build_info2.id, "_kd" => "n" } } + + it 'returns the next page of records' do + expect(json_response.pluck('id')).to contain_exactly(pipeline1.id) + end + end + + context 'with a cursor for the previous page' do + let(:cursor_attributes) { { "id" => build_info1.id, "_kd" => "p" } } + + it 'returns the previous page of records' do + expect(json_response.pluck('id')).to contain_exactly(pipeline3.id, pipeline2.id) + end + end + end + end + end + end + end + describe 'DELETE /projects/:id/packages/:package_id' do context 'without the need for a license' do context 'project is public' do @@ -379,7 +610,7 @@ RSpec.describe API::ProjectPackages, feature_category: :package_registry do end context 'project is private' do - let(:project) { create(:project, :private) } + let_it_be(:project) { create(:project, :private) } before do expect(::Packages::Maven::Metadata::SyncWorker).not_to receive(:perform_async) diff --git a/spec/requests/api/project_templates_spec.rb b/spec/requests/api/project_templates_spec.rb index 91e5ed76c37..e1d156afd54 100644 --- a/spec/requests/api/project_templates_spec.rb +++ b/spec/requests/api/project_templates_spec.rb @@ -63,27 +63,6 @@ RSpec.describe API::ProjectTemplates, feature_category: :source_code_management expect(json_response).to satisfy_one { |template| template['key'] == 'mit' } end - it 'returns metrics_dashboard_ymls' do - get api("/projects/#{public_project.id}/templates/metrics_dashboard_ymls") - - expect(response).to have_gitlab_http_status(:ok) - expect(response).to include_pagination_headers - expect(response).to match_response_schema('public_api/v4/template_list') - expect(json_response).to satisfy_one { |template| template['key'] == 'Default' } - end - - context 'when metrics dashboard feature is unavailable' do - before do - stub_feature_flags(remove_monitor_metrics: true) - end - - it 'returns 400 bad request like other unknown types' do - get api("/projects/#{public_project.id}/templates/metrics_dashboard_ymls") - - expect(response).to have_gitlab_http_status(:bad_request) - end - end - it 'returns issue templates' do get api("/projects/#{private_project.id}/templates/issues", developer) @@ -176,26 +155,6 @@ RSpec.describe API::ProjectTemplates, feature_category: :source_code_management expect(json_response['name']).to eq('Android') end - it 'returns a specific metrics_dashboard_yml' do - get api("/projects/#{public_project.id}/templates/metrics_dashboard_ymls/Default") - - expect(response).to have_gitlab_http_status(:ok) - expect(response).to match_response_schema('public_api/v4/template') - expect(json_response['name']).to eq('Default') - end - - context 'when metrics dashboard feature is unavailable' do - before do - stub_feature_flags(remove_monitor_metrics: true) - end - - it 'returns 400 bad request like other unknown types' do - get api("/projects/#{public_project.id}/templates/metrics_dashboard_ymls/Default") - - expect(response).to have_gitlab_http_status(:bad_request) - end - end - it 'returns a specific license' do get api("/projects/#{public_project.id}/templates/licenses/mit") @@ -256,10 +215,6 @@ RSpec.describe API::ProjectTemplates, feature_category: :source_code_management subject { get api("/projects/#{url_encoded_path}/templates/gitlab_ci_ymls/Android") } end - it_behaves_like 'accepts project paths with dots' do - subject { get api("/projects/#{url_encoded_path}/templates/metrics_dashboard_ymls/Default") } - end - shared_examples 'path traversal attempt' do |template_type| it 'rejects invalid filenames' do get api("/projects/#{public_project.id}/templates/#{template_type}/%2e%2e%2fPython%2ea") diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 349101a092f..bb96771b3d5 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -46,7 +46,7 @@ RSpec.shared_examples 'languages and percentages JSON response' do end end -RSpec.describe API::Projects, :aggregate_failures, feature_category: :projects do +RSpec.describe API::Projects, :aggregate_failures, feature_category: :groups_and_projects do include ProjectForksHelper include WorkhorseHelpers include StubRequests @@ -2158,7 +2158,7 @@ RSpec.describe API::Projects, :aggregate_failures, feature_category: :projects d end shared_examples 'capped upload attachments' do |upload_allowed| - it "limits the upload to 1 GB" do + it "limits the upload to 1 GiB" do expect_next_instance_of(UploadService) do |instance| expect(instance).to receive(:override_max_attachment_size=).with(1.gigabyte).and_call_original end @@ -5154,7 +5154,7 @@ RSpec.describe API::Projects, :aggregate_failures, feature_category: :projects d it 'includes groups where the user has permissions to transfer a project to' do request - expect(project_ids_from_response).to include(maintainer_group.id, owner_group.id) + expect(project_ids_from_response).to match_array [maintainer_group.id, owner_group.id] end it 'does not include groups where the user doesn not have permissions to transfer a project' do @@ -5163,6 +5163,12 @@ RSpec.describe API::Projects, :aggregate_failures, feature_category: :projects d expect(project_ids_from_response).not_to include(guest_group.id) end + it 'does not include the group id of the current project' do + request + + expect(project_ids_from_response).not_to include(project.group.id) + end + context 'with search' do let(:params) { { search: 'maintainer' } } diff --git a/spec/requests/api/release/links_spec.rb b/spec/requests/api/release/links_spec.rb index b8c10de2302..3420e38f4af 100644 --- a/spec/requests/api/release/links_spec.rb +++ b/spec/requests/api/release/links_spec.rb @@ -5,12 +5,12 @@ require 'spec_helper' RSpec.describe API::Release::Links, feature_category: :release_orchestration do include Ci::JobTokenScopeHelpers - let(:project) { create(:project, :repository, :private) } - let(:maintainer) { create(:user) } - let(:developer) { create(:user) } - let(:reporter) { create(:user) } - let(:non_project_member) { create(:user) } - let(:commit) { create(:commit, project: project) } + let_it_be_with_reload(:project) { create(:project, :repository, :private) } + let_it_be(:maintainer) { create(:user) } + let_it_be(:developer) { create(:user) } + let_it_be(:reporter) { create(:user) } + let_it_be(:non_project_member) { create(:user) } + let_it_be(:commit) { create(:commit, project: project) } let!(:release) do create(:release, @@ -19,7 +19,7 @@ RSpec.describe API::Release::Links, feature_category: :release_orchestration do author: maintainer) end - before do + before_all do project.add_maintainer(maintainer) project.add_developer(developer) project.add_reporter(reporter) diff --git a/spec/requests/api/releases_spec.rb b/spec/requests/api/releases_spec.rb index 0b5cc3611bd..a018b91019b 100644 --- a/spec/requests/api/releases_spec.rb +++ b/spec/requests/api/releases_spec.rb @@ -791,16 +791,16 @@ RSpec.describe API::Releases, :aggregate_failures, feature_category: :release_or name: 'New release', tag_name: 'v0.1', description: 'Super nice release', - assets: { - links: [ - { - name: 'An example runbook link', - url: 'https://example.com/runbook', - link_type: 'runbook', - filepath: '/permanent/path/to/runbook' - } - ] - } + assets: { links: [link_asset] } + } + end + + let(:link_asset) do + { + name: 'An example runbook link', + url: 'https://example.com/runbook', + link_type: 'runbook', + filepath: '/permanent/path/to/runbook' } end @@ -906,8 +906,13 @@ RSpec.describe API::Releases, :aggregate_failures, feature_category: :release_or end context 'when using `direct_asset_path` for the asset link' do - before do - params[:direct_asset_path] = params.delete(:filepath) + let(:link_asset) do + { + name: 'An example runbook link', + url: 'https://example.com/runbook', + link_type: 'runbook', + direct_asset_path: '/permanent/path/to/runbook' + } end it 'creates a new release successfully' do @@ -915,8 +920,9 @@ RSpec.describe API::Releases, :aggregate_failures, feature_category: :release_or post api("/projects/#{project.id}/releases", maintainer), params: params end.to change { Release.count }.by(1) - release = project.releases.last + expect(response).to have_gitlab_http_status(:created) + release = project.releases.last expect(release.links.last.filepath).to eq('/permanent/path/to/runbook') end end diff --git a/spec/requests/api/resource_access_tokens_spec.rb b/spec/requests/api/resource_access_tokens_spec.rb index ce05fa2b383..dcb6572d413 100644 --- a/spec/requests/api/resource_access_tokens_spec.rb +++ b/spec/requests/api/resource_access_tokens_spec.rb @@ -336,32 +336,15 @@ RSpec.describe API::ResourceAccessTokens, feature_category: :system_access do context "when 'expires_at' is not set" do let(:expires_at) { nil } - context 'when default_pat_expiration feature flag is true' do - it "creates a #{source_type} access token with the default expires_at value", :aggregate_failures do - freeze_time do - create_token - expires_at = PersonalAccessToken::MAX_PERSONAL_ACCESS_TOKEN_LIFETIME_IN_DAYS.days.from_now - - expect(response).to have_gitlab_http_status(:created) - expect(json_response["name"]).to eq("test") - expect(json_response["scopes"]).to eq(["api"]) - expect(json_response["expires_at"]).to eq(expires_at.to_date.iso8601) - end - end - end - - context 'when default_pat_expiration feature flag is false' do - before do - stub_feature_flags(default_pat_expiration: false) - end - - it "creates a #{source_type} access token with the params", :aggregate_failures do + it "creates a #{source_type} access token with the default expires_at value", :aggregate_failures do + freeze_time do create_token + expires_at = PersonalAccessToken::MAX_PERSONAL_ACCESS_TOKEN_LIFETIME_IN_DAYS.days.from_now expect(response).to have_gitlab_http_status(:created) expect(json_response["name"]).to eq("test") expect(json_response["scopes"]).to eq(["api"]) - expect(json_response["expires_at"]).to eq(nil) + expect(json_response["expires_at"]).to eq(expires_at.to_date.iso8601) end end end diff --git a/spec/requests/api/search_spec.rb b/spec/requests/api/search_spec.rb index a315bca58d1..1b331e9c099 100644 --- a/spec/requests/api/search_spec.rb +++ b/spec/requests/api/search_spec.rb @@ -412,6 +412,22 @@ RSpec.describe API::Search, :clean_gitlab_redis_rate_limiting, feature_category: end end + context 'global snippet search is disabled' do + it 'returns forbidden response' do + stub_feature_flags(global_search_snippet_titles_tab: false) + get api(endpoint, user), params: { search: 'awesome', scope: 'snippet_titles' } + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + context 'global snippet search is enabled' do + it 'returns ok response' do + stub_feature_flags(global_search_snippet_titles_tab: true) + get api(endpoint, user), params: { search: 'awesome', scope: 'snippet_titles' } + expect(response).to have_gitlab_http_status(:ok) + end + end + it 'increments the custom search sli error rate with error false if no error occurred' do expect(Gitlab::Metrics::GlobalSearchSlis).to receive(:record_error_rate).with( error: false, diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb index 3f66cbaf2b7..79e96d7ea3e 100644 --- a/spec/requests/api/settings_spec.rb +++ b/spec/requests/api/settings_spec.rb @@ -19,6 +19,8 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting, featu expect(json_response['password_authentication_enabled']).to be_truthy expect(json_response['plantuml_enabled']).to be_falsey expect(json_response['plantuml_url']).to be_nil + expect(json_response['diagramsnet_enabled']).to be_truthy + expect(json_response['diagramsnet_url']).to eq('https://embed.diagrams.net') expect(json_response['default_ci_config_path']).to be_nil expect(json_response['sourcegraph_enabled']).to be_falsey expect(json_response['sourcegraph_url']).to be_nil @@ -46,6 +48,7 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting, featu expect(json_response['spam_check_endpoint_url']).to be_nil expect(json_response['spam_check_api_key']).to be_nil expect(json_response['wiki_page_max_content_bytes']).to be_a(Integer) + expect(json_response['wiki_asciidoc_allow_uri_includes']).to be_falsey expect(json_response['require_admin_approval_after_user_signup']).to eq(true) expect(json_response['personal_access_token_prefix']).to eq('glpat-') expect(json_response['admin_mode']).to be(false) @@ -76,6 +79,7 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting, featu expect(json_response['slack_app_verification_token']).to be_nil 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) end end @@ -123,6 +127,8 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting, featu repository_storages_weighted: { 'custom' => 100 }, plantuml_enabled: true, plantuml_url: 'http://plantuml.example.com', + diagramsnet_enabled: false, + diagramsnet_url: nil, sourcegraph_enabled: true, sourcegraph_url: 'https://sourcegraph.com', sourcegraph_public_only: false, @@ -165,6 +171,7 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting, featu disabled_oauth_sign_in_sources: 'unknown', import_sources: 'github,bitbucket', wiki_page_max_content_bytes: 12345, + wiki_asciidoc_allow_uri_includes: true, personal_access_token_prefix: "GL-", user_deactivation_emails_enabled: false, admin_mode: true, @@ -188,7 +195,8 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting, featu 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'] + valid_runner_registrars: ['group'], + allow_account_deletion: false } expect(response).to have_gitlab_http_status(:ok) @@ -199,6 +207,8 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting, featu expect(json_response['repository_storages_weighted']).to eq({ 'custom' => 100 }) expect(json_response['plantuml_enabled']).to be_truthy expect(json_response['plantuml_url']).to eq('http://plantuml.example.com') + expect(json_response['diagramsnet_enabled']).to be_falsey + expect(json_response['diagramsnet_url']).to be_nil expect(json_response['sourcegraph_enabled']).to be_truthy expect(json_response['sourcegraph_url']).to eq('https://sourcegraph.com') expect(json_response['sourcegraph_public_only']).to eq(false) @@ -241,6 +251,7 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting, featu expect(json_response['disabled_oauth_sign_in_sources']).to eq([]) expect(json_response['import_sources']).to match_array(%w(github bitbucket)) expect(json_response['wiki_page_max_content_bytes']).to eq(12345) + expect(json_response['wiki_asciidoc_allow_uri_includes']).to be(true) expect(json_response['personal_access_token_prefix']).to eq("GL-") expect(json_response['admin_mode']).to be(true) expect(json_response['user_deactivation_emails_enabled']).to be(false) @@ -265,6 +276,7 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting, featu 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) end end @@ -547,6 +559,15 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting, featu end end + context "missing diagramsnet_url value when diagramsnet_enabled is true" do + it "returns a blank parameter error message" do + put api("/application/settings", admin), params: { diagramsnet_enabled: true } + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['error']).to eq('diagramsnet_url is missing') + end + end + context 'asset_proxy settings' do it 'updates application settings' do put api('/application/settings', admin), diff --git a/spec/requests/api/system_hooks_spec.rb b/spec/requests/api/system_hooks_spec.rb index 51edf4b3b3e..16912fd279b 100644 --- a/spec/requests/api/system_hooks_spec.rb +++ b/spec/requests/api/system_hooks_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe API::SystemHooks, feature_category: :integrations do +RSpec.describe API::SystemHooks, feature_category: :webhooks do let_it_be(:non_admin) { create(:user) } let_it_be(:admin) { create(:admin) } let_it_be_with_refind(:hook) { create(:system_hook, url: "http://example.com") } diff --git a/spec/requests/api/topics_spec.rb b/spec/requests/api/topics_spec.rb index 560f22c94be..0d64a96acb8 100644 --- a/spec/requests/api/topics_spec.rb +++ b/spec/requests/api/topics_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe API::Topics, :aggregate_failures, feature_category: :projects do +RSpec.describe API::Topics, :aggregate_failures, feature_category: :groups_and_projects do include WorkhorseHelpers let_it_be(:file) { fixture_file_upload('spec/fixtures/dk.png') } diff --git a/spec/requests/api/usage_data_non_sql_metrics_spec.rb b/spec/requests/api/usage_data_non_sql_metrics_spec.rb index b2929caf676..4ca6c5cace3 100644 --- a/spec/requests/api/usage_data_non_sql_metrics_spec.rb +++ b/spec/requests/api/usage_data_non_sql_metrics_spec.rb @@ -12,7 +12,7 @@ RSpec.describe API::UsageDataNonSqlMetrics, :aggregate_failures, feature_categor stub_usage_data_connections end - describe 'GET /usage_data/non_sql_metrics' do + describe 'GET /usage_data/non_sql_metrics', :with_license do let(:endpoint) { '/usage_data/non_sql_metrics' } context 'with authentication' do diff --git a/spec/requests/api/usage_data_queries_spec.rb b/spec/requests/api/usage_data_queries_spec.rb index ab3c38adb81..584b0f31a07 100644 --- a/spec/requests/api/usage_data_queries_spec.rb +++ b/spec/requests/api/usage_data_queries_spec.rb @@ -14,7 +14,7 @@ RSpec.describe API::UsageDataQueries, :aggregate_failures, feature_category: :se stub_database_flavor_check end - describe 'GET /usage_data/usage_data_queries' do + describe 'GET /usage_data/usage_data_queries', :with_license do let(:endpoint) { '/usage_data/queries' } context 'with authentication' do diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index cc8be312c71..3737c91adbc 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -3480,7 +3480,7 @@ RSpec.describe API::Users, :aggregate_failures, feature_category: :user_profile activate expect(response).to have_gitlab_http_status(:forbidden) - expect(json_response['message']).to eq('403 Forbidden - A blocked user must be unblocked to be activated') + expect(json_response['message']).to eq('Error occurred. A blocked user must be unblocked to be activated') expect(blocked_user.reload.state).to eq('blocked') end end @@ -3494,7 +3494,7 @@ RSpec.describe API::Users, :aggregate_failures, feature_category: :user_profile activate expect(response).to have_gitlab_http_status(:forbidden) - expect(json_response['message']).to eq('403 Forbidden - A blocked user must be unblocked to be activated') + expect(json_response['message']).to eq('Error occurred. A blocked user must be unblocked to be activated') expect(user.reload.state).to eq('ldap_blocked') end end @@ -4516,7 +4516,7 @@ RSpec.describe API::Users, :aggregate_failures, feature_category: :user_profile post api(path, admin, admin_mode: true) expect(response).to have_gitlab_http_status(:bad_request) - expect(json_response['error']).to eq('name is missing, scopes is missing, scopes does not have a valid value') + expect(json_response['error']).to eq('name is missing, scopes is missing') end it 'returns a 404 error if user not found' do diff --git a/spec/requests/api/v3/github_spec.rb b/spec/requests/api/v3/github_spec.rb index b6fccd9b7cb..fbda291e901 100644 --- a/spec/requests/api/v3/github_spec.rb +++ b/spec/requests/api/v3/github_spec.rb @@ -13,16 +13,33 @@ RSpec.describe API::V3::Github, :aggregate_failures, feature_category: :integrat end describe 'GET /orgs/:namespace/repos' do + let_it_be(:group) { create(:group) } + it_behaves_like 'a GitHub Enterprise Jira DVCS reversible end of life endpoint' do subject do - group = create(:group) jira_get v3_api("/orgs/#{group.path}/repos", user) end end - it 'returns an empty array' do - group = create(:group) + it 'logs when the endpoint is hit and `jira_dvcs_end_of_life_amnesty` is enabled' do + expect(Gitlab::JsonLogger).to receive(:info).with( + including( + namespace: group.path, + user_id: user.id, + message: 'Deprecated Jira DVCS endpoint request' + ) + ) + + jira_get v3_api("/orgs/#{group.path}/repos", user) + + stub_feature_flags(jira_dvcs_end_of_life_amnesty: false) + expect(Gitlab::JsonLogger).not_to receive(:info) + + jira_get v3_api("/orgs/#{group.path}/repos", user) + end + + it 'returns an empty array' do jira_get v3_api("/orgs/#{group.path}/repos", user) expect(response).to have_gitlab_http_status(:ok) diff --git a/spec/requests/concerns/planning_hierarchy_spec.rb b/spec/requests/concerns/planning_hierarchy_spec.rb index 89232916936..97b987fa770 100644 --- a/spec/requests/concerns/planning_hierarchy_spec.rb +++ b/spec/requests/concerns/planning_hierarchy_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe PlanningHierarchy, type: :request, feature_category: :projects do +RSpec.describe PlanningHierarchy, type: :request, feature_category: :groups_and_projects do let_it_be(:user) { create(:user) } let_it_be(:group) { create(:group) } let_it_be(:project) { create(:project, group: group) } diff --git a/spec/requests/dashboard/projects_controller_spec.rb b/spec/requests/dashboard/projects_controller_spec.rb index 752799196c9..478804bba29 100644 --- a/spec/requests/dashboard/projects_controller_spec.rb +++ b/spec/requests/dashboard/projects_controller_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Dashboard::ProjectsController, feature_category: :projects do +RSpec.describe Dashboard::ProjectsController, feature_category: :groups_and_projects do context 'token authentication' do it_behaves_like 'authenticates sessionless user for the request spec', 'index atom', public_resource: false do let(:url) { dashboard_projects_url(:atom) } diff --git a/spec/requests/groups/autocomplete_sources_spec.rb b/spec/requests/groups/autocomplete_sources_spec.rb index e44fb9f6c37..02fb04a4af8 100644 --- a/spec/requests/groups/autocomplete_sources_spec.rb +++ b/spec/requests/groups/autocomplete_sources_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe 'groups autocomplete', feature_category: :subgroups do +RSpec.describe 'groups autocomplete', feature_category: :groups_and_projects do let_it_be(:user) { create(:user) } let_it_be_with_reload(:group) { create(:group, :private) } diff --git a/spec/requests/groups_controller_spec.rb b/spec/requests/groups_controller_spec.rb index 7fc14910819..cd4b32e4982 100644 --- a/spec/requests/groups_controller_spec.rb +++ b/spec/requests/groups_controller_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe GroupsController, feature_category: :subgroups do +RSpec.describe GroupsController, feature_category: :groups_and_projects do context 'token authentication' do context 'when public group' do let_it_be(:public_group) { create(:group, :public) } diff --git a/spec/requests/ide_controller_spec.rb b/spec/requests/ide_controller_spec.rb index fe7210e4372..5b7da9ce84f 100644 --- a/spec/requests/ide_controller_spec.rb +++ b/spec/requests/ide_controller_spec.rb @@ -3,6 +3,7 @@ require 'spec_helper' RSpec.describe IdeController, feature_category: :web_ide do + include ContentSecurityPolicyHelpers using RSpec::Parameterized::TableSyntax let_it_be(:reporter) { create(:user) } @@ -20,18 +21,6 @@ RSpec.describe IdeController, feature_category: :web_ide do let(:user) { creator } - def find_csp_source(key) - csp = response.headers['Content-Security-Policy'] - - # Transform "default-src foo bar; connect-src foo bar; script-src ..." - # into array of values for a single directive based on the given key - csp.split(';') - .map(&:strip) - .find { |entry| entry.starts_with?(key) } - .split(' ') - .drop(1) - end - before do stub_feature_flags(vscode_web_ide: true) sign_in(user) @@ -198,8 +187,8 @@ RSpec.describe IdeController, feature_category: :web_ide do it 'updates the content security policy with the correct frame sources' do subject - expect(find_csp_source('frame-src')).to include("http://www.example.com/assets/webpack/", "https://*.vscode-cdn.net/") - expect(find_csp_source('worker-src')).to include("http://www.example.com/assets/webpack/") + expect(find_csp_directive('frame-src')).to include("http://www.example.com/assets/webpack/", "https://*.vscode-cdn.net/") + expect(find_csp_directive('worker-src')).to include("http://www.example.com/assets/webpack/") end it 'with relative_url_root, updates the content security policy with the correct frame sources' do @@ -207,8 +196,8 @@ RSpec.describe IdeController, feature_category: :web_ide do subject - expect(find_csp_source('frame-src')).to include("http://www.example.com/gitlab/assets/webpack/") - expect(find_csp_source('worker-src')).to include("http://www.example.com/gitlab/assets/webpack/") + expect(find_csp_directive('frame-src')).to include("http://www.example.com/gitlab/assets/webpack/") + expect(find_csp_directive('worker-src')).to include("http://www.example.com/gitlab/assets/webpack/") end end end diff --git a/spec/requests/import/github_controller_spec.rb b/spec/requests/import/github_controller_spec.rb index 8d57c2895de..0f28f5e93f3 100644 --- a/spec/requests/import/github_controller_spec.rb +++ b/spec/requests/import/github_controller_spec.rb @@ -4,7 +4,7 @@ require 'spec_helper' RSpec.describe Import::GithubController, feature_category: :importers do describe 'GET details' do - subject { get details_import_github_path } + subject(:request) { get details_import_github_path } let_it_be(:user) { create(:user) } @@ -18,10 +18,10 @@ RSpec.describe Import::GithubController, feature_category: :importers do before do stub_feature_flags(import_details_page: true) - subject + request end - it 'responds with a 200 and shows the template' do + it 'responds with a 200 and shows the template', :aggregate_failures do expect(response).to have_gitlab_http_status(:ok) expect(response).to render_template(:details) end @@ -31,7 +31,7 @@ RSpec.describe Import::GithubController, feature_category: :importers do before do stub_feature_flags(import_details_page: false) - subject + request end it 'responds with a 404' do diff --git a/spec/requests/import/gitlab_groups_controller_spec.rb b/spec/requests/import/gitlab_groups_controller_spec.rb index 1766c48cca1..734a4cefc5c 100644 --- a/spec/requests/import/gitlab_groups_controller_spec.rb +++ b/spec/requests/import/gitlab_groups_controller_spec.rb @@ -69,7 +69,7 @@ RSpec.describe Import::GitlabGroupsController, feature_category: :importers do expect(GroupImportWorker).to have_received(:perform_async).with(user.id, group.id) expect(group.description).to eq 'A voluptate non sequi temporibus quam at.' - expect(group.visibility_level).to eq Gitlab::VisibilityLevel::PRIVATE + expect(group.visibility_level).to eq Gitlab::VisibilityLevel::PUBLIC end end diff --git a/spec/requests/jira_connect/installations_controller_spec.rb b/spec/requests/jira_connect/installations_controller_spec.rb index 67544bbca2e..8728313569c 100644 --- a/spec/requests/jira_connect/installations_controller_spec.rb +++ b/spec/requests/jira_connect/installations_controller_spec.rb @@ -106,13 +106,9 @@ RSpec.describe JiraConnect::InstallationsController, feature_category: :integrat do_request expect(response).to have_gitlab_http_status(:unprocessable_entity) - expect(json_response).to eq({ - 'errors' => { - 'instance_url' => [ - 'Could not be installed on the instance. Error response code 422' - ] - } - }) + expect(json_response).to eq( + { 'errors' => 'Could not be installed on the instance. Error response code 422' } + ) end end diff --git a/spec/requests/organizations/organizations_controller_spec.rb b/spec/requests/organizations/organizations_controller_spec.rb new file mode 100644 index 00000000000..a51a5751831 --- /dev/null +++ b/spec/requests/organizations/organizations_controller_spec.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +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) } + + before do + sign_in(user) + end + + context 'when the user does not have authorization' do + let_it_be(:user) { create(:user) } + + it 'renders 404' do + gitlab_request + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'when the user has authorization', :enable_admin_mode do + let_it_be(:user) { create(:admin) } + + it 'renders 200 OK' do + gitlab_request + + expect(response).to have_gitlab_http_status(:ok) + end + + context 'when the feature flag `ui_for_organizations` is disabled' do + it 'renders 404' do + stub_feature_flags(ui_for_organizations: false) + + gitlab_request + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + end +end diff --git a/spec/requests/projects/hook_logs_controller_spec.rb b/spec/requests/projects/hook_logs_controller_spec.rb index c71906b4895..7983fc540d5 100644 --- a/spec/requests/projects/hook_logs_controller_spec.rb +++ b/spec/requests/projects/hook_logs_controller_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Projects::HookLogsController, feature_category: :integrations do +RSpec.describe Projects::HookLogsController, feature_category: :webhooks do let_it_be(:user) { create(:user) } let_it_be_with_refind(:web_hook) { create(:project_hook) } let_it_be_with_refind(:web_hook_log) { create(:web_hook_log, web_hook: web_hook) } diff --git a/spec/requests/projects/merge_requests_discussions_spec.rb b/spec/requests/projects/merge_requests_discussions_spec.rb index caf62c251b6..644f26af006 100644 --- a/spec/requests/projects/merge_requests_discussions_spec.rb +++ b/spec/requests/projects/merge_requests_discussions_spec.rb @@ -53,16 +53,6 @@ RSpec.describe 'merge requests discussions', feature_category: :source_code_mana let(:notes_metadata_threshold) { 1 } it_behaves_like 'N+1 queries' - - context 'when external_note_author_service_desk feature flag is disabled' do - let(:notes_metadata_threshold) { 0 } - - before do - stub_feature_flags(external_note_author_service_desk: false) - end - - it_behaves_like 'N+1 queries' - end end it 'limits Gitaly queries', :request_store do diff --git a/spec/requests/projects/metrics_dashboard_spec.rb b/spec/requests/projects/metrics_dashboard_spec.rb deleted file mode 100644 index d0181275927..00000000000 --- a/spec/requests/projects/metrics_dashboard_spec.rb +++ /dev/null @@ -1,147 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe 'Projects::MetricsDashboardController', feature_category: :metrics do - let_it_be(:project) { create(:project) } - let_it_be(:environment) { create(:environment, project: project) } - let_it_be(:environment2) { create(:environment, project: project) } - let_it_be(:user) { project.first_owner } - - before do - project.add_developer(user) - login_as(user) - stub_feature_flags(remove_monitor_metrics: false) - end - - describe 'GET /:namespace/:project/-/metrics' do - it "redirects to default environment's metrics dashboard" do - send_request - expect(response).to redirect_to(dashboard_route(environment: environment)) - end - - it 'assigns default_environment' do - send_request - expect(assigns(:default_environment).id).to eq(environment.id) - end - - it 'retains existing parameters when redirecting' do - params = { - dashboard_path: '.gitlab/dashboards/dashboard_path.yml', - page: 'panel/new', - group: 'System metrics (Kubernetes)', - title: 'Memory Usage (Pod average)', - y_label: 'Memory Used per Pod (MB)' - } - send_request(params) - - expect(response).to redirect_to(dashboard_route(params.merge(environment: environment.id))) - end - - context 'with remove_monitor_metrics returning true' do - before do - stub_feature_flags(remove_monitor_metrics: true) - end - - it 'renders 404 page' do - send_request - expect(response).to have_gitlab_http_status(:not_found) - end - end - - context 'with anonymous user and public dashboard visibility' do - let(:anonymous_user) { create(:user) } - let(:project) do - create(:project, :public, :metrics_dashboard_enabled) - end - - before do - project.update!(metrics_dashboard_access_level: 'enabled') - - login_as(anonymous_user) - end - - it 'returns 200' do - send_request - - expect(response).to have_gitlab_http_status(:ok) - end - end - end - - describe 'GET /:namespace/:project/-/metrics?environment=:environment.id' do - it 'returns 200' do - send_request(environment: environment2.id) - expect(response).to have_gitlab_http_status(:ok) - end - - it 'assigns query param environment' do - send_request(environment: environment2.id) - expect(assigns(:environment).id).to eq(environment2.id) - end - - context 'when query param environment does not exist' do - it 'responds with 404' do - send_request(environment: non_existing_record_id) - expect(response).to have_gitlab_http_status(:not_found) - end - end - end - - describe 'GET /:namespace/:project/-/metrics/:dashboard_path' do - let(:dashboard_path) { '.gitlab/dashboards/dashboard_path.yml' } - - it 'returns 200' do - send_request(dashboard_path: dashboard_path, environment: environment.id) - expect(response).to have_gitlab_http_status(:ok) - end - - it 'assigns environment' do - send_request(dashboard_path: dashboard_path, environment: environment.id) - expect(assigns(:environment).id).to eq(environment.id) - end - end - - describe 'GET :/namespace/:project/-/metrics/:dashboard_path?environment=:environment.id' do - let(:dashboard_path) { '.gitlab/dashboards/dashboard_path.yml' } - - it 'returns 200' do - send_request(dahboard_path: dashboard_path, environment: environment.id) - expect(response).to have_gitlab_http_status(:ok) - end - - it 'assigns query param environment' do - send_request(dashboard_path: dashboard_path, environment: environment2.id) - expect(assigns(:environment).id).to eq(environment2.id) - end - - context 'when query param environment does not exist' do - it 'responds with 404' do - send_request(dashboard_path: dashboard_path, environment: non_existing_record_id) - expect(response).to have_gitlab_http_status(:not_found) - end - end - end - - describe 'GET :/namespace/:project/-/metrics/:page' do - it 'returns 200 with path param page' do - send_request(page: 'panel/new', environment: environment.id) - - expect(response).to have_gitlab_http_status(:ok) - end - - it 'returns 200 with dashboard and path param page' do - send_request(dashboard_path: 'dashboard.yml', page: 'panel/new', environment: environment.id) - - expect(response).to have_gitlab_http_status(:ok) - end - end - - def send_request(params = {}) - get dashboard_route(params) - end - - def dashboard_route(params = {}) - namespace_project_metrics_dashboard_path(namespace_id: project.namespace, project_id: project, **params) - end -end diff --git a/spec/requests/projects/ml/candidates_controller_spec.rb b/spec/requests/projects/ml/candidates_controller_spec.rb index 78c8e99e3f3..eec7af99063 100644 --- a/spec/requests/projects/ml/candidates_controller_spec.rb +++ b/spec/requests/projects/ml/candidates_controller_spec.rb @@ -10,11 +10,13 @@ RSpec.describe Projects::Ml::CandidatesController, feature_category: :mlops do let(:ff_value) { true } let(:candidate_iid) { candidate.iid } + let(:model_experiments_enabled) { true } before do - stub_feature_flags(ml_experiment_tracking: false) - stub_feature_flags(ml_experiment_tracking: project) if ff_value - + allow(Ability).to receive(:allowed?).and_call_original + allow(Ability).to receive(:allowed?) + .with(user, :read_model_experiments, project) + .and_return(model_experiments_enabled) sign_in(user) end @@ -32,9 +34,9 @@ RSpec.describe Projects::Ml::CandidatesController, feature_category: :mlops do end end - shared_examples '404 if feature flag disabled' do - context 'when :ml_experiment_tracking disabled' do - let(:ff_value) { false } + shared_examples '404 when model experiments is unavailable' do + context 'when user does not have access' do + let(:model_experiments_enabled) { false } it_behaves_like 'renders 404' end @@ -59,7 +61,7 @@ RSpec.describe Projects::Ml::CandidatesController, feature_category: :mlops do end it_behaves_like '404 if candidate does not exist' - it_behaves_like '404 if feature flag disabled' + it_behaves_like '404 when model experiments is unavailable' end describe 'DELETE #destroy' do @@ -81,7 +83,7 @@ RSpec.describe Projects::Ml::CandidatesController, feature_category: :mlops do end it_behaves_like '404 if candidate does not exist' - it_behaves_like '404 if feature flag disabled' + it_behaves_like '404 when model experiments is unavailable' end private diff --git a/spec/requests/projects/ml/experiments_controller_spec.rb b/spec/requests/projects/ml/experiments_controller_spec.rb index 5a8496a250a..e2d26e84f75 100644 --- a/spec/requests/projects/ml/experiments_controller_spec.rb +++ b/spec/requests/projects/ml/experiments_controller_spec.rb @@ -3,27 +3,25 @@ require 'spec_helper' RSpec.describe Projects::Ml::ExperimentsController, feature_category: :mlops do - let_it_be(:project_with_feature) { create(:project, :repository) } - let_it_be(:user) { project_with_feature.first_owner } - let_it_be(:project_without_feature) do - create(:project, :repository).tap { |p| p.add_developer(user) } - end - + let_it_be(:project) { create(:project, :repository) } + let_it_be(:user) { project.first_owner } let_it_be(:experiment) do - create(:ml_experiments, project: project_with_feature, user: user).tap do |e| + create(:ml_experiments, project: project, user: user).tap do |e| create(:ml_candidates, experiment: e, user: user) end end let(:params) { basic_params } let(:ff_value) { true } - let(:project) { project_with_feature } let(:basic_params) { { namespace_id: project.namespace.to_param, project_id: project } } let(:experiment_iid) { experiment.iid } + let(:model_experiments_enabled) { true } before do - stub_feature_flags(ml_experiment_tracking: false) - stub_feature_flags(ml_experiment_tracking: project_with_feature) if ff_value + allow(Ability).to receive(:allowed?).and_call_original + allow(Ability).to receive(:allowed?) + .with(user, :read_model_experiments, project) + .and_return(model_experiments_enabled) sign_in(user) end @@ -42,9 +40,9 @@ RSpec.describe Projects::Ml::ExperimentsController, feature_category: :mlops do end end - shared_examples '404 if feature flag disabled' do - context 'when :ml_experiment_tracking disabled' do - let(:ff_value) { false } + shared_examples '404 when model experiments is unavailable' do + context 'when user does not have access' do + let(:model_experiments_enabled) { false } it_behaves_like 'renders 404' end @@ -71,7 +69,7 @@ RSpec.describe Projects::Ml::ExperimentsController, feature_category: :mlops do describe 'pagination' do let_it_be(:experiments) do - create_list(:ml_experiments, 3, project: project_with_feature) + create_list(:ml_experiments, 3, project: project) end let(:params) { basic_params.merge(id: experiment.iid) } @@ -102,19 +100,7 @@ RSpec.describe Projects::Ml::ExperimentsController, feature_category: :mlops do end end - context 'when :ml_experiment_tracking is disabled for the project' do - let(:project) { project_without_feature } - - before do - list_experiments - end - - it 'responds with a 404' do - expect(response).to have_gitlab_http_status(:not_found) - end - end - - it_behaves_like '404 if feature flag disabled' do + it_behaves_like '404 when model experiments is unavailable' do before do list_experiments end @@ -225,7 +211,7 @@ RSpec.describe Projects::Ml::ExperimentsController, feature_category: :mlops do end it_behaves_like '404 if experiment does not exist' - it_behaves_like '404 if feature flag disabled' + it_behaves_like '404 when model experiments is unavailable' end end @@ -257,14 +243,14 @@ RSpec.describe Projects::Ml::ExperimentsController, feature_category: :mlops do end it_behaves_like '404 if experiment does not exist' - it_behaves_like '404 if feature flag disabled' + it_behaves_like '404 when model experiments is unavailable' end end end describe 'DELETE #destroy' do let_it_be(:experiment_for_deletion) do - create(:ml_experiments, project: project_with_feature, user: user).tap do |e| + create(:ml_experiments, project: project, user: user).tap do |e| create(:ml_candidates, experiment: e, user: user) end end @@ -282,7 +268,7 @@ RSpec.describe Projects::Ml::ExperimentsController, feature_category: :mlops do end it_behaves_like '404 if experiment does not exist' - it_behaves_like '404 if feature flag disabled' + it_behaves_like '404 when model experiments is unavailable' end private diff --git a/spec/requests/projects/redirect_controller_spec.rb b/spec/requests/projects/redirect_controller_spec.rb index e828c546198..962d8062161 100644 --- a/spec/requests/projects/redirect_controller_spec.rb +++ b/spec/requests/projects/redirect_controller_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe "Projects::RedirectController requests", feature_category: :projects do +RSpec.describe "Projects::RedirectController requests", feature_category: :groups_and_projects do using RSpec::Parameterized::TableSyntax let_it_be(:private_project) { create(:project, :private) } diff --git a/spec/requests/projects/settings/integration_hook_logs_controller_spec.rb b/spec/requests/projects/settings/integration_hook_logs_controller_spec.rb index 6cd0df19468..7d256929b31 100644 --- a/spec/requests/projects/settings/integration_hook_logs_controller_spec.rb +++ b/spec/requests/projects/settings/integration_hook_logs_controller_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Projects::Settings::IntegrationHookLogsController, feature_category: :integrations do +RSpec.describe Projects::Settings::IntegrationHookLogsController, feature_category: :webhooks do let_it_be(:user) { create(:user) } let_it_be(:integration) { create(:datadog_integration) } let_it_be_with_refind(:web_hook) { integration.service_hook } diff --git a/spec/requests/projects/wikis_controller_spec.rb b/spec/requests/projects/wikis_controller_spec.rb index 3c434b36b21..9f69faf499e 100644 --- a/spec/requests/projects/wikis_controller_spec.rb +++ b/spec/requests/projects/wikis_controller_spec.rb @@ -6,6 +6,8 @@ RSpec.describe Projects::WikisController, feature_category: :wiki do using RSpec::Parameterized::TableSyntax let_it_be(:user) { create(:user) } + let_it_be(:diagramsnet_is_enabled) { false } + let_it_be(:diagramsnet_url) { 'https://url.diagrams.net' } let_it_be(:project) { create(:project, :wiki_repo, namespace: user.namespace) } let_it_be(:project_wiki) { create(:project_wiki, project: project, user: user) } let_it_be(:wiki_page) do @@ -18,6 +20,12 @@ RSpec.describe Projects::WikisController, feature_category: :wiki do before do sign_in(user) + allow(Gitlab::CurrentSettings) + .to receive(:diagramsnet_enabled?) + .and_return(diagramsnet_is_enabled) + allow(Gitlab::CurrentSettings) + .to receive(:diagramsnet_url) + .and_return(diagramsnet_url) allow_next_instance_of(described_class) do |instance| allow(instance).to receive(:content_security_policy_nonce).and_return(csp_nonce) @@ -25,12 +33,26 @@ RSpec.describe Projects::WikisController, feature_category: :wiki do end shared_examples 'embed.diagrams.net frame-src directive' do - it 'adds drawio frame-src directive to the Content Security Policy header' do - frame_src = response.headers['Content-Security-Policy'].split(';') - .map(&:strip) - .find { |entry| entry.starts_with?('frame-src') } + context 'when diagrams.net disabled' do + it 'drawio frame-src directive to the Content Security Policy header' do + frame_src = response.headers['Content-Security-Policy'].split(';') + .map(&:strip) + .find { |entry| entry.starts_with?('frame-src') } - expect(frame_src).to include('https://embed.diagrams.net') + expect(frame_src).not_to include(diagramsnet_url) + end + end + + context 'when diagrams.net enabled' do + let(:diagramsnet_is_enabled) { true } + + it 'drawio frame-src directive to the Content Security Policy header' do + frame_src = response.headers['Content-Security-Policy'].split(';') + .map(&:strip) + .find { |entry| entry.starts_with?('frame-src') } + + expect(frame_src).to include(diagramsnet_url) + end end end diff --git a/spec/requests/projects/work_items_spec.rb b/spec/requests/projects/work_items_spec.rb index c02f76d2c65..ee9a0ff0a4e 100644 --- a/spec/requests/projects/work_items_spec.rb +++ b/spec/requests/projects/work_items_spec.rb @@ -27,8 +27,8 @@ RSpec.describe 'Work Items', feature_category: :team_planning do shared_examples 'safely handles uploaded files' do it 'ensures the upload is handled safely', :aggregate_failures do - allow(Gitlab::Utils).to receive(:check_path_traversal!).and_call_original - expect(Gitlab::Utils).to receive(:check_path_traversal!).with(filename).at_least(:once) + allow(Gitlab::PathTraversal).to receive(:check_path_traversal!).and_call_original + expect(Gitlab::PathTraversal).to receive(:check_path_traversal!).with(filename).at_least(:once) expect(FileUploader).not_to receive(:cache) subject diff --git a/spec/requests/projects_controller_spec.rb b/spec/requests/projects_controller_spec.rb index 613f528e8c2..a4cfad32e4a 100644 --- a/spec/requests/projects_controller_spec.rb +++ b/spec/requests/projects_controller_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe ProjectsController, :with_license, feature_category: :projects do +RSpec.describe ProjectsController, :with_license, feature_category: :groups_and_projects do context 'token authentication' do context 'when public project' do let_it_be(:public_project) { create(:project, :public) } diff --git a/spec/requests/rack_middlewares/omniauth_spec.rb b/spec/requests/rack_middlewares/omniauth_spec.rb new file mode 100644 index 00000000000..ac10845bb1a --- /dev/null +++ b/spec/requests/rack_middlewares/omniauth_spec.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'OmniAuth Rack middlewares', feature_category: :system_access do + describe 'OmniAuth before_request_phase callback' do + it 'increments Prometheus counter' do + post('/users/auth/google_oauth2') + + counter = Gitlab::Metrics.registry.get(:gitlab_omniauth_login_total) + expect(counter.get(omniauth_provider: 'google_oauth2', status: 'initiated')).to eq(1) + end + end +end diff --git a/spec/requests/recursive_webhook_detection_spec.rb b/spec/requests/recursive_webhook_detection_spec.rb index a74d4f9a603..f9672257c6f 100644 --- a/spec/requests/recursive_webhook_detection_spec.rb +++ b/spec/requests/recursive_webhook_detection_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe 'Recursive webhook detection', :sidekiq_inline, :clean_gitlab_redis_shared_state, :request_store, -feature_category: :integrations do +feature_category: :webhooks do include StubRequests let_it_be(:user) { create(:user) } diff --git a/spec/requests/warden_spec.rb b/spec/requests/warden_spec.rb new file mode 100644 index 00000000000..b5423af58a7 --- /dev/null +++ b/spec/requests/warden_spec.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe "Warden", feature_category: :system_access do + describe "rate limit" do + include_context 'unique ips sign in limit' + let(:user) { create(:user) } + + before do + # Set the rate limit to 1 request per IP address per user. + stub_application_setting(unique_ips_limit_per_user: 1) + sign_in(user) + end + + it 'limits the number of requests that can be made from a single IP address per user' do + change_ip('ip1') + get user_path(user) + expect(response).to be_successful + + change_ip('ip2') + get user_path(user) + expect(response).to be_forbidden + end + end +end diff --git a/spec/requests/web_ide/remote_ide_controller_spec.rb b/spec/requests/web_ide/remote_ide_controller_spec.rb index 9e9d3dfc703..62f5cb90e0a 100644 --- a/spec/requests/web_ide/remote_ide_controller_spec.rb +++ b/spec/requests/web_ide/remote_ide_controller_spec.rb @@ -3,6 +3,7 @@ require 'spec_helper' RSpec.describe WebIde::RemoteIdeController, feature_category: :remote_development do + include ContentSecurityPolicyHelpers using RSpec::Parameterized::TableSyntax let_it_be(:user) { create(:user) } @@ -63,7 +64,7 @@ RSpec.describe WebIde::RemoteIdeController, feature_category: :remote_developmen end it "updates the content security policy with the correct connect sources" do - expect(find_csp_source('connect-src')).to include( + expect(find_csp_directive('connect-src')).to include( "ws://#{remote_host}", "wss://#{remote_host}", "http://#{remote_host}", @@ -72,7 +73,7 @@ RSpec.describe WebIde::RemoteIdeController, feature_category: :remote_developmen end it "updates the content security policy with the correct frame sources" do - expect(find_csp_source('frame-src')).to include("http://www.example.com/assets/webpack/", "https://*.vscode-cdn.net/") + expect(find_csp_directive('frame-src')).to include("http://www.example.com/assets/webpack/", "https://*.vscode-cdn.net/") end end @@ -84,7 +85,7 @@ RSpec.describe WebIde::RemoteIdeController, feature_category: :remote_developmen end it "updates the content security policy with the correct remote_host" do - expect(find_csp_source('connect-src')).to include( + expect(find_csp_directive('connect-src')).to include( "ws://#{remote_host}", "wss://#{remote_host}", "http://#{remote_host}", @@ -122,18 +123,6 @@ RSpec.describe WebIde::RemoteIdeController, feature_category: :remote_developmen } end - def find_csp_source(key) - csp = response.headers['Content-Security-Policy'] - - # Transform "default-src foo bar; connect-src foo bar; script-src ..." - # into array of values for a single directive based on the given key - csp.split(';') - .map(&:strip) - .find { |entry| entry.starts_with?(key) } - .split(' ') - .drop(1) - end - def post_to_remote_ide params = { connection_token: connection_token, diff --git a/spec/requests/well_known_routing_spec.rb b/spec/requests/well_known_routing_spec.rb new file mode 100644 index 00000000000..d4e77a06953 --- /dev/null +++ b/spec/requests/well_known_routing_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'well-known URLs', feature_category: :system_access do + describe '/.well-known/change-password' do + it 'redirects to edit profile password path' do + get('/.well-known/change-password') + + expect(response).to redirect_to(edit_profile_password_path) + end + end +end |