diff options
Diffstat (limited to 'spec/requests')
118 files changed, 5934 insertions, 1790 deletions
diff --git a/spec/requests/admin/hook_logs_controller_spec.rb b/spec/requests/admin/hook_logs_controller_spec.rb new file mode 100644 index 00000000000..f8d3381c052 --- /dev/null +++ b/spec/requests/admin/hook_logs_controller_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Admin::HookLogsController, :enable_admin_mode 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) } + + it_behaves_like WebHooks::HookLogActions do + let!(:show_path) { admin_hook_hook_log_path(web_hook, web_hook_log) } + let!(:retry_path) { retry_admin_hook_hook_log_path(web_hook, web_hook_log) } + let(:edit_hook_path) { edit_admin_hook_path(web_hook) } + end +end diff --git a/spec/requests/api/admin/batched_background_migrations_spec.rb b/spec/requests/api/admin/batched_background_migrations_spec.rb new file mode 100644 index 00000000000..c99b21c0c27 --- /dev/null +++ b/spec/requests/api/admin/batched_background_migrations_spec.rb @@ -0,0 +1,230 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::Admin::BatchedBackgroundMigrations do + let(:admin) { create(:admin) } + let(:unauthorized_user) { create(:user) } + + describe 'GET /admin/batched_background_migrations/:id' do + let!(:migration) { create(:batched_background_migration, :paused) } + let(:database) { :main } + + subject(:show_migration) do + get api("/admin/batched_background_migrations/#{migration.id}", admin), params: { database: database } + end + + it 'fetches the batched background migration' do + show_migration + + aggregate_failures "testing response" do + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['id']).to eq(migration.id) + expect(json_response['status']).to eq('paused') + expect(json_response['job_class_name']).to eq(migration.job_class_name) + expect(json_response['progress']).to be_zero + end + end + + context 'when the batched background migration does not exist' do + let(:params) { { database: database } } + + it 'returns 404' do + put api("/admin/batched_background_migrations/#{non_existing_record_id}", admin), params: params + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'when multiple database is enabled' do + before do + skip_if_multiple_databases_not_setup + end + + let(:ci_model) { Ci::ApplicationRecord } + let(:database) { :ci } + + it 'uses the correct connection' do + expect(Gitlab::Database::SharedModel).to receive(:using_connection).with(ci_model.connection).and_yield + + show_migration + end + end + + context 'when authenticated as a non-admin user' do + it 'returns 403' do + get api("/admin/batched_background_migrations/#{migration.id}", unauthorized_user) + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + end + + describe 'GET /admin/batched_background_migrations' do + let!(:migration) { create(:batched_background_migration) } + + context 'when is an admin user' do + it 'returns batched background migrations' do + get api('/admin/batched_background_migrations', admin) + + aggregate_failures "testing response" do + expect(response).to have_gitlab_http_status(:ok) + expect(json_response.count).to eq(1) + expect(json_response.first['id']).to eq(migration.id) + expect(json_response.first['job_class_name']).to eq(migration.job_class_name) + expect(json_response.first['table_name']).to eq(migration.table_name) + expect(json_response.first['status']).to eq(migration.status_name.to_s) + expect(json_response.first['progress']).to be_zero + end + end + + context 'when multiple database is enabled', :add_ci_connection do + let(:database) { :ci } + let(:schema) { :gitlab_ci } + let(:ci_model) { Ci::ApplicationRecord } + + context 'when CI database is provided' do + let(:db_config) { instance_double(ActiveRecord::DatabaseConfigurations::HashConfig, name: 'fake_db') } + let(:default_model) { ActiveRecord::Base } + let(:base_models) { { 'fake_db' => default_model, 'ci' => ci_model }.with_indifferent_access } + + it "uses CI database connection" do + allow(Gitlab::Database).to receive(:db_config_for_connection).and_return(db_config) + allow(Gitlab::Database).to receive(:database_base_models).and_return(base_models) + + expect(Gitlab::Database::SharedModel).to receive(:using_connection).with(ci_model.connection).and_yield + + get api('/admin/batched_background_migrations', admin), params: { database: :ci } + end + + it 'returns CI database records' do + # If we only have one DB we'll see both migrations + skip_if_multiple_databases_not_setup + + ci_database_migration = Gitlab::Database::SharedModel.using_connection(ci_model.connection) do + create(:batched_background_migration, :active, gitlab_schema: schema) + end + + get api('/admin/batched_background_migrations', admin), params: { database: :ci } + + aggregate_failures "testing response" do + expect(response).to have_gitlab_http_status(:ok) + expect(json_response.count).to eq(1) + expect(json_response.first['id']).to eq(ci_database_migration.id) + expect(json_response.first['job_class_name']).to eq(ci_database_migration.job_class_name) + expect(json_response.first['table_name']).to eq(ci_database_migration.table_name) + expect(json_response.first['status']).to eq(ci_database_migration.status_name.to_s) + expect(json_response.first['progress']).to be_zero + end + end + end + end + end + + context 'when authenticated as a non-admin user' do + it 'returns 403' do + get api('/admin/batched_background_migrations', unauthorized_user) + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + end + + describe 'PUT /admin/batched_background_migrations/:id/resume' do + let!(:migration) { create(:batched_background_migration, :paused) } + let(:database) { :main } + + subject(:resume) do + put api("/admin/batched_background_migrations/#{migration.id}/resume", admin), params: { database: database } + end + + it 'pauses the batched background migration' do + resume + + aggregate_failures "testing response" do + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['id']).to eq(migration.id) + expect(json_response['status']).to eq('active') + end + end + + context 'when the batched background migration does not exist' do + let(:params) { { database: database } } + + it 'returns 404' do + put api("/admin/batched_background_migrations/#{non_existing_record_id}/resume", admin), params: params + + expect(response).to have_gitlab_http_status(:not_found) + 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 + end + + it 'uses the correct connection' do + expect(Gitlab::Database::SharedModel).to receive(:using_connection).with(ci_model.connection).and_yield + + resume + end + end + + context 'when authenticated as a non-admin user' do + it 'returns 403' do + put api("/admin/batched_background_migrations/#{migration.id}/resume", unauthorized_user) + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + end + + describe 'PUT /admin/batched_background_migrations/:id/pause' do + let!(:migration) { create(:batched_background_migration, :active) } + + it 'pauses the batched background migration' do + put api("/admin/batched_background_migrations/#{migration.id}/pause", admin), params: { database: :main } + + aggregate_failures "testing response" do + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['id']).to eq(migration.id) + expect(json_response['status']).to eq('paused') + end + end + + context 'when the batched background migration does not exist' do + let(:params) { { database: :main } } + + it 'returns 404' do + put api("/admin/batched_background_migrations/#{non_existing_record_id}/pause", admin), params: params + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'when multiple database is enabled' do + let(:ci_model) { Ci::ApplicationRecord } + + before do + skip_if_multiple_databases_not_setup + end + + it 'uses the correct connection' do + expect(Gitlab::Database::SharedModel).to receive(:using_connection).with(ci_model.connection).and_yield + + put api("/admin/batched_background_migrations/#{migration.id}/pause", admin), params: { database: :ci } + end + end + + context 'when authenticated as a non-admin user' do + it 'returns 403' do + put api("/admin/batched_background_migrations/#{non_existing_record_id}/pause", unauthorized_user) + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + end +end diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb index cc696d76a02..f7539e13b80 100644 --- a/spec/requests/api/branches_spec.rb +++ b/spec/requests/api/branches_spec.rb @@ -211,6 +211,68 @@ RSpec.describe API::Branches do end it_behaves_like 'repository branches' + + context 'caching' do + it 'caches the query' do + get api(route), params: { per_page: 1 } + + expect(API::Entities::Branch).not_to receive(:represent) + + get api(route), params: { per_page: 1 } + end + + context 'when increase_branch_cache_expiry is enabled' do + it 'uses the cache up to 60 minutes' do + time_of_request = Time.current + + get api(route), params: { per_page: 1 } + + travel_to time_of_request + 59.minutes do + expect(API::Entities::Branch).not_to receive(:represent) + + get api(route), params: { per_page: 1 } + end + end + + it 'requests for new value after 60 minutes' do + get api(route), params: { per_page: 1 } + + travel_to 61.minutes.from_now do + expect(API::Entities::Branch).to receive(:represent) + + get api(route), params: { per_page: 1 } + end + end + end + + context 'when increase_branch_cache_expiry is disabled' do + before do + stub_feature_flags(increase_branch_cache_expiry: false) + end + + it 'uses the cache up to 10 minutes' do + time_of_request = Time.current + + get api(route), params: { per_page: 1 } + + travel_to time_of_request + 9.minutes do + expect(API::Entities::Branch).not_to receive(:represent) + + get api(route), params: { per_page: 1 } + end + end + + it 'requests for new value after 10 minutes' do + get api(route), params: { per_page: 1 } + + travel_to 11.minutes.from_now do + expect(API::Entities::Branch).to receive(:represent) + + get api(route), params: { per_page: 1 } + end + end + end + end end context 'when unauthenticated', 'and project is private' do @@ -586,13 +648,36 @@ RSpec.describe API::Branches do let(:route) { "/projects/#{project_id}/repository/branches/#{branch_name}/unprotect" } shared_examples_for 'repository unprotected branch' do - it 'unprotects a single branch' do - put api(route, current_user) + context 'when branch is protected' do + let!(:protected_branch) { create(:protected_branch, project: project, name: protected_branch_name) } - expect(response).to have_gitlab_http_status(:ok) - expect(response).to match_response_schema('public_api/v4/branch') - expect(json_response['name']).to eq(CGI.unescape(branch_name)) - expect(json_response['protected']).to eq(false) + it 'unprotects a single branch' do + expect_next_instance_of(::ProtectedBranches::DestroyService, project, current_user) do |instance| + expect(instance).to receive(:execute).with(protected_branch).and_call_original + end + + put api(route, current_user) + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('public_api/v4/branch') + expect(json_response['name']).to eq(CGI.unescape(branch_name)) + expect(json_response['protected']).to eq(false) + + expect { protected_branch.reload }.to raise_error(ActiveRecord::RecordNotFound) + end + end + + context 'when branch is not protected' do + it 'returns a single branch response' do + expect(::ProtectedBranches::DestroyService).not_to receive(:new) + + put api(route, current_user) + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('public_api/v4/branch') + expect(json_response['name']).to eq(CGI.unescape(branch_name)) + expect(json_response['protected']).to eq(false) + end end context 'when branch does not exist' do @@ -637,40 +722,40 @@ RSpec.describe API::Branches do context 'when authenticated', 'as a maintainer' do let(:current_user) { user } + let(:protected_branch_name) { branch_name } - context "when a protected branch doesn't already exist" do - it_behaves_like 'repository unprotected branch' + it_behaves_like 'repository unprotected branch' - context 'when branch contains a dot' do - let(:branch_name) { branch_with_dot } + context 'when branch contains a dot' do + let(:branch_name) { branch_with_dot } - it_behaves_like 'repository unprotected branch' - end + it_behaves_like 'repository unprotected branch' + end - context 'when branch contains a slash' do - let(:branch_name) { branch_with_slash } + context 'when branch contains a slash' do + let(:branch_name) { branch_with_slash } - it_behaves_like '404 response' do - let(:request) { put api(route, current_user) } - end + it_behaves_like '404 response' do + let(:request) { put api(route, current_user) } end + end - context 'when branch contains an escaped slash' do - let(:branch_name) { CGI.escape(branch_with_slash) } + context 'when branch contains an escaped slash' do + let(:branch_name) { CGI.escape(branch_with_slash) } + let(:protected_branch_name) { branch_with_slash } - it_behaves_like 'repository unprotected branch' - end + it_behaves_like 'repository unprotected branch' + end - context 'requesting with the escaped project full path' do - let(:project_id) { CGI.escape(project.full_path) } + context 'requesting with the escaped project full path' do + let(:project_id) { CGI.escape(project.full_path) } - it_behaves_like 'repository unprotected branch' + it_behaves_like 'repository unprotected branch' - context 'when branch contains a dot' do - let(:branch_name) { branch_with_dot } + context 'when branch contains a dot' do + let(:branch_name) { branch_with_dot } - it_behaves_like 'repository unprotected branch' - end + it_behaves_like 'repository unprotected branch' end end end diff --git a/spec/requests/api/ci/job_artifacts_spec.rb b/spec/requests/api/ci/job_artifacts_spec.rb index 2fa1ffb4974..0fb11bf98d2 100644 --- a/spec/requests/api/ci/job_artifacts_spec.rb +++ b/spec/requests/api/ci/job_artifacts_spec.rb @@ -24,8 +24,7 @@ RSpec.describe API::Ci::JobArtifacts do let(:guest) { create(:project_member, :guest, project: project).user } let!(:job) do - create(:ci_build, :success, :tags, pipeline: pipeline, - artifacts_expire_at: 1.day.since) + create(:ci_build, :success, :tags, pipeline: pipeline, artifacts_expire_at: 1.day.since) end before do @@ -535,8 +534,7 @@ RSpec.describe API::Ci::JobArtifacts do context 'with regular branch' do before do pipeline.reload - pipeline.update!(ref: 'master', - sha: project.commit('master').sha) + pipeline.update!(ref: 'master', sha: project.commit('master').sha) get_for_ref('master') end @@ -579,8 +577,7 @@ RSpec.describe API::Ci::JobArtifacts do stub_artifacts_object_storage job.success - project.update!(visibility_level: visibility_level, - public_builds: public_builds) + project.update!(visibility_level: visibility_level, public_builds: public_builds) get_artifact_file(artifact) end @@ -676,8 +673,7 @@ RSpec.describe API::Ci::JobArtifacts do context 'with branch name containing slash' do before do pipeline.reload - pipeline.update!(ref: 'improve/awesome', - sha: project.commit('improve/awesome').sha) + pipeline.update!(ref: 'improve/awesome', sha: project.commit('improve/awesome').sha) end it 'returns a specific artifact file for a valid path', :sidekiq_might_not_need_inline do diff --git a/spec/requests/api/ci/jobs_spec.rb b/spec/requests/api/ci/jobs_spec.rb index 57828e50320..b8983e9632e 100644 --- a/spec/requests/api/ci/jobs_spec.rb +++ b/spec/requests/api/ci/jobs_spec.rb @@ -32,8 +32,7 @@ RSpec.describe API::Ci::Jobs do end let!(:job) do - create(:ci_build, :success, :tags, pipeline: pipeline, - artifacts_expire_at: 1.day.since) + create(:ci_build, :success, :tags, pipeline: pipeline, artifacts_expire_at: 1.day.since) end before do @@ -94,9 +93,13 @@ RSpec.describe API::Ci::Jobs do let(:params_with_token) { {} } end + def perform_request + get api('/job'), headers: headers_with_token, params: params_with_token + end + before do |example| unless example.metadata[:skip_before_request] - get api('/job'), headers: headers_with_token, params: params_with_token + perform_request end end @@ -125,6 +128,15 @@ RSpec.describe API::Ci::Jobs do expect(json_response['finished_at']).to be_nil end + it 'avoids N+1 queries', :skip_before_request do + control_count = ActiveRecord::QueryRecorder.new { perform_request }.count + + running_job = create(:ci_build, :running, project: project, user: user, pipeline: pipeline, artifacts_expire_at: 1.day.since) + running_job.save! + + expect { perform_request }.not_to exceed_query_limit(control_count) + end + it_behaves_like 'returns common pipeline data' do let(:jobx) { running_job } end @@ -237,6 +249,10 @@ RSpec.describe API::Ci::Jobs do it 'includes environment slug' do expect(json_response.dig('environment', 'slug')).to eq('production') end + + it 'includes environment tier' do + expect(json_response.dig('environment', 'tier')).to eq('production') + end end context 'when non-deployment environment action' do @@ -248,6 +264,10 @@ RSpec.describe API::Ci::Jobs do it 'includes environment slug' do expect(json_response.dig('environment', 'slug')).to eq('review') end + + it 'includes environment tier' do + expect(json_response.dig('environment', 'tier')).to eq('development') + end end context 'when passing the token as params' do 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 cd58251cfcc..b33b97f90d7 100644 --- a/spec/requests/api/ci/runner/jobs_request_post_spec.rb +++ b/spec/requests/api/ci/runner/jobs_request_post_spec.rb @@ -17,11 +17,12 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do end describe '/api/v4/jobs' do - let(:group) { create(:group, :nested) } + let_it_be(:group) { create(:group, :nested) } + let_it_be(:user) { create(:user) } + let(:project) { create(:project, namespace: group, shared_runners_enabled: false) } - let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master') } let(:runner) { create(:ci_runner, :project, projects: [project]) } - let(:user) { create(:user) } + let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master') } let(:job) do create(:ci_build, :pending, :queued, :artifacts, :extended_options, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) @@ -145,7 +146,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do let(:expected_job_info) do { 'id' => job.id, 'name' => job.name, - 'stage' => job.stage, + 'stage' => job.stage_name, 'project_id' => job.project.id, 'project_name' => job.project.name } end @@ -354,6 +355,9 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do end context 'when GIT_DEPTH is not specified and there is no default git depth for the project' do + let(:project) { create(:project, namespace: group, shared_runners_enabled: false) } + let(:runner) { create(:ci_runner, :project, projects: [project]) } + before do project.update!(ci_default_git_depth: nil) end @@ -411,7 +415,8 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do context 'when job is made for merge request' do let(:pipeline) { create(:ci_pipeline, source: :merge_request_event, project: project, ref: 'feature', merge_request: merge_request) } let!(:job) { create(:ci_build, :pending, :queued, pipeline: pipeline, name: 'spinach', ref: 'feature', stage: 'test', stage_idx: 0) } - let(:merge_request) { create(:merge_request) } + + let_it_be(:merge_request) { create(:merge_request) } it 'sets branch as ref_type' do request_job @@ -546,9 +551,12 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do let!(:job) { create(:ci_build, :pending, :queued, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) } let!(:job2) { create(:ci_build, :pending, :queued, :tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) } let!(:test_job) do - create(:ci_build, :pending, :queued, pipeline: pipeline, name: 'deploy', - stage: 'deploy', stage_idx: 1, - options: { script: ['bash'], dependencies: [job2.name] }) + create(:ci_build, :pending, :queued, + pipeline: pipeline, + name: 'deploy', + stage: 'deploy', + stage_idx: 1, + options: { script: ['bash'], dependencies: [job2.name] }) end before do @@ -570,9 +578,12 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do let!(:job) { create(:ci_build, :pending, :queued, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) } let!(:job2) { create(:ci_build, :pending, :queued, :tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) } let!(:empty_dependencies_job) do - create(:ci_build, :pending, :queued, pipeline: pipeline, name: 'empty_dependencies_job', - stage: 'deploy', stage_idx: 1, - options: { script: ['bash'], dependencies: [] }) + create(:ci_build, :pending, :queued, + pipeline: pipeline, + name: 'empty_dependencies_job', + stage: 'deploy', + stage_idx: 1, + options: { script: ['bash'], dependencies: [] }) end before do @@ -722,7 +733,9 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do describe 'timeout support' do context 'when project specifies job timeout' do - let(:project) { create(:project, shared_runners_enabled: false, build_timeout: 1234) } + let_it_be(:project) { create(:project, shared_runners_enabled: false, build_timeout: 1234) } + + let(:runner) { create(:ci_runner, :project, projects: [project]) } it 'contains info about timeout taken from project' do request_job @@ -827,22 +840,6 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do 'image' => { 'name' => 'ruby', 'pull_policy' => ['if-not-present'], 'entrypoint' => nil, 'ports' => [] } ) end - - context 'when the FF ci_docker_image_pull_policy is disabled' do - before do - stub_feature_flags(ci_docker_image_pull_policy: false) - end - - it 'returns the image without pull policy' do - request_job - - expect(response).to have_gitlab_http_status(:created) - expect(json_response).to include( - 'id' => job.id, - 'image' => { 'name' => 'ruby', 'entrypoint' => nil, 'ports' => [] } - ) - end - end end context 'when service has pull_policy' do @@ -867,31 +864,17 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do 'ports' => [], 'pull_policy' => ['if-not-present'], 'variables' => [] }] ) end - - context 'when the FF ci_docker_image_pull_policy is disabled' do - before do - stub_feature_flags(ci_docker_image_pull_policy: false) - end - - it 'returns the service without pull policy' do - request_job - - expect(response).to have_gitlab_http_status(:created) - expect(json_response).to include( - 'id' => job.id, - 'services' => [{ 'alias' => nil, 'command' => nil, 'entrypoint' => nil, 'name' => 'postgres:11.9', - 'ports' => [], 'variables' => [] }] - ) - end - end end describe 'a job with excluded artifacts' do context 'when excluded paths are defined' do let(:job) do - create(:ci_build, :pending, :queued, pipeline: pipeline, name: 'test', - stage: 'deploy', stage_idx: 1, - options: { artifacts: { paths: ['abc'], exclude: ['cde'] } }) + create(:ci_build, :pending, :queued, + pipeline: pipeline, + name: 'test', + stage: 'deploy', + stage_idx: 1, + options: { artifacts: { paths: ['abc'], exclude: ['cde'] } }) end context 'when a runner supports this feature' do @@ -950,8 +933,8 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do end context 'when the runner is of group type' do - let(:group) { create(:group) } - let(:runner) { create(:ci_runner, :group, groups: [group]) } + let_it_be(:group) { create(:group) } + let_it_be(:runner) { create(:ci_runner, :group, groups: [group]) } it_behaves_like 'storing arguments in the application context for the API' do let(:expected_params) { { root_namespace: group.full_path_components.first, client_id: "runner/#{runner.id}" } } diff --git a/spec/requests/api/ci/runners_spec.rb b/spec/requests/api/ci/runners_spec.rb index 31b85a0b1d6..fa1f713e757 100644 --- a/spec/requests/api/ci/runners_spec.rb +++ b/spec/requests/api/ci/runners_spec.rb @@ -889,6 +889,44 @@ RSpec.describe API::Ci::Runners do end end + it 'avoids N+1 DB queries' do + get api("/runners/#{shared_runner.id}/jobs", admin) + + control = ActiveRecord::QueryRecorder.new do + get api("/runners/#{shared_runner.id}/jobs", admin) + end + + create(:ci_build, :failed, runner: shared_runner, project: project) + + expect do + get api("/runners/#{shared_runner.id}/jobs", admin) + end.not_to exceed_query_limit(control.count) + end + + it 'batches loading of commits' do + shared_runner = create(:ci_runner, :instance, description: 'Shared runner') + + project_with_repo = create(:project, :repository) + + pipeline = create(:ci_pipeline, project: project_with_repo, sha: 'ddd0f15ae83993f5cb66a927a28673882e99100b') + create(:ci_build, :running, runner: shared_runner, project: project_with_repo, pipeline: pipeline) + + pipeline = create(:ci_pipeline, project: project_with_repo, sha: 'c1c67abbaf91f624347bb3ae96eabe3a1b742478') + create(:ci_build, :failed, runner: shared_runner, project: project_with_repo, pipeline: pipeline) + + pipeline = create(:ci_pipeline, project: project_with_repo, sha: '1a0b36b3cdad1d2ee32457c102a8c0b7056fa863') + create(:ci_build, :failed, runner: shared_runner, project: project_with_repo, pipeline: pipeline) + + expect_next_instance_of(Repository) do |repo| + expect(repo).to receive(:commits_by).with(oids: %w[ + 1a0b36b3cdad1d2ee32457c102a8c0b7056fa863 + c1c67abbaf91f624347bb3ae96eabe3a1b742478 + ]).once.and_call_original + end + + get api("/runners/#{shared_runner.id}/jobs", admin), params: { per_page: 2, order_by: 'id', sort: 'desc' } + end + context "when runner doesn't exist" do it 'returns 404' do get api('/runners/0/jobs', admin) diff --git a/spec/requests/api/commit_statuses_spec.rb b/spec/requests/api/commit_statuses_spec.rb index 39be28d7427..dc5d9620dc4 100644 --- a/spec/requests/api/commit_statuses_spec.rb +++ b/spec/requests/api/commit_statuses_spec.rb @@ -478,6 +478,26 @@ RSpec.describe API::CommitStatuses do .to include 'has already been taken' end end + + context 'with partitions' do + let(:current_partition_id) { 123 } + + before do + allow(Ci::Pipeline) + .to receive(:current_partition_value) { current_partition_id } + end + + it 'creates records in the current partition' do + expect { post api(post_url, developer), params: { state: 'running' } } + .to change(CommitStatus, :count).by(1) + .and change(Ci::Pipeline, :count).by(1) + + status = CommitStatus.find(json_response['id']) + + expect(status.partition_id).to eq(current_partition_id) + expect(status.pipeline.partition_id).to eq(current_partition_id) + end + end end context 'reporter user' do diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index 68fe45cd026..8a08d5203fd 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -7,14 +7,17 @@ RSpec.describe API::Commits do include ProjectForksHelper include SessionHelpers - let(:user) { create(:user) } - let(:guest) { create(:user).tap { |u| project.add_guest(u) } } - let(:developer) { create(:user).tap { |u| project.add_developer(u) } } - let(:project) { create(:project, :repository, creator: user, path: 'my.project') } + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project, :repository, creator: user, path: 'my.project') } + let_it_be(:guest) { create(:user).tap { |u| project.add_guest(u) } } + let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } } + let(:branch_with_dot) { project.repository.find_branch('ends-with.json') } let(:branch_with_slash) { project.repository.find_branch('improve/awesome') } let(:project_id) { project.id } let(:current_user) { nil } + let(:group) { create(:group, :public) } + let(:inherited_guest) { create(:user).tap { |u| group.add_guest(u) } } before do project.add_maintainer(user) @@ -44,7 +47,7 @@ RSpec.describe API::Commits do end context 'when unauthenticated', 'and project is public' do - let(:project) { create(:project, :public, :repository) } + let_it_be(:project) { create(:project, :public, :repository) } it_behaves_like 'project commits' end @@ -56,311 +59,340 @@ RSpec.describe API::Commits do end end - context 'when authenticated', 'as a maintainer' do - let(:current_user) { user } + context 'when authenticated' do + context 'when user is a direct project member' do + context 'and user is a maintainer' do + let(:current_user) { user } - it_behaves_like 'project commits' + it_behaves_like 'project commits' - context "since optional parameter" do - it "returns project commits since provided parameter" do - commits = project.repository.commits("master", limit: 2) - after = commits.second.created_at + context "since optional parameter" do + it "returns project commits since provided parameter" do + commits = project.repository.commits("master", limit: 2) + after = commits.second.created_at - get api("/projects/#{project_id}/repository/commits?since=#{after.utc.iso8601}", user) + get api("/projects/#{project_id}/repository/commits?since=#{after.utc.iso8601}", user) - expect(json_response.size).to eq 2 - expect(json_response.first["id"]).to eq(commits.first.id) - expect(json_response.second["id"]).to eq(commits.second.id) - end + expect(json_response.size).to eq 2 + expect(json_response.first["id"]).to eq(commits.first.id) + expect(json_response.second["id"]).to eq(commits.second.id) + end - it 'include correct pagination headers' do - commits = project.repository.commits("master", limit: 2) - after = commits.second.created_at + it 'include correct pagination headers' do + commits = project.repository.commits("master", limit: 2) + after = commits.second.created_at - get api("/projects/#{project_id}/repository/commits?since=#{after.utc.iso8601}", user) + get api("/projects/#{project_id}/repository/commits?since=#{after.utc.iso8601}", user) - expect(response).to include_limited_pagination_headers - expect(response.headers['X-Page']).to eql('1') - end - end + expect(response).to include_limited_pagination_headers + expect(response.headers['X-Page']).to eql('1') + end + end - context "until optional parameter" do - it "returns project commits until provided parameter" do - commits = project.repository.commits("master", limit: 20) - before = commits.second.created_at + context "until optional parameter" do + it "returns project commits until provided parameter" do + commits = project.repository.commits("master", limit: 20) + before = commits.second.created_at - get api("/projects/#{project_id}/repository/commits?until=#{before.utc.iso8601}", user) + get api("/projects/#{project_id}/repository/commits?until=#{before.utc.iso8601}", user) - if commits.size == 20 - expect(json_response.size).to eq(20) - else - expect(json_response.size).to eq(commits.size - 1) - end + if commits.size == 20 + expect(json_response.size).to eq(20) + else + expect(json_response.size).to eq(commits.size - 1) + end - expect(json_response.first["id"]).to eq(commits.second.id) - expect(json_response.second["id"]).to eq(commits.third.id) - end + expect(json_response.first["id"]).to eq(commits.second.id) + expect(json_response.second["id"]).to eq(commits.third.id) + end - it 'include correct pagination headers' do - commits = project.repository.commits("master", limit: 2) - before = commits.second.created_at + it 'include correct pagination headers' do + commits = project.repository.commits("master", limit: 2) + before = commits.second.created_at - get api("/projects/#{project_id}/repository/commits?until=#{before.utc.iso8601}", user) + get api("/projects/#{project_id}/repository/commits?until=#{before.utc.iso8601}", user) - expect(response).to include_limited_pagination_headers - expect(response.headers['X-Page']).to eql('1') - end - end + expect(response).to include_limited_pagination_headers + expect(response.headers['X-Page']).to eql('1') + end + end - context "invalid xmlschema date parameters" do - it "returns an invalid parameter error message" do - get api("/projects/#{project_id}/repository/commits?since=invalid-date", user) + context "invalid xmlschema date parameters" do + it "returns an invalid parameter error message" do + get api("/projects/#{project_id}/repository/commits?since=invalid-date", user) - expect(response).to have_gitlab_http_status(:bad_request) - expect(json_response['error']).to eq('since is invalid') - end - end + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['error']).to eq('since is invalid') + end + end - context "with empty ref_name parameter" do - let(:route) { "/projects/#{project_id}/repository/commits?ref_name=" } + context "with empty ref_name parameter" do + let(:route) { "/projects/#{project_id}/repository/commits?ref_name=" } - it_behaves_like 'project commits' - end + it_behaves_like 'project commits' + end - context 'when repository does not exist' do - let(:project) { create(:project, creator: user, path: 'my.project') } + context 'when repository does not exist' do + let(:project) { create(:project, creator: user, path: 'my.project') } - it_behaves_like '404 response' do - let(:request) { get api(route, current_user) } - let(:message) { '404 Repository Not Found' } - end - end + it_behaves_like '404 response' do + let(:request) { get api(route, current_user) } + let(:message) { '404 Repository Not Found' } + end + end - context "path optional parameter" do - it "returns project commits matching provided path parameter" do - path = 'files/ruby/popen.rb' + context "path optional parameter" do + it "returns project commits matching provided path parameter" do + path = 'files/ruby/popen.rb' - get api("/projects/#{project_id}/repository/commits?path=#{path}", user) + get api("/projects/#{project_id}/repository/commits?path=#{path}", user) - expect(json_response.size).to eq(3) - expect(json_response.first["id"]).to eq("570e7b2abdd848b95f2f578043fc23bd6f6fd24d") - expect(response).to include_limited_pagination_headers - end + expect(json_response.size).to eq(3) + expect(json_response.first["id"]).to eq("570e7b2abdd848b95f2f578043fc23bd6f6fd24d") + expect(response).to include_limited_pagination_headers + end - it 'include correct pagination headers' do - path = 'files/ruby/popen.rb' + it 'include correct pagination headers' do + path = 'files/ruby/popen.rb' - get api("/projects/#{project_id}/repository/commits?path=#{path}", user) + get api("/projects/#{project_id}/repository/commits?path=#{path}", user) - expect(response).to include_limited_pagination_headers - expect(response.headers['X-Page']).to eql('1') - end - end + expect(response).to include_limited_pagination_headers + expect(response.headers['X-Page']).to eql('1') + end + end - context 'all optional parameter' do - it 'returns all project commits' do - expected_commit_ids = project.repository.commits(nil, all: true, limit: 50).map(&:id) + context 'all optional parameter' do + it 'returns all project commits' do + expected_commit_ids = project.repository.commits(nil, all: true, limit: 50).map(&:id) - get api("/projects/#{project_id}/repository/commits?all=true&per_page=50", user) + get api("/projects/#{project_id}/repository/commits?all=true&per_page=50", user) - commit_ids = json_response.map { |c| c['id'] } + commit_ids = json_response.map { |c| c['id'] } - expect(response).to include_limited_pagination_headers - expect(commit_ids).to eq(expected_commit_ids) - expect(response.headers['X-Page']).to eql('1') - end - end + expect(response).to include_limited_pagination_headers + expect(commit_ids).to eq(expected_commit_ids) + expect(response.headers['X-Page']).to eql('1') + end + end - context 'first_parent optional parameter' do - it 'returns all first_parent commits' do - expected_commit_ids = project.repository.commits(SeedRepo::Commit::ID, limit: 50, first_parent: true).map(&:id) + context 'first_parent optional parameter' do + it 'returns all first_parent commits' do + expected_commit_ids = project.repository.commits(SeedRepo::Commit::ID, limit: 50, first_parent: true).map(&:id) - get api("/projects/#{project_id}/repository/commits?per_page=50", user), params: { ref_name: SeedRepo::Commit::ID, first_parent: 'true' } + get api("/projects/#{project_id}/repository/commits?per_page=50", user), params: { ref_name: SeedRepo::Commit::ID, first_parent: 'true' } - commit_ids = json_response.map { |c| c['id'] } + commit_ids = json_response.map { |c| c['id'] } - expect(response).to include_limited_pagination_headers - expect(expected_commit_ids.size).to eq(12) - expect(commit_ids).to eq(expected_commit_ids) - end - end + expect(response).to include_limited_pagination_headers + expect(expected_commit_ids.size).to eq(12) + expect(commit_ids).to eq(expected_commit_ids) + end + end - context 'with_stats optional parameter' do - let(:project) { create(:project, :public, :repository) } + context 'with_stats optional parameter' do + let(:project) { create(:project, :public, :repository) } - it_behaves_like 'project commits', schema: 'public_api/v4/commits_with_stats' do - let(:route) { "/projects/#{project_id}/repository/commits?with_stats=true" } + it_behaves_like 'project commits', schema: 'public_api/v4/commits_with_stats' do + let(:route) { "/projects/#{project_id}/repository/commits?with_stats=true" } - it 'include commits details' do - commit = project.repository.commit - get api(route, current_user) + it 'include commits details' do + commit = project.repository.commit + get api(route, current_user) - expect(json_response.first['stats']['additions']).to eq(commit.stats.additions) - expect(json_response.first['stats']['deletions']).to eq(commit.stats.deletions) - expect(json_response.first['stats']['total']).to eq(commit.stats.total) + expect(json_response.first['stats']['additions']).to eq(commit.stats.additions) + expect(json_response.first['stats']['deletions']).to eq(commit.stats.deletions) + expect(json_response.first['stats']['total']).to eq(commit.stats.total) + end + end end - end - end - context 'with pagination params' do - let(:page) { 1 } - let(:per_page) { 5 } - let(:ref_name) { 'master' } - let(:request) do - get api("/projects/#{project_id}/repository/commits?page=#{page}&per_page=#{per_page}&ref_name=#{ref_name}", user) - end + context 'with pagination params' do + let(:page) { 1 } + let(:per_page) { 5 } + let(:ref_name) { 'master' } + let(:request) do + get api("/projects/#{project_id}/repository/commits?page=#{page}&per_page=#{per_page}&ref_name=#{ref_name}", user) + end - it 'returns correct headers' do - request + it 'returns correct headers' do + request - expect(response).to include_limited_pagination_headers - expect(response.headers['Link']).to match(/page=1&per_page=5/) - expect(response.headers['Link']).to match(/page=2&per_page=5/) - end + expect(response).to include_limited_pagination_headers + expect(response.headers['Link']).to match(/page=1&per_page=5/) + expect(response.headers['Link']).to match(/page=2&per_page=5/) + end - context 'viewing the first page' do - it 'returns the first 5 commits' do - request + context 'viewing the first page' do + it 'returns the first 5 commits' do + request - commit = project.repository.commit + commit = project.repository.commit - expect(json_response.size).to eq(per_page) - expect(json_response.first['id']).to eq(commit.id) - expect(response.headers['X-Page']).to eq('1') - end - end + expect(json_response.size).to eq(per_page) + expect(json_response.first['id']).to eq(commit.id) + expect(response.headers['X-Page']).to eq('1') + end + end - context 'viewing the third page' do - let(:page) { 3 } + context 'viewing the third page' do + let(:page) { 3 } - it 'returns the third 5 commits' do - request + it 'returns the third 5 commits' do + request - commit = project.repository.commits('HEAD', limit: per_page, offset: (page - 1) * per_page).first + commit = project.repository.commits('HEAD', limit: per_page, offset: (page - 1) * per_page).first - expect(json_response.size).to eq(per_page) - expect(json_response.first['id']).to eq(commit.id) - expect(response.headers['X-Page']).to eq('3') - end - end + expect(json_response.size).to eq(per_page) + expect(json_response.first['id']).to eq(commit.id) + expect(response.headers['X-Page']).to eq('3') + end + end - context 'when pagination params are invalid' do - let_it_be(:project) { create(:project, :repository) } + context 'when pagination params are invalid' do + let_it_be(:project) { create(:project, :repository) } - using RSpec::Parameterized::TableSyntax + using RSpec::Parameterized::TableSyntax - where(:page, :per_page, :error_message) do - 0 | nil | 'page does not have a valid value' - -1 | nil | 'page does not have a valid value' - 'a' | nil | 'page is invalid' - nil | 0 | 'per_page does not have a valid value' - nil | -1 | 'per_page does not have a valid value' - nil | 'a' | 'per_page is invalid' - end + where(:page, :per_page, :error_message) do + 0 | nil | 'page does not have a valid value' + -1 | nil | 'page does not have a valid value' + 'a' | nil | 'page is invalid' + nil | 0 | 'per_page does not have a valid value' + nil | -1 | 'per_page does not have a valid value' + nil | 'a' | 'per_page is invalid' + end - with_them do - it 'returns 400 response' do - request + with_them do + it 'returns 400 response' do + request - expect(response).to have_gitlab_http_status(:bad_request) - expect(json_response['error']).to eq(error_message) - end - end + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['error']).to eq(error_message) + end + end - context 'when FF is off' do - before do - stub_feature_flags(only_positive_pagination_values: false) - end + context 'when FF is off' do + before do + stub_feature_flags(only_positive_pagination_values: false) + end - where(:page, :per_page, :error_message, :status) do - 0 | nil | nil | :success - -10 | nil | nil | :internal_server_error - 'a' | nil | 'page is invalid' | :bad_request - nil | 0 | 'per_page has a value not allowed' | :bad_request - nil | -1 | nil | :success - nil | 'a' | 'per_page is invalid' | :bad_request - end + where(:page, :per_page, :error_message, :status) do + 0 | nil | nil | :success + -10 | nil | nil | :internal_server_error + 'a' | nil | 'page is invalid' | :bad_request + nil | 0 | 'per_page has a value not allowed' | :bad_request + nil | -1 | nil | :success + nil | 'a' | 'per_page is invalid' | :bad_request + end - with_them do - it 'returns a response' do - request + with_them do + it 'returns a response' do + request - expect(response).to have_gitlab_http_status(status) + expect(response).to have_gitlab_http_status(status) - if error_message - expect(json_response['error']).to eq(error_message) + if error_message + expect(json_response['error']).to eq(error_message) + end + end end end end end - end - end - context 'with order parameter' do - let(:route) { "/projects/#{project_id}/repository/commits?ref_name=0031876&per_page=6&order=#{order}" } + context 'with order parameter' do + let(:route) { "/projects/#{project_id}/repository/commits?ref_name=0031876&per_page=6&order=#{order}" } - context 'set to topo' do - let(:order) { 'topo' } + context 'set to topo' do + let(:order) { 'topo' } - # git log --graph -n 6 --pretty=format:"%h" --topo-order 0031876 - # * 0031876 - # |\ - # | * 48ca272 - # | * 335bc94 - # * | bf6e164 - # * | 9d526f8 - # |/ - # * 1039376 - it 'returns project commits ordered by topo order' do - commits = project.repository.commits("0031876", limit: 6, order: 'topo') + # git log --graph -n 6 --pretty=format:"%h" --topo-order 0031876 + # * 0031876 + # |\ + # | * 48ca272 + # | * 335bc94 + # * | bf6e164 + # * | 9d526f8 + # |/ + # * 1039376 + it 'returns project commits ordered by topo order' do + commits = project.repository.commits("0031876", limit: 6, order: 'topo') - get api(route, current_user) + get api(route, current_user) - expect(json_response.size).to eq(6) - expect(json_response.map { |entry| entry["id"] }).to eq(commits.map(&:id)) - end - end + expect(json_response.size).to eq(6) + expect(json_response.map { |entry| entry["id"] }).to eq(commits.map(&:id)) + end + end + + context 'set to default' do + let(:order) { 'default' } + + # git log --graph -n 6 --pretty=format:"%h" --date-order 0031876 + # * 0031876 + # |\ + # * | bf6e164 + # | * 48ca272 + # * | 9d526f8 + # | * 335bc94 + # |/ + # * 1039376 + it 'returns project commits ordered by default order' do + commits = project.repository.commits("0031876", limit: 6, order: 'default') + + get api(route, current_user) + + expect(json_response.size).to eq(6) + expect(json_response.map { |entry| entry["id"] }).to eq(commits.map(&:id)) + end + end - context 'set to default' do - let(:order) { 'default' } + context 'set to an invalid parameter' do + let(:order) { 'invalid' } - # git log --graph -n 6 --pretty=format:"%h" --date-order 0031876 - # * 0031876 - # |\ - # * | bf6e164 - # | * 48ca272 - # * | 9d526f8 - # | * 335bc94 - # |/ - # * 1039376 - it 'returns project commits ordered by default order' do - commits = project.repository.commits("0031876", limit: 6, order: 'default') + it_behaves_like '400 response' do + let(:request) { get api(route, current_user) } + end + end + end - get api(route, current_user) + context 'with the optional trailers parameter' do + it 'includes the Git trailers' do + get api("/projects/#{project_id}/repository/commits?ref_name=6d394385cf567f80a8fd85055db1ab4c5295806f&trailers=true", current_user) - expect(json_response.size).to eq(6) - expect(json_response.map { |entry| entry["id"] }).to eq(commits.map(&:id)) + commit = json_response[0] + + expect(commit['trailers']).to eq( + 'Signed-off-by' => 'Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>' + ) + end end end + end - context 'set to an invalid parameter' do - let(:order) { 'invalid' } + context 'when user is an inherited member from the group' do + context 'when project is public with private repository' do + let(:project) { create(:project, :public, :repository, :repository_private, group: group) } - it_behaves_like '400 response' do - let(:request) { get api(route, current_user) } + context 'and user is a guest' do + let(:current_user) { inherited_guest } + + it_behaves_like 'project commits' end end - end - context 'with the optional trailers parameter' do - it 'includes the Git trailers' do - get api("/projects/#{project_id}/repository/commits?ref_name=6d394385cf567f80a8fd85055db1ab4c5295806f&trailers=true", current_user) + context 'when project is private' do + let(:project) { create(:project, :private, :repository, group: group) } - commit = json_response[0] + context 'and user is a guest' do + let(:current_user) { inherited_guest } - expect(commit['trailers']).to eq( - 'Signed-off-by' => 'Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>' - ) + it_behaves_like '404 response' do + let(:request) { get api(route) } + let(:message) { '404 Project Not Found' } + end + end end end end @@ -382,6 +414,9 @@ RSpec.describe API::Commits do end describe 'create' do + let_it_be(:sequencer) { FactoryBot::Sequence.new(:new_file_path) { |n| "files/test/#{n}.rb" } } + + let(:new_file_path) { sequencer.next } let(:message) { 'Created a new file with a very very looooooooooooooooooooooooooooooooooooooooooooooong commit message' } let(:invalid_c_params) do { @@ -404,7 +439,7 @@ RSpec.describe API::Commits do actions: [ { action: 'create', - file_path: 'foo/bar/baz.txt', + file_path: new_file_path, content: 'puts 8' } ] @@ -418,7 +453,7 @@ RSpec.describe API::Commits do actions: [ { action: 'create', - file_path: 'foo/bar/baz.txt', + file_path: new_file_path, content: 'puts 🦊' } ] @@ -466,11 +501,57 @@ RSpec.describe API::Commits do end context 'a new file in project repo' do - before do - post api(url, user), params: valid_c_params + context 'when user is a direct project member' do + before do + post api(url, user), params: valid_c_params + end + + it_behaves_like 'successfully creates the commit' end - it_behaves_like "successfully creates the commit" + context 'when user is an inherited member from the group' do + context 'when project is public with private repository' do + let(:project) { create(:project, :public, :repository, :repository_private, group: group) } + + context 'and user is a guest' do + it_behaves_like '403 response' do + let(:request) { post api(url, inherited_guest), params: valid_c_params } + let(:message) { '403 Forbidden' } + end + end + end + + context 'when project is private' do + let(:project) { create(:project, :private, :repository, group: group) } + + context 'and user is a guest' do + it_behaves_like '403 response' do + let(:request) { post api(url, inherited_guest), params: valid_c_params } + let(:message) { '403 Forbidden' } + end + end + end + end + end + + context 'when repository is empty' do + let!(:project) { create(:project, :empty_repo) } + + context 'when params are valid' do + before do + post api(url, user), params: valid_c_params + end + + it_behaves_like "successfully creates the commit" + end + + context 'when branch name is invalid' do + before do + post api(url, user), params: valid_c_params.merge(branch: 'wrong:name') + end + + it { expect(response).to have_gitlab_http_status(:bad_request) } + end end context 'a new file with utf8 chars in project repo' do @@ -882,6 +963,7 @@ RSpec.describe API::Commits do end describe 'multiple operations' do + let(:project) { create(:project, :repository, creator: user, path: 'my.project') } let(:message) { 'Multiple actions' } let(:invalid_mo_params) do { @@ -951,17 +1033,11 @@ RSpec.describe API::Commits do } end - it 'are committed as one in project repo' do + it 'is committed as one in project repo and includes stats' do post api(url, user), params: valid_mo_params expect(response).to have_gitlab_http_status(:created) expect(json_response['title']).to eq(message) - end - - it 'includes the commit stats' do - post api(url, user), params: valid_mo_params - - expect(response).to have_gitlab_http_status(:created) expect(json_response).to include 'stats' end @@ -1047,7 +1123,8 @@ RSpec.describe API::Commits do end describe 'GET /projects/:id/repository/commits/:sha/refs' do - let(:project) { create(:project, :public, :repository) } + let_it_be(:project) { create(:project, :public, :repository) } + let(:tag) { project.repository.find_tag('v1.1.0') } let(:commit_id) { tag.dereferenced_target.id } let(:route) { "/projects/#{project_id}/repository/commits/#{commit_id}/refs" } @@ -1062,6 +1139,8 @@ RSpec.describe API::Commits do end context 'when repository is disabled' do + let(:project) { create(:project, :repository, creator: user, path: 'my.project') } + include_context 'disabled repository' it_behaves_like '404 response' do @@ -1151,6 +1230,8 @@ RSpec.describe API::Commits do end context 'when repository is disabled' do + let(:project) { create(:project, :repository, creator: user, path: 'my.project') } + include_context 'disabled repository' it_behaves_like '404 response' do @@ -1192,8 +1273,14 @@ RSpec.describe API::Commits do end shared_examples_for 'ref with unaccessible pipeline' do - let!(:pipeline) do - create(:ci_empty_pipeline, project: project, status: :created, source: :push, ref: 'master', sha: commit.sha, protected: false) + let(:pipeline) do + create(:ci_empty_pipeline, + project: project, + status: :created, + source: :push, + ref: 'master', + sha: commit.sha, + protected: false) end it 'does not include last_pipeline' do @@ -1231,7 +1318,7 @@ RSpec.describe API::Commits do end context 'when unauthenticated', 'and project is public' do - let(:project) { create(:project, :public, :repository) } + let_it_be_with_reload(:project) { create(:project, :public, :repository) } it_behaves_like 'ref commit' it_behaves_like 'ref with pipeline' @@ -1261,6 +1348,7 @@ RSpec.describe API::Commits do context 'when builds are disabled' do before do project + .reload .project_feature .update!(builds_access_level: ProjectFeature::DISABLED) end @@ -1312,7 +1400,7 @@ RSpec.describe API::Commits do context 'with private builds' do before do - project.project_feature.update!(builds_access_level: ProjectFeature::PRIVATE) + project.reload.project_feature.update!(builds_access_level: ProjectFeature::PRIVATE) end it_behaves_like 'ref with pipeline' @@ -1338,8 +1426,8 @@ RSpec.describe API::Commits do end context 'when authenticated', 'as non_member and project is public' do - let(:current_user) { create(:user) } - let(:project) { create(:project, :public, :repository) } + let_it_be(:current_user) { create(:user) } + let_it_be_with_reload(:project) { create(:project, :public, :repository) } it_behaves_like 'ref with pipeline' @@ -1392,6 +1480,8 @@ RSpec.describe API::Commits do end context 'when repository is disabled' do + let(:project) { create(:project, :repository, creator: user, path: 'my.project') } + include_context 'disabled repository' it_behaves_like '404 response' do @@ -1401,7 +1491,7 @@ RSpec.describe API::Commits do end context 'when unauthenticated', 'and project is public' do - let(:project) { create(:project, :public, :repository) } + let_it_be(:project) { create(:project, :public, :repository) } it_behaves_like 'ref diff' end @@ -1491,6 +1581,8 @@ RSpec.describe API::Commits do end context 'when repository is disabled' do + let(:project) { create(:project, :repository, creator: user, path: 'my.project') } + include_context 'disabled repository' it_behaves_like '404 response' do @@ -1500,7 +1592,7 @@ RSpec.describe API::Commits do end context 'when unauthenticated', 'and project is public' do - let(:project) { create(:project, :public, :repository) } + let_it_be(:project) { create(:project, :public, :repository) } it_behaves_like 'ref comments' end @@ -1589,6 +1681,7 @@ RSpec.describe API::Commits do end describe 'POST :id/repository/commits/:sha/cherry_pick' do + let(:project) { create(:project, :repository, creator: user, path: 'my.project') } let(:commit) { project.commit('7d3b0f7cff5f37573aea97cebfd5692ea1689924') } let(:commit_id) { commit.id } let(:branch) { 'master' } @@ -1626,6 +1719,8 @@ RSpec.describe API::Commits do end context 'when repository is disabled' do + let(:project) { create(:project, :repository, creator: user, path: 'my.project') } + include_context 'disabled repository' it_behaves_like '404 response' do @@ -1635,7 +1730,7 @@ RSpec.describe API::Commits do end context 'when unauthenticated', 'and project is public' do - let(:project) { create(:project, :public, :repository) } + let_it_be(:project) { create(:project, :public, :repository) } it_behaves_like '403 response' do let(:request) { post api(route), params: { branch: 'master' } } @@ -1774,6 +1869,7 @@ RSpec.describe API::Commits do end describe 'POST :id/repository/commits/:sha/revert' do + let(:project) { create(:project, :repository, creator: user, path: 'my.project') } let(:commit_id) { 'b83d6e391c22777fca1ed3012fce84f633d7fed0' } let(:commit) { project.commit(commit_id) } let(:branch) { 'master' } @@ -1814,7 +1910,7 @@ RSpec.describe API::Commits do end context 'when unauthenticated', 'and project is public' do - let(:project) { create(:project, :public, :repository) } + let_it_be(:project) { create(:project, :public, :repository) } it_behaves_like '403 response' do let(:request) { post api(route), params: { branch: branch } } @@ -1921,6 +2017,7 @@ RSpec.describe API::Commits do end describe 'POST /projects/:id/repository/commits/:sha/comments' do + let(:project) { create(:project, :repository, :private) } let(:commit) { project.repository.commit } let(:commit_id) { commit.id } let(:note) { 'My comment' } @@ -1941,6 +2038,8 @@ RSpec.describe API::Commits do end context 'when repository is disabled' do + let(:project) { create(:project, :repository, creator: user, path: 'my.project') } + include_context 'disabled repository' it_behaves_like '404 response' do @@ -1950,7 +2049,7 @@ RSpec.describe API::Commits do end context 'when unauthenticated', 'and project is public' do - let(:project) { create(:project, :public, :repository) } + let_it_be(:project) { create(:project, :public, :repository) } it_behaves_like '400 response' do let(:request) { post api(route), params: { note: 'My comment' } } @@ -1970,12 +2069,13 @@ RSpec.describe API::Commits do it_behaves_like 'ref new comment' it 'returns the inline comment' do - post api(route, current_user), params: { note: 'My comment', path: project.repository.commit.raw_diffs.first.new_path, line: 1, line_type: 'new' } + path = project.repository.commit.raw_diffs.first.new_path + post api(route, current_user), params: { note: 'My comment', path: path, line: 1, line_type: 'new' } expect(response).to have_gitlab_http_status(:created) expect(response).to match_response_schema('public_api/v4/commit_note') expect(json_response['note']).to eq('My comment') - expect(json_response['path']).to eq(project.repository.commit.raw_diffs.first.new_path) + expect(json_response['path']).to eq(path) expect(json_response['line']).to eq(1) expect(json_response['line_type']).to eq('new') end @@ -2050,7 +2150,8 @@ RSpec.describe API::Commits do end describe 'GET /projects/:id/repository/commits/:sha/merge_requests' do - let(:project) { create(:project, :repository, :private) } + let_it_be(:project) { create(:project, :repository, :private) } + let(:merged_mr) { create(:merge_request, source_project: project, source_branch: 'master', target_branch: 'feature') } let(:commit) { merged_mr.merge_request_diff.commits.last } @@ -2082,7 +2183,8 @@ RSpec.describe API::Commits do end context 'public project' do - let(:project) { create(:project, :repository, :public, :merge_requests_private) } + let_it_be(:project) { create(:project, :repository, :public, :merge_requests_private) } + let(:non_member) { create(:user) } it 'responds 403 when only members are allowed to read merge requests' do diff --git a/spec/requests/api/conan_instance_packages_spec.rb b/spec/requests/api/conan_instance_packages_spec.rb index e4747e0eb99..b343e0cfc97 100644 --- a/spec/requests/api/conan_instance_packages_spec.rb +++ b/spec/requests/api/conan_instance_packages_spec.rb @@ -103,8 +103,7 @@ RSpec.describe API::ConanInstancePackages do context 'file download endpoints' do include_context 'conan file download endpoints' - describe 'GET /api/v4/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/ -:recipe_revision/export/:file_name' do + describe 'GET /api/v4/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/:recipe_revision/export/:file_name' do subject do get api("/packages/conan/v1/files/#{recipe_path}/#{metadata.recipe_revision}/export/#{recipe_file.file_name}"), headers: headers @@ -114,8 +113,7 @@ RSpec.describe API::ConanInstancePackages do it_behaves_like 'project not found by recipe' end - describe 'GET /api/v4/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/ -:recipe_revision/package/:conan_package_reference/:package_revision/:file_name' do + describe 'GET /api/v4/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/:recipe_revision/package/:conan_package_reference/:package_revision/:file_name' do subject do get api("/packages/conan/v1/files/#{recipe_path}/#{metadata.recipe_revision}/package/#{metadata.conan_package_reference}/#{metadata.package_revision}/#{package_file.file_name}"), headers: headers diff --git a/spec/requests/api/conan_project_packages_spec.rb b/spec/requests/api/conan_project_packages_spec.rb index 48e36b55a68..4e6af9942ef 100644 --- a/spec/requests/api/conan_project_packages_spec.rb +++ b/spec/requests/api/conan_project_packages_spec.rb @@ -102,8 +102,7 @@ RSpec.describe API::ConanProjectPackages do context 'file download endpoints', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/326194' do include_context 'conan file download endpoints' - describe 'GET /api/v4/projects/:id/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/ -:recipe_revision/export/:file_name' do + describe 'GET /api/v4/projects/:id/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/:recipe_revision/export/:file_name' do subject do get api("/projects/#{project_id}/packages/conan/v1/files/#{recipe_path}/#{metadata.recipe_revision}/export/#{recipe_file.file_name}"), headers: headers @@ -113,8 +112,7 @@ RSpec.describe API::ConanProjectPackages do it_behaves_like 'project not found by project id' end - describe 'GET /api/v4/projects/:id/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/ -:recipe_revision/package/:conan_package_reference/:package_revision/:file_name' do + describe 'GET /api/v4/projects/:id/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/:recipe_revision/package/:conan_package_reference/:package_revision/:file_name' do subject do get api("/projects/#{project_id}/packages/conan/v1/files/#{recipe_path}/#{metadata.recipe_revision}/package/#{metadata.conan_package_reference}/#{metadata.package_revision}/#{package_file.file_name}"), headers: headers diff --git a/spec/requests/api/debian_group_packages_spec.rb b/spec/requests/api/debian_group_packages_spec.rb index d881d4350fb..9dbb75becf8 100644 --- a/spec/requests/api/debian_group_packages_spec.rb +++ b/spec/requests/api/debian_group_packages_spec.rb @@ -36,12 +36,42 @@ RSpec.describe API::DebianGroupPackages do it_behaves_like 'Debian packages read endpoint', 'GET', :success, /Description: This is an incomplete Packages file/ end + describe 'GET groups/:id/-/packages/debian/dists/*distribution/:component/binary-:architecture/by-hash/SHA256/:file_sha256' do + let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{component.name}/binary-#{architecture.name}/by-hash/SHA256/#{component_file_older_sha256.file_sha256}" } + + it_behaves_like 'Debian packages read endpoint', 'GET', :success, /^Other SHA256$/ + end + + describe 'GET groups/:id/-/packages/debian/dists/*distribution/:component/source/Sources' do + let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{component.name}/source/Sources" } + + it_behaves_like 'Debian packages read endpoint', 'GET', :success, /Description: This is an incomplete Sources file/ + end + + describe 'GET groups/:id/-/packages/debian/dists/*distribution/:component/source/by-hash/SHA256/:file_sha256' do + let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{component.name}/source/by-hash/SHA256/#{component_file_sources_older_sha256.file_sha256}" } + + it_behaves_like 'Debian packages read endpoint', 'GET', :success, /^Other SHA256$/ + end + + describe 'GET groups/:id/-/packages/debian/dists/*distribution/:component/debian-installer/binary-:architecture/Packages' do + let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{component.name}/debian-installer/binary-#{architecture.name}/Packages" } + + it_behaves_like 'Debian packages read endpoint', 'GET', :success, /Description: This is an incomplete D-I Packages file/ + end + + describe 'GET groups/:id/-/packages/debian/dists/*distribution/:component/debian-installer/binary-:architecture/by-hash/SHA256/:file_sha256' do + let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{component.name}/debian-installer/binary-#{architecture.name}/by-hash/SHA256/#{component_file_di_older_sha256.file_sha256}" } + + it_behaves_like 'Debian packages read endpoint', 'GET', :success, /^Other SHA256$/ + end + describe 'GET groups/:id/-/packages/debian/pool/:codename/:project_id/:letter/:package_name/:package_version/:file_name' do + using RSpec::Parameterized::TableSyntax + let(:url) { "/groups/#{container.id}/-/packages/debian/pool/#{package.debian_distribution.codename}/#{project.id}/#{letter}/#{package.name}/#{package.version}/#{file_name}" } let(:file_name) { params[:file_name] } - using RSpec::Parameterized::TableSyntax - where(:file_name, :success_body) do 'sample_1.2.3~alpha2.tar.xz' | /^.7zXZ/ 'sample_1.2.3~alpha2.dsc' | /^Format: 3.0 \(native\)/ @@ -53,6 +83,12 @@ RSpec.describe API::DebianGroupPackages do with_them do it_behaves_like 'Debian packages read endpoint', 'GET', :success, params[:success_body] + + context 'for bumping last downloaded at' do + include_context 'Debian repository access', :public, :developer, :basic do + it_behaves_like 'bumping the package last downloaded at field' + end + end end end end diff --git a/spec/requests/api/debian_project_packages_spec.rb b/spec/requests/api/debian_project_packages_spec.rb index bd68bf912e1..6bef669cb3a 100644 --- a/spec/requests/api/debian_project_packages_spec.rb +++ b/spec/requests/api/debian_project_packages_spec.rb @@ -36,12 +36,42 @@ RSpec.describe API::DebianProjectPackages do it_behaves_like 'Debian packages read endpoint', 'GET', :success, /Description: This is an incomplete Packages file/ end + describe 'GET projects/:id/packages/debian/dists/*distribution/:component/binary-:architecture/by-hash/SHA256/:file_sha256' do + let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{component.name}/binary-#{architecture.name}/by-hash/SHA256/#{component_file_older_sha256.file_sha256}" } + + it_behaves_like 'Debian packages read endpoint', 'GET', :success, /^Other SHA256$/ + end + + describe 'GET projects/:id/packages/debian/dists/*distribution/source/Sources' do + let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{component.name}/source/Sources" } + + it_behaves_like 'Debian packages read endpoint', 'GET', :success, /Description: This is an incomplete Sources file/ + end + + describe 'GET projects/:id/packages/debian/dists/*distribution/source/by-hash/SHA256/:file_sha256' do + let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{component.name}/source/by-hash/SHA256/#{component_file_sources_older_sha256.file_sha256}" } + + it_behaves_like 'Debian packages read endpoint', 'GET', :success, /^Other SHA256$/ + end + + describe 'GET projects/:id/packages/debian/dists/*distribution/:component/debian-installer/binary-:architecture/Packages' do + let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{component.name}/debian-installer/binary-#{architecture.name}/Packages" } + + it_behaves_like 'Debian packages read endpoint', 'GET', :success, /Description: This is an incomplete D-I Packages file/ + end + + describe 'GET projects/:id/packages/debian/dists/*distribution/:component/debian-installer/binary-:architecture/by-hash/SHA256/:file_sha256' do + let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{component.name}/debian-installer/binary-#{architecture.name}/by-hash/SHA256/#{component_file_di_older_sha256.file_sha256}" } + + it_behaves_like 'Debian packages read endpoint', 'GET', :success, /^Other SHA256$/ + end + describe 'GET projects/:id/packages/debian/pool/:codename/:letter/:package_name/:package_version/:file_name' do + using RSpec::Parameterized::TableSyntax + let(:url) { "/projects/#{container.id}/packages/debian/pool/#{package.debian_distribution.codename}/#{letter}/#{package.name}/#{package.version}/#{file_name}" } let(:file_name) { params[:file_name] } - using RSpec::Parameterized::TableSyntax - where(:file_name, :success_body) do 'sample_1.2.3~alpha2.tar.xz' | /^.7zXZ/ 'sample_1.2.3~alpha2.dsc' | /^Format: 3.0 \(native\)/ @@ -53,6 +83,12 @@ RSpec.describe API::DebianProjectPackages do with_them do it_behaves_like 'Debian packages read endpoint', 'GET', :success, params[:success_body] + + context 'for bumping last downloaded at' do + include_context 'Debian repository access', :public, :developer, :basic do + it_behaves_like 'bumping the package last downloaded at field' + end + end end end diff --git a/spec/requests/api/deployments_spec.rb b/spec/requests/api/deployments_spec.rb index 24c3ee59c18..24e0e5d3180 100644 --- a/spec/requests/api/deployments_spec.rb +++ b/spec/requests/api/deployments_spec.rb @@ -14,9 +14,10 @@ RSpec.describe API::Deployments do let_it_be(:project) { create(:project, :repository) } let_it_be(:production) { create(:environment, :production, project: project) } let_it_be(:staging) { create(:environment, :staging, project: project) } - let_it_be(:deployment_1) { create(:deployment, :success, project: project, environment: production, ref: 'master', created_at: Time.now, updated_at: Time.now) } - let_it_be(:deployment_2) { create(:deployment, :success, project: project, environment: staging, ref: 'master', created_at: 1.day.ago, updated_at: 2.hours.ago) } - let_it_be(:deployment_3) { create(:deployment, :success, project: project, environment: staging, ref: 'master', created_at: 2.days.ago, updated_at: 1.hour.ago) } + let_it_be(:build) { create(:ci_build, :success, project: project) } + let_it_be(:deployment_1) { create(:deployment, :success, project: project, environment: production, deployable: build, ref: 'master', created_at: Time.now, updated_at: Time.now) } + let_it_be(:deployment_2) { create(:deployment, :success, project: project, environment: staging, deployable: build, ref: 'master', created_at: 1.day.ago, updated_at: 2.hours.ago) } + let_it_be(:deployment_3) { create(:deployment, :success, project: project, environment: staging, deployable: build, ref: 'master', created_at: 2.days.ago, updated_at: 1.hour.ago) } def perform_request(params = {}) get api("/projects/#{project.id}/deployments", user), params: params @@ -104,7 +105,7 @@ RSpec.describe API::Deployments do control_count = ActiveRecord::QueryRecorder.new { perform_request }.count - create(:deployment, :success, project: project, iid: 21, ref: 'master') + create(:deployment, :success, project: project, deployable: build, iid: 21, ref: 'master') expect { perform_request }.not_to exceed_query_limit(control_count) end diff --git a/spec/requests/api/feature_flags_spec.rb b/spec/requests/api/feature_flags_spec.rb index a1aedc1d6b2..bf7eec167f5 100644 --- a/spec/requests/api/feature_flags_spec.rb +++ b/spec/requests/api/feature_flags_spec.rb @@ -365,8 +365,8 @@ RSpec.describe API::FeatureFlags do describe 'PUT /projects/:id/feature_flags/:name' do context 'with a version 2 feature flag' do let!(:feature_flag) do - create(:operations_feature_flag, :new_version_flag, project: project, active: true, - name: 'feature1', description: 'old description') + create(:operations_feature_flag, :new_version_flag, + project: project, active: true, name: 'feature1', description: 'old description') end it 'returns a 404 if the feature flag does not exist' do @@ -591,8 +591,8 @@ RSpec.describe API::FeatureFlags do it 'deletes a feature flag strategy' do strategy_a = create(:operations_strategy, feature_flag: feature_flag, name: 'default', parameters: {}) - strategy_b = create(:operations_strategy, feature_flag: feature_flag, - name: 'userWithId', parameters: { userIds: 'userA,userB' }) + strategy_b = create(:operations_strategy, + feature_flag: feature_flag, name: 'userWithId', parameters: { userIds: 'userA,userB' }) params = { strategies: [{ id: strategy_a.id, diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb index 06d22e7e218..e95a626b4aa 100644 --- a/spec/requests/api/files_spec.rb +++ b/spec/requests/api/files_spec.rb @@ -5,13 +5,21 @@ require 'spec_helper' RSpec.describe API::Files do include RepoHelpers - let(:user) { create(:user) } + let_it_be(:group) { create(:group, :public) } + let_it_be_with_refind(:user) { create(:user) } + let_it_be(:inherited_guest) { create(:user) } + let_it_be(:inherited_reporter) { create(:user) } + let_it_be(:inherited_developer) { create(:user) } + let!(:project) { create(:project, :repository, namespace: user.namespace ) } let(:guest) { create(:user) { |u| project.add_guest(u) } } - let(:file_path) { "files%2Fruby%2Fpopen%2Erb" } - let(:executable_file_path) { "files%2Fexecutables%2Fls" } - let(:rouge_file_path) { "%2e%2e%2f" } - let(:absolute_path) { "%2Fetc%2Fpasswd.rb" } + let(:file_path) { 'files%2Fruby%2Fpopen%2Erb' } + let(:file_name) { 'popen.rb' } + let(:last_commit_id) { '570e7b2abdd848b95f2f578043fc23bd6f6fd24d' } + let(:content_sha256) { 'c440cd09bae50c4632cc58638ad33c6aa375b6109d811e76a9cc3a613c1e8887' } + let(:executable_file_path) { 'files%2Fexecutables%2Fls' } + let(:invalid_file_path) { '%2e%2e%2f' } + let(:absolute_path) { '%2Fetc%2Fpasswd.rb' } let(:invalid_file_message) { 'file_path should be a valid file path' } let(:params) do { @@ -46,6 +54,12 @@ RSpec.describe API::Files do fake_class.new end + before_all do + group.add_guest(inherited_guest) + group.add_reporter(inherited_reporter) + group.add_developer(inherited_developer) + end + before do project.add_developer(user) end @@ -70,8 +84,10 @@ RSpec.describe API::Files do expect(helper.headers).to eq({ 'X-Gitlab-Test' => '1' }) end - it 'raises exception if value is an Enumerable' do - expect { helper.set_http_headers(test: [1]) }.to raise_error(ArgumentError) + context 'when value is an Enumerable' do + it 'raises an exception' do + expect { helper.set_http_headers(test: [1]) }.to raise_error(ArgumentError) + end end end @@ -87,12 +103,12 @@ RSpec.describe API::Files do end end - describe "HEAD /projects/:id/repository/files/:file_path" do + describe 'HEAD /projects/:id/repository/files/:file_path' do shared_examples_for 'repository files' do let(:options) { {} } it 'returns 400 when file path is invalid' do - head api(route(rouge_file_path), current_user, **options), params: params + head api(route(invalid_file_path), current_user, **options), params: params expect(response).to have_gitlab_http_status(:bad_request) end @@ -106,16 +122,16 @@ RSpec.describe API::Files do expect(response).to have_gitlab_http_status(:ok) expect(response.headers['X-Gitlab-File-Path']).to eq(CGI.unescape(file_path)) - expect(response.headers['X-Gitlab-File-Name']).to eq('popen.rb') - expect(response.headers['X-Gitlab-Last-Commit-Id']).to eq('570e7b2abdd848b95f2f578043fc23bd6f6fd24d') - expect(response.headers['X-Gitlab-Content-Sha256']).to eq('c440cd09bae50c4632cc58638ad33c6aa375b6109d811e76a9cc3a613c1e8887') + expect(response.headers['X-Gitlab-File-Name']).to eq(file_name) + expect(response.headers['X-Gitlab-Last-Commit-Id']).to eq(last_commit_id) + expect(response.headers['X-Gitlab-Content-Sha256']).to eq(content_sha256) end it 'caches sha256 of the content', :use_clean_rails_redis_caching do head api(route(file_path), current_user, **options), params: params expect(Rails.cache.fetch("blob_content_sha256:#{project.full_path}:#{response.headers['X-Gitlab-Blob-Id']}")) - .to eq('c440cd09bae50c4632cc58638ad33c6aa375b6109d811e76a9cc3a613c1e8887') + .to eq(content_sha256) expect_next_instance_of(Gitlab::Git::Blob) do |instance| expect(instance).not_to receive(:load_all_data!) @@ -126,8 +142,8 @@ RSpec.describe API::Files do it 'returns file by commit sha' do # This file is deleted on HEAD - file_path = "files%2Fjs%2Fcommit%2Ejs%2Ecoffee" - params[:ref] = "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9" + file_path = 'files%2Fjs%2Fcommit%2Ejs%2Ecoffee' + params[:ref] = '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9' head api(route(file_path), current_user, **options), params: params @@ -137,15 +153,15 @@ RSpec.describe API::Files do end context 'when mandatory params are not given' do - it "responds with a 400 status" do - head api(route("any%2Ffile"), current_user, **options) + it 'responds with a 400 status' do + head api(route('any%2Ffile'), current_user, **options) expect(response).to have_gitlab_http_status(:bad_request) end end context 'when file_path does not exist' do - it "responds with a 404 status" do + it 'responds with a 404 status' do params[:ref] = 'master' head api(route('app%2Fmodels%2Fapplication%2Erb'), current_user, **options), params: params @@ -157,7 +173,7 @@ RSpec.describe API::Files do context 'when file_path does not exist' do include_context 'disabled repository' - it "responds with a 403 status" do + it 'responds with a 403 status' do head api(route(file_path), current_user, **options), params: params expect(response).to have_gitlab_http_status(:forbidden) @@ -165,20 +181,22 @@ RSpec.describe API::Files do end end - context 'when unauthenticated', 'and project is public' do - it_behaves_like 'repository files' do - let(:project) { create(:project, :public, :repository) } - let(:current_user) { nil } + context 'when unauthenticated' do + context 'and project is public' do + it_behaves_like 'repository files' do + let(:project) { create(:project, :public, :repository) } + let(:current_user) { nil } + end end - end - context 'when unauthenticated', 'and project is private' do - it "responds with a 404 status" do - current_user = nil + context 'and project is private' do + it 'responds with a 404 status' do + current_user = nil - head api(route(file_path), current_user), params: params + head api(route(file_path), current_user), params: params - expect(response).to have_gitlab_http_status(:not_found) + expect(response).to have_gitlab_http_status(:not_found) + end end end @@ -190,25 +208,41 @@ RSpec.describe API::Files do end end - context 'when authenticated', 'as a developer' do - it_behaves_like 'repository files' do - let(:current_user) { user } + context 'when authenticated' do + context 'and user is a developer' do + it_behaves_like 'repository files' do + let(:current_user) { user } + end end - end - context 'when authenticated', 'as a guest' do - it_behaves_like '403 response' do - let(:request) { head api(route(file_path), guest), params: params } + context 'and user is a guest' do + it_behaves_like '403 response' do + let(:request) { head api(route(file_path), guest), params: params } + end end end end - describe "GET /projects/:id/repository/files/:file_path" do - shared_examples_for 'repository files' do - let(:options) { {} } + describe 'GET /projects/:id/repository/files/:file_path' do + let(:options) { {} } + + shared_examples 'returns non-executable file attributes as json' do + specify do + get api(route(file_path), api_user, **options), params: params + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['file_path']).to eq(CGI.unescape(file_path)) + expect(json_response['file_name']).to eq(file_name) + expect(json_response['last_commit_id']).to eq(last_commit_id) + expect(json_response['content_sha256']).to eq(content_sha256) + expect(json_response['execute_filemode']).to eq(false) + expect(Base64.decode64(json_response['content']).lines.first).to eq("require 'fileutils'\n") + end + end + shared_examples_for 'repository files' do it 'returns 400 for invalid file path' do - get api(route(rouge_file_path), api_user, **options), params: params + get api(route(invalid_file_path), api_user, **options), params: params expect(response).to have_gitlab_http_status(:bad_request) expect(json_response['error']).to eq(invalid_file_message) @@ -218,17 +252,7 @@ RSpec.describe API::Files do subject { get api(route(absolute_path), api_user, **options), params: params } end - it 'returns file attributes as json' do - get api(route(file_path), api_user, **options), params: params - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response['file_path']).to eq(CGI.unescape(file_path)) - expect(json_response['file_name']).to eq('popen.rb') - expect(json_response['last_commit_id']).to eq('570e7b2abdd848b95f2f578043fc23bd6f6fd24d') - expect(json_response['content_sha256']).to eq('c440cd09bae50c4632cc58638ad33c6aa375b6109d811e76a9cc3a613c1e8887') - expect(json_response['execute_filemode']).to eq(false) - expect(Base64.decode64(json_response['content']).lines.first).to eq("require 'fileutils'\n") - end + it_behaves_like 'returns non-executable file attributes as json' context 'for executable file' do it 'returns file attributes as json' do @@ -247,7 +271,7 @@ RSpec.describe API::Files do end it 'returns json when file has txt extension' do - file_path = "bar%2Fbranch-test.txt" + file_path = 'bar%2Fbranch-test.txt' get api(route(file_path), api_user, **options), params: params @@ -277,8 +301,8 @@ RSpec.describe API::Files do it 'returns file by commit sha' do # This file is deleted on HEAD - file_path = "files%2Fjs%2Fcommit%2Ejs%2Ecoffee" - params[:ref] = "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9" + file_path = 'files%2Fjs%2Fcommit%2Ejs%2Ecoffee' + params[:ref] = '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9' get api(route(file_path), api_user, **options), params: params @@ -289,9 +313,9 @@ RSpec.describe API::Files do end it 'returns raw file info' do - url = route(file_path) + "/raw" + url = route(file_path) + '/raw' expect_to_send_git_blob(api(url, api_user, **options), params) - expect(headers[Gitlab::Workhorse::DETECT_HEADER]).to eq "true" + expect(headers[Gitlab::Workhorse::DETECT_HEADER]).to eq 'true' end it 'returns blame file info' do @@ -303,16 +327,16 @@ RSpec.describe API::Files do end it 'sets inline content disposition by default' do - url = route(file_path) + "/raw" + url = route(file_path) + '/raw' get api(url, api_user, **options), params: params - expect(headers['Content-Disposition']).to eq(%q(inline; filename="popen.rb"; filename*=UTF-8''popen.rb)) + expect(headers['Content-Disposition']).to eq(%(inline; filename="#{file_name}"; filename*=UTF-8''#{file_name})) end context 'when mandatory params are not given' do it_behaves_like '400 response' do - let(:request) { get api(route("any%2Ffile"), current_user, **options) } + let(:request) { get api(route('any%2Ffile'), current_user, **options) } end end @@ -334,40 +358,96 @@ RSpec.describe API::Files do end end - context 'when unauthenticated', 'and project is public' do - it_behaves_like 'repository files' do - let(:project) { create(:project, :public, :repository) } - let(:current_user) { nil } - let(:api_user) { nil } + context 'when unauthenticated' do + context 'and project is public' do + it_behaves_like 'repository files' do + let(:project) { create(:project, :public, :repository) } + let(:current_user) { nil } + let(:api_user) { nil } + end end - end - context 'when PATs are used' do - it_behaves_like 'repository files' do - let(:token) { create(:personal_access_token, scopes: ['read_repository'], user: user) } - let(:current_user) { user } - let(:api_user) { nil } - let(:options) { { personal_access_token: token } } + context 'and project is private' do + it_behaves_like '404 response' do + let(:request) { get api(route(file_path)), params: params } + let(:message) { '404 Project Not Found' } + end end end - context 'when unauthenticated', 'and project is private' do - it_behaves_like '404 response' do - let(:request) { get api(route(file_path)), params: params } - let(:message) { '404 Project Not Found' } - end - end + context 'when authenticated' do + context 'and user is a direct project member' do + context 'and project is private' do + context 'and user is a developer' do + it_behaves_like 'repository files' do + let(:current_user) { user } + let(:api_user) { user } + end + + context 'and PATs are used' do + it_behaves_like 'repository files' do + let(:token) { create(:personal_access_token, scopes: ['read_repository'], user: user) } + let(:current_user) { user } + let(:api_user) { nil } + let(:options) { { personal_access_token: token } } + end + end + end - context 'when authenticated', 'as a developer' do - it_behaves_like 'repository files' do - let(:current_user) { user } - let(:api_user) { user } + context 'and user is a guest' do + it_behaves_like '403 response' do + let(:request) { get api(route(file_path), guest), params: params } + end + end + end end end - context 'when authenticated', 'as a guest' do - it_behaves_like '403 response' do - let(:request) { get api(route(file_path), guest), params: params } + context 'when authenticated' do + context 'and user is an inherited member from the group' do + context 'when project is public with private repository' do + let_it_be(:project) { create(:project, :public, :repository, :repository_private, group: group) } + + context 'and user is a guest' do + it_behaves_like 'returns non-executable file attributes as json' do + let(:api_user) { inherited_guest } + end + end + + context 'and user is a reporter' do + it_behaves_like 'returns non-executable file attributes as json' do + let(:api_user) { inherited_reporter } + end + end + + context 'and user is a developer' do + it_behaves_like 'returns non-executable file attributes as json' do + let(:api_user) { inherited_developer } + end + end + end + + context 'when project is private' do + let_it_be(:project) { create(:project, :private, :repository, group: group) } + + context 'and user is a guest' do + it_behaves_like '403 response' do + let(:request) { get api(route(file_path), inherited_guest), params: params } + end + end + + context 'and user is a reporter' do + it_behaves_like 'returns non-executable file attributes as json' do + let(:api_user) { inherited_reporter } + end + end + + context 'and user is a developer' do + it_behaves_like 'returns non-executable file attributes as json' do + let(:api_user) { inherited_developer } + end + end + end end end end @@ -406,11 +486,10 @@ RSpec.describe API::Files do expect(response).to have_gitlab_http_status(:ok) expect(response.headers['X-Gitlab-File-Path']).to eq(CGI.unescape(file_path)) - expect(response.headers['X-Gitlab-File-Name']).to eq('popen.rb') - expect(response.headers['X-Gitlab-Last-Commit-Id']).to eq('570e7b2abdd848b95f2f578043fc23bd6f6fd24d') - expect(response.headers['X-Gitlab-Content-Sha256']) - .to eq('c440cd09bae50c4632cc58638ad33c6aa375b6109d811e76a9cc3a613c1e8887') - expect(response.headers['X-Gitlab-Execute-Filemode']).to eq("false") + expect(response.headers['X-Gitlab-File-Name']).to eq(file_name) + expect(response.headers['X-Gitlab-Last-Commit-Id']).to eq(last_commit_id) + expect(response.headers['X-Gitlab-Content-Sha256']).to eq(content_sha256) + expect(response.headers['X-Gitlab-Execute-Filemode']).to eq('false') end context 'for executable file' do @@ -424,13 +503,13 @@ RSpec.describe API::Files do expect(response.headers['X-Gitlab-Last-Commit-Id']).to eq('6b8dc4a827797aa025ff6b8f425e583858a10d4f') expect(response.headers['X-Gitlab-Content-Sha256']) .to eq('2c74b1181ef780dfb692c030d3a0df6e0b624135c38a9344e56b9f80007b6191') - expect(response.headers['X-Gitlab-Execute-Filemode']).to eq("true") + expect(response.headers['X-Gitlab-Execute-Filemode']).to eq('true') end end end it 'returns 400 when file path is invalid' do - get api(route(rouge_file_path) + '/blame', current_user), params: params + get api(route(invalid_file_path) + '/blame', current_user), params: params expect(response).to have_gitlab_http_status(:bad_request) expect(json_response['error']).to eq(invalid_file_message) @@ -573,29 +652,33 @@ RSpec.describe API::Files do end end - context 'when unauthenticated', 'and project is public' do - it_behaves_like 'repository blame files' do - let(:project) { create(:project, :public, :repository) } - let(:current_user) { nil } + context 'when unauthenticated' do + context 'and project is public' do + it_behaves_like 'repository blame files' do + let(:project) { create(:project, :public, :repository) } + let(:current_user) { nil } + end end - end - context 'when unauthenticated', 'and project is private' do - it_behaves_like '404 response' do - let(:request) { get api(route(file_path)), params: params } - let(:message) { '404 Project Not Found' } + context 'and project is private' do + it_behaves_like '404 response' do + let(:request) { get api(route(file_path)), params: params } + let(:message) { '404 Project Not Found' } + end end end - context 'when authenticated', 'as a developer' do - it_behaves_like 'repository blame files' do - let(:current_user) { user } + context 'when authenticated' do + context 'and user is a developer' do + it_behaves_like 'repository blame files' do + let(:current_user) { user } + end end - end - context 'when authenticated', 'as a guest' do - it_behaves_like '403 response' do - let(:request) { get api(route(file_path) + '/blame', guest), params: params } + context 'and user is a guest' do + it_behaves_like '403 response' do + let(:request) { get api(route(file_path) + '/blame', guest), params: params } + end end end @@ -614,10 +697,10 @@ RSpec.describe API::Files do end end - describe "GET /projects/:id/repository/files/:file_path/raw" do + describe 'GET /projects/:id/repository/files/:file_path/raw' do shared_examples_for 'repository raw files' do it 'returns 400 when file path is invalid' do - get api(route(rouge_file_path) + "/raw", current_user), params: params + get api(route(invalid_file_path) + '/raw', current_user), params: params expect(response).to have_gitlab_http_status(:bad_request) expect(json_response['error']).to eq(invalid_file_message) @@ -628,7 +711,7 @@ RSpec.describe API::Files do end it 'returns raw file info' do - url = route(file_path) + "/raw" + url = route(file_path) + '/raw' expect_to_send_git_blob(api(url, current_user), params) end @@ -639,39 +722,39 @@ RSpec.describe API::Files do end it 'returns response :ok', :aggregate_failures do - url = route(file_path) + "/raw" + url = route(file_path) + '/raw' expect_to_send_git_blob(api(url, current_user), {}) end end it 'returns raw file info for files with dots' do - url = route('.gitignore') + "/raw" + url = route('.gitignore') + '/raw' expect_to_send_git_blob(api(url, current_user), params) end it 'returns file by commit sha' do # This file is deleted on HEAD - file_path = "files%2Fjs%2Fcommit%2Ejs%2Ecoffee" - params[:ref] = "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9" + file_path = 'files%2Fjs%2Fcommit%2Ejs%2Ecoffee' + params[:ref] = '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9' - expect_to_send_git_blob(api(route(file_path) + "/raw", current_user), params) + expect_to_send_git_blob(api(route(file_path) + '/raw', current_user), params) end it 'sets no-cache headers' do - url = route('.gitignore') + "/raw" + url = route('.gitignore') + '/raw' expect_to_send_git_blob(api(url, current_user), params) - expect(response.headers["Cache-Control"]).to eq("max-age=0, private, must-revalidate, no-store, no-cache") - expect(response.headers["Pragma"]).to eq("no-cache") - expect(response.headers["Expires"]).to eq("Fri, 01 Jan 1990 00:00:00 GMT") + expect(response.headers['Cache-Control']).to eq('max-age=0, private, must-revalidate, no-store, no-cache') + expect(response.headers['Pragma']).to eq('no-cache') + expect(response.headers['Expires']).to eq('Fri, 01 Jan 1990 00:00:00 GMT') end context 'when mandatory params are not given' do it_behaves_like '400 response' do - let(:request) { get api(route("any%2Ffile"), current_user) } + let(:request) { get api(route('any%2Ffile'), current_user) } end end @@ -693,29 +776,33 @@ RSpec.describe API::Files do end end - context 'when unauthenticated', 'and project is public' do - it_behaves_like 'repository raw files' do - let(:project) { create(:project, :public, :repository) } - let(:current_user) { nil } + context 'when unauthenticated' do + context 'and project is public' do + it_behaves_like 'repository raw files' do + let(:project) { create(:project, :public, :repository) } + let(:current_user) { nil } + end end - end - context 'when unauthenticated', 'and project is private' do - it_behaves_like '404 response' do - let(:request) { get api(route(file_path)), params: params } - let(:message) { '404 Project Not Found' } + context 'and project is private' do + it_behaves_like '404 response' do + let(:request) { get api(route(file_path)), params: params } + let(:message) { '404 Project Not Found' } + end end end - context 'when authenticated', 'as a developer' do - it_behaves_like 'repository raw files' do - let(:current_user) { user } + context 'when authenticated' do + context 'and user is a developer' do + it_behaves_like 'repository raw files' do + let(:current_user) { user } + end end - end - context 'when authenticated', 'as a guest' do - it_behaves_like '403 response' do - let(:request) { get api(route(file_path), guest), params: params } + context 'and user is a guest' do + it_behaves_like '403 response' do + let(:request) { get api(route(file_path), guest), params: params } + end end end @@ -724,139 +811,205 @@ RSpec.describe API::Files do token = create(:personal_access_token, scopes: ['read_repository'], user: user) # This file is deleted on HEAD - file_path = "files%2Fjs%2Fcommit%2Ejs%2Ecoffee" - params[:ref] = "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9" - url = api(route(file_path) + "/raw", personal_access_token: token) + file_path = 'files%2Fjs%2Fcommit%2Ejs%2Ecoffee' + params[:ref] = '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9' + url = api(route(file_path) + '/raw', personal_access_token: token) expect_to_send_git_blob(url, params) end end end - describe "POST /projects/:id/repository/files/:file_path" do - let!(:file_path) { "new_subfolder%2Fnewfile%2Erb" } + describe 'POST /projects/:id/repository/files/:file_path' do + let!(:file_path) { 'new_subfolder%2Fnewfile%2Erb' } + let(:params) do { - branch: "master", - content: "puts 8", - commit_message: "Added newfile" + branch: 'master', + content: 'puts 8', + commit_message: 'Added newfile' } end let(:executable_params) do { - branch: "master", - content: "puts 8", - commit_message: "Added newfile", + branch: 'master', + content: 'puts 8', + commit_message: 'Added newfile', execute_filemode: true } end - it 'returns 400 when file path is invalid' do - post api(route(rouge_file_path), user), params: params + shared_examples 'creates a new file in the project repo' do + specify do + post api(route(file_path), current_user), params: params - expect(response).to have_gitlab_http_status(:bad_request) - expect(json_response['error']).to eq(invalid_file_message) + expect(response).to have_gitlab_http_status(:created) + expect(json_response['file_path']).to eq(CGI.unescape(file_path)) + last_commit = project.repository.commit.raw + expect(last_commit.author_email).to eq(current_user.email) + expect(last_commit.author_name).to eq(current_user.name) + expect(project.repository.blob_at_branch(params[:branch], CGI.unescape(file_path)).executable?).to eq(false) + end end - it_behaves_like 'when path is absolute' do - subject { post api(route(absolute_path), user), params: params } - end + context 'when authenticated', 'as a direct project member' do + context 'when project is private' do + context 'and user is a developer' do + it 'returns 400 when file path is invalid' do + post api(route(invalid_file_path), user), params: params - it "creates a new file in project repo" do - post api(route(file_path), user), params: params + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['error']).to eq(invalid_file_message) + end - expect(response).to have_gitlab_http_status(:created) - expect(json_response["file_path"]).to eq(CGI.unescape(file_path)) - last_commit = project.repository.commit.raw - expect(last_commit.author_email).to eq(user.email) - expect(last_commit.author_name).to eq(user.name) - expect(project.repository.blob_at_branch(params[:branch], CGI.unescape(file_path)).executable?).to eq(false) - end + it_behaves_like 'when path is absolute' do + subject { post api(route(absolute_path), user), params: params } + end - it "creates a new executable file in project repo" do - post api(route(file_path), user), params: executable_params + it_behaves_like 'creates a new file in the project repo' do + let(:current_user) { user } + end - expect(response).to have_gitlab_http_status(:created) - expect(json_response["file_path"]).to eq(CGI.unescape(file_path)) - last_commit = project.repository.commit.raw - expect(last_commit.author_email).to eq(user.email) - expect(last_commit.author_name).to eq(user.name) - expect(project.repository.blob_at_branch(params[:branch], CGI.unescape(file_path)).executable?).to eq(true) - end + it 'creates a new executable file in project repo' do + post api(route(file_path), user), params: executable_params - it "returns a 400 bad request if no mandatory params given" do - post api(route("any%2Etxt"), user) + expect(response).to have_gitlab_http_status(:created) + expect(json_response['file_path']).to eq(CGI.unescape(file_path)) + last_commit = project.repository.commit.raw + expect(last_commit.author_email).to eq(user.email) + expect(last_commit.author_name).to eq(user.name) + expect(project.repository.blob_at_branch(params[:branch], CGI.unescape(file_path)).executable?).to eq(true) + end - expect(response).to have_gitlab_http_status(:bad_request) - end + context 'when no mandatory params given' do + it 'returns a 400 bad request' do + post api(route('any%2Etxt'), user) - it 'returns a 400 bad request if the commit message is empty' do - params[:commit_message] = '' + expect(response).to have_gitlab_http_status(:bad_request) + end + end - post api(route(file_path), user), params: params + context 'when the commit message is empty' do + before do + params[:commit_message] = '' + end - expect(response).to have_gitlab_http_status(:bad_request) - end + it 'returns a 400 bad request' do + post api(route(file_path), user), params: params - it "returns a 400 if editor fails to create file" do - allow_next_instance_of(Repository) do |instance| - allow(instance).to receive(:create_file).and_raise(Gitlab::Git::CommitError, 'Cannot create file') - end + expect(response).to have_gitlab_http_status(:bad_request) + end + end - post api(route("any%2Etxt"), user), params: params + context 'when editor fails to create file' do + before do + allow_next_instance_of(Repository) do |instance| + allow(instance).to receive(:create_file).and_raise(Gitlab::Git::CommitError, 'Cannot create file') + end + end - expect(response).to have_gitlab_http_status(:bad_request) - end + it 'returns a 400 bad request' do + post api(route('any%2Etxt'), user), params: params - context 'with PATs' do - it 'returns 403 with `read_repository` scope' do - token = create(:personal_access_token, scopes: ['read_repository'], user: user) + expect(response).to have_gitlab_http_status(:bad_request) + end + end - post api(route(file_path), personal_access_token: token), params: params + context 'and PATs are used' do + it 'returns 403 with `read_repository` scope' do + token = create(:personal_access_token, scopes: ['read_repository'], user: user) - expect(response).to have_gitlab_http_status(:forbidden) - end + post api(route(file_path), personal_access_token: token), params: params - it 'returns 201 with `api` scope' do - token = create(:personal_access_token, scopes: ['api'], user: user) + expect(response).to have_gitlab_http_status(:forbidden) + end - post api(route(file_path), personal_access_token: token), params: params + it 'returns 201 with `api` scope' do + token = create(:personal_access_token, scopes: ['api'], user: user) - expect(response).to have_gitlab_http_status(:created) - end - end + post api(route(file_path), personal_access_token: token), params: params - context "when specifying an author" do - it "creates a new file with the specified author" do - params.merge!(author_email: author_email, author_name: author_name) + expect(response).to have_gitlab_http_status(:created) + end + end - post api(route("new_file_with_author%2Etxt"), user), params: params + context 'and the repo is empty' do + let!(:project) { create(:project_empty_repo, namespace: user.namespace ) } - expect(response).to have_gitlab_http_status(:created) - expect(response.media_type).to eq('application/json') - last_commit = project.repository.commit.raw - expect(last_commit.author_email).to eq(author_email) - expect(last_commit.author_name).to eq(author_name) + it_behaves_like 'creates a new file in the project repo' do + let(:current_user) { user } + let(:file_path) { 'newfile%2Erb' } + end + end + + context 'when specifying an author' do + it 'creates a new file with the specified author' do + params.merge!(author_email: author_email, author_name: author_name) + + post api(route('new_file_with_author%2Etxt'), user), params: params + + expect(response).to have_gitlab_http_status(:created) + expect(response.media_type).to eq('application/json') + last_commit = project.repository.commit.raw + expect(last_commit.author_email).to eq(author_email) + expect(last_commit.author_name).to eq(author_name) + end + end + end end end - context 'when the repo is empty' do - let!(:project) { create(:project_empty_repo, namespace: user.namespace ) } + context 'when authenticated' do + context 'and user is an inherited member from the group' do + context 'when project is public with private repository' do + let_it_be(:project) { create(:project, :public, :repository, :repository_private, group: group) } - it "creates a new file in project repo" do - post api(route("newfile%2Erb"), user), params: params + context 'and user is a guest' do + it_behaves_like '403 response' do + let(:request) { post api(route(file_path), inherited_guest), params: params } + end + end - expect(response).to have_gitlab_http_status(:created) - expect(json_response['file_path']).to eq('newfile.rb') - last_commit = project.repository.commit.raw - expect(last_commit.author_email).to eq(user.email) - expect(last_commit.author_name).to eq(user.name) + context 'and user is a reporter' do + it_behaves_like '403 response' do + let(:request) { post api(route(file_path), inherited_reporter), params: params } + end + end + + context 'and user is a developer' do + it_behaves_like 'creates a new file in the project repo' do + let(:current_user) { inherited_developer } + end + end + end + + context 'when project is private' do + let_it_be(:project) { create(:project, :private, :repository, group: group) } + + context 'and user is a guest' do + it_behaves_like '403 response' do + let(:request) { post api(route(file_path), inherited_guest), params: params } + end + end + + context 'and user is a reporter' do + it_behaves_like '403 response' do + let(:request) { post api(route(file_path), inherited_reporter), params: params } + end + end + + context 'and user is a developer' do + it_behaves_like 'creates a new file in the project repo' do + let(:current_user) { inherited_developer } + end + end + end end end end - describe "PUT /projects/:id/repository/files" do + describe 'PUT /projects/:id/repository/files' do let(:params) do { branch: 'master', @@ -865,7 +1018,7 @@ RSpec.describe API::Files do } end - it "updates existing file in project repo" do + it 'updates existing file in project repo' do put api(route(file_path), user), params: params expect(response).to have_gitlab_http_status(:ok) @@ -875,42 +1028,58 @@ RSpec.describe API::Files do expect(last_commit.author_name).to eq(user.name) end - it 'returns a 400 bad request if the commit message is empty' do - params[:commit_message] = '' + context 'when the commit message is empty' do + before do + params[:commit_message] = '' + end - put api(route(file_path), user), params: params + it 'returns a 400 bad request' do + put api(route(file_path), user), params: params - expect(response).to have_gitlab_http_status(:bad_request) + expect(response).to have_gitlab_http_status(:bad_request) + end end - it "returns a 400 bad request if update existing file with stale last commit id" do - params_with_stale_id = params.merge(last_commit_id: 'stale') + context 'when updating an existing file with stale last commit id' do + let(:params_with_stale_id) { params.merge(last_commit_id: 'stale') } - put api(route(file_path), user), params: params_with_stale_id + it 'returns a 400 bad request' do + put api(route(file_path), user), params: params_with_stale_id - expect(response).to have_gitlab_http_status(:bad_request) - expect(json_response['message']).to eq(_('You are attempting to update a file that has changed since you started editing it.')) + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['message']).to eq(_('You are attempting to update a file that has changed since you started editing it.')) + end end - it "updates existing file in project repo with accepts correct last commit id" do - last_commit = Gitlab::Git::Commit - .last_for_path(project.repository, 'master', Addressable::URI.unencode_component(file_path)) - params_with_correct_id = params.merge(last_commit_id: last_commit.id) + context 'with correct last commit id' do + let(:last_commit) do + Gitlab::Git::Commit + .last_for_path(project.repository, 'master', Addressable::URI.unencode_component(file_path)) + end - put api(route(file_path), user), params: params_with_correct_id + let(:params_with_correct_id) { params.merge(last_commit_id: last_commit.id) } - expect(response).to have_gitlab_http_status(:ok) + it 'updates existing file in project repo' do + put api(route(file_path), user), params: params_with_correct_id + + expect(response).to have_gitlab_http_status(:ok) + end end - it "returns 400 when file path is invalid" do - last_commit = Gitlab::Git::Commit - .last_for_path(project.repository, 'master', Addressable::URI.unencode_component(file_path)) - params_with_correct_id = params.merge(last_commit_id: last_commit.id) + context 'when file path is invalid' do + let(:last_commit) do + Gitlab::Git::Commit + .last_for_path(project.repository, 'master', Addressable::URI.unencode_component(file_path)) + end - put api(route(rouge_file_path), user), params: params_with_correct_id + let(:params_with_correct_id) { params.merge(last_commit_id: last_commit.id) } - expect(response).to have_gitlab_http_status(:bad_request) - expect(json_response['error']).to eq(invalid_file_message) + it 'returns a 400 bad request' do + put api(route(invalid_file_path), user), params: params_with_correct_id + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['error']).to eq(invalid_file_message) + end end it_behaves_like 'when path is absolute' do @@ -924,15 +1093,17 @@ RSpec.describe API::Files do subject { put api(route(absolute_path), user), params: params_with_correct_id } end - it "returns a 400 bad request if no params given" do - put api(route(file_path), user) + context 'when no params given' do + it 'returns a 400 bad request' do + put api(route(file_path), user) - expect(response).to have_gitlab_http_status(:bad_request) + expect(response).to have_gitlab_http_status(:bad_request) + end end - context "when specifying an author" do - it "updates a file with the specified author" do - params.merge!(author_email: author_email, author_name: author_name, content: "New content") + context 'when specifying an author' do + it 'updates a file with the specified author' do + params.merge!(author_email: author_email, author_name: author_name, content: 'New content') put api(route(file_path), user), params: params @@ -982,7 +1153,7 @@ RSpec.describe API::Files do end end - describe "DELETE /projects/:id/repository/files" do + describe 'DELETE /projects/:id/repository/files' do let(:params) do { branch: 'master', @@ -991,7 +1162,7 @@ RSpec.describe API::Files do end it 'returns 400 when file path is invalid' do - delete api(route(rouge_file_path), user), params: params + delete api(route(invalid_file_path), user), params: params expect(response).to have_gitlab_http_status(:bad_request) expect(json_response['error']).to eq(invalid_file_message) @@ -1001,38 +1172,48 @@ RSpec.describe API::Files do subject { delete api(route(absolute_path), user), params: params } end - it "deletes existing file in project repo" do + it 'deletes existing file in project repo' do delete api(route(file_path), user), params: params expect(response).to have_gitlab_http_status(:no_content) end - it "returns a 400 bad request if no params given" do - delete api(route(file_path), user) + context 'when no params given' do + it 'returns a 400 bad request' do + delete api(route(file_path), user) - expect(response).to have_gitlab_http_status(:bad_request) + expect(response).to have_gitlab_http_status(:bad_request) + end end - it 'returns a 400 bad request if the commit message is empty' do - params[:commit_message] = '' + context 'when the commit message is empty' do + before do + params[:commit_message] = '' + end - delete api(route(file_path), user), params: params + it 'returns a 400 bad request' do + delete api(route(file_path), user), params: params - expect(response).to have_gitlab_http_status(:bad_request) + expect(response).to have_gitlab_http_status(:bad_request) + end end - it "returns a 400 if fails to delete file" do - allow_next_instance_of(Repository) do |instance| - allow(instance).to receive(:delete_file).and_raise(Gitlab::Git::CommitError, 'Cannot delete file') + context 'when fails to delete file' do + before do + allow_next_instance_of(Repository) do |instance| + allow(instance).to receive(:delete_file).and_raise(Gitlab::Git::CommitError, 'Cannot delete file') + end end - delete api(route(file_path), user), params: params + it 'returns a 400 bad request' do + delete api(route(file_path), user), params: params - expect(response).to have_gitlab_http_status(:bad_request) + expect(response).to have_gitlab_http_status(:bad_request) + end end - context "when specifying an author" do - it "removes a file with the specified author" do + context 'when specifying an author' do + it 'removes a file with the specified author' do params.merge!(author_email: author_email, author_name: author_name) delete api(route(file_path), user), params: params @@ -1042,7 +1223,7 @@ RSpec.describe API::Files do end end - describe "POST /projects/:id/repository/files with binary file" do + describe 'POST /projects/:id/repository/files with binary file' do let(:file_path) { 'test%2Ebin' } let(:put_params) do { @@ -1063,7 +1244,7 @@ RSpec.describe API::Files do post api(route(file_path), user), params: put_params end - it "remains unchanged" do + it 'remains unchanged' do get api(route(file_path), user), params: get_params expect(response).to have_gitlab_http_status(:ok) diff --git a/spec/requests/api/generic_packages_spec.rb b/spec/requests/api/generic_packages_spec.rb index 3a5c6103781..823eafab734 100644 --- a/spec/requests/api/generic_packages_spec.rb +++ b/spec/requests/api/generic_packages_spec.rb @@ -572,6 +572,12 @@ RSpec.describe API::GenericPackages do expect(response).to have_gitlab_http_status(expected_status) end + + if params[:expected_status] == :success + it_behaves_like 'bumping the package last downloaded at field' do + subject { download_file(auth_header) } + end + end end where(:authenticate_with, :expected_status) do @@ -587,6 +593,12 @@ RSpec.describe API::GenericPackages do expect(response).to have_gitlab_http_status(expected_status) end + + if params[:expected_status] == :success + it_behaves_like 'bumping the package last downloaded at field' do + subject { download_file(deploy_token_auth_header) } + end + end end end @@ -608,6 +620,12 @@ RSpec.describe API::GenericPackages do expect(response).to have_gitlab_http_status(expected_status) end + + if params[:expected_status] == :success + it_behaves_like 'bumping the package last downloaded at field' do + subject { download_file(personal_access_token_header) } + end + end end end diff --git a/spec/requests/api/graphql/ci/config_spec.rb b/spec/requests/api/graphql/ci/config_spec.rb index 5f8a895b16e..960fda80dd9 100644 --- a/spec/requests/api/graphql/ci/config_spec.rb +++ b/spec/requests/api/graphql/ci/config_spec.rb @@ -173,7 +173,7 @@ RSpec.describe 'Query.ciConfig' do { "name" => "docker", "size" => 1, - "jobs" => + "jobs" => { "nodes" => [ { @@ -206,7 +206,7 @@ RSpec.describe 'Query.ciConfig' do { "name" => "deploy_job", "size" => 1, - "jobs" => + "jobs" => { "nodes" => [ { @@ -332,7 +332,7 @@ RSpec.describe 'Query.ciConfig' do "only" => { "refs" => %w[branches tags] }, "when" => "on_success", "tags" => [], - "needs" => { "nodes" => [] } } + "needs" => { "nodes" => [] } } ] } } diff --git a/spec/requests/api/graphql/ci/config_variables_spec.rb b/spec/requests/api/graphql/ci/config_variables_spec.rb new file mode 100644 index 00000000000..2b5a5d0dc93 --- /dev/null +++ b/spec/requests/api/graphql/ci/config_variables_spec.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Query.project(fullPath).ciConfigVariables(sha)' do + include GraphqlHelpers + include ReactiveCachingHelpers + + let_it_be(:content) do + File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')) + end + + let_it_be(:project) { create(:project, :custom_repo, :public, files: { '.gitlab-ci.yml' => content }) } + let_it_be(:user) { create(:user) } + + let(:service) { Ci::ListConfigVariablesService.new(project, user) } + let(:sha) { project.repository.commit.sha } + + let(:query) do + %( + query { + project(fullPath: "#{project.full_path}") { + ciConfigVariables(sha: "#{sha}") { + key + value + description + } + } + } + ) + end + + context 'when the user has the correct permissions' do + before do + project.add_maintainer(user) + allow(Ci::ListConfigVariablesService) + .to receive(:new) + .and_return(service) + end + + context 'when the cache is not empty' do + before do + synchronous_reactive_cache(service) + end + + it 'returns the CI variables for the config' do + expect(service) + .to receive(:execute) + .with(sha) + .and_call_original + + post_graphql(query, current_user: user) + + expect(graphql_data.dig('project', 'ciConfigVariables')).to contain_exactly( + { + 'key' => 'DB_NAME', + 'value' => 'postgres', + 'description' => nil + }, + { + 'key' => 'ENVIRONMENT_VAR', + 'value' => 'env var value', + 'description' => 'env var description' + } + ) + end + end + + context 'when the cache is empty' do + it 'returns nothing' do + post_graphql(query, current_user: user) + + expect(graphql_data.dig('project', 'ciConfigVariables')).to be_nil + end + end + end + + context 'when the user is not authorized' do + before do + project.add_guest(user) + allow(Ci::ListConfigVariablesService) + .to receive(:new) + .and_return(service) + synchronous_reactive_cache(service) + end + + it 'returns nothing' do + post_graphql(query, current_user: user) + + expect(graphql_data.dig('project', 'ciConfigVariables')).to be_nil + end + end +end diff --git a/spec/requests/api/graphql/ci/group_variables_spec.rb b/spec/requests/api/graphql/ci/group_variables_spec.rb index 5ea6646ec2c..7baf26c7648 100644 --- a/spec/requests/api/graphql/ci/group_variables_spec.rb +++ b/spec/requests/api/graphql/ci/group_variables_spec.rb @@ -13,6 +13,7 @@ RSpec.describe 'Query.group(fullPath).ciVariables' do query { group(fullPath: "#{group.full_path}") { ciVariables { + limit nodes { id key @@ -35,11 +36,18 @@ RSpec.describe 'Query.group(fullPath).ciVariables' do end it "returns the group's CI variables" do - variable = create(:ci_group_variable, group: group, key: 'TEST_VAR', value: 'test', - masked: false, protected: true, raw: true, environment_scope: 'staging') + variable = create(:ci_group_variable, + group: group, + key: 'TEST_VAR', + value: 'test', + masked: false, + protected: true, + raw: true, + environment_scope: 'staging') post_graphql(query, current_user: user) + expect(graphql_data.dig('group', 'ciVariables', 'limit')).to be(200) expect(graphql_data.dig('group', 'ciVariables', 'nodes')).to contain_exactly({ 'id' => variable.to_global_id.to_s, 'key' => 'TEST_VAR', diff --git a/spec/requests/api/graphql/ci/instance_variables_spec.rb b/spec/requests/api/graphql/ci/instance_variables_spec.rb index c5c88697bf4..cd6b2de98a1 100644 --- a/spec/requests/api/graphql/ci/instance_variables_spec.rb +++ b/spec/requests/api/graphql/ci/instance_variables_spec.rb @@ -29,7 +29,7 @@ RSpec.describe 'Query.ciVariables' do it "returns the instance's CI variables" do variable = create(:ci_instance_variable, key: 'TEST_VAR', value: 'test', - masked: false, protected: true, raw: true) + masked: false, protected: true, raw: true) post_graphql(query, current_user: user) diff --git a/spec/requests/api/graphql/ci/jobs_spec.rb b/spec/requests/api/graphql/ci/jobs_spec.rb index 8c4ab13fc35..fa8fb1d54aa 100644 --- a/spec/requests/api/graphql/ci/jobs_spec.rb +++ b/spec/requests/api/graphql/ci/jobs_spec.rb @@ -335,4 +335,35 @@ RSpec.describe 'Query.project.pipeline' do end end end + + context 'when querying jobs for multiple projects' do + let(:query) do + %( + query { + projects { + nodes { + jobs { + nodes { + name + } + } + } + } + } + ) + end + + before do + create_list(:project, 2).each do |project| + project.add_developer(user) + create(:ci_build, project: project) + end + end + + it 'returns an error' do + post_graphql(query, current_user: user) + + expect_graphql_errors_to_include [/"jobs" field can be requested only for 1 Project\(s\) at a time./] + end + end end diff --git a/spec/requests/api/graphql/ci/project_variables_spec.rb b/spec/requests/api/graphql/ci/project_variables_spec.rb index e61f146b24c..d49a4a7e768 100644 --- a/spec/requests/api/graphql/ci/project_variables_spec.rb +++ b/spec/requests/api/graphql/ci/project_variables_spec.rb @@ -13,6 +13,7 @@ RSpec.describe 'Query.project(fullPath).ciVariables' do query { project(fullPath: "#{project.full_path}") { ciVariables { + limit nodes { id key @@ -36,10 +37,11 @@ RSpec.describe 'Query.project(fullPath).ciVariables' do it "returns the project's CI variables" do variable = create(:ci_variable, project: project, key: 'TEST_VAR', value: 'test', - masked: false, protected: true, raw: true, environment_scope: 'production') + masked: false, protected: true, raw: true, environment_scope: 'production') post_graphql(query, current_user: user) + expect(graphql_data.dig('project', 'ciVariables', 'limit')).to be(200) expect(graphql_data.dig('project', 'ciVariables', 'nodes')).to contain_exactly({ 'id' => variable.to_global_id.to_s, 'key' => 'TEST_VAR', diff --git a/spec/requests/api/graphql/ci/runner_spec.rb b/spec/requests/api/graphql/ci/runner_spec.rb index e17a83d8e47..bd90753f9ad 100644 --- a/spec/requests/api/graphql/ci/runner_spec.rb +++ b/spec/requests/api/graphql/ci/runner_spec.rb @@ -9,24 +9,53 @@ RSpec.describe 'Query.runner(id)' do let_it_be(:group) { create(:group) } let_it_be(:active_instance_runner) do - create(:ci_runner, :instance, description: 'Runner 1', contacted_at: 2.hours.ago, - active: true, version: 'adfe156', revision: 'a', locked: true, ip_address: '127.0.0.1', maximum_timeout: 600, - access_level: 0, tag_list: %w[tag1 tag2], run_untagged: true, executor_type: :custom, - maintenance_note: '**Test maintenance note**') + create(:ci_runner, :instance, + description: 'Runner 1', + contacted_at: 2.hours.ago, + active: true, + version: 'adfe156', + revision: 'a', + locked: true, + ip_address: '127.0.0.1', + maximum_timeout: 600, + access_level: 0, + tag_list: %w[tag1 tag2], + run_untagged: true, + executor_type: :custom, + maintenance_note: '**Test maintenance note**') end let_it_be(:inactive_instance_runner) do - create(:ci_runner, :instance, description: 'Runner 2', contacted_at: 1.day.ago, active: false, - version: 'adfe157', revision: 'b', ip_address: '10.10.10.10', access_level: 1, run_untagged: true) + create(:ci_runner, :instance, + description: 'Runner 2', + contacted_at: 1.day.ago, + active: false, + version: 'adfe157', + revision: 'b', + ip_address: '10.10.10.10', + access_level: 1, + run_untagged: true) end let_it_be(:active_group_runner) do - create(:ci_runner, :group, groups: [group], description: 'Group runner 1', contacted_at: 2.hours.ago, - active: true, version: 'adfe156', revision: 'a', locked: true, ip_address: '127.0.0.1', maximum_timeout: 600, - access_level: 0, tag_list: %w[tag1 tag2], run_untagged: true, executor_type: :shell) + create(:ci_runner, :group, + groups: [group], + description: 'Group runner 1', + contacted_at: 2.hours.ago, + active: true, + version: 'adfe156', + revision: 'a', + locked: true, + ip_address: '127.0.0.1', + maximum_timeout: 600, + access_level: 0, + tag_list: %w[tag1 tag2], + run_untagged: true, + executor_type: :shell) end - let_it_be(:active_project_runner) { create(:ci_runner, :project) } + let_it_be(:project1) { create(:project) } + let_it_be(:active_project_runner) { create(:ci_runner, :project, projects: [project1]) } shared_examples 'runner details fetch' do let(:query) do @@ -159,8 +188,16 @@ RSpec.describe 'Query.runner(id)' do with_them do let(:project_runner) do - create(:ci_runner, :project, description: 'Runner 3', contacted_at: 1.day.ago, active: false, locked: is_locked, - version: 'adfe157', revision: 'b', ip_address: '10.10.10.10', access_level: 1, run_untagged: true) + create(:ci_runner, :project, + description: 'Runner 3', + contacted_at: 1.day.ago, + active: false, + locked: is_locked, + version: 'adfe157', + revision: 'b', + ip_address: '10.10.10.10', + access_level: 1, + run_untagged: true) end let(:query) do @@ -187,7 +224,6 @@ RSpec.describe 'Query.runner(id)' do end describe 'ownerProject' do - let_it_be(:project1) { create(:project) } let_it_be(:project2) { create(:project) } let_it_be(:runner1) { create(:ci_runner, :project, projects: [project2, project1]) } let_it_be(:runner2) { create(:ci_runner, :project, projects: [project1, project2]) } @@ -301,7 +337,6 @@ RSpec.describe 'Query.runner(id)' do end describe 'for multiple runners' do - let_it_be(:project1) { create(:project, :test_repo) } let_it_be(:project2) { create(:project, :test_repo) } let_it_be(:project_runner1) { create(:ci_runner, :project, projects: [project1, project2], description: 'Runner 1') } let_it_be(:project_runner2) { create(:ci_runner, :project, projects: [], description: 'Runner 2') } @@ -394,6 +429,8 @@ RSpec.describe 'Query.runner(id)' do 'jobs' => nil, # returning jobs not allowed for more than 1 runner (see RunnerJobsResolver) 'projectCount' => nil, 'projects' => nil) + + expect_graphql_errors_to_include [/"jobs" field can be requested only for 1 CiRunner\(s\) at a time./] end end end @@ -472,8 +509,8 @@ RSpec.describe 'Query.runner(id)' do <<~QUERY { instance_runner1: #{runner_query(active_instance_runner)} - project_runner1: #{runner_query(active_project_runner)} group_runner1: #{runner_query(active_group_runner)} + project_runner1: #{runner_query(active_project_runner)} } QUERY end @@ -493,12 +530,13 @@ RSpec.describe 'Query.runner(id)' do it 'does not execute more queries per runner', :aggregate_failures do # warm-up license cache and so on: - post_graphql(double_query, current_user: user) + personal_access_token = create(:personal_access_token, user: user) + args = { current_user: user, token: { personal_access_token: personal_access_token } } + post_graphql(double_query, **args) - control = ActiveRecord::QueryRecorder.new { post_graphql(single_query, current_user: user) } + control = ActiveRecord::QueryRecorder.new { post_graphql(single_query, **args) } - expect { post_graphql(double_query, current_user: user) } - .not_to exceed_query_limit(control) + expect { post_graphql(double_query, **args) }.not_to exceed_query_limit(control) expect(graphql_data.count).to eq 6 expect(graphql_data).to match( @@ -528,4 +566,91 @@ RSpec.describe 'Query.runner(id)' do )) end end + + describe 'sorting and pagination' do + let(:query) do + <<~GQL + query($id: CiRunnerID!, $projectSearchTerm: String, $n: Int, $cursor: String) { + runner(id: $id) { + #{fields} + } + } + GQL + end + + before do + post_graphql(query, current_user: user, variables: variables) + end + + context 'with project search term' do + let_it_be(:project1) { create(:project, description: 'abc') } + let_it_be(:project2) { create(:project, description: 'def') } + let_it_be(:project_runner) do + create(:ci_runner, :project, projects: [project1, project2]) + end + + let(:variables) { { id: project_runner.to_global_id.to_s, n: n, project_search_term: search_term } } + + let(:fields) do + <<~QUERY + projects(search: $projectSearchTerm, first: $n, after: $cursor) { + count + nodes { + id + } + pageInfo { + hasPreviousPage + startCursor + endCursor + hasNextPage + } + } + QUERY + end + + let(:projects_data) { graphql_data_at('runner', 'projects') } + + context 'set to empty string' do + let(:search_term) { '' } + + context 'with n = 1' do + let(:n) { 1 } + + it_behaves_like 'a working graphql query' + + it 'returns paged result' do + expect(projects_data).not_to be_nil + expect(projects_data['count']).to eq 2 + expect(projects_data['pageInfo']['hasNextPage']).to eq true + end + end + + context 'with n = 2' do + let(:n) { 2 } + + it 'returns non-paged result' do + expect(projects_data).not_to be_nil + expect(projects_data['count']).to eq 2 + expect(projects_data['pageInfo']['hasNextPage']).to eq false + end + end + end + + context 'set to partial match' do + let(:search_term) { 'def' } + + context 'with n = 1' do + let(:n) { 1 } + + it_behaves_like 'a working graphql query' + + it 'returns paged result with no additional pages' do + expect(projects_data).not_to be_nil + expect(projects_data['count']).to eq 1 + expect(projects_data['pageInfo']['hasNextPage']).to eq false + end + end + end + end + end end diff --git a/spec/requests/api/graphql/ci/runners_spec.rb b/spec/requests/api/graphql/ci/runners_spec.rb index 749f6839cb5..3054b866812 100644 --- a/spec/requests/api/graphql/ci/runners_spec.rb +++ b/spec/requests/api/graphql/ci/runners_spec.rb @@ -69,15 +69,6 @@ RSpec.describe 'Query.runners' do it_behaves_like 'a working graphql query returning expected runner' end - - context 'runner_type is PROJECT_TYPE and status is NEVER_CONTACTED' do - let(:runner_type) { 'PROJECT_TYPE' } - let(:status) { 'NEVER_CONTACTED' } - - let!(:expected_runner) { project_runner } - - it_behaves_like 'a working graphql query returning expected runner' - end end describe 'pagination' do @@ -141,8 +132,13 @@ RSpec.describe 'Group.runners' do describe 'edges' do let_it_be(:runner) do - create(:ci_runner, :group, active: false, version: 'def', revision: '456', - description: 'Project runner', groups: [group], ip_address: '127.0.0.1') + create(:ci_runner, :group, + active: false, + version: 'def', + revision: '456', + description: 'Project runner', + groups: [group], + ip_address: '127.0.0.1') end let(:query) do diff --git a/spec/requests/api/graphql/custom_emoji_query_spec.rb b/spec/requests/api/graphql/custom_emoji_query_spec.rb index 13b7a22e791..5dd5ad117b0 100644 --- a/spec/requests/api/graphql/custom_emoji_query_spec.rb +++ b/spec/requests/api/graphql/custom_emoji_query_spec.rb @@ -35,7 +35,17 @@ RSpec.describe 'getting custom emoji within namespace' do expect(graphql_data['group']['customEmoji']['nodes'].first['name']).to eq(custom_emoji.name) end - it 'returns nil when unauthorised' do + it 'returns nil custom emoji when the custom_emoji feature flag is disabled' do + stub_feature_flags(custom_emoji: false) + + post_graphql(custom_emoji_query(group), current_user: current_user) + + expect(response).to have_gitlab_http_status(:ok) + expect(graphql_data['group']).to be_present + expect(graphql_data['group']['customEmoji']).to be_nil + end + + it 'returns nil group when unauthorised' do user = create(:user) post_graphql(custom_emoji_query(group), current_user: user) diff --git a/spec/requests/api/graphql/environments/deployments_query_spec.rb b/spec/requests/api/graphql/environments/deployments_query_spec.rb new file mode 100644 index 00000000000..6da00057449 --- /dev/null +++ b/spec/requests/api/graphql/environments/deployments_query_spec.rb @@ -0,0 +1,487 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Environments Deployments query' do + include GraphqlHelpers + + let_it_be(:project) { create(:project, :private, :repository) } + let_it_be(:environment) { create(:environment, project: project) } + let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } } + let_it_be(:guest) { create(:user).tap { |u| project.add_guest(u) } } + + let(:user) { developer } + + subject { GitlabSchema.execute(query, context: { current_user: user }).as_json } + + context 'when there are deployments in the environment' do + let_it_be(:finished_deployment_old) do + create(:deployment, :success, environment: environment, project: project, finished_at: 2.days.ago) + end + + let_it_be(:finished_deployment_new) do + create(:deployment, :success, environment: environment, project: project, finished_at: 1.day.ago) + end + + let_it_be(:upcoming_deployment_old) do + create(:deployment, :created, environment: environment, project: project, created_at: 2.hours.ago) + end + + let_it_be(:upcoming_deployment_new) do + create(:deployment, :created, environment: environment, project: project, created_at: 1.hour.ago) + end + + let_it_be(:other_environment) { create(:environment, project: project) } + let_it_be(:other_deployment) { create(:deployment, :success, environment: other_environment, project: project) } + + let(:query) do + %( + query { + project(fullPath: "#{project.full_path}") { + environment(name: "#{environment.name}") { + deployments { + nodes { + id + iid + ref + tag + sha + createdAt + updatedAt + finishedAt + status + } + } + } + } + } + ) + end + + it 'returns all deployments of the environment' do + deployments = subject.dig('data', 'project', 'environment', 'deployments', 'nodes') + + expect(deployments.count).to eq(4) + end + + context 'when query last deployment' do + let(:query) do + %( + query { + project(fullPath: "#{project.full_path}") { + environment(name: "#{environment.name}") { + deployments(statuses: [SUCCESS], orderBy: { finishedAt: DESC }, first: 1) { + nodes { + iid + } + } + } + } + } + ) + end + + it 'returns deployment' do + deployments = subject.dig('data', 'project', 'environment', 'deployments', 'nodes') + + expect(deployments.count).to eq(1) + expect(deployments[0]['iid']).to eq(finished_deployment_new.iid.to_s) + end + end + + context 'when query latest upcoming deployment' do + let(:query) do + %( + query { + project(fullPath: "#{project.full_path}") { + environment(name: "#{environment.name}") { + deployments(statuses: [CREATED RUNNING BLOCKED], orderBy: { createdAt: DESC }, first: 1) { + nodes { + iid + } + } + } + } + } + ) + end + + it 'returns deployment' do + deployments = subject.dig('data', 'project', 'environment', 'deployments', 'nodes') + + expect(deployments.count).to eq(1) + expect(deployments[0]['iid']).to eq(upcoming_deployment_new.iid.to_s) + end + end + + context 'when query finished deployments in descending order' do + let(:query) do + %( + query { + project(fullPath: "#{project.full_path}") { + environment(name: "#{environment.name}") { + deployments(statuses: [SUCCESS FAILED CANCELED], orderBy: { finishedAt: DESC }) { + nodes { + iid + } + } + } + } + } + ) + end + + it 'returns deployments' do + deployments = subject.dig('data', 'project', 'environment', 'deployments', 'nodes') + + expect(deployments.count).to eq(2) + expect(deployments[0]['iid']).to eq(finished_deployment_new.iid.to_s) + expect(deployments[1]['iid']).to eq(finished_deployment_old.iid.to_s) + end + end + + context 'when query finished deployments in ascending order' do + let(:query) do + %( + query { + project(fullPath: "#{project.full_path}") { + environment(name: "#{environment.name}") { + deployments(statuses: [SUCCESS FAILED CANCELED], orderBy: { finishedAt: ASC }) { + nodes { + iid + } + } + } + } + } + ) + end + + it 'returns deployments' do + deployments = subject.dig('data', 'project', 'environment', 'deployments', 'nodes') + + expect(deployments.count).to eq(2) + expect(deployments[0]['iid']).to eq(finished_deployment_old.iid.to_s) + expect(deployments[1]['iid']).to eq(finished_deployment_new.iid.to_s) + end + end + + context 'when query upcoming deployments in descending order' do + let(:query) do + %( + query { + project(fullPath: "#{project.full_path}") { + environment(name: "#{environment.name}") { + deployments(statuses: [CREATED RUNNING BLOCKED], orderBy: { createdAt: DESC }) { + nodes { + iid + } + } + } + } + } + ) + end + + it 'returns deployments' do + deployments = subject.dig('data', 'project', 'environment', 'deployments', 'nodes') + + expect(deployments.count).to eq(2) + expect(deployments[0]['iid']).to eq(upcoming_deployment_new.iid.to_s) + expect(deployments[1]['iid']).to eq(upcoming_deployment_old.iid.to_s) + end + end + + context 'when query upcoming deployments in ascending order' do + let(:query) do + %( + query { + project(fullPath: "#{project.full_path}") { + environment(name: "#{environment.name}") { + deployments(statuses: [CREATED RUNNING BLOCKED], orderBy: { createdAt: ASC }) { + nodes { + iid + } + } + } + } + } + ) + end + + it 'returns deployments' do + deployments = subject.dig('data', 'project', 'environment', 'deployments', 'nodes') + + expect(deployments.count).to eq(2) + expect(deployments[0]['iid']).to eq(upcoming_deployment_old.iid.to_s) + expect(deployments[1]['iid']).to eq(upcoming_deployment_new.iid.to_s) + end + end + + context 'when query last deployments of multiple environments' do + let(:query) do + %( + query { + project(fullPath: "#{project.full_path}") { + environments { + nodes { + name + deployments(statuses: [SUCCESS], orderBy: { finishedAt: DESC }, first: 1) { + nodes { + iid + } + } + } + } + } + } + ) + end + + it 'returns an error for preventing N+1 queries' do + expect(subject['errors'][0]['message']) + .to include('"deployments" field can be requested only for 1 Environment(s) at a time.') + end + end + + context 'when query finished and upcoming deployments together' do + let(:query) do + %( + query { + project(fullPath: "#{project.full_path}") { + environment(name: "#{environment.name}") { + deployments(statuses: [CREATED SUCCESS]) { + nodes { + iid + } + } + } + } + } + ) + end + + it 'raises an error' do + expect { subject }.to raise_error(DeploymentsFinder::InefficientQueryError) + end + end + + context 'when multiple orderBy input are specified' do + let(:query) do + %( + query { + project(fullPath: "#{project.full_path}") { + environment(name: "#{environment.name}") { + deployments(orderBy: { finishedAt: DESC, createdAt: ASC }) { + nodes { + iid + } + } + } + } + } + ) + end + + it 'raises an error' do + expect(subject['errors'][0]['message']).to include('orderBy parameter must contain one key-value pair.') + end + end + + context 'when user is guest' do + let(:user) { guest } + + it 'returns nothing' do + expect(subject['data']['project']['environment']).to be_nil + end + end + + shared_examples_for 'avoids N+1 database queries' do + it 'does not increase the query count' do + create_deployments + + baseline = ActiveRecord::QueryRecorder.new do + run_with_clean_state(query, context: { current_user: user }) + end + + create_deployments + + multi = ActiveRecord::QueryRecorder.new do + run_with_clean_state(query, context: { current_user: user }) + end + + expect(multi).not_to exceed_query_limit(baseline) + end + + def create_deployments + create_list(:deployment, 3, environment: environment, project: project).each do |deployment| + deployment.user = create(:user).tap { |u| project.add_developer(u) } + deployment.deployable = + create(:ci_build, project: project, environment: environment.name, deployment: deployment, + user: deployment.user) + + deployment.save! + end + end + end + + context 'when requesting commits of deployments' do + let(:query) do + %( + query { + project(fullPath: "#{project.full_path}") { + environment(name: "#{environment.name}") { + deployments { + nodes { + iid + commit { + author { + avatarUrl + name + webPath + } + fullTitle + webPath + sha + } + } + } + } + } + } + ) + end + + it_behaves_like 'avoids N+1 database queries' + + it 'returns commits of deployments' do + deployments = subject.dig('data', 'project', 'environment', 'deployments', 'nodes') + + deployments.each do |deployment| + deployment_in_record = project.deployments.find_by_iid(deployment['iid']) + + expect(deployment_in_record.sha).to eq(deployment['commit']['sha']) + end + end + end + + context 'when requesting triggerers of deployments' do + let(:query) do + %( + query { + project(fullPath: "#{project.full_path}") { + environment(name: "#{environment.name}") { + deployments { + nodes { + iid + triggerer { + id + avatarUrl + name + webPath + } + } + } + } + } + } + ) + end + + it_behaves_like 'avoids N+1 database queries' + + it 'returns triggerers of deployments' do + deployments = subject.dig('data', 'project', 'environment', 'deployments', 'nodes') + + deployments.each do |deployment| + deployment_in_record = project.deployments.find_by_iid(deployment['iid']) + + expect(deployment_in_record.deployed_by.name).to eq(deployment['triggerer']['name']) + end + end + end + + context 'when requesting jobs of deployments' do + let(:query) do + %( + query { + project(fullPath: "#{project.full_path}") { + environment(name: "#{environment.name}") { + deployments { + nodes { + iid + job { + id + status + name + webPath + } + } + } + } + } + } + ) + end + + it_behaves_like 'avoids N+1 database queries' + + it 'returns jobs of deployments' do + deployments = subject.dig('data', 'project', 'environment', 'deployments', 'nodes') + + deployments.each do |deployment| + deployment_in_record = project.deployments.find_by_iid(deployment['iid']) + + expect(deployment_in_record.build.to_global_id.to_s).to eq(deployment['job']['id']) + end + end + end + + describe 'sorting and pagination' do + let(:data_path) { [:project, :environment, :deployments] } + let(:current_user) { user } + + def pagination_query(params) + %( + query { + project(fullPath: "#{project.full_path}") { + environment(name: "#{environment.name}") { + deployments(statuses: [SUCCESS], #{params}) { + nodes { + iid + } + pageInfo { + startCursor + endCursor + hasNextPage + hasPreviousPage + } + } + } + } + } + ) + end + + def pagination_results_data(nodes) + nodes.map { |deployment| deployment['iid'].to_i } + end + + context 'when sorting by finished_at in ascending order' do + it_behaves_like 'sorted paginated query' do + let(:sort_argument) { graphql_args(orderBy: { finishedAt: :ASC }) } + let(:first_param) { 2 } + let(:all_records) { [finished_deployment_old.iid, finished_deployment_new.iid] } + end + end + + context 'when sorting by finished_at in descending order' do + it_behaves_like 'sorted paginated query' do + let(:sort_argument) { graphql_args(orderBy: { finishedAt: :DESC }) } + let(:first_param) { 2 } + let(:all_records) { [finished_deployment_new.iid, finished_deployment_old.iid] } + 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 bab8d5b770c..5f8becc0726 100644 --- a/spec/requests/api/graphql/group/group_members_spec.rb +++ b/spec/requests/api/graphql/group/group_members_spec.rb @@ -156,13 +156,20 @@ RSpec.describe 'getting group members information' do expect_array_response(child_user) end - it 'returns invited members plus inherited members' do + it 'returns invited members and inherited members of a shared group' do fetch_members(group: child_group, args: { relations: [:DIRECT, :INHERITED, :SHARED_FROM_GROUPS] }) expect(graphql_errors).to be_nil expect_array_response(invited_user, user_1, user_2, child_user) end + it 'returns invited members and inherited members of an ancestor of a shared group' do + fetch_members(group: grandchild_group, args: { relations: [:DIRECT, :INHERITED, :SHARED_FROM_GROUPS] }) + + expect(graphql_errors).to be_nil + expect_array_response(grandchild_user, invited_user, user_1, user_2, child_user) + end + it 'returns direct and inherited members' do fetch_members(group: child_group, args: { relations: [:DIRECT, :INHERITED] }) diff --git a/spec/requests/api/graphql/group/packages_spec.rb b/spec/requests/api/graphql/group/packages_spec.rb index adee556db3a..cf8736db5af 100644 --- a/spec/requests/api/graphql/group/packages_spec.rb +++ b/spec/requests/api/graphql/group/packages_spec.rb @@ -39,7 +39,7 @@ RSpec.describe 'getting a package list for a group' do it 'returns an error for the second group and data for the first' do expect(a_packages_names).to contain_exactly(group_one_package.name) - expect_graphql_errors_to_include [/Packages can be requested only for one group at a time/] + expect_graphql_errors_to_include [/"packages" field can be requested only for 1 Group\(s\) at a time./] expect(graphql_data_at(:b, :packages)).to be(nil) end end diff --git a/spec/requests/api/graphql/group/work_item_types_spec.rb b/spec/requests/api/graphql/group/work_item_types_spec.rb index a33e3ae5427..d6b0673e4f8 100644 --- a/spec/requests/api/graphql/group/work_item_types_spec.rb +++ b/spec/requests/api/graphql/group/work_item_types_spec.rb @@ -46,7 +46,7 @@ RSpec.describe 'getting a list of work item types for a group' do end end - context "when user doesn't have acces to the group" do + context "when user doesn't have access to the group" do let(:current_user) { create(:user) } before do diff --git a/spec/requests/api/graphql/mutations/boards/issues/issue_move_list_spec.rb b/spec/requests/api/graphql/mutations/boards/issues/issue_move_list_spec.rb index 46ec22e7ef8..06093e9f7c2 100644 --- a/spec/requests/api/graphql/mutations/boards/issues/issue_move_list_spec.rb +++ b/spec/requests/api/graphql/mutations/boards/issues/issue_move_list_spec.rb @@ -100,6 +100,20 @@ RSpec.describe 'Reposition and move issue within board lists' do expect(response_issue['labels']['edges'][0]['node']['title']).to eq(testing.title) end end + + context 'when moving an issue using position_in_list' do + let(:issue_move_params) { { from_list_id: list1.id, to_list_id: list2.id, position_in_list: 0 } } + + it 'repositions an issue' do + post_graphql_mutation(mutation(params), current_user: current_user) + + expect(response).to have_gitlab_http_status(:success) + response_issue = json_response['data'][mutation_result_identifier]['issue'] + expect(response_issue['iid']).to eq(issue1.iid.to_s) + expect(response_issue['labels']['edges'][0]['node']['title']).to eq(testing.title) + expect(response_issue['relativePosition']).to be < existing_issue1.relative_position + end + end end context 'when user has no access to resources' do diff --git a/spec/requests/api/graphql/mutations/branches/create_spec.rb b/spec/requests/api/graphql/mutations/branches/create_spec.rb index 6a098002963..9ee2f41e8fc 100644 --- a/spec/requests/api/graphql/mutations/branches/create_spec.rb +++ b/spec/requests/api/graphql/mutations/branches/create_spec.rb @@ -5,26 +5,18 @@ require 'spec_helper' RSpec.describe 'Creation of a new branch' do include GraphqlHelpers + let_it_be(:group) { create(:group, :public) } let_it_be(:current_user) { create(:user) } - let_it_be(:project) { create(:project, :public, :empty_repo) } let(:input) { { project_path: project.full_path, name: new_branch, ref: ref } } - let(:new_branch) { 'new_branch' } + let(:new_branch) { "new_branch_#{SecureRandom.hex(4)}" } let(:ref) { 'master' } let(:mutation) { graphql_mutation(:create_branch, input) } let(:mutation_response) { graphql_mutation_response(:create_branch) } - context 'the user is not allowed to create a branch' do - it_behaves_like 'a mutation that returns a top-level access error' - end - - context 'when user has permissions to create a branch' do - before do - project.add_developer(current_user) - end - - it 'creates a new branch' do + shared_examples 'creates a new branch' do + specify do post_graphql_mutation(mutation, current_user: current_user) expect(response).to have_gitlab_http_status(:success) @@ -33,14 +25,75 @@ RSpec.describe 'Creation of a new branch' do 'commit' => a_hash_including('id') ) end + end + + context 'when project is public' do + let_it_be(:project) { create(:project, :public, :empty_repo) } + + context 'when user is not allowed to create a branch' do + it_behaves_like 'a mutation that returns a top-level access error' + end + + context 'when user is a direct project member' do + context 'and user is a developer' do + before do + project.add_developer(current_user) + end + + it_behaves_like 'creates a new branch' + + context 'when ref is not correct' do + err_msg = 'Failed to create branch \'another_branch\': invalid reference name \'unknown\'' + let(:new_branch) { 'another_branch' } + let(:ref) { 'unknown' } + + it_behaves_like 'a mutation that returns errors in the response', errors: [err_msg] + end + end + end + + context 'when user is an inherited member from the group' do + context 'when project has a private repository' do + let_it_be(:project) { create(:project, :public, :empty_repo, :repository_private, group: group) } + + context 'and user is a guest' do + before do + group.add_guest(current_user) + end + + it_behaves_like 'a mutation that returns a top-level access error' + end + + context 'and user is a developer' do + before do + group.add_developer(current_user) + end + + it_behaves_like 'creates a new branch' + end + end + end + end + + context 'when project is private' do + let_it_be(:project) { create(:project, :private, :empty_repo, group: group) } + + context 'when user is an inherited member from the group' do + context 'and user is a guest' do + before do + group.add_guest(current_user) + end + + it_behaves_like 'a mutation that returns a top-level access error' + end - context 'when ref is not correct' do - err_msg = 'Failed to create branch \'another_branch\': invalid reference name \'unknown\'' - let(:new_branch) { 'another_branch' } - let(:ref) { 'unknown' } + context 'and user is a developer' do + before do + group.add_developer(current_user) + end - it_behaves_like 'a mutation that returns errors in the response', - errors: [err_msg] + it_behaves_like 'creates a new branch' + end end end end diff --git a/spec/requests/api/graphql/mutations/ci/job/destroy_spec.rb b/spec/requests/api/graphql/mutations/ci/job/destroy_spec.rb new file mode 100644 index 00000000000..5855eb6bb51 --- /dev/null +++ b/spec/requests/api/graphql/mutations/ci/job/destroy_spec.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'JobArtifactsDestroy' do + include GraphqlHelpers + + let_it_be(:user) { create(:user) } + let_it_be(:job) { create(:ci_build) } + + let(:mutation) do + variables = { + id: job.to_global_id.to_s + } + graphql_mutation(:job_artifacts_destroy, variables, <<~FIELDS) + job { + name + } + destroyedArtifactsCount + errors + FIELDS + end + + before do + create(:ci_job_artifact, :archive, job: job) + create(:ci_job_artifact, :junit, job: job) + end + + it 'returns an error if the user is not allowed to destroy the job artifacts' do + post_graphql_mutation(mutation, current_user: user) + + expect(graphql_errors).not_to be_empty + expect(job.reload.job_artifacts.count).to be(2) + end + + it 'destroys the job artifacts and returns the expected data' do + job.project.add_maintainer(user) + expected_data = { + 'jobArtifactsDestroy' => { + 'errors' => [], + 'destroyedArtifactsCount' => 2, + 'job' => { + 'name' => job.name + } + } + } + + post_graphql_mutation(mutation, current_user: user) + + expect(response).to have_gitlab_http_status(:success) + expect(graphql_data).to eq(expected_data) + expect(job.reload.job_artifacts.count).to be(0) + end +end diff --git a/spec/requests/api/graphql/mutations/ci/job_artifact/destroy_spec.rb b/spec/requests/api/graphql/mutations/ci/job_artifact/destroy_spec.rb new file mode 100644 index 00000000000..a5ec9ea343d --- /dev/null +++ b/spec/requests/api/graphql/mutations/ci/job_artifact/destroy_spec.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'ArtifactDestroy' do + include GraphqlHelpers + + let(:user) { create(:user) } + let(:artifact) { create(:ci_job_artifact) } + + let(:mutation) do + variables = { + id: artifact.to_global_id.to_s + } + graphql_mutation(:artifact_destroy, variables, 'errors') + end + + it 'returns an error if the user is not allowed to destroy the artifact' do + post_graphql_mutation(mutation, current_user: user) + + expect(graphql_errors).not_to be_empty + end + + context 'when the user is allowed to destroy the artifact' do + before do + artifact.job.project.add_maintainer(user) + end + + it 'destroys the artifact' do + post_graphql_mutation(mutation, current_user: user) + + expect(response).to have_gitlab_http_status(:success) + expect { artifact.reload }.to raise_error(ActiveRecord::RecordNotFound) + end + + it 'returns error if destory fails' do + allow_next_found_instance_of(Ci::JobArtifact) do |instance| + allow(instance).to receive(:destroy).and_return(false) + allow(instance).to receive_message_chain(:errors, :full_messages).and_return(['cannot be removed']) + end + + post_graphql_mutation(mutation, current_user: user) + + expect(graphql_data_at(:artifact_destroy, :errors)).to contain_exactly('cannot be removed') + end + end +end diff --git a/spec/requests/api/graphql/mutations/custom_emoji/create_spec.rb b/spec/requests/api/graphql/mutations/custom_emoji/create_spec.rb index c91437fa355..66facdebe78 100644 --- a/spec/requests/api/graphql/mutations/custom_emoji/create_spec.rb +++ b/spec/requests/api/graphql/mutations/custom_emoji/create_spec.rb @@ -39,5 +39,19 @@ RSpec.describe 'Creation of a new Custom Emoji' do expect(gql_response['customEmoji']['name']).to eq(attributes[:name]) expect(gql_response['customEmoji']['url']).to eq(attributes[:url]) end + + context 'when the custom_emoji feature flag is disabled' do + before do + stub_feature_flags(custom_emoji: false) + end + + it 'does nothing and returns and error' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + end.to not_change(CustomEmoji, :count) + + expect_graphql_errors_to_include('Custom emoji feature is disabled') + end + end end end diff --git a/spec/requests/api/graphql/mutations/custom_emoji/destroy_spec.rb b/spec/requests/api/graphql/mutations/custom_emoji/destroy_spec.rb index 07fd57a2cee..7d25206e617 100644 --- a/spec/requests/api/graphql/mutations/custom_emoji/destroy_spec.rb +++ b/spec/requests/api/graphql/mutations/custom_emoji/destroy_spec.rb @@ -68,6 +68,20 @@ RSpec.describe 'Deletion of custom emoji' do end it_behaves_like 'deletes custom emoji' + + context 'when the custom_emoji feature flag is disabled' do + before do + stub_feature_flags(custom_emoji: false) + end + + it_behaves_like 'does not delete custom emoji' + + it 'returns an error' do + post_graphql_mutation(mutation, current_user: current_user) + + expect_graphql_errors_to_include('Custom emoji feature is disabled') + end + end end end end diff --git a/spec/requests/api/graphql/mutations/incident_management/timeline_event/promote_from_note_spec.rb b/spec/requests/api/graphql/mutations/incident_management/timeline_event/promote_from_note_spec.rb index 9272e218172..85eaec90f47 100644 --- a/spec/requests/api/graphql/mutations/incident_management/timeline_event/promote_from_note_spec.rb +++ b/spec/requests/api/graphql/mutations/incident_management/timeline_event/promote_from_note_spec.rb @@ -4,6 +4,7 @@ require 'spec_helper' RSpec.describe 'Promote an incident timeline event from a comment' do include GraphqlHelpers + include NotesHelper let_it_be(:user) { create(:user) } let_it_be(:project) { create(:project) } @@ -53,7 +54,7 @@ RSpec.describe 'Promote an incident timeline event from a comment' do 'promotedFromNote' => { 'id' => comment.to_global_id.to_s }, - 'note' => comment.note, + 'note' => "@#{comment.author.username} [commented](#{noteable_note_url(comment)}): '#{comment.note}'", 'action' => 'comment', 'editable' => true, 'occurredAt' => comment.created_at.iso8601 diff --git a/spec/requests/api/graphql/mutations/merge_requests/set_assignees_spec.rb b/spec/requests/api/graphql/mutations/merge_requests/set_assignees_spec.rb index 608b36e4f15..8cec5867aca 100644 --- a/spec/requests/api/graphql/mutations/merge_requests/set_assignees_spec.rb +++ b/spec/requests/api/graphql/mutations/merge_requests/set_assignees_spec.rb @@ -93,6 +93,16 @@ RSpec.describe 'Setting assignees of a merge request', :assume_throttled do expect(response).to have_gitlab_http_status(:success) expect(mutation_assignee_nodes).to match_array(expected_result) end + + it 'triggers webhooks', :sidekiq_inline do + hook = create(:project_hook, merge_requests_events: true, project: merge_request.project) + + expect(WebHookWorker).to receive(:perform_async).with(hook.id, anything, 'merge_request_hooks', anything) + + post_graphql_mutation(mutation, current_user: current_user) + + expect(response).to have_gitlab_http_status(:success) + end end context 'when passing an empty list of assignees' do diff --git a/spec/requests/api/graphql/mutations/releases/update_spec.rb b/spec/requests/api/graphql/mutations/releases/update_spec.rb index 33d4e57904c..240db764f40 100644 --- a/spec/requests/api/graphql/mutations/releases/update_spec.rb +++ b/spec/requests/api/graphql/mutations/releases/update_spec.rb @@ -22,9 +22,14 @@ RSpec.describe 'Updating an existing release' do let_it_be(:milestones) { [milestone_12_3, milestone_12_4] } let_it_be(:release) do - create(:release, project: project, tag: tag_name, name: name, - description: description, released_at: Time.parse(released_at).utc, - created_at: Time.parse(created_at).utc, milestones: milestones) + create(:release, + project: project, + tag: tag_name, + name: name, + description: description, + released_at: Time.parse(released_at).utc, + created_at: Time.parse(created_at).utc, + milestones: milestones) end let(:mutation_name) { :release_update } diff --git a/spec/requests/api/graphql/packages/composer_spec.rb b/spec/requests/api/graphql/packages/composer_spec.rb index 9830623ede8..89c01d44771 100644 --- a/spec/requests/api/graphql/packages/composer_spec.rb +++ b/spec/requests/api/graphql/packages/composer_spec.rb @@ -5,7 +5,7 @@ RSpec.describe 'package details' do include GraphqlHelpers include_context 'package details setup' - let_it_be(:package) { create(:composer_package, project: project) } + let_it_be(:package) { create(:composer_package, :last_downloaded_at, project: project) } let_it_be(:composer_json) { { name: 'name', type: 'type', license: 'license', version: 1 } } let_it_be(:composer_metadatum) do # we are forced to manually create the metadatum, without using the factory to force the sha to be a string diff --git a/spec/requests/api/graphql/packages/conan_spec.rb b/spec/requests/api/graphql/packages/conan_spec.rb index 5bd5a71bbeb..7ad85edecef 100644 --- a/spec/requests/api/graphql/packages/conan_spec.rb +++ b/spec/requests/api/graphql/packages/conan_spec.rb @@ -5,7 +5,7 @@ RSpec.describe 'conan package details' do include GraphqlHelpers include_context 'package details setup' - let_it_be(:package) { create(:conan_package, project: project) } + let_it_be(:package) { create(:conan_package, :last_downloaded_at, project: project) } let(:metadata) { query_graphql_fragment('ConanMetadata') } let(:package_files_metadata) { query_graphql_fragment('ConanFileMetadata') } diff --git a/spec/requests/api/graphql/packages/helm_spec.rb b/spec/requests/api/graphql/packages/helm_spec.rb index 1675b8faa23..79a589e2dc2 100644 --- a/spec/requests/api/graphql/packages/helm_spec.rb +++ b/spec/requests/api/graphql/packages/helm_spec.rb @@ -5,7 +5,7 @@ RSpec.describe 'helm package details' do include GraphqlHelpers include_context 'package details setup' - let_it_be(:package) { create(:helm_package, project: project) } + let_it_be(:package) { create(:helm_package, :last_downloaded_at, project: project) } let(:package_files_metadata) { query_graphql_fragment('HelmFileMetadata') } diff --git a/spec/requests/api/graphql/packages/maven_spec.rb b/spec/requests/api/graphql/packages/maven_spec.rb index 9d59a922660..b7f39efcf73 100644 --- a/spec/requests/api/graphql/packages/maven_spec.rb +++ b/spec/requests/api/graphql/packages/maven_spec.rb @@ -5,7 +5,7 @@ RSpec.describe 'maven package details' do include GraphqlHelpers include_context 'package details setup' - let_it_be(:package) { create(:maven_package, project: project) } + let_it_be(:package) { create(:maven_package, :last_downloaded_at, project: project) } let(:metadata) { query_graphql_fragment('MavenMetadata') } @@ -31,7 +31,9 @@ RSpec.describe 'maven package details' do context 'a versionless maven package' do let_it_be(:maven_metadatum) { create(:maven_metadatum, app_version: nil) } - let_it_be(:package) { create(:maven_package, project: project, version: nil, maven_metadatum: maven_metadatum) } + let_it_be(:package) do + create(:maven_package, :last_downloaded_at, project: project, version: nil, maven_metadatum: maven_metadatum) + end subject { post_graphql(query, current_user: user) } diff --git a/spec/requests/api/graphql/packages/nuget_spec.rb b/spec/requests/api/graphql/packages/nuget_spec.rb index 87cffc67ce5..7de132d1574 100644 --- a/spec/requests/api/graphql/packages/nuget_spec.rb +++ b/spec/requests/api/graphql/packages/nuget_spec.rb @@ -5,7 +5,7 @@ RSpec.describe 'nuget package details' do include GraphqlHelpers include_context 'package details setup' - let_it_be(:package) { create(:nuget_package, :with_metadatum, project: project) } + let_it_be(:package) { create(:nuget_package, :last_downloaded_at, :with_metadatum, project: project) } let_it_be(:dependency_link) { create(:packages_dependency_link, :with_nuget_metadatum, package: package) } let(:metadata) { query_graphql_fragment('NugetMetadata') } diff --git a/spec/requests/api/graphql/packages/package_spec.rb b/spec/requests/api/graphql/packages/package_spec.rb index c28b37db5af..e9f82d66775 100644 --- a/spec/requests/api/graphql/packages/package_spec.rb +++ b/spec/requests/api/graphql/packages/package_spec.rb @@ -6,8 +6,8 @@ RSpec.describe 'package details' do let_it_be_with_reload(:group) { create(:group) } let_it_be_with_reload(:project) { create(:project, group: group) } + let_it_be_with_reload(:composer_package) { create(:composer_package, :last_downloaded_at, project: project) } let_it_be(:user) { create(:user) } - let_it_be(:composer_package) { create(:composer_package, project: project) } let_it_be(:composer_json) { { name: 'name', type: 'type', license: 'license', version: 1 } } let_it_be(:composer_metadatum) do # we are forced to manually create the metadatum, without using the factory to force the sha to be a string @@ -65,6 +65,17 @@ RSpec.describe 'package details' do end end + context 'with package without last_downloaded_at' do + before do + composer_package.update!(last_downloaded_at: nil) + subject + end + + it 'matches the JSON schema' do + expect(package_details).to match_schema('graphql/packages/package_details') + end + end + context 'with package files pending destruction' do let_it_be(:package_file) { create(:package_file, package: composer_package) } let_it_be(:package_file_pending_destruction) { create(:package_file, :pending_destruction, package: composer_package) } @@ -97,7 +108,7 @@ RSpec.describe 'package details' do expect(graphql_data_at(:a, :name)).to eq(composer_package.name) - expect_graphql_errors_to_include [/Package details can be requested only for one package at a time/] + expect_graphql_errors_to_include [/"package" field can be requested only for 1 Query\(s\) at a time./] expect(graphql_data_at(:b)).to be(nil) end end diff --git a/spec/requests/api/graphql/packages/pypi_spec.rb b/spec/requests/api/graphql/packages/pypi_spec.rb index 0cc5bd2e3b2..c0e589f3597 100644 --- a/spec/requests/api/graphql/packages/pypi_spec.rb +++ b/spec/requests/api/graphql/packages/pypi_spec.rb @@ -5,7 +5,7 @@ RSpec.describe 'pypi package details' do include GraphqlHelpers include_context 'package details setup' - let_it_be(:package) { create(:pypi_package, project: project) } + let_it_be(:package) { create(:pypi_package, :last_downloaded_at, project: project) } let(:metadata) { query_graphql_fragment('PypiMetadata') } diff --git a/spec/requests/api/graphql/project/branch_protections/merge_access_levels_spec.rb b/spec/requests/api/graphql/project/branch_protections/merge_access_levels_spec.rb new file mode 100644 index 00000000000..cb5006ec8e4 --- /dev/null +++ b/spec/requests/api/graphql/project/branch_protections/merge_access_levels_spec.rb @@ -0,0 +1,109 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'getting merge access levels for a branch protection' do + include GraphqlHelpers + + let_it_be(:current_user) { create(:user) } + + let(:merge_access_level_data) { merge_access_levels_data[0] } + + let(:merge_access_levels_data) do + graphql_data_at('project', + 'branchRules', + 'nodes', + 0, + 'branchProtection', + 'mergeAccessLevels', + 'nodes') + end + + let(:project) { protected_branch.project } + + let(:merge_access_levels_count) { protected_branch.merge_access_levels.size } + + let(:variables) { { path: project.full_path } } + + let(:fields) { all_graphql_fields_for('MergeAccessLevel') } + + let(:query) do + <<~GQL + query($path: ID!) { + project(fullPath: $path) { + branchRules(first: 1) { + nodes { + branchProtection { + mergeAccessLevels { + nodes { + #{fields} + } + } + } + } + } + } + } + GQL + end + + context 'when the user does not have read_protected_branch abilities' do + let_it_be(:protected_branch) { create(:protected_branch) } + + before do + project.add_guest(current_user) + post_graphql(query, current_user: current_user, variables: variables) + end + + it_behaves_like 'a working graphql query' + + it { expect(merge_access_levels_data).not_to be_present } + end + + shared_examples 'merge access request' do + let(:merge_access) { protected_branch.merge_access_levels.first } + + before do + project.add_maintainer(current_user) + post_graphql(query, current_user: current_user, variables: variables) + end + + it_behaves_like 'a working graphql query' + + it 'returns all merge access levels' do + expect(merge_access_levels_data.size).to eq(merge_access_levels_count) + end + + it 'includes access_level' do + expect(merge_access_level_data['accessLevel']) + .to eq(merge_access.access_level) + end + + it 'includes access_level_description' do + expect(merge_access_level_data['accessLevelDescription']) + .to eq(merge_access.humanize) + end + end + + context 'when the user does have read_protected_branch abilities' do + let(:merge_access) { protected_branch.merge_access_levels.first } + + context 'when no one has access' do + let_it_be(:protected_branch) { create(:protected_branch, :no_one_can_merge) } + + it_behaves_like 'merge access request' + end + + context 'when developers have access' do + let_it_be(:protected_branch) { create(:protected_branch, :developers_can_merge) } + + it_behaves_like 'merge access request' + end + + context 'when maintainers have access' do + let_it_be(:protected_branch) { create(:protected_branch, :maintainers_can_merge) } + + it_behaves_like 'merge access request' + end + end +end diff --git a/spec/requests/api/graphql/project/branch_protections/push_access_levels_spec.rb b/spec/requests/api/graphql/project/branch_protections/push_access_levels_spec.rb new file mode 100644 index 00000000000..59f9c7d61cb --- /dev/null +++ b/spec/requests/api/graphql/project/branch_protections/push_access_levels_spec.rb @@ -0,0 +1,109 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'getting push access levels for a branch protection' do + include GraphqlHelpers + + let_it_be(:current_user) { create(:user) } + + let(:push_access_level_data) { push_access_levels_data[0] } + + let(:push_access_levels_data) do + graphql_data_at('project', + 'branchRules', + 'nodes', + 0, + 'branchProtection', + 'pushAccessLevels', + 'nodes') + end + + let(:project) { protected_branch.project } + + let(:push_access_levels_count) { protected_branch.push_access_levels.size } + + let(:variables) { { path: project.full_path } } + + let(:fields) { all_graphql_fields_for('PushAccessLevel'.classify) } + + let(:query) do + <<~GQL + query($path: ID!) { + project(fullPath: $path) { + branchRules(first: 1) { + nodes { + branchProtection { + pushAccessLevels { + nodes { + #{fields} + } + } + } + } + } + } + } + GQL + end + + context 'when the user does not have read_protected_branch abilities' do + let_it_be(:protected_branch) { create(:protected_branch) } + + before do + project.add_guest(current_user) + post_graphql(query, current_user: current_user, variables: variables) + end + + it_behaves_like 'a working graphql query' + + it { expect(push_access_levels_data).not_to be_present } + end + + shared_examples 'push access request' do + let(:push_access) { protected_branch.push_access_levels.first } + + before do + project.add_maintainer(current_user) + post_graphql(query, current_user: current_user, variables: variables) + end + + it_behaves_like 'a working graphql query' + + it 'returns all push access levels' do + expect(push_access_levels_data.size).to eq(push_access_levels_count) + end + + it 'includes access_level' do + expect(push_access_level_data['accessLevel']) + .to eq(push_access.access_level) + end + + it 'includes access_level_description' do + expect(push_access_level_data['accessLevelDescription']) + .to eq(push_access.humanize) + end + end + + context 'when the user does have read_protected_branch abilities' do + let(:push_access) { protected_branch.push_access_levels.first } + + context 'when no one has access' do + let_it_be(:protected_branch) { create(:protected_branch, :no_one_can_push) } + + it_behaves_like 'push access request' + end + + context 'when developers have access' do + let_it_be(:protected_branch) { create(:protected_branch, :developers_can_push) } + + it_behaves_like 'push access request' + end + + context 'when maintainers have access' do + let_it_be(:protected_branch) { create(:protected_branch, :maintainers_can_push) } + + it_behaves_like 'push access request' + end + end +end diff --git a/spec/requests/api/graphql/project/branch_rules/branch_protection_spec.rb b/spec/requests/api/graphql/project/branch_rules/branch_protection_spec.rb new file mode 100644 index 00000000000..8a3f546ef95 --- /dev/null +++ b/spec/requests/api/graphql/project/branch_rules/branch_protection_spec.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'getting branch protection for a branch rule' do + include GraphqlHelpers + + let_it_be(:current_user) { create(:user) } + let_it_be(:branch_rule) { create(:protected_branch) } + let_it_be(:project) { branch_rule.project } + + let(:branch_protection_data) do + graphql_data_at('project', 'branchRules', 'nodes', 0, 'branchProtection') + end + + let(:variables) { { path: project.full_path } } + + let(:fields) { all_graphql_fields_for('BranchProtection') } + + let(:query) do + <<~GQL + query($path: ID!) { + project(fullPath: $path) { + branchRules(first: 1) { + nodes { + branchProtection { + #{fields} + } + } + } + } + } + GQL + end + + context 'when the user does not have read_protected_branch abilities' do + before do + project.add_guest(current_user) + post_graphql(query, current_user: current_user, variables: variables) + end + + it_behaves_like 'a working graphql query' + + it { expect(branch_protection_data).not_to be_present } + end + + context 'when the user does have read_protected_branch abilities' do + before do + project.add_maintainer(current_user) + post_graphql(query, current_user: current_user, variables: variables) + end + + it_behaves_like 'a working graphql query' + + it 'includes allow_force_push' do + expect(branch_protection_data['allowForcePush']).to be_in([true, false]) + expect(branch_protection_data['allowForcePush']).to eq(branch_rule.allow_force_push) + end + end +end diff --git a/spec/requests/api/graphql/project/branch_rules_spec.rb b/spec/requests/api/graphql/project/branch_rules_spec.rb new file mode 100644 index 00000000000..70fb37941e2 --- /dev/null +++ b/spec/requests/api/graphql/project/branch_rules_spec.rb @@ -0,0 +1,122 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'getting list of branch rules for a project' do + include GraphqlHelpers + + let_it_be(:project) { create(:project, :repository, :public) } + let_it_be(:current_user) { create(:user) } + let_it_be(:branch_name_a) { 'branch_name_a' } + let_it_be(:branch_name_b) { 'wildcard-*' } + let_it_be(:branch_rules) { [branch_rule_a, branch_rule_b] } + + let_it_be(:branch_rule_a) do + create(:protected_branch, project: project, name: branch_name_a) + end + + let_it_be(:branch_rule_b) do + create(:protected_branch, project: project, name: branch_name_b) + end + + let(:branch_rules_data) { graphql_data_at('project', 'branchRules', 'edges') } + let(:variables) { { path: project.full_path } } + + let(:fields) do + <<~QUERY + pageInfo { + hasNextPage + hasPreviousPage + } + edges { + cursor + node { + #{all_graphql_fields_for('branch_rules'.classify)} + } + } + QUERY + end + + let(:query) do + <<~GQL + query($path: ID!, $n: Int, $cursor: String) { + project(fullPath: $path) { + branchRules(first: $n, after: $cursor) { #{fields} } + } + } + GQL + end + + context 'when the user does not have read_protected_branch abilities' do + before do + project.add_guest(current_user) + post_graphql(query, current_user: current_user, variables: variables) + end + + it_behaves_like 'a working graphql query' + + it { expect(branch_rules_data).to be_empty } + end + + context 'when the user does have read_protected_branch abilities' do + before do + project.add_maintainer(current_user) + post_graphql(query, current_user: current_user, variables: variables) + end + + it_behaves_like 'a working graphql query' + + it 'includes a name' do + expect(branch_rules_data.dig(0, 'node', 'name')).to be_present + end + + it 'includes created_at and updated_at' do + expect(branch_rules_data.dig(0, 'node', 'createdAt')).to be_present + expect(branch_rules_data.dig(1, 'node', 'updatedAt')).to be_present + end + + context 'when limiting the number of results' do + let(:branch_rule_limit) { 1 } + let(:variables) { { path: project.full_path, n: branch_rule_limit } } + let(:next_variables) do + { path: project.full_path, n: branch_rule_limit, cursor: last_cursor } + end + + it_behaves_like 'a working graphql query' do + it 'only returns N branch_rules' do + expect(branch_rules_data.size).to eq(branch_rule_limit) + expect(has_next_page).to be_truthy + expect(has_prev_page).to be_falsey + post_graphql(query, current_user: current_user, variables: next_variables) + expect(branch_rules_data.size).to eq(branch_rule_limit) + expect(has_next_page).to be_falsey + expect(has_prev_page).to be_truthy + end + end + + context 'when no limit is provided' do + let(:branch_rule_limit) { nil } + + it 'returns all branch_rules' do + expect(branch_rules_data.size).to eq(branch_rules.size) + end + end + end + end + + def pagination_info + graphql_data_at('project', 'branchRules', 'pageInfo') + end + + def has_next_page + pagination_info['hasNextPage'] + end + + def has_prev_page + pagination_info['hasPreviousPage'] + end + + def last_cursor + branch_rules_data.last['cursor'] + end +end diff --git a/spec/requests/api/graphql/project/deployment_spec.rb b/spec/requests/api/graphql/project/deployment_spec.rb new file mode 100644 index 00000000000..e5ef7bcafbf --- /dev/null +++ b/spec/requests/api/graphql/project/deployment_spec.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Project Deployment query' do + let_it_be(:project) { create(:project, :private, :repository) } + let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } } + let_it_be(:guest) { create(:user).tap { |u| project.add_guest(u) } } + let_it_be(:environment) { create(:environment, project: project) } + let_it_be(:deployment) { create(:deployment, environment: environment, project: project) } + + subject { GitlabSchema.execute(query, context: { current_user: user }).as_json } + + let(:user) { developer } + + let(:query) do + %( + query { + project(fullPath: "#{project.full_path}") { + deployment(iid: #{deployment.iid}) { + id + iid + ref + tag + sha + createdAt + updatedAt + finishedAt + status + } + } + } + ) + end + + it 'returns the deployment of the project' do + deployment_data = subject.dig('data', 'project', 'deployment') + + expect(deployment_data['iid']).to eq(deployment.iid.to_s) + end + + context 'when user is guest' do + let(:user) { guest } + + it 'returns nothing' do + deployment_data = subject.dig('data', 'project', 'deployment') + + expect(deployment_data).to be_nil + end + end +end diff --git a/spec/requests/api/graphql/project/environments_spec.rb b/spec/requests/api/graphql/project/environments_spec.rb new file mode 100644 index 00000000000..e5b6aebbf2c --- /dev/null +++ b/spec/requests/api/graphql/project/environments_spec.rb @@ -0,0 +1,133 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Project Environments query' do + include GraphqlHelpers + + let_it_be(:project) { create(:project, :private, :repository) } + let_it_be_with_refind(:production) { create(:environment, :production, project: project) } + let_it_be_with_refind(:staging) { create(:environment, :staging, project: project) } + let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } } + + subject { post_graphql(query, current_user: user) } + + let(:user) { developer } + + let(:query) do + %( + query { + project(fullPath: "#{project.full_path}") { + environment(name: "#{production.name}") { + slug + createdAt + updatedAt + autoStopAt + autoDeleteAt + tier + environmentType + } + } + } + ) + end + + it 'returns the specified fields of the environment', :aggregate_failures do + production.update!(auto_stop_at: 1.day.ago, auto_delete_at: 2.days.ago, environment_type: 'review') + + subject + + environment_data = graphql_data.dig('project', 'environment') + expect(environment_data['slug']).to eq(production.slug) + expect(environment_data['createdAt']).to eq(production.created_at.iso8601) + expect(environment_data['updatedAt']).to eq(production.updated_at.iso8601) + expect(environment_data['autoStopAt']).to eq(production.auto_stop_at.iso8601) + expect(environment_data['autoDeleteAt']).to eq(production.auto_delete_at.iso8601) + expect(environment_data['tier']).to eq(production.tier.upcase) + expect(environment_data['environmentType']).to eq(production.environment_type) + end + + describe 'last deployments of environments' do + ::Deployment.statuses.each do |status, _| + let_it_be(:"production_#{status}_deployment") do + create(:deployment, status.to_sym, environment: production, project: project) + end + + let_it_be(:"staging_#{status}_deployment") do + create(:deployment, status.to_sym, environment: staging, project: project) + end + end + + let(:query) do + %( + query { + project(fullPath: "#{project.full_path}") { + environments { + nodes { + name + lastSuccessDeployment: lastDeployment(status: SUCCESS) { + iid + } + lastRunningDeployment: lastDeployment(status: RUNNING) { + iid + } + lastBlockedDeployment: lastDeployment(status: BLOCKED) { + iid + } + } + } + } + } + ) + end + + it 'returns all last deployments of the environment' do + subject + + environments_data = graphql_data_at(:project, :environments, :nodes) + + environments_data.each do |environment_data| + name = environment_data['name'] + success_deployment = public_send(:"#{name}_success_deployment") + running_deployment = public_send(:"#{name}_running_deployment") + blocked_deployment = public_send(:"#{name}_blocked_deployment") + + expect(environment_data['lastSuccessDeployment']['iid']).to eq(success_deployment.iid.to_s) + expect(environment_data['lastRunningDeployment']['iid']).to eq(running_deployment.iid.to_s) + expect(environment_data['lastBlockedDeployment']['iid']).to eq(blocked_deployment.iid.to_s) + end + end + + it 'executes the same number of queries in single environment and multiple environments' do + single_environment_query = + %( + query { + project(fullPath: "#{project.full_path}") { + environment(name: "#{production.name}") { + name + lastSuccessDeployment: lastDeployment(status: SUCCESS) { + iid + } + lastRunningDeployment: lastDeployment(status: RUNNING) { + iid + } + lastBlockedDeployment: lastDeployment(status: BLOCKED) { + iid + } + } + } + } + ) + + baseline = ActiveRecord::QueryRecorder.new do + run_with_clean_state(single_environment_query, context: { current_user: user }) + end + + multi = ActiveRecord::QueryRecorder.new do + run_with_clean_state(query, context: { current_user: user }) + end + + expect(multi).not_to exceed_query_limit(baseline) + end + end +end diff --git a/spec/requests/api/graphql/project/issue/design_collection/version_spec.rb b/spec/requests/api/graphql/project/issue/design_collection/version_spec.rb index 8cda61f0628..0444ce43c22 100644 --- a/spec/requests/api/graphql/project/issue/design_collection/version_spec.rb +++ b/spec/requests/api/graphql/project/issue/design_collection/version_spec.rb @@ -11,14 +11,14 @@ RSpec.describe 'Query.project(fullPath).issue(iid).designCollection.version(sha) let_it_be(:developer) { create(:user) } let_it_be(:stranger) { create(:user) } let_it_be(:old_version) do - create(:design_version, issue: issue, - created_designs: create_list(:design, 3, issue: issue)) + create(:design_version, issue: issue, created_designs: create_list(:design, 3, issue: issue)) end let_it_be(:version) do - create(:design_version, issue: issue, - modified_designs: old_version.designs, - created_designs: create_list(:design, 2, issue: issue)) + create(:design_version, + issue: issue, + modified_designs: old_version.designs, + created_designs: create_list(:design, 2, issue: issue)) end let(:current_user) { developer } diff --git a/spec/requests/api/graphql/project/issues_spec.rb b/spec/requests/api/graphql/project/issues_spec.rb index 596e023a027..28282860416 100644 --- a/spec/requests/api/graphql/project/issues_spec.rb +++ b/spec/requests/api/graphql/project/issues_spec.rb @@ -27,14 +27,6 @@ RSpec.describe 'getting an issue list for a project' do QUERY end - let(:query) do - graphql_query_for( - 'project', - { 'fullPath' => project.full_path }, - query_graphql_field('issues', issue_filter_params, fields) - ) - end - it_behaves_like 'a working graphql query' do before do post_graphql(query, current_user: current_user) @@ -89,6 +81,14 @@ RSpec.describe 'getting an issue list for a project' do end end + context 'when filtering by search' do + it_behaves_like 'query with a search term' do + let(:issuable_data) { issues_data } + let(:user) { current_user } + let_it_be(:issuable) { create(:issue, project: project, description: 'bar') } + end + end + context 'when limiting the number of results' do let(:query) do <<~GQL @@ -301,7 +301,7 @@ RSpec.describe 'getting an issue list for a project' do let_it_be(:relative_issue5) { create(:issue, project: sort_project, relative_position: 500) } context 'when ascending' do - it_behaves_like 'sorted paginated query' do + it_behaves_like 'sorted paginated query', is_reversible: true do let(:sort_param) { :RELATIVE_POSITION_ASC } let(:first_param) { 2 } let(:all_records) do @@ -679,4 +679,12 @@ RSpec.describe 'getting an issue list for a project' do def issues_ids graphql_dig_at(issues_data, :node, :id) end + + def query(params = issue_filter_params) + graphql_query_for( + 'project', + { 'fullPath' => project.full_path }, + query_graphql_field('issues', params, fields) + ) + end end diff --git a/spec/requests/api/graphql/project/job_spec.rb b/spec/requests/api/graphql/project/job_spec.rb new file mode 100644 index 00000000000..6edd4cf753f --- /dev/null +++ b/spec/requests/api/graphql/project/job_spec.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Query.project.job' do + include GraphqlHelpers + + let_it_be(:project) { create(:project) } + let_it_be(:user) { create(:user) } + + let(:job) { create(:ci_build, project: project, name: 'GQL test job') } + + let(:query) do + <<~QUERY + { + project(fullPath: "#{project.full_path}") { + job(id: "#{job.to_global_id}") { + name + } + } + } + QUERY + end + + context 'when the user can read jobs on the project' do + before do + project.add_developer(user) + end + + it 'returns the job that matches the given ID' do + post_graphql(query, current_user: user) + + expect(graphql_data.dig('project', 'job', 'name')).to eq('GQL test job') + end + + context 'when no job matches the given ID' do + let(:job) { create(:ci_build, project: create(:project), name: 'Job from another project') } + + it 'returns null' do + post_graphql(query, current_user: user) + + expect(graphql_data.dig('project', 'job')).to be_nil + end + end + end + + context 'when the user cannot read jobs on the project' do + it 'returns null' do + post_graphql(query, current_user: user) + + expect(graphql_data.dig('project', 'job')).to be_nil + end + end +end diff --git a/spec/requests/api/graphql/project/merge_request_spec.rb b/spec/requests/api/graphql/project/merge_request_spec.rb index d2f34080be3..6a59df81405 100644 --- a/spec/requests/api/graphql/project/merge_request_spec.rb +++ b/spec/requests/api/graphql/project/merge_request_spec.rb @@ -365,7 +365,7 @@ RSpec.describe 'getting merge request information nested in a project' do expect(interaction_data).to contain_exactly a_hash_including( 'canMerge' => false, 'canUpdate' => can_update, - 'reviewState' => attention_requested, + 'reviewState' => unreviewed, 'reviewed' => false, 'approved' => false ) @@ -398,8 +398,8 @@ RSpec.describe 'getting merge request information nested in a project' do describe 'scalability' do let_it_be(:other_users) { create_list(:user, 3) } - let(:attention_requested) do - { 'reviewState' => 'ATTENTION_REQUESTED' } + let(:unreviewed) do + { 'reviewState' => 'UNREVIEWED' } end let(:reviewed) do @@ -425,15 +425,15 @@ RSpec.describe 'getting merge request information nested in a project' do other_users.each do |user| assign_user(user) - merge_request.merge_request_reviewers.find_or_create_by!(reviewer: user, state: :attention_requested) + merge_request.merge_request_reviewers.find_or_create_by!(reviewer: user) end expect { post_graphql(query) }.not_to exceed_query_limit(baseline) expect(interaction_data).to contain_exactly( - include(attention_requested), - include(attention_requested), - include(attention_requested), + include(unreviewed), + include(unreviewed), + include(unreviewed), include(reviewed) ) end @@ -462,17 +462,17 @@ RSpec.describe 'getting merge request information nested in a project' do it_behaves_like 'when requesting information about MR interactions' do let(:field) { :reviewers } - let(:attention_requested) { 'ATTENTION_REQUESTED' } + let(:unreviewed) { 'UNREVIEWED' } let(:can_update) { false } def assign_user(user) - merge_request.merge_request_reviewers.create!(reviewer: user, state: :attention_requested) + merge_request.merge_request_reviewers.create!(reviewer: user) end end it_behaves_like 'when requesting information about MR interactions' do let(:field) { :assignees } - let(:attention_requested) { nil } + let(:unreviewed) { nil } let(:can_update) { true } # assignees can update MRs def assign_user(user) diff --git a/spec/requests/api/graphql/project/pipeline_spec.rb b/spec/requests/api/graphql/project/pipeline_spec.rb index 08c6a2d9927..41915d3cdee 100644 --- a/spec/requests/api/graphql/project/pipeline_spec.rb +++ b/spec/requests/api/graphql/project/pipeline_spec.rb @@ -111,7 +111,7 @@ RSpec.describe 'getting pipeline information nested in a project' do name: build_job.name, pipeline: pipeline, stage_idx: 0, - stage: build_job.stage) + stage: build_job.stage_name) end let(:fields) do diff --git a/spec/requests/api/graphql/project/terraform/state_spec.rb b/spec/requests/api/graphql/project/terraform/state_spec.rb index 8f2d2cffef2..5e207ec0963 100644 --- a/spec/requests/api/graphql/project/terraform/state_spec.rb +++ b/spec/requests/api/graphql/project/terraform/state_spec.rb @@ -60,17 +60,17 @@ RSpec.describe 'query a single terraform state' do expect(data).to match a_graphql_entity_for( terraform_state, :name, - 'lockedAt' => terraform_state.locked_at.iso8601, - 'createdAt' => terraform_state.created_at.iso8601, - 'updatedAt' => terraform_state.updated_at.iso8601, - 'lockedByUser' => a_graphql_entity_for(terraform_state.locked_by_user), + 'lockedAt' => terraform_state.locked_at.iso8601, + 'createdAt' => terraform_state.created_at.iso8601, + 'updatedAt' => terraform_state.updated_at.iso8601, + 'lockedByUser' => a_graphql_entity_for(terraform_state.locked_by_user), 'latestVersion' => a_graphql_entity_for( latest_version, - 'serial' => eq(latest_version.version), - 'createdAt' => eq(latest_version.created_at.iso8601), - 'updatedAt' => eq(latest_version.updated_at.iso8601), + 'serial' => eq(latest_version.version), + 'createdAt' => eq(latest_version.created_at.iso8601), + 'updatedAt' => eq(latest_version.updated_at.iso8601), 'createdByUser' => a_graphql_entity_for(latest_version.created_by_user), - 'job' => { 'name' => eq(latest_version.build.name) } + 'job' => { 'name' => eq(latest_version.build.name) } ) ) end diff --git a/spec/requests/api/graphql/project/terraform/states_spec.rb b/spec/requests/api/graphql/project/terraform/states_spec.rb index a7ec6f69776..cc3660bcc6b 100644 --- a/spec/requests/api/graphql/project/terraform/states_spec.rb +++ b/spec/requests/api/graphql/project/terraform/states_spec.rb @@ -64,18 +64,18 @@ RSpec.describe 'query terraform states' do expect(data['nodes']).to contain_exactly a_graphql_entity_for( terraform_state, :name, - 'lockedAt' => terraform_state.locked_at.iso8601, - 'createdAt' => terraform_state.created_at.iso8601, - 'updatedAt' => terraform_state.updated_at.iso8601, - 'lockedByUser' => a_graphql_entity_for(terraform_state.locked_by_user), + 'lockedAt' => terraform_state.locked_at.iso8601, + 'createdAt' => terraform_state.created_at.iso8601, + 'updatedAt' => terraform_state.updated_at.iso8601, + 'lockedByUser' => a_graphql_entity_for(terraform_state.locked_by_user), 'latestVersion' => a_graphql_entity_for( latest_version, - 'serial' => eq(latest_version.version), - 'downloadPath' => eq(download_path), - 'createdAt' => eq(latest_version.created_at.iso8601), - 'updatedAt' => eq(latest_version.updated_at.iso8601), + 'serial' => eq(latest_version.version), + 'downloadPath' => eq(download_path), + 'createdAt' => eq(latest_version.created_at.iso8601), + 'updatedAt' => eq(latest_version.updated_at.iso8601), 'createdByUser' => a_graphql_entity_for(latest_version.created_by_user), - 'job' => { 'name' => eq(latest_version.build.name) } + 'job' => { 'name' => eq(latest_version.build.name) } ) ) end diff --git a/spec/requests/api/graphql/project/work_items_spec.rb b/spec/requests/api/graphql/project/work_items_spec.rb index 6ef28392b8b..69f8d1cac74 100644 --- a/spec/requests/api/graphql/project/work_items_spec.rb +++ b/spec/requests/api/graphql/project/work_items_spec.rb @@ -10,7 +10,10 @@ RSpec.describe 'getting an work item list for a project' do let_it_be(:current_user) { create(:user) } let_it_be(:item1) { create(:work_item, project: project, discussion_locked: true, title: 'item1') } - let_it_be(:item2) { create(:work_item, project: project, title: 'item2') } + let_it_be(:item2) do + create(:work_item, project: project, title: 'item2', last_edited_by: current_user, last_edited_at: 1.day.ago) + end + let_it_be(:confidential_item) { create(:work_item, confidential: true, project: project, title: 'item3') } let_it_be(:other_item) { create(:work_item) } @@ -27,14 +30,6 @@ RSpec.describe 'getting an work item list for a project' do QUERY end - let(:query) do - graphql_query_for( - 'project', - { 'fullPath' => project.full_path }, - query_graphql_field('workItems', item_filter_params, fields) - ) - end - it_behaves_like 'a working graphql query' do before do post_graphql(query, current_user: current_user) @@ -83,6 +78,48 @@ RSpec.describe 'getting an work item list for a project' do end end + context 'when fetching description edit information' do + let(:fields) do + <<~GRAPHQL + nodes { + widgets { + type + ... on WorkItemWidgetDescription { + edited + lastEditedAt + lastEditedBy { + webPath + username + } + } + } + } + GRAPHQL + end + + it 'avoids N+1 queries' do + post_graphql(query, current_user: current_user) # warm-up + + control = ActiveRecord::QueryRecorder.new do + post_graphql(query, current_user: current_user) + end + expect_graphql_errors_to_be_empty + + create_list(:work_item, 3, :last_edited_by_user, last_edited_at: 1.week.ago, project: project) + + expect_graphql_errors_to_be_empty + expect { post_graphql(query, current_user: current_user) }.not_to exceed_query_limit(control) + end + end + + context 'when filtering by search' do + it_behaves_like 'query with a search term' do + let(:issuable_data) { items_data } + let(:user) { current_user } + let_it_be(:issuable) { create(:work_item, project: project, description: 'bar') } + end + end + describe 'sorting and pagination' do let(:data_path) { [:project, :work_items] } @@ -118,4 +155,12 @@ RSpec.describe 'getting an work item list for a project' do def item_ids graphql_dig_at(items_data, :node, :id) end + + def query(params = item_filter_params) + graphql_query_for( + 'project', + { 'fullPath' => project.full_path }, + query_graphql_field('workItems', params, fields) + ) + end end diff --git a/spec/requests/api/graphql/query_spec.rb b/spec/requests/api/graphql/query_spec.rb index 4aa9c4b8254..359c599cd3a 100644 --- a/spec/requests/api/graphql/query_spec.rb +++ b/spec/requests/api/graphql/query_spec.rb @@ -108,8 +108,8 @@ RSpec.describe 'Query' do design_at_version, 'filename' => design_at_version.design.filename, 'version' => a_graphql_entity_for(version, :sha), - 'design' => a_graphql_entity_for(design), - 'issue' => { 'title' => issue.title, 'iid' => issue.iid.to_s }, + 'design' => a_graphql_entity_for(design), + 'issue' => { 'title' => issue.title, 'iid' => issue.iid.to_s }, 'project' => a_graphql_entity_for(project, :full_path) ) end diff --git a/spec/requests/api/graphql/work_item_spec.rb b/spec/requests/api/graphql/work_item_spec.rb index 34644e5893a..e4bb4109c76 100644 --- a/spec/requests/api/graphql/work_item_spec.rb +++ b/spec/requests/api/graphql/work_item_spec.rb @@ -14,7 +14,10 @@ RSpec.describe 'Query.work_item(id)' do project: project, description: '- List item', start_date: Date.today, - due_date: 1.week.from_now + due_date: 1.week.from_now, + created_at: 1.week.ago, + last_edited_at: 1.day.ago, + last_edited_by: guest ) end @@ -67,6 +70,12 @@ RSpec.describe 'Query.work_item(id)' do ... on WorkItemWidgetDescription { description descriptionHtml + edited + lastEditedBy { + webPath + username + } + lastEditedAt } } GRAPHQL @@ -79,7 +88,13 @@ RSpec.describe 'Query.work_item(id)' do hash_including( 'type' => 'DESCRIPTION', 'description' => work_item.description, - 'descriptionHtml' => ::MarkupHelper.markdown_field(work_item, :description, {}) + 'descriptionHtml' => ::MarkupHelper.markdown_field(work_item, :description, {}), + 'edited' => true, + 'lastEditedAt' => work_item.last_edited_at.iso8601, + 'lastEditedBy' => { + 'webPath' => "/#{guest.full_path}", + 'username' => guest.username + } ) ) ) diff --git a/spec/requests/api/group_export_spec.rb b/spec/requests/api/group_export_spec.rb index bda46f85140..83c34204c78 100644 --- a/spec/requests/api/group_export_spec.rb +++ b/spec/requests/api/group_export_spec.rb @@ -34,6 +34,7 @@ RSpec.describe API::GroupExport do before do allow_next_instance_of(Gitlab::ApplicationRateLimiter::BaseStrategy) do |strategy| allow(strategy).to receive(:increment).and_return(0) + allow(strategy).to receive(:read).and_return(0) end upload.export_file = fixture_file_upload('spec/fixtures/group_export.tar.gz', "`/tar.gz") diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index bc37f8e4655..6169bc9b2a2 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -5,6 +5,7 @@ require 'spec_helper' RSpec.describe API::Groups do include GroupAPIHelpers include UploadHelpers + include WorkhorseHelpers let_it_be(:user1) { create(:user, can_create_group: false) } let_it_be(:user2) { create(:user) } @@ -540,9 +541,9 @@ RSpec.describe API::Groups do # Returns a Hash of visibility_level => Project pairs def add_projects_to_group(group, share_with: nil) projects = { - public: create(:project, :public, namespace: group), + public: create(:project, :public, namespace: group), internal: create(:project, :internal, namespace: group), - private: create(:project, :private, namespace: group) + private: create(:project, :private, namespace: group) } if share_with @@ -872,21 +873,31 @@ RSpec.describe API::Groups do group_param = { avatar: fixture_file_upload(file_path) } - put api("/groups/#{group1.id}", user1), params: group_param + workhorse_form_with_file( + api("/groups/#{group1.id}", user1), + method: :put, + file_key: :avatar, + params: group_param + ) end end context 'when authenticated as the group owner' do it 'updates the group' do - put api("/groups/#{group1.id}", user1), params: { - name: new_group_name, - request_access_enabled: true, - project_creation_level: "noone", - subgroup_creation_level: "maintainer", - default_branch_protection: ::Gitlab::Access::MAINTAINER_PROJECT_ACCESS, - prevent_sharing_groups_outside_hierarchy: true, - avatar: fixture_file_upload(file_path) - } + workhorse_form_with_file( + api("/groups/#{group1.id}", user1), + method: :put, + file_key: :avatar, + params: { + name: new_group_name, + request_access_enabled: true, + project_creation_level: "noone", + subgroup_creation_level: "maintainer", + default_branch_protection: ::Gitlab::Access::MAINTAINER_PROJECT_ACCESS, + prevent_sharing_groups_outside_hierarchy: true, + avatar: fixture_file_upload(file_path) + } + ) expect(response).to have_gitlab_http_status(:ok) expect(json_response['name']).to eq(new_group_name) @@ -912,6 +923,16 @@ RSpec.describe API::Groups do expect(json_response['prevent_sharing_groups_outside_hierarchy']).to eq(true) end + it 'removes the group avatar' do + put api("/groups/#{group1.id}", user1), params: { avatar: '' } + + aggregate_failures "testing response" do + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['avatar_url']).to be_nil + expect(group1.reload.avatar_url).to be_nil + end + end + it 'does not update visibility_level if it is restricted' do stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::INTERNAL]) @@ -1787,7 +1808,12 @@ RSpec.describe API::Groups do attrs[:avatar] = fixture_file_upload(file_path) end - post api("/groups", user3), params: params + workhorse_form_with_file( + api('/groups', user3), + method: :post, + file_key: :avatar, + params: params + ) end end @@ -2029,6 +2055,90 @@ RSpec.describe API::Groups do end end + describe 'GET /groups/:id/transfer_locations' do + let_it_be(:user) { create(:user) } + let_it_be(:source_group) { create(:group, :private) } + + let(:params) { {} } + + subject(:request) do + get api("/groups/#{source_group.id}/transfer_locations", user), params: params + end + + context 'when the user has rights to transfer the group' do + let_it_be(:guest_group) { create(:group) } + let_it_be(:maintainer_group) { create(:group, name: 'maintainer group', path: 'maintainer-group') } + let_it_be(:owner_group_1) { create(:group, name: 'owner group', path: 'owner-group') } + let_it_be(:owner_group_2) { create(:group, name: 'gitlab group', path: 'gitlab-group') } + let_it_be(:shared_with_group_where_direct_owner_as_owner) { create(:group) } + + before do + source_group.add_owner(user) + guest_group.add_guest(user) + maintainer_group.add_maintainer(user) + owner_group_1.add_owner(user) + owner_group_2.add_owner(user) + create(:group_group_link, :owner, + shared_with_group: owner_group_1, + shared_group: shared_with_group_where_direct_owner_as_owner + ) + end + + it 'returns 200' do + request + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + end + + it 'only includes groups where the user has permissions to transfer a group to' do + request + + expect(group_ids_from_response).to contain_exactly( + owner_group_1.id, + owner_group_2.id, + shared_with_group_where_direct_owner_as_owner.id + ) + end + + context 'with search' do + let(:params) { { search: 'gitlab' } } + + it 'includes groups where the user has permissions to transfer a group to, matching the search term' do + request + + expect(group_ids_from_response).to contain_exactly(owner_group_2.id) + end + end + + def group_ids_from_response + json_response.map { |group| group['id'] } + end + end + + context 'when the user does not have permissions to transfer the group' do + before do + source_group.add_developer(user) + end + + it 'returns 403' do + request + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + context 'for an anonymous user' do + let_it_be(:user) { nil } + + it 'returns 404' do + request + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + describe 'POST /groups/:id/transfer' do let_it_be(:user) { create(:user) } let_it_be_with_reload(:new_parent_group) { create(:group, :private) } diff --git a/spec/requests/api/import_github_spec.rb b/spec/requests/api/import_github_spec.rb index 7de72de3940..d2fa3dabe69 100644 --- a/spec/requests/api/import_github_spec.rb +++ b/spec/requests/api/import_github_spec.rb @@ -13,15 +13,15 @@ RSpec.describe API::ImportGithub do let(:provider_username) { user.username } let(:provider_user) { double('provider', login: provider_username) } let(:provider_repo) do - double('provider', + { name: 'vim', full_name: "#{provider_username}/vim", owner: double('provider', login: provider_username), description: 'provider', private: false, clone_url: 'https://fake.url/vim.git', - has_wiki?: true - ) + has_wiki: true + } end before do @@ -48,7 +48,7 @@ RSpec.describe API::ImportGithub do it 'returns 201 response when the project is imported successfully' do allow(Gitlab::LegacyGithubImport::ProjectCreator) - .to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, type: provider, **access_params) + .to receive(:new).with(provider_repo, provider_repo[:name], user.namespace, user, type: provider, **access_params) .and_return(double(execute: project)) post api("/import/github", user), params: { @@ -63,7 +63,7 @@ RSpec.describe API::ImportGithub do it 'returns 201 response when the project is imported successfully from GHE' do allow(Gitlab::LegacyGithubImport::ProjectCreator) - .to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, type: provider, **access_params) + .to receive(:new).with(provider_repo, provider_repo[:name], user.namespace, user, type: provider, **access_params) .and_return(double(execute: project)) post api("/import/github", user), params: { diff --git a/spec/requests/api/integrations/slack/events_spec.rb b/spec/requests/api/integrations/slack/events_spec.rb deleted file mode 100644 index 176e9eded31..00000000000 --- a/spec/requests/api/integrations/slack/events_spec.rb +++ /dev/null @@ -1,112 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe API::Integrations::Slack::Events do - describe 'POST /integrations/slack/events' do - let(:params) { {} } - let(:headers) do - { - ::API::Integrations::Slack::Request::VERIFICATION_TIMESTAMP_HEADER => Time.current.to_i.to_s, - ::API::Integrations::Slack::Request::VERIFICATION_SIGNATURE_HEADER => 'mock_verified_signature' - } - end - - before do - allow(ActiveSupport::SecurityUtils).to receive(:secure_compare) do |signature| - signature == 'mock_verified_signature' - end - - stub_application_setting(slack_app_signing_secret: 'mock_key') - end - - subject { post api('/integrations/slack/events'), params: params, headers: headers } - - shared_examples 'an unauthorized request' do - specify do - subject - - expect(response).to have_gitlab_http_status(:unauthorized) - end - end - - shared_examples 'a successful request that generates a tracked error' do - specify do - expect(Gitlab::ErrorTracking).to receive(:track_exception).once - - subject - - expect(response).to have_gitlab_http_status(:no_content) - expect(response.body).to be_empty - end - end - - context 'when the slack_app_signing_secret setting is not set' do - before do - stub_application_setting(slack_app_signing_secret: nil) - end - - it_behaves_like 'an unauthorized request' - end - - context 'when the timestamp header has expired' do - before do - headers[::API::Integrations::Slack::Request::VERIFICATION_TIMESTAMP_HEADER] = 5.minutes.ago.to_i.to_s - end - - it_behaves_like 'an unauthorized request' - end - - context 'when the timestamp header is missing' do - before do - headers.delete(::API::Integrations::Slack::Request::VERIFICATION_TIMESTAMP_HEADER) - end - - it_behaves_like 'an unauthorized request' - end - - context 'when the signature header is missing' do - before do - headers.delete(::API::Integrations::Slack::Request::VERIFICATION_SIGNATURE_HEADER) - end - - it_behaves_like 'an unauthorized request' - end - - context 'when the signature is not verified' do - before do - headers[::API::Integrations::Slack::Request::VERIFICATION_SIGNATURE_HEADER] = 'unverified_signature' - end - - it_behaves_like 'an unauthorized request' - end - - context 'when type param is missing' do - it_behaves_like 'a successful request that generates a tracked error' - end - - context 'when type param is unknown' do - let(:params) do - { type: 'unknown_type' } - end - - it_behaves_like 'a successful request that generates a tracked error' - end - - context 'when type param is url_verification' do - let(:params) do - { - type: 'url_verification', - challenge: '3eZbrw1aBm2rZgRNFdxV2595E9CY3gmdALWMmHkvFXO7tYXAYM8P' - } - end - - it 'responds in-request with the challenge' do - subject - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response).to eq({ 'challenge' => '3eZbrw1aBm2rZgRNFdxV2595E9CY3gmdALWMmHkvFXO7tYXAYM8P' }) - end - end - end -end diff --git a/spec/requests/api/internal/base_spec.rb b/spec/requests/api/internal/base_spec.rb index e100684018a..1f6c241b3f5 100644 --- a/spec/requests/api/internal/base_spec.rb +++ b/spec/requests/api/internal/base_spec.rb @@ -3,6 +3,7 @@ require 'spec_helper' RSpec.describe API::Internal::Base do + include GitlabShellHelpers include APIInternalBaseHelpers let_it_be(:user, reload: true) { create(:user) } @@ -17,10 +18,14 @@ RSpec.describe API::Internal::Base do let(:snippet_changes) { "#{TestEnv::BRANCH_SHA['snippet/single-file']} #{TestEnv::BRANCH_SHA['snippet/edit-file']} refs/heads/snippet/edit-file" } describe "GET /internal/check" do + def perform_request(headers: gitlab_shell_internal_api_request_header) + get api("/internal/check"), headers: headers + end + it do expect_any_instance_of(Redis).to receive(:ping).and_return('PONG') - get api("/internal/check"), params: { secret_token: secret_token } + perform_request expect(response).to have_gitlab_http_status(:ok) expect(json_response['api_version']).to eq(API::API.version) @@ -30,24 +35,57 @@ RSpec.describe API::Internal::Base do it 'returns false for field `redis` when redis is unavailable' do expect_any_instance_of(Redis).to receive(:ping).and_raise(Errno::ENOENT) - get api("/internal/check"), params: { secret_token: secret_token } + perform_request expect(json_response['redis']).to be(false) end context 'authenticating' do - it 'authenticates using a header' do - get api("/internal/check"), - headers: { API::Helpers::GITLAB_SHARED_SECRET_HEADER => Base64.encode64(secret_token) } + it 'authenticates using a jwt token in a header' do + perform_request expect(response).to have_gitlab_http_status(:ok) end - it 'returns 401 when no credentials provided' do - get(api("/internal/check")) + it 'returns 401 when jwt token is expired' do + headers = gitlab_shell_internal_api_request_header + + travel_to(2.minutes.since) do + perform_request(headers: headers) + end + + expect(response).to have_gitlab_http_status(:unauthorized) + end + + it 'returns 401 when jwt issuer is not Gitlab-Shell' do + perform_request(headers: gitlab_shell_internal_api_request_header(issuer: "gitlab-workhorse")) expect(response).to have_gitlab_http_status(:unauthorized) end + + it 'returns 401 when jwt token is not provided, even if plain secret is provided' do + perform_request(headers: { API::Helpers::GITLAB_SHARED_SECRET_HEADER => Base64.encode64(secret_token) }) + + expect(response).to have_gitlab_http_status(:unauthorized) + end + + context 'when gitlab_shell_jwt_token is disabled' do + before do + stub_feature_flags(gitlab_shell_jwt_token: false) + end + + it 'authenticates using a header' do + perform_request(headers: { API::Helpers::GITLAB_SHARED_SECRET_HEADER => Base64.encode64(secret_token) }) + + expect(response).to have_gitlab_http_status(:ok) + end + + it 'returns 401 when no credentials provided' do + get(api("/internal/check")) + + expect(response).to have_gitlab_http_status(:unauthorized) + end + end end end @@ -56,10 +94,8 @@ RSpec.describe API::Internal::Base do subject do post api('/internal/two_factor_recovery_codes'), - params: { - secret_token: secret_token, - key_id: key_id - } + params: { key_id: key_id }, + headers: gitlab_shell_internal_api_request_header end it_behaves_like 'actor key validations' @@ -105,10 +141,8 @@ RSpec.describe API::Internal::Base do subject do post api('/internal/personal_access_token'), - params: { - secret_token: secret_token, - key_id: key_id - } + params: { key_id: key_id }, + headers: gitlab_shell_internal_api_request_header end it_behaves_like 'actor key validations' @@ -126,10 +160,8 @@ RSpec.describe API::Internal::Base do it 'returns an error message when given an non existent user' do post api('/internal/personal_access_token'), - params: { - secret_token: secret_token, - user_id: 0 - } + params: { user_id: 0 }, + headers: gitlab_shell_internal_api_request_header expect(json_response['success']).to be_falsey expect(json_response['message']).to eq("Could not find the given user") @@ -137,10 +169,8 @@ RSpec.describe API::Internal::Base do it 'returns an error message when no name parameter is received' do post api('/internal/personal_access_token'), - params: { - secret_token: secret_token, - key_id: key.id - } + params: { key_id: key.id }, + headers: gitlab_shell_internal_api_request_header expect(json_response['success']).to be_falsey expect(json_response['message']).to eq("No token name specified") @@ -148,11 +178,8 @@ RSpec.describe API::Internal::Base do it 'returns an error message when no scopes parameter is received' do post api('/internal/personal_access_token'), - params: { - secret_token: secret_token, - key_id: key.id, - name: 'newtoken' - } + params: { key_id: key.id, name: 'newtoken' }, + headers: gitlab_shell_internal_api_request_header expect(json_response['success']).to be_falsey expect(json_response['message']).to eq("No token scopes specified") @@ -161,12 +188,12 @@ RSpec.describe API::Internal::Base do it 'returns an error message when expires_at contains an invalid date' do post api('/internal/personal_access_token'), params: { - secret_token: secret_token, - key_id: key.id, + key_id: key.id, name: 'newtoken', scopes: ['api'], expires_at: 'invalid-date' - } + }, + headers: gitlab_shell_internal_api_request_header expect(json_response['success']).to be_falsey expect(json_response['message']).to eq("Invalid token expiry date: 'invalid-date'") @@ -175,11 +202,11 @@ RSpec.describe API::Internal::Base do it 'returns an error message when it receives an invalid scope' do post api('/internal/personal_access_token'), params: { - secret_token: secret_token, - key_id: key.id, + key_id: key.id, name: 'newtoken', scopes: %w(read_api badscope read_repository) - } + }, + headers: gitlab_shell_internal_api_request_header expect(json_response['success']).to be_falsey expect(json_response['message']).to match(/\AInvalid scope: 'badscope'. Valid scopes are: /) @@ -190,11 +217,11 @@ RSpec.describe API::Internal::Base do post api('/internal/personal_access_token'), params: { - secret_token: secret_token, - key_id: key.id, + 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/) @@ -207,12 +234,12 @@ RSpec.describe API::Internal::Base do post api('/internal/personal_access_token'), params: { - secret_token: secret_token, - key_id: key.id, + key_id: key.id, name: 'newtoken', scopes: %w(read_api read_repository), expires_at: '9001-11-17' - } + }, + 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/) @@ -309,7 +336,7 @@ RSpec.describe API::Internal::Base do describe "GET /internal/discover" do it "finds a user by key id" do - get(api("/internal/discover"), params: { key_id: key.id, secret_token: secret_token }) + get(api("/internal/discover"), params: { key_id: key.id }, headers: gitlab_shell_internal_api_request_header) expect(response).to have_gitlab_http_status(:ok) @@ -317,7 +344,7 @@ RSpec.describe API::Internal::Base do end it "finds a user by username" do - get(api("/internal/discover"), params: { username: user.username, secret_token: secret_token }) + get(api("/internal/discover"), params: { username: user.username }, headers: gitlab_shell_internal_api_request_header) expect(response).to have_gitlab_http_status(:ok) @@ -325,7 +352,7 @@ RSpec.describe API::Internal::Base do end it 'responds successfully when a user is not found' do - get(api('/internal/discover'), params: { username: 'noone', secret_token: secret_token }) + get(api('/internal/discover'), params: { username: 'noone' }, headers: gitlab_shell_internal_api_request_header) expect(response).to have_gitlab_http_status(:ok) @@ -333,7 +360,7 @@ RSpec.describe API::Internal::Base do end it 'response successfully when passing invalid params' do - get(api('/internal/discover'), params: { nothing: 'to find a user', secret_token: secret_token }) + get(api('/internal/discover'), params: { nothing: 'to find a user' }, headers: gitlab_shell_internal_api_request_header) expect(response).to have_gitlab_http_status(:ok) @@ -344,7 +371,7 @@ RSpec.describe API::Internal::Base do describe "GET /internal/authorized_keys" do context "using an existing key" do it "finds the key" do - get(api('/internal/authorized_keys'), params: { key: key.key.split[1], secret_token: secret_token }) + get(api('/internal/authorized_keys'), params: { key: key.key.split[1] }, headers: gitlab_shell_internal_api_request_header) expect(response).to have_gitlab_http_status(:ok) expect(json_response['id']).to eq(key.id) @@ -352,7 +379,7 @@ RSpec.describe API::Internal::Base do end it 'exposes the comment of the key as a simple identifier of username + hostname' do - get(api('/internal/authorized_keys'), params: { key: key.key.split[1], secret_token: secret_token }) + get(api('/internal/authorized_keys'), params: { key: key.key.split[1] }, headers: gitlab_shell_internal_api_request_header) expect(response).to have_gitlab_http_status(:ok) expect(json_response['key']).to include("#{key.user_name} (#{Gitlab.config.gitlab.host})") @@ -360,13 +387,13 @@ RSpec.describe API::Internal::Base do end it "returns 404 with a partial key" do - get(api('/internal/authorized_keys'), params: { key: key.key.split[1][0...-3], secret_token: secret_token }) + get(api('/internal/authorized_keys'), params: { key: key.key.split[1][0...-3] }, headers: gitlab_shell_internal_api_request_header) expect(response).to have_gitlab_http_status(:not_found) end it "returns 404 with an not valid base64 string" do - get(api('/internal/authorized_keys'), params: { key: "whatever!", secret_token: secret_token }) + get(api('/internal/authorized_keys'), params: { key: "whatever!" }, headers: gitlab_shell_internal_api_request_header) expect(response).to have_gitlab_http_status(:not_found) end @@ -609,9 +636,9 @@ RSpec.describe API::Internal::Base do project: full_path_for(project), gl_repository: gl_repository_for(project), action: 'git-upload-pack', - secret_token: secret_token, protocol: 'ssh' - } + }, + headers: gitlab_shell_internal_api_request_header ) end end @@ -994,9 +1021,9 @@ RSpec.describe API::Internal::Base do key_id: key.id, project: 'project/does-not-exist.git', action: 'git-upload-pack', - secret_token: secret_token, protocol: 'ssh' - } + }, + headers: gitlab_shell_internal_api_request_header ) expect(response).to have_gitlab_http_status(:not_found) @@ -1170,9 +1197,9 @@ RSpec.describe API::Internal::Base do key_id: key.id, project: project.full_path, gl_repository: gl_repository, - secret_token: secret_token, protocol: 'ssh' - }) + }, headers: gitlab_shell_internal_api_request_header + ) expect(response).to have_gitlab_http_status(:unauthorized) end @@ -1285,7 +1312,6 @@ RSpec.describe API::Internal::Base do let(:valid_params) do { gl_repository: gl_repository, - secret_token: secret_token, identifier: identifier, changes: changes, push_options: push_options @@ -1296,7 +1322,7 @@ RSpec.describe API::Internal::Base do "#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/#{branch_name}" end - subject { post api('/internal/post_receive'), params: valid_params } + subject { post api('/internal/post_receive'), params: valid_params, headers: gitlab_shell_internal_api_request_header } before do project.add_developer(user) @@ -1397,7 +1423,7 @@ RSpec.describe API::Internal::Base do describe 'POST /internal/pre_receive' do let(:valid_params) do - { gl_repository: gl_repository, secret_token: secret_token } + { gl_repository: gl_repository } end it 'decreases the reference counter and returns the result' do @@ -1405,7 +1431,7 @@ RSpec.describe API::Internal::Base do .and_return(reference_counter) expect(reference_counter).to receive(:increase).and_return(true) - post api("/internal/pre_receive"), params: valid_params + post api("/internal/pre_receive"), params: valid_params, headers: gitlab_shell_internal_api_request_header expect(json_response['reference_counter_increased']).to be(true) end @@ -1420,10 +1446,8 @@ RSpec.describe API::Internal::Base do subject do post api('/internal/two_factor_config'), - params: { - secret_token: secret_token, - key_id: key_id - } + params: { key_id: key_id }, + headers: gitlab_shell_internal_api_request_header end it_behaves_like 'actor key validations' @@ -1478,27 +1502,6 @@ RSpec.describe API::Internal::Base do end end - describe 'POST /internal/two_factor_otp_check' do - let(:key_id) { key.id } - let(:otp) { '123456' } - - subject do - post api('/internal/two_factor_otp_check'), - params: { - secret_token: secret_token, - key_id: key_id, - otp_attempt: otp - } - end - - it 'is not available' do - subject - - expect(json_response['success']).to be_falsey - expect(json_response['message']).to eq 'Feature is not available' - end - end - describe 'POST /internal/two_factor_manual_otp_check' do let(:key_id) { key.id } let(:otp) { '123456' } @@ -1509,7 +1512,8 @@ RSpec.describe API::Internal::Base do secret_token: secret_token, key_id: key_id, otp_attempt: otp - } + }, + headers: gitlab_shell_internal_api_request_header end it 'is not available' do @@ -1530,7 +1534,8 @@ RSpec.describe API::Internal::Base do secret_token: secret_token, key_id: key_id, otp_attempt: otp - } + }, + headers: gitlab_shell_internal_api_request_header end it 'is not available' do @@ -1551,7 +1556,8 @@ RSpec.describe API::Internal::Base do secret_token: secret_token, key_id: key_id, otp_attempt: otp - } + }, + headers: gitlab_shell_internal_api_request_header end it 'is not available' do @@ -1571,7 +1577,8 @@ RSpec.describe API::Internal::Base do secret_token: secret_token, key_id: key_id, otp_attempt: otp - } + }, + headers: gitlab_shell_internal_api_request_header end it 'is not available' do @@ -1584,32 +1591,24 @@ RSpec.describe API::Internal::Base do def lfs_auth_project(project) post( api("/internal/lfs_authenticate"), - params: { - secret_token: secret_token, - project: project.full_path - } + params: { project: project.full_path }, + headers: gitlab_shell_internal_api_request_header ) end def lfs_auth_key(key_id, project) post( api("/internal/lfs_authenticate"), - params: { - key_id: key_id, - secret_token: secret_token, - project: project.full_path - } + params: { key_id: key_id, project: project.full_path }, + headers: gitlab_shell_internal_api_request_header ) end def lfs_auth_user(user_id, project) post( api("/internal/lfs_authenticate"), - params: { - user_id: user_id, - secret_token: secret_token, - project: project.full_path - } + params: { user_id: user_id, project: project.full_path }, + headers: gitlab_shell_internal_api_request_header ) end end diff --git a/spec/requests/api/internal/lfs_spec.rb b/spec/requests/api/internal/lfs_spec.rb index 4739ec62992..9eb48db5bd5 100644 --- a/spec/requests/api/internal/lfs_spec.rb +++ b/spec/requests/api/internal/lfs_spec.rb @@ -3,6 +3,7 @@ require 'spec_helper' RSpec.describe API::Internal::Lfs do + include GitlabShellHelpers include APIInternalBaseHelpers let_it_be(:project) { create(:project) } @@ -11,25 +12,23 @@ RSpec.describe API::Internal::Lfs do let_it_be(:gl_repository) { "project-#{project.id}" } let_it_be(:filename) { lfs_object.file.path } - let(:secret_token) { Gitlab::Shell.secret_token } - describe 'GET /internal/lfs' do let(:valid_params) do - { oid: lfs_object.oid, gl_repository: gl_repository, secret_token: secret_token } + { oid: lfs_object.oid, gl_repository: gl_repository } end context 'with invalid auth' do - let(:invalid_params) { valid_params.merge!(secret_token: 'invalid_tokne') } - it 'returns 401' do - get api("/internal/lfs"), params: invalid_params + get api("/internal/lfs"), + params: valid_params, + headers: gitlab_shell_internal_api_request_header(issuer: 'gitlab-workhorse') end end context 'with valid auth' do context 'LFS in local storage' do it 'sends the file' do - get api("/internal/lfs"), params: valid_params + get api("/internal/lfs"), params: valid_params, headers: gitlab_shell_internal_api_request_header expect(response).to have_gitlab_http_status(:ok) expect(response.headers['Content-Type']).to eq('application/octet-stream') @@ -39,7 +38,10 @@ RSpec.describe API::Internal::Lfs do # https://www.rubydoc.info/github/rack/rack/master/Rack/Sendfile it 'delegates sending to Web server' do - get api("/internal/lfs"), params: valid_params, env: { 'HTTP_X_SENDFILE_TYPE' => 'X-Sendfile' } + get api("/internal/lfs"), + params: valid_params, + env: { 'HTTP_X_SENDFILE_TYPE' => 'X-Sendfile' }, + headers: gitlab_shell_internal_api_request_header expect(response).to have_gitlab_http_status(:ok) expect(response.headers['Content-Type']).to eq('application/octet-stream') @@ -51,7 +53,7 @@ RSpec.describe API::Internal::Lfs do it 'retuns 404 for unknown file' do params = valid_params.merge(oid: SecureRandom.hex) - get api("/internal/lfs"), params: params + get api("/internal/lfs"), params: params, headers: gitlab_shell_internal_api_request_header expect(response).to have_gitlab_http_status(:not_found) end @@ -60,7 +62,7 @@ RSpec.describe API::Internal::Lfs do other_lfs = create(:lfs_object, :with_file) params = valid_params.merge(oid: other_lfs.oid) - get api("/internal/lfs"), params: params + get api("/internal/lfs"), params: params, headers: gitlab_shell_internal_api_request_header expect(response).to have_gitlab_http_status(:not_found) end @@ -70,7 +72,7 @@ RSpec.describe API::Internal::Lfs do let!(:lfs_object2) { create(:lfs_object, :with_file) } let!(:lfs_objects_project2) { create(:lfs_objects_project, project: project, lfs_object: lfs_object2) } let(:valid_params) do - { oid: lfs_object2.oid, gl_repository: gl_repository, secret_token: secret_token } + { oid: lfs_object2.oid, gl_repository: gl_repository } end before do @@ -79,7 +81,7 @@ RSpec.describe API::Internal::Lfs do end it 'notifies Workhorse to send the file' do - get api("/internal/lfs"), params: valid_params + get api("/internal/lfs"), params: valid_params, headers: gitlab_shell_internal_api_request_header expect(response).to have_gitlab_http_status(:ok) expect(response.headers[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("send-url:") diff --git a/spec/requests/api/issues/get_group_issues_spec.rb b/spec/requests/api/issues/get_group_issues_spec.rb index 3663a82891c..5c06214316b 100644 --- a/spec/requests/api/issues/get_group_issues_spec.rb +++ b/spec/requests/api/issues/get_group_issues_spec.rb @@ -465,10 +465,10 @@ RSpec.describe API::Issues do context 'with archived projects' do let_it_be(:archived_issue) do - create( - :issue, author: user, assignees: [user], - project: create(:project, :public, :archived, creator_id: user.id, namespace: group) - ) + create(:issue, + author: user, + assignees: [user], + project: create(:project, :public, :archived, creator_id: user.id, namespace: group)) end it 'returns only non archived projects issues' do diff --git a/spec/requests/api/markdown_snapshot_spec.rb b/spec/requests/api/markdown_snapshot_spec.rb index 1270efdfd6f..f2019172a54 100644 --- a/spec/requests/api/markdown_snapshot_spec.rb +++ b/spec/requests/api/markdown_snapshot_spec.rb @@ -5,7 +5,5 @@ require 'spec_helper' # See https://docs.gitlab.com/ee/development/gitlab_flavored_markdown/specification_guide/#markdown-snapshot-testing # for documentation on this spec. RSpec.describe API::Markdown, 'Snapshot' do - # noinspection RubyMismatchedArgumentType (ignore RBS type warning: __dir__ can be nil, but 2nd argument can't be nil) - glfm_specification_dir = File.expand_path('../../../glfm_specification', __dir__) - include_context 'with API::Markdown Snapshot shared context', glfm_specification_dir + include_context 'with API::Markdown Snapshot shared context' end diff --git a/spec/requests/api/maven_packages_spec.rb b/spec/requests/api/maven_packages_spec.rb index 1b378788b6a..d7cc6991ef4 100644 --- a/spec/requests/api/maven_packages_spec.rb +++ b/spec/requests/api/maven_packages_spec.rb @@ -2,6 +2,7 @@ require 'spec_helper' RSpec.describe API::MavenPackages do + using RSpec::Parameterized::TableSyntax include WorkhorseHelpers include_context 'workhorse headers' @@ -40,15 +41,15 @@ RSpec.describe API::MavenPackages do project.add_developer(user) end - shared_examples 'handling groups and subgroups for' do |shared_example_name, visibilities: %i[public]| + shared_examples 'handling groups and subgroups for' do |shared_example_name, visibilities: { public: :redirect }| context 'within a group' do - visibilities.each do |visibility| + visibilities.each do |visibility, not_found_response| context "that is #{visibility}" do before do group.update!(visibility_level: Gitlab::VisibilityLevel.level_value(visibility.to_s)) end - it_behaves_like shared_example_name + it_behaves_like shared_example_name, not_found_response end end end @@ -60,20 +61,20 @@ RSpec.describe API::MavenPackages do move_project_to_namespace(subgroup) end - visibilities.each do |visibility| + visibilities.each do |visibility, not_found_response| context "that is #{visibility}" do before do subgroup.update!(visibility_level: Gitlab::VisibilityLevel.level_value(visibility.to_s)) group.update!(visibility_level: Gitlab::VisibilityLevel.level_value(visibility.to_s)) end - it_behaves_like shared_example_name + it_behaves_like shared_example_name, not_found_response end end end end - shared_examples 'handling groups, subgroups and user namespaces for' do |shared_example_name, visibilities: %i[public]| + shared_examples 'handling groups, subgroups and user namespaces for' do |shared_example_name, visibilities: { public: :redirect }| it_behaves_like 'handling groups and subgroups for', shared_example_name, visibilities: visibilities context 'within a user namespace' do @@ -103,16 +104,6 @@ RSpec.describe API::MavenPackages do end end - shared_examples 'rejecting the request for non existing maven path' do |expected_status: :not_found| - it 'rejects the request' do - expect(::Packages::Maven::PackageFinder).not_to receive(:new) - - subject - - expect(response).to have_gitlab_http_status(expected_status) - end - end - shared_examples 'processing HEAD requests' do |instance_level: false| subject { head api(url) } @@ -162,7 +153,7 @@ RSpec.describe API::MavenPackages do context 'with a non existing maven path' do let(:path) { 'foo/bar/1.2.3' } - it_behaves_like 'rejecting the request for non existing maven path', expected_status: instance_level ? :forbidden : :not_found + it_behaves_like 'returning response status', instance_level ? :forbidden : :redirect end end end @@ -238,12 +229,66 @@ RSpec.describe API::MavenPackages do end end + shared_examples 'forwarding package requests' do + context 'request forwarding' do + include_context 'dependency proxy helpers context' + + subject { download_file(file_name: package_name) } + + shared_examples 'redirecting the request' do + it_behaves_like 'returning response status', :redirect + end + + shared_examples 'package not found' do + it_behaves_like 'returning response status', :not_found + end + + where(:forward, :package_in_project, :shared_examples_name) do + true | true | 'successfully returning the file' + true | false | 'redirecting the request' + false | true | 'successfully returning the file' + false | false | 'package not found' + end + + with_them do + let(:package_name) { package_in_project ? package_file.file_name : 'foo' } + + before do + allow_fetch_application_setting(attribute: 'maven_package_requests_forwarding', return_value: forward) + end + + it_behaves_like params[:shared_examples_name] + end + + context 'with maven_central_request_forwarding disabled' do + where(:forward, :package_in_project, :shared_examples_name) do + true | true | 'successfully returning the file' + true | false | 'package not found' + false | true | 'successfully returning the file' + false | false | 'package not found' + end + + with_them do + let(:package_name) { package_in_project ? package_file.file_name : 'foo' } + + before do + stub_feature_flags(maven_central_request_forwarding: false) + allow_fetch_application_setting(attribute: 'maven_package_requests_forwarding', return_value: forward) + end + + it_behaves_like params[:shared_examples_name] + end + end + end + end + describe 'GET /api/v4/packages/maven/*path/:file_name' do context 'a public project' do subject { download_file(file_name: package_file.file_name) } shared_examples 'getting a file' do it_behaves_like 'tracking the file download event' + it_behaves_like 'bumping the package last downloaded at field' it_behaves_like 'successfully returning the file' it_behaves_like 'file download in FIPS mode' @@ -258,7 +303,16 @@ RSpec.describe API::MavenPackages do context 'with a non existing maven path' do subject { download_file(file_name: package_file.file_name, path: 'foo/bar/1.2.3') } - it_behaves_like 'rejecting the request for non existing maven path', expected_status: :forbidden + it_behaves_like 'returning response status', :forbidden + end + + it 'returns not found when a package is not found' do + finder = double('finder', execute: nil) + expect(::Packages::Maven::PackageFinder).to receive(:new).and_return(finder) + + subject + + expect(response).to have_gitlab_http_status(:not_found) end end @@ -275,7 +329,7 @@ RSpec.describe API::MavenPackages do shared_examples 'getting a file' do it_behaves_like 'tracking the file download event' - + it_behaves_like 'bumping the package last downloaded at field' it_behaves_like 'successfully returning the file' it 'denies download when no private token' do @@ -285,17 +339,16 @@ RSpec.describe API::MavenPackages do end it_behaves_like 'downloads with a job token' - it_behaves_like 'downloads with a deploy token' context 'with a non existing maven path' do subject { download_file_with_token(file_name: package_file.file_name, path: 'foo/bar/1.2.3') } - it_behaves_like 'rejecting the request for non existing maven path', expected_status: :forbidden + it_behaves_like 'returning response status', :forbidden end end - it_behaves_like 'handling groups, subgroups and user namespaces for', 'getting a file', visibilities: %i[public internal] + it_behaves_like 'handling groups, subgroups and user namespaces for', 'getting a file', visibilities: { public: :redirect, internal: :not_found } end context 'private project' do @@ -307,7 +360,7 @@ RSpec.describe API::MavenPackages do shared_examples 'getting a file' do it_behaves_like 'tracking the file download event' - + it_behaves_like 'bumping the package last downloaded at field' it_behaves_like 'successfully returning the file' it 'denies download when not enough permissions' do @@ -327,7 +380,6 @@ RSpec.describe API::MavenPackages do end it_behaves_like 'downloads with a job token' - it_behaves_like 'downloads with a deploy token' it 'does not allow download by a unauthorized deploy token with same id as a user with access' do @@ -350,11 +402,11 @@ RSpec.describe API::MavenPackages do context 'with a non existing maven path' do subject { download_file_with_token(file_name: package_file.file_name, path: 'foo/bar/1.2.3') } - it_behaves_like 'rejecting the request for non existing maven path', expected_status: :forbidden + it_behaves_like 'returning response status', :forbidden end end - it_behaves_like 'handling groups, subgroups and user namespaces for', 'getting a file', visibilities: %i[public internal private] + it_behaves_like 'handling groups, subgroups and user namespaces for', 'getting a file', visibilities: { public: :redirect, internal: :not_found, private: :not_found } end context 'project name is different from a package name' do @@ -409,11 +461,14 @@ RSpec.describe API::MavenPackages do group.add_developer(user) end + it_behaves_like 'forwarding package requests' + context 'a public project' do subject { download_file(file_name: package_file.file_name) } shared_examples 'getting a file for a group' do it_behaves_like 'tracking the file download event' + it_behaves_like 'bumping the package last downloaded at field' it_behaves_like 'successfully returning the file' it_behaves_like 'file download in FIPS mode' @@ -428,7 +483,7 @@ RSpec.describe API::MavenPackages do context 'with a non existing maven path' do subject { download_file(file_name: package_file.file_name, path: 'foo/bar/1.2.3') } - it_behaves_like 'rejecting the request for non existing maven path' + it_behaves_like 'returning response status', :redirect end end @@ -443,29 +498,28 @@ RSpec.describe API::MavenPackages do subject { download_file_with_token(file_name: package_file.file_name) } - shared_examples 'getting a file for a group' do + shared_examples 'getting a file for a group' do |not_found_response| it_behaves_like 'tracking the file download event' - + it_behaves_like 'bumping the package last downloaded at field' it_behaves_like 'successfully returning the file' - it 'denies download when no private token' do + it 'forwards download when no private token' do download_file(file_name: package_file.file_name) - expect(response).to have_gitlab_http_status(:not_found) + expect(response).to have_gitlab_http_status(not_found_response) end it_behaves_like 'downloads with a job token' - it_behaves_like 'downloads with a deploy token' context 'with a non existing maven path' do subject { download_file_with_token(file_name: package_file.file_name, path: 'foo/bar/1.2.3') } - it_behaves_like 'rejecting the request for non existing maven path' + it_behaves_like 'returning response status', :redirect end end - it_behaves_like 'handling groups and subgroups for', 'getting a file for a group', visibilities: %i[internal public] + it_behaves_like 'handling groups and subgroups for', 'getting a file for a group', visibilities: { internal: :not_found, public: :redirect } end context 'private project' do @@ -475,9 +529,9 @@ RSpec.describe API::MavenPackages do subject { download_file_with_token(file_name: package_file.file_name) } - shared_examples 'getting a file for a group' do + shared_examples 'getting a file for a group' do |not_found_response| it_behaves_like 'tracking the file download event' - + it_behaves_like 'bumping the package last downloaded at field' it_behaves_like 'successfully returning the file' it 'denies download when not enough permissions' do @@ -485,23 +539,22 @@ RSpec.describe API::MavenPackages do subject - expect(response).to have_gitlab_http_status(:not_found) + expect(response).to have_gitlab_http_status(:redirect) end it 'denies download when no private token' do download_file(file_name: package_file.file_name) - expect(response).to have_gitlab_http_status(:not_found) + expect(response).to have_gitlab_http_status(not_found_response) end it_behaves_like 'downloads with a job token' - it_behaves_like 'downloads with a deploy token' context 'with a non existing maven path' do subject { download_file_with_token(file_name: package_file.file_name, path: 'foo/bar/1.2.3') } - it_behaves_like 'rejecting the request for non existing maven path' + it_behaves_like 'returning response status', :redirect end context 'with group deploy token' do @@ -521,12 +574,12 @@ RSpec.describe API::MavenPackages do context 'with a non existing maven path' do subject { download_file_with_token(file_name: package_file.file_name, path: 'foo/bar/1.2.3', request_headers: group_deploy_token_headers) } - it_behaves_like 'rejecting the request for non existing maven path' + it_behaves_like 'returning response status', :redirect end end end - it_behaves_like 'handling groups and subgroups for', 'getting a file for a group', visibilities: %i[private internal public] + it_behaves_like 'handling groups and subgroups for', 'getting a file for a group', visibilities: { private: :not_found, internal: :not_found, public: :redirect } context 'with a reporter from a subgroup accessing the root group' do let_it_be(:root_group) { create(:group, :private) } @@ -544,7 +597,7 @@ RSpec.describe API::MavenPackages do context 'with a non existing maven path' do subject { download_file_with_token(file_name: package_file.file_name, path: 'foo/bar/1.2.3', request_headers: headers_with_token, group_id: root_group.id) } - it_behaves_like 'rejecting the request for non existing maven path' + it_behaves_like 'returning response status', :redirect end end end @@ -640,12 +693,14 @@ RSpec.describe API::MavenPackages do it_behaves_like 'successfully returning the file' it_behaves_like 'file download in FIPS mode' - it 'returns sha1 of the file' do - download_file(file_name: package_file.file_name + '.sha1') + %w[sha1 md5].each do |format| + it "returns #{format} of the file" do + download_file(file_name: package_file.file_name + ".#{format}") - expect(response).to have_gitlab_http_status(:ok) - expect(response.media_type).to eq('text/plain') - expect(response.body).to eq(package_file.file_sha1) + expect(response).to have_gitlab_http_status(:ok) + expect(response.media_type).to eq('text/plain') + expect(response.body).to eq(package_file.send("file_#{format}".to_sym)) + end end context 'when the repository is disabled' do @@ -664,7 +719,7 @@ RSpec.describe API::MavenPackages do context 'with a non existing maven path' do subject { download_file(file_name: package_file.file_name, path: 'foo/bar/1.2.3') } - it_behaves_like 'rejecting the request for non existing maven path' + it_behaves_like 'returning response status', :redirect end end @@ -676,7 +731,7 @@ RSpec.describe API::MavenPackages do subject { download_file_with_token(file_name: package_file.file_name) } it_behaves_like 'tracking the file download event' - + it_behaves_like 'bumping the package last downloaded at field' it_behaves_like 'successfully returning the file' it 'denies download when not enough permissions' do @@ -694,16 +749,17 @@ RSpec.describe API::MavenPackages do end it_behaves_like 'downloads with a job token' - it_behaves_like 'downloads with a deploy token' context 'with a non existing maven path' do subject { download_file_with_token(file_name: package_file.file_name, path: 'foo/bar/1.2.3') } - it_behaves_like 'rejecting the request for non existing maven path' + it_behaves_like 'returning response status', :redirect end end + it_behaves_like 'forwarding package requests' + def download_file(file_name:, params: {}, request_headers: headers, path: maven_metadatum.path) get api("/projects/#{project.id}/packages/maven/" \ "#{path}/#{file_name}"), params: params, headers: request_headers diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 2a03ae89389..9d153286d14 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -9,6 +9,7 @@ RSpec.describe API::MergeRequests do let_it_be(:user) { create(:user) } let_it_be(:user2) { create(:user) } let_it_be(:admin) { create(:user, :admin) } + let_it_be(:bot) { create(:user, :project_bot) } let_it_be(:project) { create(:project, :public, :repository, creator: user, namespace: user.namespace, only_allow_merge_if_pipeline_succeeds: false) } let(:milestone1) { create(:milestone, title: '0.9', project: project) } @@ -1022,6 +1023,22 @@ RSpec.describe API::MergeRequests do it_behaves_like 'a non-cached MergeRequest api request', 1 end + context 'when the assignees change' do + before do + merge_request.assignees << create(:user) + end + + it_behaves_like 'a non-cached MergeRequest api request', 1 + end + + context 'when the reviewers change' do + before do + merge_request.reviewers << create(:user) + end + + it_behaves_like 'a non-cached MergeRequest api request', 1 + end + context 'when another user requests' do before do sign_in(user2) @@ -1120,6 +1137,44 @@ RSpec.describe API::MergeRequests do end.not_to exceed_query_limit(control) end end + + context 'when user is an inherited member from the group' do + let_it_be(:group) { create(:group) } + + shared_examples 'user cannot view merge requests' do + it 'returns 403 forbidden' do + get api("/projects/#{group_project.id}/merge_requests", inherited_user) + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + context 'and user is a guest' do + let_it_be(:inherited_user) { create(:user) } + + before_all do + group.add_guest(inherited_user) + end + + context 'when project is public with private merge requests' do + let(:group_project) do + create(:project, + :public, + :repository, + group: group, + merge_requests_access_level: ProjectFeature::DISABLED) + end + + it_behaves_like 'user cannot view merge requests' + end + + context 'when project is private' do + let(:group_project) { create(:project, :private, :repository, group: group) } + + it_behaves_like 'user cannot view merge requests' + end + end + end end describe "GET /groups/:id/merge_requests" do @@ -1528,7 +1583,6 @@ RSpec.describe API::MergeRequests do expect(json_response.last['user']['name']).to eq(reviewer.name) expect(json_response.last['user']['username']).to eq(reviewer.username) expect(json_response.last['state']).to eq('unreviewed') - expect(json_response.last['updated_state_by']).to be_nil expect(json_response.last['created_at']).to be_present end @@ -2219,6 +2273,59 @@ RSpec.describe API::MergeRequests do expect(response).to have_gitlab_http_status(:created) end end + + context 'when user is an inherited member from the group' do + let_it_be(:group) { create(:group) } + + shared_examples 'user cannot create merge requests' do + it 'returns 403 forbidden' do + post api("/projects/#{group_project.id}/merge_requests", inherited_user), params: params + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + context 'and user is a guest' do + let_it_be(:inherited_user) { create(:user) } + let_it_be(:params) do + { + title: 'Test merge request', + source_branch: 'feature_conflict', + target_branch: 'master', + author_id: inherited_user.id + } + end + + before_all do + group.add_guest(inherited_user) + end + + context 'when project is public with private merge requests' do + let(:group_project) do + create(:project, + :public, + :repository, + group: group, + merge_requests_access_level: ProjectFeature::DISABLED, + only_allow_merge_if_pipeline_succeeds: false) + end + + it_behaves_like 'user cannot create merge requests' + end + + context 'when project is private' do + let(:group_project) do + create(:project, + :private, + :repository, + group: group, + only_allow_merge_if_pipeline_succeeds: false) + end + + it_behaves_like 'user cannot create merge requests' + end + end + end end describe 'PUT /projects/:id/merge_requests/:merge_request_iid' do @@ -2247,6 +2354,16 @@ RSpec.describe API::MergeRequests do expect(merge_request.notes.system.last.note).to include("assigned to #{user2.to_reference}") end + + it 'triggers webhooks', :sidekiq_inline do + hook = create(:project_hook, merge_requests_events: true, project: merge_request.project) + + expect(WebHookWorker).to receive(:perform_async).with(hook.id, anything, 'merge_request_hooks', anything) + + put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), params: params + + expect(response).to have_gitlab_http_status(:ok) + end end context 'when assignee_id=user2.id' do @@ -3373,7 +3490,8 @@ RSpec.describe API::MergeRequests do context 'when merge request branch does not allow force push' do before do - create(:protected_branch, project: project, name: merge_request.source_branch, allow_force_push: false) + create_params = { name: merge_request.source_branch, allow_force_push: false, merge_access_levels_attributes: [{ access_level: Gitlab::Access::DEVELOPER }] } + ProtectedBranches::CreateService.new(project, project.first_owner, create_params).execute end it 'returns 403' do @@ -3413,6 +3531,71 @@ RSpec.describe API::MergeRequests do end end + describe 'PUT :id/merge_requests/:merge_request_iid/reset_approvals' do + before do + merge_request.approvals.create!(user: user2) + create(:project_member, :maintainer, user: bot, source: project) + end + + context 'when reset_approvals can be performed' do + it 'clears approvals of the merge_request' do + put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/reset_approvals", bot) + + merge_request.reload + expect(response).to have_gitlab_http_status(:accepted) + expect(merge_request.approvals).to be_empty + end + + it 'for users with bot role' do + put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/reset_approvals", bot) + + expect(response).to have_gitlab_http_status(:accepted) + end + + context 'for users with non-bot roles' do + let(:human_user) { create(:user) } + + [:add_owner, :add_maintainer, :add_developer, :add_guest].each do |role_method| + it 'returns 401' do + project.send(role_method, human_user) + + put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/reset_approvals", human_user) + + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + end + + context 'for bot-users from external namespaces' do + let_it_be(:external_bot) { create(:user, :project_bot) } + + context 'external group bot-user' do + before do + create(:group_member, :maintainer, user: external_bot, source: create(:group)) + end + + it 'returns 401' do + put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/reset_approvals", external_bot) + + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + + context 'external project bot-user' do + before do + create(:project_member, :maintainer, user: external_bot, source: create(:project)) + end + + it 'returns 401' do + put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/reset_approvals", external_bot) + + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + end + end + end + describe 'Time tracking' do let!(:issuable) { create(:merge_request, :simple, author: user, assignees: [user], source_project: project, target_project: project, source_branch: 'markdown', title: "Test", created_at: base_time) } diff --git a/spec/requests/api/ml/mlflow_spec.rb b/spec/requests/api/ml/mlflow_spec.rb new file mode 100644 index 00000000000..4e7091a5b0f --- /dev/null +++ b/spec/requests/api/ml/mlflow_spec.rb @@ -0,0 +1,366 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'mime/types' + +RSpec.describe API::Ml::Mlflow do + include SessionHelpers + include ApiHelpers + include HttpBasicAuthHelpers + + let_it_be(:project) { create(:project, :private) } + let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } } + let_it_be(:experiment) do + create(:ml_experiments, user: project.creator, project: project) + end + + let_it_be(:candidate) do + create(:ml_candidates, user: experiment.user, start_time: 1234, experiment: experiment) + end + + let_it_be(:another_candidate) do + create(:ml_candidates, + experiment: create(:ml_experiments, project: create(:project))) + end + + let(:current_user) { developer } + let(:ff_value) { true } + let(:scopes) { %w[read_api api] } + let(:headers) do + { 'Authorization' => "Bearer #{create(:personal_access_token, scopes: scopes, user: current_user).token}" } + end + + let(:params) { {} } + let(:request) { get api(route), params: params, headers: headers } + + before do + stub_feature_flags(ml_experiment_tracking: ff_value) + + request + end + + shared_examples 'Not Found' do |message| + it "is Not Found" do + expect(response).to have_gitlab_http_status(:not_found) + + expect(json_response['message']).to eq(message) if message.present? + end + end + + shared_examples 'Not Found - Resource Does Not Exist' do + it "is Resource Does Not Exist" do + expect(response).to have_gitlab_http_status(:not_found) + + expect(json_response).to include({ "error_code" => 'RESOURCE_DOES_NOT_EXIST' }) + end + end + + shared_examples 'Requires api scope' do + context 'when user has access but token has wrong scope' do + let(:scopes) { %w[read_api] } + + it { expect(response).to have_gitlab_http_status(:forbidden) } + end + end + + shared_examples 'Requires read_api scope' do + context 'when user has access but token has wrong scope' do + let(:scopes) { %w[read_user] } + + it { expect(response).to have_gitlab_http_status(:forbidden) } + end + end + + shared_examples 'Bad Request' do |error_code = nil| + it "is Bad Request" do + expect(response).to have_gitlab_http_status(:bad_request) + + expect(json_response).to include({ 'error_code' => error_code }) if error_code.present? + end + end + + shared_examples 'shared error cases' do + context 'when not authenticated' do + let(:headers) { {} } + + it "is Unauthorized" do + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + + context 'when user does not have access' do + let(:current_user) { create(:user) } + + it_behaves_like 'Not Found' + end + + context 'when ff is disabled' do + let(:ff_value) { false } + + it_behaves_like 'Not Found' + end + end + + describe 'GET /projects/:id/ml/mflow/api/2.0/mlflow/get' do + let(:experiment_iid) { experiment.iid.to_s } + let(:route) { "/projects/#{project.id}/ml/mflow/api/2.0/mlflow/experiments/get?experiment_id=#{experiment_iid}" } + + it 'returns the experiment' do + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('ml/get_experiment') + expect(json_response).to include({ + 'experiment' => { + 'experiment_id' => experiment_iid, + 'name' => experiment.name, + 'lifecycle_stage' => 'active', + 'artifact_location' => 'not_implemented' + } + }) + end + + describe 'Error States' do + context 'when has access' do + context 'and experiment does not exist' do + let(:experiment_iid) { non_existing_record_iid.to_s } + + it_behaves_like 'Not Found - Resource Does Not Exist' + end + + context 'and experiment_id is not passed' do + let(:route) { "/projects/#{project.id}/ml/mflow/api/2.0/mlflow/experiments/get" } + + it_behaves_like 'Not Found - Resource Does Not Exist' + end + end + + it_behaves_like 'shared error cases' + it_behaves_like 'Requires read_api scope' + end + end + + describe 'GET /projects/:id/ml/mflow/api/2.0/mlflow/experiments/get-by-name' do + let(:experiment_name) { experiment.name } + let(:route) do + "/projects/#{project.id}/ml/mflow/api/2.0/mlflow/experiments/get-by-name?experiment_name=#{experiment_name}" + end + + it 'returns the experiment' do + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('ml/get_experiment') + expect(json_response).to include({ + 'experiment' => { + 'experiment_id' => experiment.iid.to_s, + 'name' => experiment_name, + 'lifecycle_stage' => 'active', + 'artifact_location' => 'not_implemented' + } + }) + end + + describe 'Error States' do + context 'when has access but experiment does not exist' do + let(:experiment_name) { "random_experiment" } + + it_behaves_like 'Not Found - Resource Does Not Exist' + end + + context 'when has access but experiment_name is not passed' do + let(:route) { "/projects/#{project.id}/ml/mflow/api/2.0/mlflow/experiments/get-by-name" } + + it_behaves_like 'Not Found - Resource Does Not Exist' + end + + it_behaves_like 'shared error cases' + it_behaves_like 'Requires read_api scope' + end + end + + describe 'POST /projects/:id/ml/mflow/api/2.0/mlflow/experiments/create' do + let(:route) do + "/projects/#{project.id}/ml/mflow/api/2.0/mlflow/experiments/create" + end + + let(:params) { { name: 'new_experiment' } } + let(:request) { post api(route), params: params, headers: headers } + + it 'creates the experiment' do + expect(response).to have_gitlab_http_status(:created) + expect(json_response).to include('experiment_id' ) + end + + describe 'Error States' do + context 'when experiment name is not passed' do + let(:params) { {} } + + it_behaves_like 'Bad Request' + end + + context 'when experiment name already exists' do + let(:existing_experiment) do + create(:ml_experiments, user: current_user, project: project) + end + + let(:params) { { name: existing_experiment.name } } + + it_behaves_like 'Bad Request', 'RESOURCE_ALREADY_EXISTS' + end + + context 'when project does not exist' do + let(:route) { "/projects/#{non_existing_record_id}/ml/mflow/api/2.0/mlflow/experiments/create" } + + it_behaves_like 'Not Found', '404 Project Not Found' + end + + it_behaves_like 'shared error cases' + it_behaves_like 'Requires api scope' + end + end + + describe 'Runs' do + describe 'POST /projects/:id/ml/mflow/api/2.0/mlflow/runs/create' do + let(:route) do + "/projects/#{project.id}/ml/mflow/api/2.0/mlflow/runs/create" + end + + let(:params) { { experiment_id: experiment.iid.to_s, start_time: Time.now.to_i } } + let(:request) { post api(route), params: params, headers: headers } + + it 'creates the run' do + expected_properties = { + 'experiment_id' => params[:experiment_id], + 'user_id' => current_user.id.to_s, + 'start_time' => params[:start_time], + 'artifact_uri' => 'not_implemented', + 'status' => "RUNNING", + 'lifecycle_stage' => "active" + } + + expect(response).to have_gitlab_http_status(:created) + expect(response).to match_response_schema('ml/run') + expect(json_response['run']).to include('info' => hash_including(**expected_properties), 'data' => {}) + end + + describe 'Error States' do + context 'when experiment id is not passed' do + let(:params) { {} } + + it_behaves_like 'Bad Request' + end + + context 'when experiment id does not exist' do + let(:params) { { experiment_id: non_existing_record_iid.to_s } } + + it_behaves_like 'Not Found - Resource Does Not Exist' + end + + it_behaves_like 'shared error cases' + it_behaves_like 'Requires api scope' + end + end + + describe 'GET /projects/:id/ml/mflow/api/2.0/mlflow/runs/get' do + let_it_be(:route) do + "/projects/#{project.id}/ml/mflow/api/2.0/mlflow/runs/get" + end + + let_it_be(:candidate) { create(:ml_candidates, user: experiment.user, start_time: 1234, experiment: experiment) } + + let(:params) { { 'run_id' => candidate.iid } } + + it 'gets the run' do + expected_properties = { + 'experiment_id' => candidate.experiment.iid.to_s, + 'user_id' => candidate.user.id.to_s, + 'start_time' => candidate.start_time, + 'artifact_uri' => 'not_implemented', + 'status' => "RUNNING", + 'lifecycle_stage' => "active" + } + + expect(response).to have_gitlab_http_status(:success) + expect(response).to match_response_schema('ml/run') + expect(json_response['run']).to include('info' => hash_including(**expected_properties), 'data' => {}) + end + + describe 'Error States' do + context 'when run id is not passed' do + let(:params) { {} } + + it_behaves_like 'Not Found - Resource Does Not Exist' + end + + context 'when run id does not exist' do + let(:params) { { run_id: non_existing_record_iid.to_s } } + + it_behaves_like 'Not Found - Resource Does Not Exist' + end + + context 'when run id exists but does not belong to project' do + let(:params) { { run_id: another_candidate.iid.to_s } } + + it_behaves_like 'Not Found - Resource Does Not Exist' + end + + it_behaves_like 'shared error cases' + it_behaves_like 'Requires read_api scope' + end + end + end + + describe 'POST /projects/:id/ml/mflow/api/2.0/mlflow/runs/update' do + let(:route) { "/projects/#{project.id}/ml/mflow/api/2.0/mlflow/runs/update" } + let(:params) { { run_id: candidate.iid.to_s, status: 'FAILED', end_time: Time.now.to_i } } + let(:request) { post api(route), params: params, headers: headers } + + it 'updates the run' do + expected_properties = { + 'experiment_id' => candidate.experiment.iid.to_s, + 'user_id' => candidate.user.id.to_s, + 'start_time' => candidate.start_time, + 'end_time' => params[:end_time], + 'artifact_uri' => 'not_implemented', + 'status' => 'FAILED', + 'lifecycle_stage' => 'active' + } + + expect(response).to have_gitlab_http_status(:success) + expect(response).to match_response_schema('ml/update_run') + expect(json_response).to include('run_info' => hash_including(**expected_properties)) + end + + describe 'Error States' do + context 'when run id is not passed' do + let(:params) { {} } + + it_behaves_like 'Not Found - Resource Does Not Exist' + end + + context 'when run id does not exist' do + let(:params) { { run_id: non_existing_record_iid.to_s } } + + it_behaves_like 'Not Found - Resource Does Not Exist' + end + + context 'when run id exists but does not belong to project' do + let(:params) { { run_id: another_candidate.iid.to_s } } + + it_behaves_like 'Not Found - Resource Does Not Exist' + end + + context 'when run id exists but status in invalid' do + let(:params) { { run_id: candidate.iid.to_s, status: 'YOLO', end_time: Time.now.to_i } } + + it_behaves_like 'Bad Request' + end + + context 'when run id exists but end_time is invalid' do + let(:params) { { run_id: candidate.iid.to_s, status: 'FAILED', end_time: 's' } } + + it_behaves_like 'Bad Request' + end + + it_behaves_like 'shared error cases' + it_behaves_like 'Requires api scope' + end + end +end diff --git a/spec/requests/api/namespaces_spec.rb b/spec/requests/api/namespaces_spec.rb index 09b87f41b82..ab39c29653f 100644 --- a/spec/requests/api/namespaces_spec.rb +++ b/spec/requests/api/namespaces_spec.rb @@ -285,6 +285,14 @@ RSpec.describe API::Namespaces do end context 'when authenticated' do + it_behaves_like 'rate limited endpoint', rate_limit_key: :namespace_exists do + let(:current_user) { user } + + def request + get api("/namespaces/#{namespace1.path}/exists", current_user) + end + end + it 'returns JSON indicating the namespace exists and a suggestion' do get api("/namespaces/#{namespace1.path}/exists", user) diff --git a/spec/requests/api/npm_project_packages_spec.rb b/spec/requests/api/npm_project_packages_spec.rb index 3bcffac2760..bdcd6e7278d 100644 --- a/spec/requests/api/npm_project_packages_spec.rb +++ b/spec/requests/api/npm_project_packages_spec.rb @@ -63,6 +63,7 @@ RSpec.describe API::NpmProjectPackages do it_behaves_like 'successfully downloads the file' it_behaves_like 'a package tracking event', 'API::NpmPackages', 'pull_package' + it_behaves_like 'bumping the package last downloaded at field' end context 'with job token' do @@ -70,12 +71,14 @@ RSpec.describe API::NpmProjectPackages do it_behaves_like 'successfully downloads the file' it_behaves_like 'a package tracking event', 'API::NpmPackages', 'pull_package' + it_behaves_like 'bumping the package last downloaded at field' end end context 'a public project' do it_behaves_like 'successfully downloads the file' it_behaves_like 'a package tracking event', 'API::NpmPackages', 'pull_package' + it_behaves_like 'bumping the package last downloaded at field' context 'with a job token for a different user' do let_it_be(:other_user) { create(:user) } diff --git a/spec/requests/api/personal_access_tokens/self_revocation_spec.rb b/spec/requests/api/personal_access_tokens/self_revocation_spec.rb new file mode 100644 index 00000000000..f829b39cc1e --- /dev/null +++ b/spec/requests/api/personal_access_tokens/self_revocation_spec.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::PersonalAccessTokens::SelfRevocation do + let_it_be(:current_user) { create(:user) } + + describe 'DELETE /personal_access_tokens/self' do + let(:path) { '/personal_access_tokens/self' } + let(:token) { create(:personal_access_token, user: current_user) } + + subject(:delete_token) { delete api(path, personal_access_token: token) } + + shared_examples 'revoking token succeeds' do + it 'revokes token' do + delete_token + + expect(response).to have_gitlab_http_status(:no_content) + expect(token.reload).to be_revoked + end + end + + shared_examples 'revoking token denied' do |status| + it 'cannot revoke token' do + delete_token + + expect(response).to have_gitlab_http_status(status) + end + end + + context 'when current_user is an administrator', :enable_admin_mode do + let(:current_user) { create(:admin) } + + it_behaves_like 'revoking token succeeds' + + context 'with impersonated token' do + let(:token) { create(:personal_access_token, :impersonation, user: current_user) } + + it_behaves_like 'revoking token succeeds' + end + end + + context 'when current_user is not an administrator' do + let(:current_user) { create(:user) } + + it_behaves_like 'revoking token succeeds' + + context 'with impersonated token' do + let(:token) { create(:personal_access_token, :impersonation, user: current_user) } + + it_behaves_like 'revoking token denied', :bad_request + end + + context 'with already revoked token' do + let(:token) { create(:personal_access_token, :revoked, user: current_user) } + + it_behaves_like 'revoking token denied', :unauthorized + end + end + + Gitlab::Auth.all_available_scopes.each do |scope| + context "with a '#{scope}' scoped token" do + let(:token) { create(:personal_access_token, scopes: [scope], user: current_user) } + + it_behaves_like 'revoking token succeeds' + end + end + end +end diff --git a/spec/requests/api/personal_access_tokens_spec.rb b/spec/requests/api/personal_access_tokens_spec.rb index 8d8998cfdd6..37b5a594f2a 100644 --- a/spec/requests/api/personal_access_tokens_spec.rb +++ b/spec/requests/api/personal_access_tokens_spec.rb @@ -75,6 +75,7 @@ RSpec.describe API::PersonalAccessTokens do describe 'GET /personal_access_tokens/:id' do let_it_be(:user_token) { create(:personal_access_token, user: current_user) } + let_it_be(:user_read_only_token) { create(:personal_access_token, scopes: ['read_repository'], user: current_user) } let_it_be(:user_token_path) { "/personal_access_tokens/#{user_token.id}" } let_it_be(:invalid_path) { "/personal_access_tokens/#{non_existing_record_id}" } @@ -125,53 +126,11 @@ RSpec.describe API::PersonalAccessTokens do expect(response).to have_gitlab_http_status(:unauthorized) end - end - end - - describe 'DELETE /personal_access_tokens/self' do - let(:path) { '/personal_access_tokens/self' } - let(:token) { create(:personal_access_token, user: current_user) } - - subject { delete api(path, current_user, personal_access_token: token) } - - shared_examples 'revoking token succeeds' do - it 'revokes token' do - subject - - expect(response).to have_gitlab_http_status(:no_content) - expect(token.reload).to be_revoked - end - end - shared_examples 'revoking token denied' do |status| - it 'cannot revoke token' do - subject + it 'fails to return own PAT by id with read_repository token' do + get api(user_token_path, current_user, personal_access_token: user_read_only_token) - expect(response).to have_gitlab_http_status(status) - end - end - - context 'when current_user is an administrator', :enable_admin_mode do - let(:current_user) { create(:admin) } - - it_behaves_like 'revoking token succeeds' - end - - context 'when current_user is not an administrator' do - let(:current_user) { create(:user) } - - it_behaves_like 'revoking token succeeds' - - context 'with impersonated token' do - let(:token) { create(:personal_access_token, :impersonation, user: current_user) } - - it_behaves_like 'revoking token denied', :bad_request - end - - context 'with already revoked token' do - let(:token) { create(:personal_access_token, :revoked, user: current_user) } - - it_behaves_like 'revoking token denied', :unauthorized + expect(response).to have_gitlab_http_status(:forbidden) end end end @@ -183,6 +142,9 @@ RSpec.describe API::PersonalAccessTokens do let_it_be(:admin_user) { create(:admin) } let_it_be(:admin_token) { create(:personal_access_token, user: admin_user) } let_it_be(:admin_path) { "/personal_access_tokens/#{admin_token.id}" } + let_it_be(:admin_read_only_token) do + create(:personal_access_token, scopes: ['read_repository'], user: admin_user) + end it 'revokes a different users token' do delete api(path, admin_user) @@ -196,6 +158,12 @@ RSpec.describe API::PersonalAccessTokens do expect(response).to have_gitlab_http_status(:no_content) end + + it 'fails to revoke a different user token using a readonly scope' do + delete api(path, personal_access_token: admin_read_only_token) + + expect(token1.reload.revoked?).to be false + end end context 'when current_user is not an administrator' do diff --git a/spec/requests/api/project_attributes.yml b/spec/requests/api/project_attributes.yml index 670035187cb..1335fa02aaf 100644 --- a/spec/requests/api/project_attributes.yml +++ b/spec/requests/api/project_attributes.yml @@ -154,11 +154,13 @@ project_setting: - project_id - push_rule_id - show_default_award_emojis + - show_diff_preview_in_email - updated_at - cve_id_request_enabled - mr_default_target_self - target_platforms - selective_code_owner_removals + - show_diff_preview_in_email build_service_desk_setting: # service_desk_setting unexposed_attributes: diff --git a/spec/requests/api/project_import_spec.rb b/spec/requests/api/project_import_spec.rb index afe5a7d4a21..401db766589 100644 --- a/spec/requests/api/project_import_spec.rb +++ b/spec/requests/api/project_import_spec.rb @@ -47,7 +47,7 @@ RSpec.describe API::ProjectImport, :aggregate_failures do it 'executes a limited number of queries' do control_count = ActiveRecord::QueryRecorder.new { subject }.count - expect(control_count).to be <= 109 + expect(control_count).to be <= 110 end it 'schedules an import using a namespace' do diff --git a/spec/requests/api/project_packages_spec.rb b/spec/requests/api/project_packages_spec.rb index 7a05da8e13f..00d295b3490 100644 --- a/spec/requests/api/project_packages_spec.rb +++ b/spec/requests/api/project_packages_spec.rb @@ -6,7 +6,7 @@ RSpec.describe API::ProjectPackages do let_it_be(:project) { create(:project, :public) } let(:user) { create(:user) } - let!(:package1) { create(:npm_package, project: project, version: '3.1.0', name: "@#{project.root_namespace.path}/foo1") } + 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') } let!(:another_package) { create(:npm_package) } @@ -272,6 +272,17 @@ RSpec.describe API::ProjectPackages do it_behaves_like 'returns package', :project, :no_type it_behaves_like 'returns package', :project, :guest end + + context 'with a package without last_downloaded_at' do + let(:package_url) { "/projects/#{project.id}/packages/#{package2.id}" } + + it 'returns 200 and the package information' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema(single_package_schema) + end + end end context 'project is private' do diff --git a/spec/requests/api/project_snippets_spec.rb b/spec/requests/api/project_snippets_spec.rb index 72519ed1683..6e2dd6e76a9 100644 --- a/spec/requests/api/project_snippets_spec.rb +++ b/spec/requests/api/project_snippets_spec.rb @@ -256,6 +256,7 @@ RSpec.describe API::ProjectSnippets do allow_next_instance_of(Spam::AkismetService) do |instance| allow(instance).to receive(:spam?).and_return(true) end + stub_feature_flags(allow_possible_spam: false) project.add_developer(user) end @@ -311,6 +312,8 @@ RSpec.describe API::ProjectSnippets do allow_next_instance_of(Spam::AkismetService) do |instance| allow(instance).to receive(:spam?).and_return(true) end + + stub_feature_flags(allow_possible_spam: false) end context 'when the snippet is private' do diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 94688833d88..7ad1ce0ede9 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -48,6 +48,7 @@ end RSpec.describe API::Projects do include ProjectForksHelper + include WorkhorseHelpers include StubRequests let_it_be(:user) { create(:user) } @@ -1249,9 +1250,10 @@ RSpec.describe API::Projects do stub_application_setting(import_sources: nil) endpoint_url = "#{url}/info/refs?service=git-upload-pack" - stub_full_request(endpoint_url, method: :get).to_return({ status: 200, - body: '001e# service=git-upload-pack', - headers: { 'Content-Type': 'application/x-git-upload-pack-advertisement' } }) + stub_full_request(endpoint_url, method: :get).to_return( + { status: 200, + body: '001e# service=git-upload-pack', + headers: { 'Content-Type': 'application/x-git-upload-pack-advertisement' } }) project_params = { import_url: url, path: 'path-project-Foo', name: 'Foo Project' } expect { post api('/projects', user), params: project_params } @@ -1348,7 +1350,12 @@ RSpec.describe API::Projects do it 'uploads avatar for project a project' do project = attributes_for(:project, avatar: fixture_file_upload('spec/fixtures/banana_sample.gif', 'image/gif')) - post api('/projects', user), params: project + workhorse_form_with_file( + api('/projects', user), + method: :post, + file_key: :avatar, + params: project + ) project_id = json_response['id'] expect(json_response['avatar_url']).to eq("http://localhost/uploads/-/system/project/avatar/#{project_id}/banana_sample.gif") @@ -1924,8 +1931,6 @@ RSpec.describe API::Projects do end describe "POST /projects/:id/uploads/authorize" do - include WorkhorseHelpers - let(:headers) { workhorse_internal_api_request_header.merge({ 'HTTP_GITLAB_WORKHORSE' => 1 }) } context 'with authorized user' do @@ -3583,18 +3588,77 @@ RSpec.describe API::Projects do end end - it 'updates avatar' do - project_param = { - avatar: fixture_file_upload('spec/fixtures/banana_sample.gif', - 'image/gif') - } + context 'with changes to the avatar' do + let_it_be(:avatar_file) { fixture_file_upload('spec/fixtures/banana_sample.gif', 'image/gif') } + let_it_be(:alternate_avatar_file) { fixture_file_upload('spec/fixtures/rails_sample.png', 'image/png') } + let_it_be(:project_with_avatar, reload: true) do + create(:project, + :private, + :repository, + name: 'project-with-avatar', + creator_id: user.id, + namespace: user.namespace, + avatar: avatar_file) + end - put api("/projects/#{project3.id}", user), params: project_param + it 'uploads avatar to project without an avatar' do + workhorse_form_with_file( + api("/projects/#{project3.id}", user), + method: :put, + file_key: :avatar, + params: { avatar: avatar_file } + ) - expect(response).to have_gitlab_http_status(:ok) - expect(json_response['avatar_url']).to eq('http://localhost/uploads/'\ - '-/system/project/avatar/'\ - "#{project3.id}/banana_sample.gif") + aggregate_failures "testing response" do + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['avatar_url']).to eq('http://localhost/uploads/'\ + '-/system/project/avatar/'\ + "#{project3.id}/banana_sample.gif") + end + end + + it 'uploads and changes avatar to project with an avatar' do + workhorse_form_with_file( + api("/projects/#{project_with_avatar.id}", user), + method: :put, + file_key: :avatar, + params: { avatar: alternate_avatar_file } + ) + + aggregate_failures "testing response" do + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['avatar_url']).to eq('http://localhost/uploads/'\ + '-/system/project/avatar/'\ + "#{project_with_avatar.id}/rails_sample.png") + end + end + + it 'uploads and changes avatar to project among other changes' do + workhorse_form_with_file( + api("/projects/#{project_with_avatar.id}", user), + method: :put, + file_key: :avatar, + params: { description: 'changed description', avatar: avatar_file } + ) + + aggregate_failures "testing response" do + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['description']).to eq('changed description') + expect(json_response['avatar_url']).to eq('http://localhost/uploads/'\ + '-/system/project/avatar/'\ + "#{project_with_avatar.id}/banana_sample.gif") + end + end + + it 'removes avatar from project with an avatar' do + put api("/projects/#{project_with_avatar.id}", user), params: { avatar: '' } + + aggregate_failures "testing response" do + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['avatar_url']).to be_nil + expect(project_with_avatar.reload.avatar_url).to be_nil + end + end end it 'updates auto_devops_deploy_strategy' do @@ -4645,6 +4709,100 @@ RSpec.describe API::Projects do end end + describe 'GET /projects/:id/transfer_locations' do + let_it_be(:user) { create(:user) } + let_it_be(:source_group) { create(:group) } + let_it_be(:project) { create(:project, group: source_group) } + + let(:params) { {} } + + subject(:request) do + get api("/projects/#{project.id}/transfer_locations", user), params: params + end + + context 'when the user has rights to transfer the project' do + let_it_be(:guest_group) { create(:group) } + let_it_be(:maintainer_group) { create(:group, name: 'maintainer group', path: 'maintainer-group') } + let_it_be(:owner_group) { create(:group, name: 'owner group', path: 'owner-group') } + + before do + source_group.add_owner(user) + guest_group.add_guest(user) + maintainer_group.add_maintainer(user) + owner_group.add_owner(user) + end + + it 'returns 200' do + request + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + end + + 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) + end + + it 'does not include groups where the user doesn not have permissions to transfer a project' do + request + + expect(project_ids_from_response).not_to include(guest_group.id) + end + + context 'with search' do + let(:params) { { search: 'maintainer' } } + + it 'includes groups where the user has permissions to transfer a project to' do + request + + expect(project_ids_from_response).to contain_exactly(maintainer_group.id) + end + end + + context 'group shares' do + let_it_be(:shared_to_owner_group) { create(:group) } + let_it_be(:shared_to_guest_group) { create(:group) } + + before do + create(:group_group_link, :owner, + shared_with_group: owner_group, + shared_group: shared_to_owner_group + ) + + create(:group_group_link, :guest, + shared_with_group: guest_group, + shared_group: shared_to_guest_group + ) + end + + it 'only includes groups arising from group shares where the user has permission to transfer a project to' do + request + + expect(project_ids_from_response).to include(shared_to_owner_group.id) + expect(project_ids_from_response).not_to include(shared_to_guest_group.id) + end + end + + def project_ids_from_response + json_response.map { |project| project['id'] } + end + end + + context 'when the user does not have permissions to transfer the project' do + before do + source_group.add_developer(user) + end + + it 'returns 403' do + request + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + end + describe 'GET /projects/:id/storage' do context 'when unauthenticated' do it 'does not return project storage data' do diff --git a/spec/requests/api/releases_spec.rb b/spec/requests/api/releases_spec.rb index 1d9e3a6c887..754b77af60e 100644 --- a/spec/requests/api/releases_spec.rb +++ b/spec/requests/api/releases_spec.rb @@ -573,6 +573,224 @@ RSpec.describe API::Releases do end end + describe 'GET /projects/:id/releases/:tag_name/downloads/*file_path' do + let!(:release) { create(:release, project: project, tag: 'v0.1', author: maintainer) } + let!(:link) { create(:release_link, release: release, url: "#{url}#{filepath}", filepath: filepath) } + let(:filepath) { '/bin/bigfile.exe' } + let(:url) { 'https://google.com/-/jobs/140463678/artifacts/download' } + + context 'with an invalid release tag' do + it 'returns 404 for maintater' do + get api("/projects/#{project.id}/releases/v0.2/downloads#{filepath}", maintainer) + + expect(response).to have_gitlab_http_status(:not_found) + expect(json_response['message']).to eq('404 Not Found') + end + + it 'returns project not found for no user' do + get api("/projects/#{project.id}/releases/v0.2/downloads#{filepath}", nil) + + expect(response).to have_gitlab_http_status(:not_found) + expect(json_response['message']).to eq('404 Project Not Found') + end + + it 'returns forbidden for guest' do + get api("/projects/#{project.id}/releases/v0.2/downloads#{filepath}", guest) + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + context 'with a valid release tag' do + context 'when filepath is provided' do + context 'when filepath exists' do + it 'redirects to the file download URL' do + get api("/projects/#{project.id}/releases/v0.1/downloads#{filepath}", maintainer) + + expect(response).to redirect_to("#{url}#{filepath}") + end + + it 'redirects to the file download URL when using JOB-TOKEN auth' do + job = create(:ci_build, :running, project: project, user: maintainer) + + get api("/projects/#{project.id}/releases/v0.1/downloads#{filepath}"), params: { job_token: job.token } + + expect(response).to redirect_to("#{url}#{filepath}") + end + + context 'when user is a guest' do + it 'responds 403 Forbidden' do + get api("/projects/#{project.id}/releases/v0.1/downloads#{filepath}", guest) + + expect(response).to have_gitlab_http_status(:forbidden) + end + + context 'when project is public' do + let(:project) { create(:project, :repository, :public) } + + it 'responds 200 OK' do + get api("/projects/#{project.id}/releases/v0.1/downloads#{filepath}", guest) + + expect(response).to redirect_to("#{url}#{filepath}") + end + end + end + end + + context 'when filepath does not exists' do + it 'returns 404 for maintater' do + get api("/projects/#{project.id}/releases/v0.1/downloads/bin/not_existing.exe", maintainer) + + expect(response).to have_gitlab_http_status(:not_found) + expect(json_response['message']).to eq('404 Not found') + end + + it 'returns project not found for no user' do + get api("/projects/#{project.id}/releases/v0.1/downloads/bin/not_existing.exe", nil) + + expect(response).to have_gitlab_http_status(:not_found) + expect(json_response['message']).to eq('404 Project Not Found') + end + + it 'returns forbidden for guest' do + get api("/projects/#{project.id}/releases/v0.1/downloads/bin/not_existing.exe", guest) + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + end + + context 'when filepath is not provided' do + it 'returns 404 for maintater' do + get api("/projects/#{project.id}/releases/v0.1/downloads", maintainer) + + expect(response).to have_gitlab_http_status(:not_found) + end + + it 'returns project not found for no user' do + get api("/projects/#{project.id}/releases/v0.1/downloads", nil) + + expect(response).to have_gitlab_http_status(:not_found) + end + + it 'returns forbidden for guest' do + get api("/projects/#{project.id}/releases/v0.1/downloads", guest) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + end + + describe 'GET /projects/:id/releases/permalink/latest' do + context 'when there is no release' do + it 'returns not found' do + get api("/projects/#{project.id}/releases/permalink/latest", maintainer) + + expect(response).to have_gitlab_http_status(:not_found) + end + + it 'returns not found when using JOB-TOKEN auth' do + job = create(:ci_build, :running, project: project, user: maintainer) + + get api("/projects/#{project.id}/releases/permalink/latest"), params: { job_token: job.token } + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'when there are more than one release' do + let!(:release_a) do + create(:release, + project: project, + tag: 'v0.1', + author: maintainer, + description: 'This is v0.1', + released_at: 3.days.ago) + end + + let!(:release_b) do + create(:release, + project: project, + tag: 'v0.2', + author: maintainer, + description: 'This is v0.2', + released_at: 2.days.ago) + end + + it 'redirects to the latest release tag' do + get api("/projects/#{project.id}/releases/permalink/latest", maintainer) + + uri = URI(response.header["Location"]) + + expect(response).to have_gitlab_http_status(:redirect) + expect(uri.path).to eq("/api/v4/projects/#{project.id}/releases/#{release_b.tag}") + end + + it 'redirects to the latest release tag when using JOB-TOKEN auth' do + job = create(:ci_build, :running, project: project, user: maintainer) + + get api("/projects/#{project.id}/releases/permalink/latest"), params: { job_token: job.token } + + uri = URI(response.header["Location"]) + + expect(response).to have_gitlab_http_status(:redirect) + expect(uri.path).to eq("/api/v4/projects/#{project.id}/releases/#{release_b.tag}") + end + + context 'when there are query parameters present' do + it 'includes the query params on the redirection' do + get api("/projects/#{project.id}/releases/permalink/latest", maintainer), params: { include_html_description: true, other_param: "aaa" } + + uri = URI(response.header["Location"]) + query_params = Rack::Utils.parse_nested_query(uri.query) + + expect(response).to have_gitlab_http_status(:redirect) + expect(uri.path).to eq("/api/v4/projects/#{project.id}/releases/#{release_b.tag}") + expect(query_params).to include({ + "include_html_description" => "true", + "other_param" => "aaa" + }) + end + + it 'discards the `order_by` query param' do + get api("/projects/#{project.id}/releases/permalink/latest", maintainer), params: { order_by: 'something', other_param: "aaa" } + + uri = URI(response.header["Location"]) + query_params = Rack::Utils.parse_nested_query(uri.query) + + expect(response).to have_gitlab_http_status(:redirect) + expect(uri.path).to eq("/api/v4/projects/#{project.id}/releases/#{release_b.tag}") + expect(query_params).to include({ + "other_param" => "aaa" + }) + expect(query_params).not_to include({ + "order_by" => "something" + }) + end + end + + context 'when downloading a release asset' do + it 'redirects to the right endpoint keeping the suffix_path' do + get api("/projects/#{project.id}/releases/permalink/latest/downloads/bin/example.exe", maintainer) + + uri = URI(response.header["Location"]) + + expect(response).to have_gitlab_http_status(:redirect) + expect(uri.path).to eq("/api/v4/projects/#{project.id}/releases/#{release_b.tag}/downloads/bin/example.exe") + end + + it 'returns error when there is path traversal in suffix path' do + get api("/projects/#{project.id}/releases/permalink/latest/downloads/bin/../../../../../../../password.txt", maintainer) + + expect(response).to have_gitlab_http_status(:bad_request) + + expect(json_response['error']).to eq('suffix_path should be a valid file path') + end + end + end + end + describe 'POST /projects/:id/releases' do let(:params) do { diff --git a/spec/requests/api/resource_access_tokens_spec.rb b/spec/requests/api/resource_access_tokens_spec.rb index 369a8c1b0ab..d9a12e7e148 100644 --- a/spec/requests/api/resource_access_tokens_spec.rb +++ b/spec/requests/api/resource_access_tokens_spec.rb @@ -243,27 +243,65 @@ RSpec.describe API::ResourceAccessTokens do end context "when the user has valid permissions" do - it "deletes the #{source_type} access token from the #{source_type}" do - delete_token + context 'when user_destroy_with_limited_execution_time_worker is enabled' do + it "deletes the #{source_type} access token from the #{source_type}" do + delete_token - expect(response).to have_gitlab_http_status(:no_content) - expect(User.exists?(project_bot.id)).to be_falsy - end + expect(response).to have_gitlab_http_status(:no_content) + expect( + Users::GhostUserMigration.where(user: project_bot, + initiator_user: user) + ).to be_exists + end - context "when using #{source_type} access token to DELETE other #{source_type} access token" do - let_it_be(:other_project_bot) { create(:user, :project_bot) } - let_it_be(:other_token) { create(:personal_access_token, user: other_project_bot) } - let_it_be(:token_id) { other_token.id } + context "when using #{source_type} access token to DELETE other #{source_type} access token" do + let_it_be(:other_project_bot) { create(:user, :project_bot) } + let_it_be(:other_token) { create(:personal_access_token, user: other_project_bot) } + let_it_be(:token_id) { other_token.id } + + before do + resource.add_maintainer(other_project_bot) + end + + it "deletes the #{source_type} access token from the #{source_type}" do + delete_token + expect(response).to have_gitlab_http_status(:no_content) + expect( + Users::GhostUserMigration.where(user: other_project_bot, + initiator_user: user) + ).to be_exists + end + end + end + + context 'when user_destroy_with_limited_execution_time_worker is disabled' do before do - resource.add_maintainer(other_project_bot) + stub_feature_flags(user_destroy_with_limited_execution_time_worker: false) end it "deletes the #{source_type} access token from the #{source_type}" do delete_token expect(response).to have_gitlab_http_status(:no_content) - expect(User.exists?(other_project_bot.id)).to be_falsy + expect(User.exists?(project_bot.id)).to be_falsy + end + + context "when using #{source_type} access token to DELETE other #{source_type} access token" do + let_it_be(:other_project_bot) { create(:user, :project_bot) } + let_it_be(:other_token) { create(:personal_access_token, user: other_project_bot) } + let_it_be(:token_id) { other_token.id } + + before do + resource.add_maintainer(other_project_bot) + end + + it "deletes the #{source_type} access token from the #{source_type}" do + delete_token + + expect(response).to have_gitlab_http_status(:no_content) + expect(User.exists?(other_project_bot.id)).to be_falsy + end end end diff --git a/spec/requests/api/resource_state_events_spec.rb b/spec/requests/api/resource_state_events_spec.rb index 46ca9874395..5f756bc6c63 100644 --- a/spec/requests/api/resource_state_events_spec.rb +++ b/spec/requests/api/resource_state_events_spec.rb @@ -6,87 +6,8 @@ RSpec.describe API::ResourceStateEvents do let_it_be(:user) { create(:user) } let_it_be(:project, reload: true) { create(:project, :public, namespace: user.namespace) } - before_all do - project.add_developer(user) - end - - shared_examples 'resource_state_events API' do |parent_type, eventable_type, id_name| - describe "GET /#{parent_type}/:id/#{eventable_type}/:noteable_id/resource_state_events" do - let!(:event) { create_event } - - it "returns an array of resource state events" do - url = "/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_state_events" - get api(url, user) - - expect(response).to have_gitlab_http_status(:ok) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - expect(json_response.first['id']).to eq(event.id) - expect(json_response.first['state']).to eq(event.state.to_s) - end - - it "returns a 404 error when eventable id not found" do - get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{non_existing_record_id}/resource_state_events", user) - - expect(response).to have_gitlab_http_status(:not_found) - end - - it "returns 404 when not authorized" do - parent.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) - private_user = create(:user) - - get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_state_events", private_user) - - expect(response).to have_gitlab_http_status(:not_found) - end - end - - describe "GET /#{parent_type}/:id/#{eventable_type}/:noteable_id/resource_state_events/:event_id" do - let!(:event) { create_event } - - it "returns a resource state event by id" do - get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_state_events/#{event.id}", user) - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response['id']).to eq(event.id) - expect(json_response['state']).to eq(event.state.to_s) - end - - it "returns 404 when not authorized" do - parent.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) - private_user = create(:user) - - get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_state_events/#{event.id}", private_user) - - expect(response).to have_gitlab_http_status(:not_found) - end - - it "returns a 404 error if resource state event not found" do - get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_state_events/#{non_existing_record_id}", user) - - expect(response).to have_gitlab_http_status(:not_found) - end - end - - describe 'pagination' do - # https://gitlab.com/gitlab-org/gitlab/-/issues/220192 - it 'returns the second page' do - create_event - event2 = create_event - - get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_state_events?page=2&per_page=1", user) - - expect(response).to have_gitlab_http_status(:ok) - expect(response).to include_pagination_headers - expect(response.headers['X-Total']).to eq '2' - expect(json_response.count).to eq(1) - expect(json_response.first['id']).to eq(event2.id) - end - end - - def create_event(state: :opened) - create(:resource_state_event, eventable.class.name.underscore => eventable, state: state) - end + before do + parent.add_developer(user) end context 'when eventable is an Issue' do diff --git a/spec/requests/api/rpm_project_packages_spec.rb b/spec/requests/api/rpm_project_packages_spec.rb new file mode 100644 index 00000000000..6a646c26fd2 --- /dev/null +++ b/spec/requests/api/rpm_project_packages_spec.rb @@ -0,0 +1,250 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe API::RpmProjectPackages do + include HttpBasicAuthHelpers + include WorkhorseHelpers + + include_context 'workhorse headers' + + using RSpec::Parameterized::TableSyntax + + let_it_be_with_reload(:project) { create(:project, :public) } + let_it_be(:user) { create(:user) } + let_it_be(:personal_access_token) { create(:personal_access_token, user: user) } + 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(:job) { create(:ci_build, :running, user: user, project: project) } + + let(:headers) { {} } + let(:package_name) { 'rpm-package.0-1.x86_64.rpm' } + let(:package_file_id) { 1 } + + shared_examples 'rejects rpm packages access' do |status| + it_behaves_like 'returning response status', status + + if status == :unauthorized + it 'has the correct response header' do + subject + + expect(response.headers['WWW-Authenticate']).to eq 'Basic realm="GitLab Packages Registry"' + end + end + end + + shared_examples 'process rpm packages upload/download' do |status| + it_behaves_like 'returning response status', status + end + + shared_examples 'a deploy token for RPM requests' do + context 'with deploy token headers' do + before do + project.update_column(:visibility_level, Gitlab::VisibilityLevel::PRIVATE) + end + + let(:headers) { basic_auth_header(deploy_token.username, deploy_token.token) } + + context 'when token is valid' do + it_behaves_like 'returning response status', :not_found + end + + context 'when token is invalid' do + let(:headers) { basic_auth_header(deploy_token.username, 'bar') } + + it_behaves_like 'returning response status', :unauthorized + end + end + end + + shared_examples 'a job token for RPM requests' do + context 'with job token headers' do + let(:headers) { basic_auth_header(::Gitlab::Auth::CI_JOB_USER, job.token) } + + before do + project.update_column(:visibility_level, Gitlab::VisibilityLevel::PRIVATE) + project.add_developer(user) + end + + context 'with valid token' do + it_behaves_like 'returning response status', :not_found + end + + context 'with invalid token' do + let(:headers) { basic_auth_header(::Gitlab::Auth::CI_JOB_USER, 'bar') } + + it_behaves_like 'returning response status', :unauthorized + end + + context 'with invalid user' do + let(:headers) { basic_auth_header('foo', job.token) } + + it_behaves_like 'returning response status', :unauthorized + end + end + end + + shared_examples 'a user token for RPM requests' do + context 'with valid project' do + where(:visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do + 'PUBLIC' | :developer | true | true | 'process rpm packages upload/download' | :not_found + 'PUBLIC' | :guest | true | true | 'process rpm packages upload/download' | :forbidden + 'PUBLIC' | :developer | true | false | 'rejects rpm packages access' | :unauthorized + 'PUBLIC' | :guest | true | false | 'rejects rpm packages access' | :unauthorized + 'PUBLIC' | :developer | false | true | 'process rpm packages upload/download' | :not_found + 'PUBLIC' | :guest | false | true | 'process rpm packages upload/download' | :not_found + 'PUBLIC' | :developer | false | false | 'rejects rpm packages access' | :unauthorized + 'PUBLIC' | :guest | false | false | 'rejects rpm packages access' | :unauthorized + 'PUBLIC' | :anonymous | false | true | 'process rpm packages upload/download' | :unauthorized + 'PRIVATE' | :developer | true | true | 'process rpm packages upload/download' | :not_found + 'PRIVATE' | :guest | true | true | 'rejects rpm packages access' | :forbidden + 'PRIVATE' | :developer | true | false | 'rejects rpm packages access' | :unauthorized + 'PRIVATE' | :guest | true | false | 'rejects rpm packages access' | :unauthorized + 'PRIVATE' | :developer | false | true | 'rejects rpm packages access' | :not_found + 'PRIVATE' | :guest | false | true | 'rejects rpm packages access' | :not_found + 'PRIVATE' | :developer | false | false | 'rejects rpm packages access' | :unauthorized + 'PRIVATE' | :guest | false | false | 'rejects rpm packages access' | :unauthorized + 'PRIVATE' | :anonymous | false | true | 'rejects rpm packages access' | :unauthorized + end + + with_them do + let(:token) { user_token ? personal_access_token.token : 'wrong' } + let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) } + + subject { get api(url), headers: headers } + + before do + project.update_column(:visibility_level, Gitlab::VisibilityLevel.level_value(visibility_level)) + project.send("add_#{user_role}", user) if member && user_role != :anonymous + end + + it_behaves_like params[:shared_examples_name], params[:expected_status] + end + end + end + + describe 'GET /api/v4/projects/:project_id/packages/rpm/repodata/:filename' do + let(:url) { "/projects/#{project.id}/packages/rpm/repodata/#{package_name}" } + + subject { get api(url), headers: headers } + + it_behaves_like 'a job token for RPM requests' + it_behaves_like 'a deploy token for RPM requests' + it_behaves_like 'a user token for RPM requests' + end + + describe 'GET /api/v4/projects/:id/packages/rpm/:package_file_id/:filename' do + let(:url) { "/projects/#{project.id}/packages/rpm/#{package_file_id}/#{package_name}" } + + subject { get api(url), headers: headers } + + it_behaves_like 'a job token for RPM requests' + it_behaves_like 'a deploy token for RPM requests' + it_behaves_like 'a user token for RPM requests' + end + + describe 'POST /api/v4/projects/:project_id/packages/rpm' do + let(:url) { "/projects/#{project.id}/packages/rpm" } + let(:file_upload) { fixture_file_upload('spec/fixtures/packages/rpm/hello-0.0.1-1.fc29.x86_64.rpm') } + + subject { post api(url), params: { file: file_upload }, headers: headers } + + context 'with user token' do + context 'with valid project' do + where(:visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do + 'PUBLIC' | :developer | true | true | 'process rpm packages upload/download' | :not_found + 'PUBLIC' | :guest | true | true | 'rejects rpm packages access' | :forbidden + 'PUBLIC' | :developer | true | false | 'rejects rpm packages access' | :unauthorized + 'PUBLIC' | :guest | true | false | 'rejects rpm packages access' | :unauthorized + 'PUBLIC' | :developer | false | true | 'rejects rpm packages access' | :not_found + 'PUBLIC' | :guest | false | true | 'rejects rpm packages access' | :not_found + 'PUBLIC' | :developer | false | false | 'rejects rpm packages access' | :unauthorized + 'PUBLIC' | :guest | false | false | 'rejects rpm packages access' | :unauthorized + 'PUBLIC' | :anonymous | false | true | 'rejects rpm packages access' | :unauthorized + 'PRIVATE' | :developer | true | true | 'process rpm packages upload/download' | :not_found + 'PRIVATE' | :guest | true | true | 'rejects rpm packages access' | :forbidden + 'PRIVATE' | :developer | true | false | 'rejects rpm packages access' | :unauthorized + 'PRIVATE' | :guest | true | false | 'rejects rpm packages access' | :unauthorized + 'PRIVATE' | :developer | false | true | 'rejects rpm packages access' | :not_found + 'PRIVATE' | :guest | false | true | 'rejects rpm packages access' | :not_found + 'PRIVATE' | :developer | false | false | 'rejects rpm packages access' | :unauthorized + 'PRIVATE' | :guest | false | false | 'rejects rpm packages access' | :unauthorized + 'PRIVATE' | :anonymous | false | true | 'rejects rpm packages access' | :unauthorized + end + + with_them do + let(:token) { user_token ? personal_access_token.token : 'wrong' } + let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) } + + before do + project.update_column(:visibility_level, Gitlab::VisibilityLevel.level_value(visibility_level)) + 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 'when user can upload file' do + before do + project.add_developer(user) + end + + let(:headers) { basic_auth_header(user.username, personal_access_token.token).merge(workhorse_headers) } + + context 'when file size too large' do + before do + allow_next_instance_of(UploadedFile) do |uploaded_file| + allow(uploaded_file).to receive(:size).and_return(project.actual_limits.rpm_max_file_size + 1) + end + end + + it 'returns an error' do + upload_file(params: { file: file_upload }, request_headers: headers) + + expect(response).to have_gitlab_http_status(:bad_request) + expect(response.body).to match(/File is too large/) + end + end + end + + def upload_file(params: {}, request_headers: headers) + url = "/projects/#{project.id}/packages/rpm" + workhorse_finalize( + api(url), + method: :post, + file_key: :file, + params: params, + headers: request_headers, + send_rewritten_field: true + ) + end + end + + it_behaves_like 'a deploy token for RPM requests' + it_behaves_like 'a job token for RPM requests' + end + + describe 'POST /api/v4/projects/:project_id/packages/rpm/authorize' do + let(:url) { api("/projects/#{project.id}/packages/rpm/authorize") } + + subject { post(url, headers: headers) } + + it_behaves_like 'returning response status', :not_found + + context 'when feature flag is disabled' do + before do + stub_feature_flags(rpm_packages: false) + end + + it_behaves_like 'returning response status', :not_found + end + + context 'when package feature is disabled' do + before do + stub_config(packages: { enabled: false }) + end + + it_behaves_like 'returning response status', :not_found + end + end +end diff --git a/spec/requests/api/search_spec.rb b/spec/requests/api/search_spec.rb index 6034d26f1d2..05f38aff6ab 100644 --- a/spec/requests/api/search_spec.rb +++ b/spec/requests/api/search_spec.rb @@ -9,6 +9,7 @@ RSpec.describe API::Search do let_it_be(:repo_project) { create(:project, :public, :repository, group: group) } before do + allow(Gitlab::ApplicationRateLimiter).to receive(:threshold).and_return(0) allow(Gitlab::ApplicationRateLimiter).to receive(:threshold).with(:search_rate_limit).and_return(1000) allow(Gitlab::ApplicationRateLimiter).to receive(:threshold).with(:search_rate_limit_unauthenticated).and_return(1000) end @@ -351,6 +352,43 @@ RSpec.describe API::Search do end end + it 'increments the custom search sli apdex' do + expect(Gitlab::Metrics::GlobalSearchSlis).to receive(:record_apdex).with( + elapsed: a_kind_of(Numeric), + search_scope: 'issues', + search_type: 'basic', + search_level: 'global' + ) + + get api(endpoint, user), params: { scope: 'issues', search: 'john doe' } + 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, + search_scope: 'issues', + search_type: 'basic', + search_level: 'global' + ) + + get api(endpoint, user), params: { scope: 'issues', search: 'john doe' } + end + + it 'increments the custom search sli error rate with error true if an error occurred' do + allow_next_instance_of(SearchService) do |service| + allow(service).to receive(:search_results).and_raise(ActiveRecord::QueryCanceled) + end + + expect(Gitlab::Metrics::GlobalSearchSlis).to receive(:record_error_rate).with( + error: true, + search_scope: 'issues', + search_type: 'basic', + search_level: 'global' + ) + + get api(endpoint, user), params: { scope: 'issues', search: 'john doe' } + end + it 'sets global search information for logging' do expect(Gitlab::Instrumentation::GlobalSearchApi).to receive(:set_information).with( type: 'basic', @@ -618,7 +656,7 @@ RSpec.describe API::Search do context 'when requesting basic search' do it 'passes the parameter to search service' do - expect(SearchService).to receive(:new).with(user, hash_including(basic_search: 'true')) + expect(SearchService).to receive(:new).with(user, hash_including(basic_search: 'true')).twice get api(endpoint, user), params: { scope: 'issues', search: 'awesome', basic_search: 'true' } end diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb index 6f0d5827a80..315c76c8ac3 100644 --- a/spec/requests/api/settings_spec.rb +++ b/spec/requests/api/settings_spec.rb @@ -56,6 +56,10 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting do expect(json_response['project_runner_token_expiration_interval']).to be_nil expect(json_response['max_export_size']).to eq(0) expect(json_response['pipeline_limit_per_project_user_sha']).to eq(0) + expect(json_response['delete_inactive_projects']).to be(false) + expect(json_response['inactive_projects_delete_after_months']).to eq(2) + expect(json_response['inactive_projects_min_size_mb']).to eq(0) + expect(json_response['inactive_projects_send_warning_email_after_months']).to eq(1) end end @@ -148,7 +152,11 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting do user_deactivation_emails_enabled: false, admin_mode: true, suggest_pipeline_enabled: false, - users_get_by_id_limit: 456 + users_get_by_id_limit: 456, + delete_inactive_projects: true, + inactive_projects_delete_after_months: 24, + inactive_projects_min_size_mb: 10, + inactive_projects_send_warning_email_after_months: 12 } expect(response).to have_gitlab_http_status(:ok) @@ -205,6 +213,10 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting do expect(json_response['user_deactivation_emails_enabled']).to be(false) expect(json_response['suggest_pipeline_enabled']).to be(false) expect(json_response['users_get_by_id_limit']).to eq(456) + expect(json_response['delete_inactive_projects']).to be(true) + expect(json_response['inactive_projects_delete_after_months']).to eq(24) + expect(json_response['inactive_projects_min_size_mb']).to eq(10) + expect(json_response['inactive_projects_send_warning_email_after_months']).to eq(12) end end diff --git a/spec/requests/api/snippets_spec.rb b/spec/requests/api/snippets_spec.rb index 0dd6e484e8d..031bcb612f4 100644 --- a/spec/requests/api/snippets_spec.rb +++ b/spec/requests/api/snippets_spec.rb @@ -340,6 +340,7 @@ RSpec.describe API::Snippets, factory_default: :keep do allow_next_instance_of(Spam::AkismetService) do |instance| allow(instance).to receive(:spam?).and_return(true) end + stub_feature_flags(allow_possible_spam: false) end context 'when the snippet is private' do @@ -405,6 +406,7 @@ RSpec.describe API::Snippets, factory_default: :keep do allow_next_instance_of(Spam::AkismetService) do |instance| allow(instance).to receive(:spam?).and_return(true) end + stub_feature_flags(allow_possible_spam: false) end context 'when the snippet is private' do diff --git a/spec/requests/api/suggestions_spec.rb b/spec/requests/api/suggestions_spec.rb index 7f53d379af5..2393a268693 100644 --- a/spec/requests/api/suggestions_spec.rb +++ b/spec/requests/api/suggestions_spec.rb @@ -34,15 +34,14 @@ RSpec.describe API::Suggestions do end let(:diff_note2) do - create(:diff_note_on_merge_request, noteable: merge_request, - position: position2, - project: project) + create(:diff_note_on_merge_request, noteable: merge_request, position: position2, project: project) end let(:suggestion) do - create(:suggestion, note: diff_note, - from_content: " raise RuntimeError, \"System commands must be given as an array of strings\"\n", - to_content: " raise RuntimeError, 'Explosion'\n # explosion?") + create(:suggestion, + note: diff_note, + from_content: " raise RuntimeError, \"System commands must be given as an array of strings\"\n", + to_content: " raise RuntimeError, 'Explosion'\n # explosion?") end let(:unappliable_suggestion) do @@ -119,8 +118,8 @@ RSpec.describe API::Suggestions do describe "PUT /suggestions/batch_apply" do let(:suggestion2) do create(:suggestion, note: diff_note2, - from_content: " \"PWD\" => path\n", - to_content: " *** FOO ***\n") + from_content: " \"PWD\" => path\n", + to_content: " *** FOO ***\n") end let(:url) { "/suggestions/batch_apply" } diff --git a/spec/requests/api/tags_spec.rb b/spec/requests/api/tags_spec.rb index e81e9e0bf2f..b62fbaead6f 100644 --- a/spec/requests/api/tags_spec.rb +++ b/spec/requests/api/tags_spec.rb @@ -17,10 +17,6 @@ RSpec.describe API::Tags do end describe 'GET /projects/:id/repository/tags', :use_clean_rails_memory_store_caching do - before do - stub_feature_flags(tag_list_keyset_pagination: false) - end - let(:route) { "/projects/#{project_id}/repository/tags" } context 'sorting' do @@ -59,6 +55,18 @@ RSpec.describe API::Tags do expect(json_response.map { |tag| tag['name'] }).to eq(ordered_by_name) end + + it 'sorts by version in ascending order when requested' do + repository = project.repository + repository.add_tag(user, 'v1.2.0', repository.commit.id) + repository.add_tag(user, 'v1.10.0', repository.commit.id) + + get api("#{route}?order_by=version&sort=asc", current_user) + + ordered_by_version = VersionSorter.sort(project.repository.tags.map { |tag| tag.name }) + + expect(json_response.map { |tag| tag['name'] }).to eq(ordered_by_version) + end end context 'searching' do @@ -154,50 +162,44 @@ RSpec.describe API::Tags do end end - context 'with keyset pagination on', :aggregate_errors do - before do - stub_feature_flags(tag_list_keyset_pagination: true) - end - - context 'with keyset pagination option' do - let(:base_params) { { pagination: 'keyset' } } + context 'with keyset pagination option', :aggregate_errors do + let(:base_params) { { pagination: 'keyset' } } - context 'with gitaly pagination params' do - context 'with high limit' do - let(:params) { base_params.merge(per_page: 100) } + context 'with gitaly pagination params' do + context 'with high limit' do + let(:params) { base_params.merge(per_page: 100) } - it 'returns all repository tags' do - get api(route, user), params: params + it 'returns all repository tags' do + get api(route, user), params: params - expect(response).to have_gitlab_http_status(:ok) - expect(response).to match_response_schema('public_api/v4/tags') - expect(response.headers).not_to include('Link') - tag_names = json_response.map { |x| x['name'] } - expect(tag_names).to match_array(project.repository.tag_names) - end + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('public_api/v4/tags') + expect(response.headers).not_to include('Link') + tag_names = json_response.map { |x| x['name'] } + expect(tag_names).to match_array(project.repository.tag_names) end + end - context 'with low limit' do - let(:params) { base_params.merge(per_page: 2) } + context 'with low limit' do + let(:params) { base_params.merge(per_page: 2) } - it 'returns limited repository tags' do - get api(route, user), params: params + it 'returns limited repository tags' do + get api(route, user), params: params - expect(response).to have_gitlab_http_status(:ok) - expect(response).to match_response_schema('public_api/v4/tags') - expect(response.headers).to include('Link') - tag_names = json_response.map { |x| x['name'] } - expect(tag_names).to match_array(%w(v1.1.0 v1.1.1)) - end + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('public_api/v4/tags') + expect(response.headers).to include('Link') + tag_names = json_response.map { |x| x['name'] } + expect(tag_names).to match_array(%w(v1.1.0 v1.1.1)) end + end - context 'with missing page token' do - let(:params) { base_params.merge(page_token: 'unknown') } + context 'with missing page token' do + let(:params) { base_params.merge(page_token: 'unknown') } - it_behaves_like '422 response' do - let(:request) { get api(route, user), params: params } - let(:message) { 'Invalid page token: refs/tags/unknown' } - end + it_behaves_like '422 response' do + let(:request) { get api(route, user), params: params } + let(:message) { 'Invalid page token: refs/tags/unknown' } end end end diff --git a/spec/requests/api/topics_spec.rb b/spec/requests/api/topics_spec.rb index 72221e3fb6a..1ad6f876fab 100644 --- a/spec/requests/api/topics_spec.rb +++ b/spec/requests/api/topics_spec.rb @@ -317,4 +317,66 @@ RSpec.describe API::Topics do end end end + + describe 'POST /topics/merge', :aggregate_failures do + context 'as administrator' do + let_it_be(:api_url) { api('/topics/merge', admin) } + + it 'merge topics' do + post api_url, params: { source_topic_id: topic_3.id, target_topic_id: topic_2.id } + + expect(response).to have_gitlab_http_status(:created) + expect { topic_2.reload }.not_to raise_error + expect { topic_3.reload }.to raise_error(ActiveRecord::RecordNotFound) + expect(json_response['id']).to eq(topic_2.id) + expect(json_response['total_projects_count']).to eq(topic_2.total_projects_count) + end + + it 'returns 404 for non existing source topic id' do + post api_url, params: { source_topic_id: non_existing_record_id, target_topic_id: topic_2.id } + + expect(response).to have_gitlab_http_status(:not_found) + end + + it 'returns 404 for non existing target topic id' do + post api_url, params: { source_topic_id: topic_3.id, target_topic_id: non_existing_record_id } + + expect(response).to have_gitlab_http_status(:not_found) + end + + it 'returns 400 for identical topic ids' do + post api_url, params: { source_topic_id: topic_2.id, target_topic_id: topic_2.id } + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['message']).to eql('The source topic and the target topic are identical.') + end + + it 'returns 400 if merge failed' do + allow_next_found_instance_of(Projects::Topic) do |topic| + allow(topic).to receive(:destroy!).and_raise(ActiveRecord::RecordNotDestroyed) + end + + post api_url, params: { source_topic_id: topic_3.id, target_topic_id: topic_2.id } + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['message']).to eql('Topics could not be merged!') + end + end + + context 'as normal user' do + it 'returns 403 Forbidden' do + post api('/topics/merge', user), params: { source_topic_id: topic_3.id, target_topic_id: topic_2.id } + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + context 'as anonymous' do + it 'returns 401 Unauthorized' do + post api('/topics/merge'), params: { source_topic_id: topic_3.id, target_topic_id: topic_2.id } + + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + end end diff --git a/spec/requests/api/unleash_spec.rb b/spec/requests/api/unleash_spec.rb index 3ee895d9421..51c567309b7 100644 --- a/spec/requests/api/unleash_spec.rb +++ b/spec/requests/api/unleash_spec.rb @@ -218,8 +218,7 @@ RSpec.describe API::Unleash do context 'with version 2 feature flags' do it 'does not return a flag without any strategies' do - create(:operations_feature_flag, project: project, - name: 'feature1', active: true, version: 2) + create(:operations_feature_flag, project: project, name: 'feature1', active: true, version: 2) get api(features_url), headers: { 'UNLEASH-INSTANCEID' => client.token, 'UNLEASH-APPNAME' => 'production' } @@ -228,10 +227,8 @@ RSpec.describe API::Unleash do end it 'returns a flag with a default strategy' do - feature_flag = create(:operations_feature_flag, project: project, - name: 'feature1', active: true, version: 2) - strategy = create(:operations_strategy, feature_flag: feature_flag, - name: 'default', parameters: {}) + feature_flag = create(:operations_feature_flag, project: project, name: 'feature1', active: true, version: 2) + strategy = create(:operations_strategy, feature_flag: feature_flag, name: 'default', parameters: {}) create(:operations_scope, strategy: strategy, environment_scope: 'production') get api(features_url), headers: { 'UNLEASH-INSTANCEID' => client.token, 'UNLEASH-APPNAME' => 'production' } @@ -248,10 +245,9 @@ RSpec.describe API::Unleash do end it 'returns a flag with a userWithId strategy' do - feature_flag = create(:operations_feature_flag, project: project, - name: 'feature1', active: true, version: 2) - strategy = create(:operations_strategy, feature_flag: feature_flag, - name: 'userWithId', parameters: { userIds: 'user123,user456' }) + feature_flag = create(:operations_feature_flag, project: project, name: 'feature1', active: true, version: 2) + strategy = create(:operations_strategy, + feature_flag: feature_flag, name: 'userWithId', parameters: { userIds: 'user123,user456' }) create(:operations_scope, strategy: strategy, environment_scope: 'production') get api(features_url), headers: { 'UNLEASH-INSTANCEID' => client.token, 'UNLEASH-APPNAME' => 'production' } @@ -268,12 +264,13 @@ RSpec.describe API::Unleash do end it 'returns a flag with multiple strategies' do - feature_flag = create(:operations_feature_flag, project: project, - name: 'feature1', active: true, version: 2) - strategy_a = create(:operations_strategy, feature_flag: feature_flag, - name: 'userWithId', parameters: { userIds: 'user_a,user_b' }) - strategy_b = create(:operations_strategy, feature_flag: feature_flag, - name: 'gradualRolloutUserId', parameters: { groupId: 'default', percentage: '45' }) + feature_flag = create(:operations_feature_flag, project: project, name: 'feature1', active: true, version: 2) + strategy_a = create(:operations_strategy, + feature_flag: feature_flag, name: 'userWithId', parameters: { userIds: 'user_a,user_b' }) + strategy_b = create(:operations_strategy, + feature_flag: feature_flag, + name: 'gradualRolloutUserId', + parameters: { groupId: 'default', percentage: '45' }) create(:operations_scope, strategy: strategy_a, environment_scope: 'production') create(:operations_scope, strategy: strategy_b, environment_scope: 'production') @@ -298,12 +295,12 @@ RSpec.describe API::Unleash do end it 'returns only flags matching the environment scope' do - feature_flag_a = create(:operations_feature_flag, project: project, - name: 'feature1', active: true, version: 2) + feature_flag_a = create(:operations_feature_flag, + project: project, name: 'feature1', active: true, version: 2) strategy_a = create(:operations_strategy, feature_flag: feature_flag_a) create(:operations_scope, strategy: strategy_a, environment_scope: 'production') - feature_flag_b = create(:operations_feature_flag, project: project, - name: 'feature2', active: true, version: 2) + feature_flag_b = create(:operations_feature_flag, + project: project, name: 'feature2', active: true, version: 2) strategy_b = create(:operations_strategy, feature_flag: feature_flag_b) create(:operations_scope, strategy: strategy_b, environment_scope: 'staging') @@ -322,13 +319,11 @@ RSpec.describe API::Unleash do end it 'returns only strategies matching the environment scope' do - feature_flag = create(:operations_feature_flag, project: project, - name: 'feature1', active: true, version: 2) - strategy_a = create(:operations_strategy, feature_flag: feature_flag, - name: 'userWithId', parameters: { userIds: 'user2,user8,user4' }) + feature_flag = create(:operations_feature_flag, project: project, name: 'feature1', active: true, version: 2) + strategy_a = create(:operations_strategy, + feature_flag: feature_flag, name: 'userWithId', parameters: { userIds: 'user2,user8,user4' }) create(:operations_scope, strategy: strategy_a, environment_scope: 'production') - strategy_b = create(:operations_strategy, feature_flag: feature_flag, - name: 'default', parameters: {}) + strategy_b = create(:operations_strategy, feature_flag: feature_flag, name: 'default', parameters: {}) create(:operations_scope, strategy: strategy_b, environment_scope: 'staging') get api(features_url), headers: { 'UNLEASH-INSTANCEID' => client.token, 'UNLEASH-APPNAME' => 'production' } @@ -346,10 +341,12 @@ RSpec.describe API::Unleash do it 'returns only flags for the given project' do project_b = create(:project) - feature_flag_a = create(:operations_feature_flag, project: project, name: 'feature_a', active: true, version: 2) + feature_flag_a = create(:operations_feature_flag, + project: project, name: 'feature_a', active: true, version: 2) strategy_a = create(:operations_strategy, feature_flag: feature_flag_a) create(:operations_scope, strategy: strategy_a, environment_scope: 'sandbox') - feature_flag_b = create(:operations_feature_flag, project: project_b, name: 'feature_b', active: true, version: 2) + feature_flag_b = create(:operations_feature_flag, + project: project_b, name: 'feature_b', active: true, version: 2) strategy_b = create(:operations_strategy, feature_flag: feature_flag_b) create(:operations_scope, strategy: strategy_b, environment_scope: 'sandbox') @@ -367,16 +364,16 @@ RSpec.describe API::Unleash do end it 'returns all strategies with a matching scope' do - feature_flag = create(:operations_feature_flag, project: project, - name: 'feature1', active: true, version: 2) - strategy_a = create(:operations_strategy, feature_flag: feature_flag, - name: 'userWithId', parameters: { userIds: 'user2,user8,user4' }) + feature_flag = create(:operations_feature_flag, project: project, name: 'feature1', active: true, version: 2) + strategy_a = create(:operations_strategy, + feature_flag: feature_flag, name: 'userWithId', parameters: { userIds: 'user2,user8,user4' }) create(:operations_scope, strategy: strategy_a, environment_scope: '*') - strategy_b = create(:operations_strategy, feature_flag: feature_flag, - name: 'default', parameters: {}) + strategy_b = create(:operations_strategy, feature_flag: feature_flag, name: 'default', parameters: {}) create(:operations_scope, strategy: strategy_b, environment_scope: 'review/*') - strategy_c = create(:operations_strategy, feature_flag: feature_flag, - name: 'gradualRolloutUserId', parameters: { groupId: 'default', percentage: '15' }) + strategy_c = create(:operations_strategy, + feature_flag: feature_flag, + name: 'gradualRolloutUserId', + parameters: { groupId: 'default', percentage: '15' }) create(:operations_scope, strategy: strategy_c, environment_scope: 'review/patch-1') get api(features_url), headers: { 'UNLEASH-INSTANCEID' => client.token, 'UNLEASH-APPNAME' => 'review/patch-1' } @@ -395,10 +392,8 @@ RSpec.describe API::Unleash do end it 'returns a strategy with more than one matching scope' do - feature_flag = create(:operations_feature_flag, project: project, - name: 'feature1', active: true, version: 2) - strategy = create(:operations_strategy, feature_flag: feature_flag, - name: 'default', parameters: {}) + feature_flag = create(:operations_feature_flag, project: project, name: 'feature1', active: true, version: 2) + strategy = create(:operations_strategy, feature_flag: feature_flag, name: 'default', parameters: {}) create(:operations_scope, strategy: strategy, environment_scope: 'production') create(:operations_scope, strategy: strategy, environment_scope: '*') @@ -416,10 +411,9 @@ RSpec.describe API::Unleash do end it 'returns a disabled flag with a matching scope' do - feature_flag = create(:operations_feature_flag, project: project, - name: 'myfeature', active: false, version: 2) - strategy = create(:operations_strategy, feature_flag: feature_flag, - name: 'default', parameters: {}) + feature_flag = create(:operations_feature_flag, + project: project, name: 'myfeature', active: false, version: 2) + strategy = create(:operations_strategy, feature_flag: feature_flag, name: 'default', parameters: {}) create(:operations_scope, strategy: strategy, environment_scope: 'production') get api(features_url), headers: { 'UNLEASH-INSTANCEID' => client.token, 'UNLEASH-APPNAME' => 'production' } @@ -436,12 +430,12 @@ RSpec.describe API::Unleash do end it 'returns a userWithId strategy for a gitlabUserList strategy' do - feature_flag = create(:operations_feature_flag, :new_version_flag, project: project, - name: 'myfeature', active: true) - user_list = create(:operations_feature_flag_user_list, project: project, - name: 'My List', user_xids: 'user1,user2') - strategy = create(:operations_strategy, feature_flag: feature_flag, - name: 'gitlabUserList', parameters: {}, user_list: user_list) + feature_flag = create(:operations_feature_flag, :new_version_flag, + project: project, name: 'myfeature', active: true) + user_list = create(:operations_feature_flag_user_list, + project: project, name: 'My List', user_xids: 'user1,user2') + strategy = create(:operations_strategy, + feature_flag: feature_flag, name: 'gitlabUserList', parameters: {}, user_list: user_list) create(:operations_scope, strategy: strategy, environment_scope: 'production') get api(features_url), headers: { 'UNLEASH-INSTANCEID' => client.token, 'UNLEASH-APPNAME' => 'production' } diff --git a/spec/requests/api/usage_data_queries_spec.rb b/spec/requests/api/usage_data_queries_spec.rb index 69a8d865a59..6ce03954246 100644 --- a/spec/requests/api/usage_data_queries_spec.rb +++ b/spec/requests/api/usage_data_queries_spec.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require 'spec_helper' +require 'rake_helper' RSpec.describe API::UsageDataQueries do include UsageDataHelpers @@ -64,5 +65,36 @@ RSpec.describe API::UsageDataQueries do expect(response).to have_gitlab_http_status(:forbidden) end end + + context 'when querying sql metrics' do + let(:file) { Rails.root.join('tmp', 'test', 'sql_metrics_queries.json') } + + before do + Rake.application.rake_require 'tasks/gitlab/usage_data' + + run_rake_task('gitlab:usage_data:generate_sql_metrics_queries') + end + + after do + FileUtils.rm_rf(file) + end + + it 'matches the generated query' do + Timecop.freeze(2021, 1, 1) do + get api(endpoint, admin) + end + + data = Gitlab::Json.parse(File.read(file)) + + expect( + json_response['counts_monthly'].except('aggregated_metrics') + ).to eq(data['counts_monthly'].except('aggregated_metrics')) + + expect(json_response['counts']).to eq(data['counts']) + expect(json_response['active_user_count']).to eq(data['active_user_count']) + expect(json_response['usage_activity_by_stage']).to eq(data['usage_activity_by_stage']) + expect(json_response['usage_activity_by_stage_monthly']).to eq(data['usage_activity_by_stage_monthly']) + end + end end end diff --git a/spec/requests/api/usage_data_spec.rb b/spec/requests/api/usage_data_spec.rb index ea50c404d92..d532fb6c168 100644 --- a/spec/requests/api/usage_data_spec.rb +++ b/spec/requests/api/usage_data_spec.rb @@ -138,7 +138,9 @@ RSpec.describe API::UsageData do context 'with correct params' do it 'returns status ok' do - expect(Gitlab::Redis::HLL).to receive(:add) + expect(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:track).with(anything, known_event, anything) + # allow other events to also get triggered + allow(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:track) post api(endpoint, user), params: { event: known_event } diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index 26238a87209..96e23337411 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' RSpec.describe API::Users do + include WorkhorseHelpers + let_it_be(:admin) { create(:admin) } let_it_be(:user, reload: true) { create(:user, username: 'user.withdot') } let_it_be(:key) { create(:key, user: user) } @@ -116,7 +118,7 @@ RSpec.describe API::Users do end it "returns a 403 if the target user is an admin" do - expect(TwoFactor::DestroyService).to receive(:new).never + expect(TwoFactor::DestroyService).not_to receive(:new) expect do patch api("/users/#{admin_with_2fa.id}/disable_two_factor", admin) @@ -127,7 +129,7 @@ RSpec.describe API::Users do end it "returns a 404 if the target user cannot be found" do - expect(TwoFactor::DestroyService).to receive(:new).never + expect(TwoFactor::DestroyService).not_to receive(:new) patch api("/users/#{non_existing_record_id}/disable_two_factor", admin) @@ -1180,6 +1182,22 @@ RSpec.describe API::Users do expect(new_user.user_preference.view_diffs_file_by_file?).to eq(true) end + it "creates user with avatar" do + workhorse_form_with_file( + api('/users', admin), + method: :post, + file_key: :avatar, + params: attributes_for(:user, avatar: fixture_file_upload('spec/fixtures/banana_sample.gif', 'image/gif')) + ) + + expect(response).to have_gitlab_http_status(:created) + + new_user = User.find_by(id: json_response['id']) + + expect(new_user).not_to eq(nil) + expect(json_response['avatar_url']).to include(new_user.avatar_path) + end + it "does not create user with invalid email" do post api('/users', admin), params: { @@ -1478,7 +1496,12 @@ RSpec.describe API::Users do end it 'updates user with avatar' do - put api("/users/#{user.id}", admin), params: { avatar: fixture_file_upload('spec/fixtures/banana_sample.gif', 'image/gif') } + workhorse_form_with_file( + api("/users/#{user.id}", admin), + method: :put, + file_key: :avatar, + params: { avatar: fixture_file_upload('spec/fixtures/banana_sample.gif', 'image/gif') } + ) user.reload @@ -2479,14 +2502,32 @@ RSpec.describe API::Users do describe "DELETE /users/:id" do let_it_be(:issue) { create(:issue, author: user) } - it "deletes user", :sidekiq_inline do - namespace_id = user.namespace.id + context 'user deletion' do + context 'when user_destroy_with_limited_execution_time_worker is enabled' do + it "deletes user", :sidekiq_inline do + perform_enqueued_jobs { delete api("/users/#{user.id}", admin) } - perform_enqueued_jobs { delete api("/users/#{user.id}", admin) } + expect(response).to have_gitlab_http_status(:no_content) + expect(Users::GhostUserMigration.where(user: user, + initiator_user: admin)).to be_exists + end + end - expect(response).to have_gitlab_http_status(:no_content) - expect { User.find(user.id) }.to raise_error ActiveRecord::RecordNotFound - expect { Namespace.find(namespace_id) }.to raise_error ActiveRecord::RecordNotFound + context 'when user_destroy_with_limited_execution_time_worker is disabled' do + before do + stub_feature_flags(user_destroy_with_limited_execution_time_worker: false) + end + + it "deletes user", :sidekiq_inline do + namespace_id = user.namespace.id + + perform_enqueued_jobs { delete api("/users/#{user.id}", admin) } + + expect(response).to have_gitlab_http_status(:no_content) + expect { User.find(user.id) }.to raise_error ActiveRecord::RecordNotFound + expect { Namespace.find(namespace_id) }.to raise_error ActiveRecord::RecordNotFound + end + end end context "sole owner of a group" do @@ -2550,22 +2591,55 @@ RSpec.describe API::Users do expect(response).to have_gitlab_http_status(:not_found) end - context "hard delete disabled" do - it "moves contributions to the ghost user", :sidekiq_might_not_need_inline do - perform_enqueued_jobs { delete api("/users/#{user.id}", admin) } + context 'hard delete' do + context 'when user_destroy_with_limited_execution_time_worker is enabled' do + context "hard delete disabled" do + it "moves contributions to the ghost user", :sidekiq_might_not_need_inline do + perform_enqueued_jobs { delete api("/users/#{user.id}", admin) } - expect(response).to have_gitlab_http_status(:no_content) - expect(issue.reload).to be_persisted - expect(issue.author.ghost?).to be_truthy + expect(response).to have_gitlab_http_status(:no_content) + expect(issue.reload).to be_persisted + expect(Users::GhostUserMigration.where(user: user, + initiator_user: admin, + hard_delete: false)).to be_exists + end + end + + context "hard delete enabled" do + it "removes contributions", :sidekiq_might_not_need_inline do + perform_enqueued_jobs { delete api("/users/#{user.id}?hard_delete=true", admin) } + + expect(response).to have_gitlab_http_status(:no_content) + expect(Users::GhostUserMigration.where(user: user, + initiator_user: admin, + hard_delete: true)).to be_exists + end + end end - end - context "hard delete enabled" do - it "removes contributions", :sidekiq_might_not_need_inline do - perform_enqueued_jobs { delete api("/users/#{user.id}?hard_delete=true", admin) } + context 'when user_destroy_with_limited_execution_time_worker is disabled' do + before do + stub_feature_flags(user_destroy_with_limited_execution_time_worker: false) + end - expect(response).to have_gitlab_http_status(:no_content) - expect(Issue.exists?(issue.id)).to be_falsy + context "hard delete disabled" do + it "moves contributions to the ghost user", :sidekiq_might_not_need_inline do + perform_enqueued_jobs { delete api("/users/#{user.id}", admin) } + + expect(response).to have_gitlab_http_status(:no_content) + expect(issue.reload).to be_persisted + expect(issue.author.ghost?).to be_truthy + end + end + + context "hard delete enabled" do + it "removes contributions", :sidekiq_might_not_need_inline do + perform_enqueued_jobs { delete api("/users/#{user.id}?hard_delete=true", admin) } + + expect(response).to have_gitlab_http_status(:no_content) + expect(Issue.exists?(issue.id)).to be_falsy + end + end end end end @@ -3238,7 +3312,7 @@ RSpec.describe API::Users do let(:user) { create(:user, **activity) } context 'with no recent activity' do - let(:activity) { { last_activity_on: ::User::MINIMUM_INACTIVE_DAYS.next.days.ago } } + let(:activity) { { last_activity_on: Gitlab::CurrentSettings.deactivate_dormant_users_period.next.days.ago } } it 'deactivates an active user' do deactivate @@ -3249,13 +3323,13 @@ RSpec.describe API::Users do end context 'with recent activity' do - let(:activity) { { last_activity_on: ::User::MINIMUM_INACTIVE_DAYS.pred.days.ago } } + let(:activity) { { last_activity_on: Gitlab::CurrentSettings.deactivate_dormant_users_period.pred.days.ago } } it 'does not deactivate an active user' do deactivate expect(response).to have_gitlab_http_status(:forbidden) - expect(json_response['message']).to eq("403 Forbidden - The user you are trying to deactivate has been active in the past #{::User::MINIMUM_INACTIVE_DAYS} days and cannot be deactivated") + expect(json_response['message']).to eq("403 Forbidden - The user you are trying to deactivate has been active in the past #{Gitlab::CurrentSettings.deactivate_dormant_users_period} days and cannot be deactivated") expect(user.reload.state).to eq('active') end end diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb index 77107d0b43c..81e923983ab 100644 --- a/spec/requests/git_http_spec.rb +++ b/spec/requests/git_http_spec.rb @@ -452,7 +452,7 @@ RSpec.describe 'Git HTTP requests' do canonical_project.add_maintainer(user) create(:merge_request, source_project: project, - target_project: canonical_project, + target_project: canonical_project, source_branch: 'fixes', allow_collaboration: true) end @@ -1105,7 +1105,7 @@ RSpec.describe 'Git HTTP requests' do canonical_project.add_maintainer(user) create(:merge_request, source_project: project, - target_project: canonical_project, + target_project: canonical_project, source_branch: 'fixes', allow_collaboration: true) end diff --git a/spec/requests/groups/observability_controller_spec.rb b/spec/requests/groups/observability_controller_spec.rb new file mode 100644 index 00000000000..9be013d4385 --- /dev/null +++ b/spec/requests/groups/observability_controller_spec.rb @@ -0,0 +1,190 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Groups::ObservabilityController do + include ContentSecurityPolicyHelpers + + let_it_be(:group) { create(:group) } + let_it_be(:user) { create(:user) } + + subject do + get group_observability_index_path(group) + response + end + + describe 'GET #index' do + context 'when user is not authenticated' do + it 'returns 404' do + expect(subject).to have_gitlab_http_status(:not_found) + end + end + + context 'when observability url is missing' do + before do + allow(described_class).to receive(:observability_url).and_return("") + end + + it 'returns 404' do + expect(subject).to have_gitlab_http_status(:not_found) + end + end + + context 'when user is not a developer' do + before do + sign_in(user) + end + + it 'returns 404' do + expect(subject).to have_gitlab_http_status(:not_found) + end + end + + context 'when user is authenticated and a developer' do + before do + sign_in(user) + group.add_developer(user) + end + + it 'returns 200' do + expect(subject).to have_gitlab_http_status(:ok) + end + + it 'renders the proper layout' do + expect(subject).to render_template("layouts/group") + expect(subject).to render_template("layouts/fullscreen") + expect(subject).not_to render_template('layouts/nav/breadcrumbs') + expect(subject).to render_template("nav/sidebar/_group") + end + + describe 'iframe' do + subject do + get group_observability_index_path(group) + Nokogiri::HTML.parse(response.body).at_css('iframe#observability-ui-iframe') + end + + it 'sets the iframe src to the proper URL' do + expect(subject.attributes['src'].value).to eq("https://observe.gitlab.com/-/#{group.id}") + end + + it 'when the env is staging, sets the iframe src to the proper URL' do + stub_config_setting(url: Gitlab::Saas.staging_com_url) + expect(subject.attributes['src'].value).to eq("https://staging.observe.gitlab.com/-/#{group.id}") + end + + it 'overrides the iframe src url if specified by OVERRIDE_OBSERVABILITY_URL env' do + stub_env('OVERRIDE_OBSERVABILITY_URL', 'http://foo.test') + + expect(subject.attributes['src'].value).to eq("http://foo.test/-/#{group.id}") + end + end + + describe 'CSP' do + before do + setup_existing_csp_for_controller(described_class, csp) + end + + subject do + get group_observability_index_path(group) + response.headers['Content-Security-Policy'] + end + + context 'when there is no CSP config' do + let(:csp) { ActionDispatch::ContentSecurityPolicy.new } + + it 'does not add any csp header' do + expect(subject).to be_blank + end + end + + context 'when frame-src exists in the CSP config' do + let(:csp) do + ActionDispatch::ContentSecurityPolicy.new do |p| + p.frame_src 'https://something.test' + end + end + + it 'appends the proper url to frame-src CSP directives' do + expect(subject).to include( + "frame-src https://something.test https://observe.gitlab.com 'self'") + end + + it 'appends the proper url to frame-src CSP directives when Gilab.staging?' do + stub_config_setting(url: Gitlab::Saas.staging_com_url) + + expect(subject).to include( + "frame-src https://something.test https://staging.observe.gitlab.com 'self'") + end + + it 'appends the proper url to frame-src CSP directives when OVERRIDE_OBSERVABILITY_URL is specified' do + stub_env('OVERRIDE_OBSERVABILITY_URL', 'http://foo.test') + + expect(subject).to include( + "frame-src https://something.test http://foo.test 'self'") + end + end + + context 'when self is already present in the policy' do + let(:csp) do + ActionDispatch::ContentSecurityPolicy.new do |p| + p.frame_src "'self'" + end + end + + it 'does not append self again' do + expect(subject).to include( + "frame-src 'self' https://observe.gitlab.com;") + end + end + + context 'when default-src exists in the CSP config' do + let(:csp) do + ActionDispatch::ContentSecurityPolicy.new do |p| + p.default_src 'https://something.test' + end + end + + it 'does not change default-src' do + expect(subject).to include( + "default-src https://something.test;") + end + + it 'appends the proper url to frame-src CSP directives' do + expect(subject).to include( + "frame-src https://something.test https://observe.gitlab.com 'self'") + end + + it 'appends the proper url to frame-src CSP directives when Gilab.staging?' do + stub_config_setting(url: Gitlab::Saas.staging_com_url) + + expect(subject).to include( + "frame-src https://something.test https://staging.observe.gitlab.com 'self'") + end + + it 'appends the proper url to frame-src CSP directives when OVERRIDE_OBSERVABILITY_URL is specified' do + stub_env('OVERRIDE_OBSERVABILITY_URL', 'http://foo.test') + + expect(subject).to include( + "frame-src https://something.test http://foo.test 'self'") + end + end + + context 'when frame-src and default-src exist in the CSP config' do + let(:csp) do + ActionDispatch::ContentSecurityPolicy.new do |p| + p.default_src 'https://something_default.test' + p.frame_src 'https://something.test' + end + end + + it 'appends to frame-src CSP directives' do + expect(subject).to include( + "frame-src https://something.test https://observe.gitlab.com 'self'") + expect(subject).to include( + "default-src https://something_default.test") + end + end + end + end + end +end diff --git a/spec/requests/health_controller_spec.rb b/spec/requests/health_controller_spec.rb index f70faf5bb9c..ae15b63df19 100644 --- a/spec/requests/health_controller_spec.rb +++ b/spec/requests/health_controller_spec.rb @@ -127,6 +127,10 @@ RSpec.describe HealthController do end it 'responds with readiness checks data' do + expect_next_instance_of(Gitlab::GitalyClient::ServerService) do |service| + expect(service).to receive(:readiness_check).and_return({ success: true }) + end + subject expect(json_response['db_check']).to contain_exactly({ 'status' => 'ok' }) @@ -138,19 +142,29 @@ RSpec.describe HealthController do end it 'responds with readiness checks data when a failure happens' do - allow(Gitlab::HealthChecks::Redis::RedisCheck).to receive(:readiness).and_return( - Gitlab::HealthChecks::Result.new('redis_check', false, "check error")) + allow(Gitlab::HealthChecks::Redis::SharedStateCheck).to receive(:readiness).and_return( + Gitlab::HealthChecks::Result.new('shared_state_check', false, "check error")) subject expect(json_response['cache_check']).to contain_exactly({ 'status' => 'ok' }) - expect(json_response['redis_check']).to contain_exactly( + expect(json_response['shared_state_check']).to contain_exactly( { 'status' => 'failed', 'message' => 'check error' }) expect(response).to have_gitlab_http_status(:service_unavailable) expect(response.headers['X-GitLab-Custom-Error']).to eq(1) end + it 'checks all redis instances' do + expected_redis_checks = Gitlab::Redis::ALL_CLASSES.map do |redis| + { "#{redis.store_name.underscore}_check" => [{ 'status' => 'ok' }] } + end + + subject + + expect(json_response).to include(*expected_redis_checks) + end + context 'when DB is not accessible and connection raises an exception' do before do expect(Gitlab::HealthChecks::DbCheck) @@ -170,7 +184,7 @@ RSpec.describe HealthController do context 'when any exception happens during the probing' do before do - expect(Gitlab::HealthChecks::Redis::RedisCheck) + expect(Gitlab::HealthChecks::Redis::CacheCheck) .to receive(:readiness) .and_raise(::Redis::CannotConnectError, 'Redis down') end diff --git a/spec/requests/jira_connect/oauth_callbacks_controller_spec.rb b/spec/requests/jira_connect/oauth_callbacks_controller_spec.rb index 1e4628e5d59..12b9429b648 100644 --- a/spec/requests/jira_connect/oauth_callbacks_controller_spec.rb +++ b/spec/requests/jira_connect/oauth_callbacks_controller_spec.rb @@ -5,12 +5,6 @@ require 'spec_helper' RSpec.describe JiraConnect::OauthCallbacksController do describe 'GET /-/jira_connect/oauth_callbacks' do context 'when logged in' do - let_it_be(:user) { create(:user) } - - before do - sign_in(user) - end - it 'renders a page prompting the user to close the window' do get '/-/jira_connect/oauth_callbacks' diff --git a/spec/requests/jira_connect/subscriptions_controller_spec.rb b/spec/requests/jira_connect/subscriptions_controller_spec.rb index d8f329f13f5..f407ea09250 100644 --- a/spec/requests/jira_connect/subscriptions_controller_spec.rb +++ b/spec/requests/jira_connect/subscriptions_controller_spec.rb @@ -12,18 +12,29 @@ RSpec.describe JiraConnect::SubscriptionsController do let(:jwt) { Atlassian::Jwt.encode({ iss: installation.client_key, qsh: qsh }, installation.shared_secret) } - before do + subject(:content_security_policy) do get '/-/jira_connect/subscriptions', params: { jwt: jwt } - end - subject(:content_security_policy) { response.headers['Content-Security-Policy'] } + response.headers['Content-Security-Policy'] + end - it { is_expected.to include('http://self-managed-gitlab.com/-/jira_connect/oauth_application_ids') } + it { is_expected.to include('http://self-managed-gitlab.com/-/jira_connect/') } + it { is_expected.to include('http://self-managed-gitlab.com/api/') } context 'with no self-managed instance configured' do let_it_be(:installation) { create(:jira_connect_installation, instance_url: '') } - it { is_expected.not_to include('http://self-managed-gitlab.com') } + it { is_expected.not_to include('http://self-managed-gitlab.com/-/jira_connect/') } + it { is_expected.not_to include('http://self-managed-gitlab.com/api/') } + end + + context 'with jira_connect_oauth_self_managed feature disabled' do + before do + stub_feature_flags(jira_connect_oauth_self_managed: false) + end + + it { is_expected.not_to include('http://self-managed-gitlab.com/-/jira_connect/') } + it { is_expected.not_to include('http://self-managed-gitlab.com/api/') } end end end diff --git a/spec/requests/jwt_controller_spec.rb b/spec/requests/jwt_controller_spec.rb index c9904ffa37b..e6916e02fde 100644 --- a/spec/requests/jwt_controller_spec.rb +++ b/spec/requests/jwt_controller_spec.rb @@ -27,6 +27,10 @@ RSpec.describe JwtController do let(:headers) { { authorization: credentials('personal_access_token', pat.token) } } it 'fails authentication' do + expect(::Gitlab::AuthLogger).to receive(:warn).with( + hash_including(message: 'JWT authentication failed', + http_user: 'personal_access_token')).and_call_original + get '/jwt/auth', params: parameters, headers: headers expect(response).to have_gitlab_http_status(:unauthorized) @@ -80,7 +84,7 @@ RSpec.describe JwtController do context 'project with enabled CI' do subject! { get '/jwt/auth', params: parameters, headers: headers } - it { expect(service_class).to have_received(:new).with(project, user, ActionController::Parameters.new(parameters).permit!) } + it { expect(service_class).to have_received(:new).with(project, user, ActionController::Parameters.new(parameters.merge(auth_type: :build)).permit!) } it_behaves_like 'user logging' end @@ -103,7 +107,12 @@ RSpec.describe JwtController do it 'authenticates correctly' do expect(response).to have_gitlab_http_status(:ok) - expect(service_class).to have_received(:new).with(nil, nil, ActionController::Parameters.new(parameters.merge(deploy_token: deploy_token)).permit!) + expect(service_class).to have_received(:new) + .with( + nil, + nil, + ActionController::Parameters.new(parameters.merge(deploy_token: deploy_token, auth_type: :deploy_token)).permit! + ) end it 'does not log a user' do @@ -123,7 +132,12 @@ RSpec.describe JwtController do it 'authenticates correctly' do expect(response).to have_gitlab_http_status(:ok) - expect(service_class).to have_received(:new).with(nil, user, ActionController::Parameters.new(parameters).permit!) + expect(service_class).to have_received(:new) + .with( + nil, + user, + ActionController::Parameters.new(parameters.merge(auth_type: :personal_access_token)).permit! + ) end it_behaves_like 'rejecting a blocked user' @@ -138,7 +152,7 @@ RSpec.describe JwtController do subject! { get '/jwt/auth', params: parameters, headers: headers } - it { expect(service_class).to have_received(:new).with(nil, user, ActionController::Parameters.new(parameters).permit!) } + it { expect(service_class).to have_received(:new).with(nil, user, ActionController::Parameters.new(parameters.merge(auth_type: :gitlab_or_ldap)).permit!) } it_behaves_like 'rejecting a blocked user' @@ -158,7 +172,7 @@ RSpec.describe JwtController do ActionController::Parameters.new({ service: service_name, scopes: %w(scope1 scope2) }).permit! end - it { expect(service_class).to have_received(:new).with(nil, user, service_parameters) } + it { expect(service_class).to have_received(:new).with(nil, user, service_parameters.merge(auth_type: :gitlab_or_ldap)) } it_behaves_like 'user logging' end diff --git a/spec/requests/oauth_tokens_spec.rb b/spec/requests/oauth_tokens_spec.rb index 180341fc85d..f2fb380bde0 100644 --- a/spec/requests/oauth_tokens_spec.rb +++ b/spec/requests/oauth_tokens_spec.rb @@ -78,11 +78,12 @@ RSpec.describe 'OAuth Tokens requests' do context 'revoked refresh token' do let!(:existing_token) do - create(:oauth_access_token, application: application, - resource_owner_id: user.id, - created_at: 2.hours.ago, - revoked_at: 1.hour.ago, - expires_in: 5) + create(:oauth_access_token, + application: application, + resource_owner_id: user.id, + created_at: 2.hours.ago, + revoked_at: 1.hour.ago, + expires_in: 5) end it 'does not issue a new token' do diff --git a/spec/requests/openid_connect_spec.rb b/spec/requests/openid_connect_spec.rb index afaa6168bfd..3a40fec58e8 100644 --- a/spec/requests/openid_connect_spec.rb +++ b/spec/requests/openid_connect_spec.rb @@ -23,24 +23,24 @@ RSpec.describe 'OpenID Connect requests' do let(:id_token_claims) do { - 'sub' => user.id.to_s, + 'sub' => user.id.to_s, 'sub_legacy' => hashed_subject } end let(:user_info_claims) do { - 'name' => 'Alice', - 'nickname' => 'alice', - 'email' => 'public@example.com', + 'name' => 'Alice', + 'nickname' => 'alice', + 'email' => 'public@example.com', 'email_verified' => true, - 'website' => 'https://example.com', - 'profile' => 'http://localhost/alice', - 'picture' => "http://localhost/uploads/-/system/user/avatar/#{user.id}/dk.png", - 'groups' => kind_of(Array), - 'https://gitlab.org/claims/groups/owner' => kind_of(Array), + 'website' => 'https://example.com', + 'profile' => 'http://localhost/alice', + 'picture' => "http://localhost/uploads/-/system/user/avatar/#{user.id}/dk.png", + 'groups' => kind_of(Array), + 'https://gitlab.org/claims/groups/owner' => kind_of(Array), 'https://gitlab.org/claims/groups/maintainer' => kind_of(Array), - 'https://gitlab.org/claims/groups/developer' => kind_of(Array) + 'https://gitlab.org/claims/groups/developer' => kind_of(Array) } end diff --git a/spec/requests/projects/environments_controller_spec.rb b/spec/requests/projects/environments_controller_spec.rb index 0890b0c45da..66ab265fc0f 100644 --- a/spec/requests/projects/environments_controller_spec.rb +++ b/spec/requests/projects/environments_controller_spec.rb @@ -30,7 +30,7 @@ RSpec.describe Projects::EnvironmentsController do deployer = create(:user) pipeline = create(:ci_pipeline, project: environment.project) build = create(:ci_build, environment: environment.name, pipeline: pipeline, user: deployer) - create(:deployment, :success, environment: environment, deployable: build, user: deployer, - project: project, sha: commit.sha) + create(:deployment, :success, + environment: environment, deployable: build, user: deployer, project: project, sha: commit.sha) end end diff --git a/spec/requests/projects/google_cloud/configuration_controller_spec.rb b/spec/requests/projects/google_cloud/configuration_controller_spec.rb index 08d4ad2f9ba..41593b8d7a7 100644 --- a/spec/requests/projects/google_cloud/configuration_controller_spec.rb +++ b/spec/requests/projects/google_cloud/configuration_controller_spec.rb @@ -2,9 +2,6 @@ require 'spec_helper' -# Mock Types -MockGoogleOAuth2Credentials = Struct.new(:app_id, :app_secret) - RSpec.describe Projects::GoogleCloud::ConfigurationController do let_it_be(:project) { create(:project, :public) } let_it_be(:url) { project_google_cloud_configuration_path(project) } @@ -29,10 +26,9 @@ RSpec.describe Projects::GoogleCloud::ConfigurationController do get url expect_snowplow_event( - category: 'Projects::GoogleCloud', - action: 'admin_project_google_cloud!', - label: 'error_access_denied', - property: 'invalid_user', + category: 'Projects::GoogleCloud::ConfigurationController', + action: 'error_invalid_user', + label: nil, project: project, user: unauthorized_member ) @@ -56,7 +52,7 @@ RSpec.describe Projects::GoogleCloud::ConfigurationController do context 'but gitlab instance is not configured for google oauth2' do it 'returns forbidden' do - unconfigured_google_oauth2 = MockGoogleOAuth2Credentials.new('', '') + unconfigured_google_oauth2 = Struct.new(:app_id, :app_secret).new('', '') allow(Gitlab::Auth::OAuth::Provider).to receive(:config_for) .with('google_oauth2') .and_return(unconfigured_google_oauth2) @@ -68,11 +64,9 @@ RSpec.describe Projects::GoogleCloud::ConfigurationController do expect(response).to have_gitlab_http_status(:forbidden) expect_snowplow_event( - category: 'Projects::GoogleCloud', - action: 'google_oauth2_enabled!', - label: 'error_access_denied', - extra: { reason: 'google_oauth2_not_configured', - config: unconfigured_google_oauth2 }, + category: 'Projects::GoogleCloud::ConfigurationController', + action: 'error_google_oauth2_not_enabled', + label: nil, project: project, user: authorized_member ) @@ -93,10 +87,9 @@ RSpec.describe Projects::GoogleCloud::ConfigurationController do expect(response).to have_gitlab_http_status(:not_found) expect_snowplow_event( - category: 'Projects::GoogleCloud', - action: 'feature_flag_enabled!', - label: 'error_access_denied', - property: 'feature_flag_not_enabled', + category: 'Projects::GoogleCloud::ConfigurationController', + action: 'error_feature_flag_not_enabled', + label: nil, project: project, user: authorized_member ) @@ -117,20 +110,9 @@ RSpec.describe Projects::GoogleCloud::ConfigurationController do expect(response).to be_successful expect_snowplow_event( - category: 'Projects::GoogleCloud', - action: 'configuration#index', - label: 'success', - extra: { - configurationUrl: project_google_cloud_configuration_path(project), - deploymentsUrl: project_google_cloud_deployments_path(project), - databasesUrl: project_google_cloud_databases_path(project), - serviceAccounts: [], - createServiceAccountUrl: project_google_cloud_service_accounts_path(project), - emptyIllustrationUrl: ActionController::Base.helpers.image_path('illustrations/pipelines_empty.svg'), - configureGcpRegionsUrl: project_google_cloud_gcp_regions_path(project), - gcpRegions: [], - revokeOauthUrl: nil - }, + category: 'Projects::GoogleCloud::ConfigurationController', + action: 'render_page', + label: nil, project: project, user: authorized_member ) diff --git a/spec/requests/projects/google_cloud/databases_controller_spec.rb b/spec/requests/projects/google_cloud/databases_controller_spec.rb index c9335f8f317..4edef71f326 100644 --- a/spec/requests/projects/google_cloud/databases_controller_spec.rb +++ b/spec/requests/projects/google_cloud/databases_controller_spec.rb @@ -2,133 +2,166 @@ require 'spec_helper' -# Mock Types -MockGoogleOAuth2Credentials = Struct.new(:app_id, :app_secret) +RSpec.describe Projects::GoogleCloud::DatabasesController, :snowplow do + shared_examples 'shared examples for database controller endpoints' do + include_examples 'requires `admin_project_google_cloud` role' -RSpec.describe Projects::GoogleCloud::DatabasesController do - let_it_be(:project) { create(:project, :public) } - let_it_be(:url) { project_google_cloud_databases_path(project) } + include_examples 'requires feature flag `incubation_5mp_google_cloud` enabled' - let_it_be(:user_guest) { create(:user) } - let_it_be(:user_developer) { create(:user) } - let_it_be(:user_maintainer) { create(:user) } + include_examples 'requires valid Google OAuth2 configuration' - let_it_be(:unauthorized_members) { [user_guest, user_developer] } - let_it_be(:authorized_members) { [user_maintainer] } + include_examples 'requires valid Google Oauth2 token' do + let_it_be(:mock_gcp_projects) { [{}, {}, {}] } + let_it_be(:mock_branches) { [] } + let_it_be(:mock_tags) { [] } + end + end + + context '-/google_cloud/databases' do + let_it_be(:project) { create(:project) } + let_it_be(:user) { create(:user) } + let_it_be(:renders_template) { 'projects/google_cloud/databases/index' } + let_it_be(:redirects_to) { nil } - before do - project.add_guest(user_guest) - project.add_developer(user_developer) - project.add_maintainer(user_maintainer) + subject { get project_google_cloud_databases_path(project) } + + include_examples 'shared examples for database controller endpoints' end - context 'when accessed by unauthorized members' do - it 'returns not found on GET request' do - unauthorized_members.each do |unauthorized_member| - sign_in(unauthorized_member) + context '-/google_cloud/databases/new/postgres' do + let_it_be(:project) { create(:project) } + let_it_be(:user) { create(:user) } + let_it_be(:renders_template) { 'projects/google_cloud/databases/cloudsql_form' } + let_it_be(:redirects_to) { nil } - get url - expect_snowplow_event( - category: 'Projects::GoogleCloud', - action: 'admin_project_google_cloud!', - label: 'error_access_denied', - property: 'invalid_user', - project: project, - user: unauthorized_member - ) + subject { get new_project_google_cloud_database_path(project, :postgres) } - expect(response).to have_gitlab_http_status(:not_found) - end - end + include_examples 'shared examples for database controller endpoints' end - context 'when accessed by authorized members' do - it 'returns successful' do - authorized_members.each do |authorized_member| - sign_in(authorized_member) + context '-/google_cloud/databases/new/mysql' do + let_it_be(:project) { create(:project) } + let_it_be(:user) { create(:user) } + let_it_be(:renders_template) { 'projects/google_cloud/databases/cloudsql_form' } + let_it_be(:redirects_to) { nil } - get url + subject { get new_project_google_cloud_database_path(project, :mysql) } - expect(response).to be_successful - expect(response).to render_template('projects/google_cloud/databases/index') - end - end + include_examples 'shared examples for database controller endpoints' + end - context 'but gitlab instance is not configured for google oauth2' do - it 'returns forbidden' do - unconfigured_google_oauth2 = MockGoogleOAuth2Credentials.new('', '') - allow(Gitlab::Auth::OAuth::Provider).to receive(:config_for) - .with('google_oauth2') - .and_return(unconfigured_google_oauth2) + context '-/google_cloud/databases/new/sqlserver' do + let_it_be(:project) { create(:project) } + let_it_be(:user) { create(:user) } + let_it_be(:renders_template) { 'projects/google_cloud/databases/cloudsql_form' } + let_it_be(:redirects_to) { nil } - authorized_members.each do |authorized_member| - sign_in(authorized_member) + subject { get new_project_google_cloud_database_path(project, :sqlserver) } - get url + include_examples 'shared examples for database controller endpoints' + end - expect(response).to have_gitlab_http_status(:forbidden) - expect_snowplow_event( - category: 'Projects::GoogleCloud', - action: 'google_oauth2_enabled!', - label: 'error_access_denied', - extra: { reason: 'google_oauth2_not_configured', - config: unconfigured_google_oauth2 }, - project: project, - user: authorized_member - ) + context '-/google_cloud/databases/create' do + let_it_be(:project) { create(:project) } + let_it_be(:user) { create(:user) } + let_it_be(:renders_template) { nil } + let_it_be(:redirects_to) { project_google_cloud_databases_path(project) } + + subject { post project_google_cloud_databases_path(project) } + + include_examples 'shared examples for database controller endpoints' + + context 'when the request is valid' do + before do + project.add_maintainer(user) + sign_in(user) + + allow_next_instance_of(GoogleApi::CloudPlatform::Client) do |client| + allow(client).to receive(:validate_token).and_return(true) + allow(client).to receive(:list_projects).and_return(mock_gcp_projects) + end + + allow_next_instance_of(BranchesFinder) do |finder| + allow(finder).to receive(:execute).and_return(mock_branches) + end + + allow_next_instance_of(TagsFinder) do |finder| + allow(finder).to receive(:execute).and_return(mock_branches) end end - end - context 'but feature flag is disabled' do - before do - stub_feature_flags(incubation_5mp_google_cloud: false) + subject do + post project_google_cloud_databases_path(project) end - it 'returns not found' do - authorized_members.each do |authorized_member| - sign_in(authorized_member) + it 'calls EnableCloudsqlService and redirects on error' do + expect_next_instance_of(::GoogleCloud::EnableCloudsqlService) do |service| + expect(service).to receive(:execute) + .and_return({ status: :error, message: 'error' }) + end - get url + subject - expect(response).to have_gitlab_http_status(:not_found) - expect_snowplow_event( - category: 'Projects::GoogleCloud', - action: 'feature_flag_enabled!', - label: 'error_access_denied', - property: 'feature_flag_not_enabled', - project: project, - user: authorized_member - ) - end + expect(response).to redirect_to(project_google_cloud_databases_path(project)) + + expect_snowplow_event( + category: 'Projects::GoogleCloud::DatabasesController', + action: 'error_enable_cloudsql_services', + label: nil, + project: project, + user: user + ) end - end - context 'but google oauth2 token is not valid' do - it 'does not return revoke oauth url' do - allow_next_instance_of(GoogleApi::CloudPlatform::Client) do |client| - allow(client).to receive(:validate_token).and_return(false) + context 'when EnableCloudsqlService is successful' do + before do + allow_next_instance_of(::GoogleCloud::EnableCloudsqlService) do |service| + allow(service).to receive(:execute) + .and_return({ status: :success, message: 'success' }) + end end - authorized_members.each do |authorized_member| - sign_in(authorized_member) + it 'calls CreateCloudsqlInstanceService and redirects on error' do + expect_next_instance_of(::GoogleCloud::CreateCloudsqlInstanceService) do |service| + expect(service).to receive(:execute) + .and_return({ status: :error, message: 'error' }) + end + + subject - get url + expect(response).to redirect_to(project_google_cloud_databases_path(project)) - expect(response).to be_successful expect_snowplow_event( - category: 'Projects::GoogleCloud', - action: 'databases#index', - label: 'success', - extra: { - configurationUrl: project_google_cloud_configuration_path(project), - deploymentsUrl: project_google_cloud_deployments_path(project), - databasesUrl: project_google_cloud_databases_path(project) - }, + category: 'Projects::GoogleCloud::DatabasesController', + action: 'error_create_cloudsql_instance', + label: nil, project: project, - user: authorized_member + user: user ) end + + context 'when CreateCloudsqlInstanceService is successful' do + before do + allow_next_instance_of(::GoogleCloud::CreateCloudsqlInstanceService) do |service| + allow(service).to receive(:execute) + .and_return({ status: :success, message: 'success' }) + end + end + + it 'redirects as expected' do + subject + + expect(response).to redirect_to(project_google_cloud_databases_path(project)) + + expect_snowplow_event( + category: 'Projects::GoogleCloud::DatabasesController', + action: 'create_cloudsql_instance', + label: "{}", + project: project, + user: user + ) + end + end end end end diff --git a/spec/requests/projects/google_cloud/deployments_controller_spec.rb b/spec/requests/projects/google_cloud/deployments_controller_spec.rb index 9e854e01516..ad6a3912e0b 100644 --- a/spec/requests/projects/google_cloud/deployments_controller_spec.rb +++ b/spec/requests/projects/google_cloud/deployments_controller_spec.rb @@ -29,10 +29,9 @@ RSpec.describe Projects::GoogleCloud::DeploymentsController do expect(response).to have_gitlab_http_status(:not_found) expect_snowplow_event( - category: 'Projects::GoogleCloud', - action: 'admin_project_google_cloud!', - label: 'error_access_denied', - property: 'invalid_user', + category: 'Projects::GoogleCloud::DeploymentsController', + action: 'error_invalid_user', + label: nil, project: project, user: nil ) @@ -48,10 +47,9 @@ RSpec.describe Projects::GoogleCloud::DeploymentsController do expect(response).to have_gitlab_http_status(:not_found) expect_snowplow_event( - category: 'Projects::GoogleCloud', - action: 'admin_project_google_cloud!', - label: 'error_access_denied', - property: 'invalid_user', + category: 'Projects::GoogleCloud::DeploymentsController', + action: 'error_invalid_user', + label: nil, project: project, user: nil ) @@ -75,6 +73,30 @@ RSpec.describe Projects::GoogleCloud::DeploymentsController do end end + describe 'Authorized GET project/-/google_cloud/deployments', :snowplow do + before do + sign_in(user_maintainer) + + allow_next_instance_of(GoogleApi::CloudPlatform::Client) do |client| + allow(client).to receive(:validate_token).and_return(true) + end + end + + it 'renders template' do + get "#{project_google_cloud_deployments_path(project)}" + + expect(response).to render_template(:index) + + expect_snowplow_event( + category: 'Projects::GoogleCloud::DeploymentsController', + action: 'render_page', + label: nil, + project: project, + user: user_maintainer + ) + end + end + describe 'Authorized GET project/-/google_cloud/deployments/cloud_run', :snowplow do let_it_be(:url) { "#{project_google_cloud_deployments_cloud_run_path(project)}" } @@ -92,11 +114,9 @@ RSpec.describe Projects::GoogleCloud::DeploymentsController do expect(response).to redirect_to(project_google_cloud_deployments_path(project)) # since GPC_PROJECT_ID is not set, enable cloud run service should return an error expect_snowplow_event( - category: 'Projects::GoogleCloud', - action: 'deployments#cloud_run', - label: 'error_enable_cloud_run', - extra: { message: 'No GCP projects found. Configure a service account or GCP_PROJECT_ID ci variable.', - status: :error }, + category: 'Projects::GoogleCloud::DeploymentsController', + action: 'error_enable_services', + label: nil, project: project, user: user_maintainer ) @@ -113,10 +133,9 @@ RSpec.describe Projects::GoogleCloud::DeploymentsController do expect(response).to redirect_to(project_google_cloud_deployments_path(project)) expect_snowplow_event( - category: 'Projects::GoogleCloud', - action: 'deployments#cloud_run', - label: 'error_gcp', - extra: mock_gcp_error, + category: 'Projects::GoogleCloud::DeploymentsController', + action: 'error_google_api', + label: nil, project: project, user: user_maintainer ) @@ -136,10 +155,9 @@ RSpec.describe Projects::GoogleCloud::DeploymentsController do expect(response).to redirect_to(project_google_cloud_deployments_path(project)) expect_snowplow_event( - category: 'Projects::GoogleCloud', - action: 'deployments#cloud_run', - label: 'error_generate_pipeline', - extra: { status: :error }, + category: 'Projects::GoogleCloud::DeploymentsController', + action: 'error_generate_cloudrun_pipeline', + label: nil, project: project, user: user_maintainer ) @@ -159,15 +177,9 @@ RSpec.describe Projects::GoogleCloud::DeploymentsController do expect(response).to have_gitlab_http_status(:found) expect(response.location).to include(project_new_merge_request_path(project)) expect_snowplow_event( - category: 'Projects::GoogleCloud', - action: 'deployments#cloud_run', - label: 'success', - extra: { "title": "Enable deployments to Cloud Run", - "description": "This merge request includes a Cloud Run deployment job in the pipeline definition (.gitlab-ci.yml).\n\nThe `deploy-to-cloud-run` job:\n* Requires the following environment variables\n * `GCP_PROJECT_ID`\n * `GCP_SERVICE_ACCOUNT_KEY`\n* Job definition can be found at: https://gitlab.com/gitlab-org/incubation-engineering/five-minute-production/library\n\nThis pipeline definition has been committed to the branch ``.\nYou may modify the pipeline definition further or accept the changes as-is if suitable.\n", - "source_project_id": project.id, - "target_project_id": project.id, - "source_branch": nil, - "target_branch": project.default_branch }, + category: 'Projects::GoogleCloud::DeploymentsController', + action: 'generate_cloudrun_pipeline', + label: nil, project: project, user: user_maintainer ) diff --git a/spec/requests/projects/google_cloud/gcp_regions_controller_spec.rb b/spec/requests/projects/google_cloud/gcp_regions_controller_spec.rb index f88273080d5..e77bcdb40b8 100644 --- a/spec/requests/projects/google_cloud/gcp_regions_controller_spec.rb +++ b/spec/requests/projects/google_cloud/gcp_regions_controller_spec.rb @@ -13,10 +13,9 @@ RSpec.describe Projects::GoogleCloud::GcpRegionsController do it "tracks event" do is_expected.to be(404) expect_snowplow_event( - category: 'Projects::GoogleCloud', - action: 'admin_project_google_cloud!', - label: 'error_access_denied', - property: 'invalid_user', + category: 'Projects::GoogleCloud::GcpRegionsController', + action: 'error_invalid_user', + label: nil, project: project, user: nil ) @@ -27,10 +26,9 @@ RSpec.describe Projects::GoogleCloud::GcpRegionsController do it "tracks event" do is_expected.to be(404) expect_snowplow_event( - category: 'Projects::GoogleCloud', - action: 'admin_project_google_cloud!', - label: 'error_access_denied', - property: 'invalid_user', + category: 'Projects::GoogleCloud::GcpRegionsController', + action: 'error_invalid_user', + label: nil, project: project, user: nil ) @@ -41,10 +39,9 @@ RSpec.describe Projects::GoogleCloud::GcpRegionsController do it "tracks event" do is_expected.to be(404) expect_snowplow_event( - category: 'Projects::GoogleCloud', - action: 'feature_flag_enabled!', - label: 'error_access_denied', - property: 'feature_flag_not_enabled', + category: 'Projects::GoogleCloud::GcpRegionsController', + action: 'error_feature_flag_not_enabled', + label: nil, project: project, user: user_maintainer ) @@ -55,10 +52,9 @@ RSpec.describe Projects::GoogleCloud::GcpRegionsController do it "tracks event" do is_expected.to be(403) expect_snowplow_event( - category: 'Projects::GoogleCloud', - action: 'google_oauth2_enabled!', - label: 'error_access_denied', - extra: { reason: 'google_oauth2_not_configured', config: config }, + category: 'Projects::GoogleCloud::GcpRegionsController', + action: 'error_google_oauth2_not_enabled', + label: nil, project: project, user: user_maintainer ) diff --git a/spec/requests/projects/google_cloud/revoke_oauth_controller_spec.rb b/spec/requests/projects/google_cloud/revoke_oauth_controller_spec.rb index 36441a184cb..9bd8468767d 100644 --- a/spec/requests/projects/google_cloud/revoke_oauth_controller_spec.rb +++ b/spec/requests/projects/google_cloud/revoke_oauth_controller_spec.rb @@ -50,10 +50,9 @@ RSpec.describe Projects::GoogleCloud::RevokeOauthController do expect(response).to redirect_to(project_google_cloud_configuration_path(project)) expect(flash[:notice]).to eq('Google OAuth2 token revocation requested') expect_snowplow_event( - category: 'Projects::GoogleCloud', - action: 'revoke_oauth#create', - label: 'success', - property: '{}', + category: 'Projects::GoogleCloud::RevokeOauthController', + action: 'revoke_oauth', + label: nil, project: project, user: user ) @@ -73,10 +72,9 @@ RSpec.describe Projects::GoogleCloud::RevokeOauthController do expect(response).to redirect_to(project_google_cloud_configuration_path(project)) expect(flash[:alert]).to eq('Google OAuth2 token revocation request failed') expect_snowplow_event( - category: 'Projects::GoogleCloud', - action: 'revoke_oauth#create', - label: 'error', - property: '{}', + category: 'Projects::GoogleCloud::RevokeOauthController', + action: 'error', + label: nil, project: project, user: user ) diff --git a/spec/requests/projects/google_cloud/service_accounts_controller_spec.rb b/spec/requests/projects/google_cloud/service_accounts_controller_spec.rb index ae2519855db..133c6f9153d 100644 --- a/spec/requests/projects/google_cloud/service_accounts_controller_spec.rb +++ b/spec/requests/projects/google_cloud/service_accounts_controller_spec.rb @@ -30,10 +30,9 @@ RSpec.describe Projects::GoogleCloud::ServiceAccountsController do expect(response).to have_gitlab_http_status(:not_found) expect_snowplow_event( - category: 'Projects::GoogleCloud', - action: 'admin_project_google_cloud!', - label: 'error_access_denied', - property: 'invalid_user', + category: 'Projects::GoogleCloud::ServiceAccountsController', + action: 'error_invalid_user', + label: nil, project: project, user: nil ) @@ -53,10 +52,9 @@ RSpec.describe Projects::GoogleCloud::ServiceAccountsController do get url expect_snowplow_event( - category: 'Projects::GoogleCloud', - action: 'admin_project_google_cloud!', - label: 'error_access_denied', - property: 'invalid_user', + category: 'Projects::GoogleCloud::ServiceAccountsController', + action: 'error_invalid_user', + label: nil, project: project, user: unauthorized_member ) @@ -71,10 +69,9 @@ RSpec.describe Projects::GoogleCloud::ServiceAccountsController do post url expect_snowplow_event( - category: 'Projects::GoogleCloud', - action: 'admin_project_google_cloud!', - label: 'error_access_denied', - property: 'invalid_user', + category: 'Projects::GoogleCloud::ServiceAccountsController', + action: 'error_invalid_user', + label: nil, project: project, user: unauthorized_member ) @@ -135,10 +132,9 @@ RSpec.describe Projects::GoogleCloud::ServiceAccountsController do expect(response).to redirect_to(project_google_cloud_configuration_path(project)) expect(flash[:warning]).to eq('No Google Cloud projects - You need at least one Google Cloud project') expect_snowplow_event( - category: 'Projects::GoogleCloud', - action: 'service_accounts#index', - label: 'error_form', - property: 'no_gcp_projects', + category: 'Projects::GoogleCloud::ServiceAccountsController', + action: 'error_no_gcp_projects', + label: nil, project: project, user: authorized_member ) @@ -207,11 +203,10 @@ RSpec.describe Projects::GoogleCloud::ServiceAccountsController do expect(response).to redirect_to(project_google_cloud_configuration_path(project)) expect(flash[:warning]).to eq('Google Cloud Error - client-error') expect_snowplow_event( - category: 'Projects::GoogleCloud', - action: 'service_accounts#index', - label: 'error_gcp', - extra: google_client_error, + category: 'Projects::GoogleCloud::ServiceAccountsController', + action: 'error_google_api', project: project, + label: nil, user: authorized_member ) end @@ -226,10 +221,9 @@ RSpec.describe Projects::GoogleCloud::ServiceAccountsController do expect(response).to redirect_to(project_google_cloud_configuration_path(project)) expect(flash[:warning]).to eq('Google Cloud Error - client-error') expect_snowplow_event( - category: 'Projects::GoogleCloud', - action: 'service_accounts#create', - label: 'error_gcp', - extra: google_client_error, + category: 'Projects::GoogleCloud::ServiceAccountsController', + action: 'error_google_api', + label: nil, project: project, user: authorized_member ) diff --git a/spec/requests/projects/hook_logs_controller_spec.rb b/spec/requests/projects/hook_logs_controller_spec.rb new file mode 100644 index 00000000000..8b3ec307e53 --- /dev/null +++ b/spec/requests/projects/hook_logs_controller_spec.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Projects::HookLogsController 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) } + + let(:project) { web_hook.project } + + it_behaves_like WebHooks::HookLogActions do + let(:edit_hook_path) { edit_project_hook_url(project, web_hook) } + + before do + project.add_owner(user) + end + end +end diff --git a/spec/requests/projects/merge_requests/context_commit_diffs_spec.rb b/spec/requests/projects/merge_requests/context_commit_diffs_spec.rb index 7be863aae75..c859e91e21a 100644 --- a/spec/requests/projects/merge_requests/context_commit_diffs_spec.rb +++ b/spec/requests/projects/merge_requests/context_commit_diffs_spec.rb @@ -42,7 +42,7 @@ RSpec.describe 'Merge Requests Context Commit Diffs' do } end - def go(extra_params = {}) + def go(headers: {}, **extra_params) params = { namespace_id: project.namespace.to_param, project_id: project, @@ -56,10 +56,20 @@ RSpec.describe 'Merge Requests Context Commit Diffs' do get diffs_batch_namespace_project_json_merge_request_path(params.merge(extra_params)), headers: headers end - context 'with caching', :use_clean_rails_memory_store_caching do - subject { go(page: 0, per_page: 5) } + context 'without caching' do + subject { go(headers: headers, page: 0, per_page: 5) } + + let(:headers) { {} } + let(:collection) { Gitlab::Diff::FileCollection::Compare } + let(:expected_options) { collection_arguments } + it_behaves_like 'serializes diffs with expected arguments' + end + + context 'with caching', :use_clean_rails_memory_store_caching do context 'when the request has not been cached' do + subject { go(headers: { 'If-None-Match' => '' }, page: 0, per_page: 5) } + it_behaves_like 'serializes diffs with expected arguments' do let(:collection) { Gitlab::Diff::FileCollection::Compare } let(:expected_options) { collection_arguments } @@ -67,16 +77,18 @@ RSpec.describe 'Merge Requests Context Commit Diffs' do end context 'when the request has already been cached' do + subject { go(headers: { 'If-None-Match' => response.etag }, page: 0, per_page: 5) } + before do go(page: 0, per_page: 5) end it 'does not serialize diffs' do - expect_next_instance_of(PaginatedDiffSerializer) do |instance| - expect(instance).not_to receive(:represent) - end + expect(PaginatedDiffSerializer).not_to receive(:new) + + go(headers: { 'If-None-Match' => response.etag }, page: 0, per_page: 5) - subject + expect(response).to have_gitlab_http_status(:not_modified) end context 'with the different user' do diff --git a/spec/requests/projects/merge_requests/diffs_spec.rb b/spec/requests/projects/merge_requests/diffs_spec.rb index 937b0f1d713..9f0b9a9cb1b 100644 --- a/spec/requests/projects/merge_requests/diffs_spec.rb +++ b/spec/requests/projects/merge_requests/diffs_spec.rb @@ -53,247 +53,154 @@ RSpec.describe 'Merge Requests Diffs' do get diffs_batch_namespace_project_json_merge_request_path(params.merge(extra_params)), headers: headers end - context 'with caching', :use_clean_rails_memory_store_caching do + context 'without caching' do subject { go(headers: headers, page: 0, per_page: 5) } let(:headers) { {} } + let(:collection) { Gitlab::Diff::FileCollection::MergeRequestDiffBatch } + let(:expected_options) { collection_arguments(total_pages: 20) } - context 'when the request has not been cached' do - let(:collection) { Gitlab::Diff::FileCollection::MergeRequestDiffBatch } - let(:expected_options) { collection_arguments(total_pages: 20) } - - it_behaves_like 'serializes diffs with expected arguments' - end - - context 'when the request has already been cached' do - before do - go(page: 0, per_page: 5) - end - - it 'does not serialize diffs' do - expect_next_instance_of(PaginatedDiffSerializer) do |instance| - expect(instance).not_to receive(:represent) - end - - subject - end - - context 'when using ETags' do - context 'when etag_merge_request_diff_batches is true' do - let(:headers) { { 'If-None-Match' => response.etag } } - - it 'does not serialize diffs' do - expect(PaginatedDiffSerializer).not_to receive(:new) - - go(headers: headers, page: 0, per_page: 5) - - expect(response).to have_gitlab_http_status(:not_modified) - end - end - - context 'when etag_merge_request_diff_batches is false' do - let(:headers) { { 'If-None-Match' => response.etag } } + it_behaves_like 'serializes diffs with expected arguments' + end - before do - stub_feature_flags(etag_merge_request_diff_batches: false) - end + context 'with caching', :use_clean_rails_memory_store_caching do + subject { go(headers: headers, page: 0, per_page: 5) } - it 'does not serialize diffs' do - expect_next_instance_of(PaginatedDiffSerializer) do |instance| - expect(instance).not_to receive(:represent) - end + let(:headers) { { 'If-None-Match' => response.etag } } - subject + before do + go(page: 0, per_page: 5) + end - expect(response).to have_gitlab_http_status(:success) - end - end - end + it 'does not serialize diffs' do + expect(PaginatedDiffSerializer).not_to receive(:new) - context 'with the different user' do - let(:another_user) { create(:user) } - let(:collection) { Gitlab::Diff::FileCollection::MergeRequestDiffBatch } - let(:expected_options) { collection_arguments(total_pages: 20) } + go(headers: headers, page: 0, per_page: 5) - before do - project.add_maintainer(another_user) - sign_in(another_user) - end + expect(response).to have_gitlab_http_status(:not_modified) + end - it_behaves_like 'serializes diffs with expected arguments' + context 'with the different user' do + let(:another_user) { create(:user) } + let(:collection) { Gitlab::Diff::FileCollection::MergeRequestDiffBatch } + let(:expected_options) { collection_arguments(total_pages: 20) } - context 'when using ETag caching' do - it_behaves_like 'serializes diffs with expected arguments' do - let(:headers) { { 'If-None-Match' => response.etag } } - end - end + before do + project.add_maintainer(another_user) + sign_in(another_user) end - context 'with a new unfoldable diff position' do - let(:collection) { Gitlab::Diff::FileCollection::MergeRequestDiffBatch } - let(:expected_options) { collection_arguments(total_pages: 20) } - - let(:unfoldable_position) do - create(:diff_position) - end - - before do - expect_next_instance_of(Gitlab::Diff::PositionCollection) do |instance| - expect(instance) - .to receive(:unfoldable) - .and_return([unfoldable_position]) - end - end + it_behaves_like 'serializes diffs with expected arguments' + end - it_behaves_like 'serializes diffs with expected arguments' + context 'with a new unfoldable diff position' do + let(:collection) { Gitlab::Diff::FileCollection::MergeRequestDiffBatch } + let(:expected_options) { collection_arguments(total_pages: 20) } - context 'when using ETag caching' do - it_behaves_like 'serializes diffs with expected arguments' do - let(:headers) { { 'If-None-Match' => response.etag } } - end - end + let(:unfoldable_position) do + create(:diff_position) end - context 'with disabled display_merge_conflicts_in_diff feature' do - let(:collection) { Gitlab::Diff::FileCollection::MergeRequestDiffBatch } - let(:expected_options) { collection_arguments(total_pages: 20).merge(allow_tree_conflicts: false) } - - before do - stub_feature_flags(display_merge_conflicts_in_diff: false) - end - - it_behaves_like 'serializes diffs with expected arguments' - - context 'when using ETag caching' do - it_behaves_like 'serializes diffs with expected arguments' do - let(:headers) { { 'If-None-Match' => response.etag } } - end + before do + expect_next_instance_of(Gitlab::Diff::PositionCollection) do |instance| + expect(instance) + .to receive(:unfoldable) + .and_return([unfoldable_position]) end end - context 'with diff_head option' do - subject { go(page: 0, per_page: 5, diff_head: true) } - - let(:collection) { Gitlab::Diff::FileCollection::MergeRequestDiffBatch } - let(:expected_options) { collection_arguments(total_pages: 20).merge(merge_ref_head_diff: true) } - - before do - merge_request.create_merge_head_diff! - end + it_behaves_like 'serializes diffs with expected arguments' + end - it_behaves_like 'serializes diffs with expected arguments' + context 'with disabled display_merge_conflicts_in_diff feature' do + let(:collection) { Gitlab::Diff::FileCollection::MergeRequestDiffBatch } + let(:expected_options) { collection_arguments(total_pages: 20).merge(allow_tree_conflicts: false) } - context 'when using ETag caching' do - it_behaves_like 'serializes diffs with expected arguments' do - let(:headers) { { 'If-None-Match' => response.etag } } - end - end + before do + stub_feature_flags(display_merge_conflicts_in_diff: false) end - context 'with the different pagination option' do - subject { go(page: 5, per_page: 5) } + it_behaves_like 'serializes diffs with expected arguments' + end - let(:collection) { Gitlab::Diff::FileCollection::MergeRequestDiffBatch } - let(:expected_options) { collection_arguments(total_pages: 20) } + context 'with diff_head option' do + subject { go(page: 0, per_page: 5, diff_head: true) } - it_behaves_like 'serializes diffs with expected arguments' + let(:collection) { Gitlab::Diff::FileCollection::MergeRequestDiffBatch } + let(:expected_options) { collection_arguments(total_pages: 20).merge(merge_ref_head_diff: true) } - context 'when using ETag caching' do - it_behaves_like 'serializes diffs with expected arguments' do - let(:headers) { { 'If-None-Match' => response.etag } } - end - end + before do + merge_request.create_merge_head_diff! end - context 'with the different diff_view' do - subject { go(page: 0, per_page: 5, view: :parallel) } - - let(:collection) { Gitlab::Diff::FileCollection::MergeRequestDiffBatch } - let(:expected_options) { collection_arguments(total_pages: 20).merge(diff_view: :parallel) } - - it_behaves_like 'serializes diffs with expected arguments' + it_behaves_like 'serializes diffs with expected arguments' + end - context 'when using ETag caching' do - it_behaves_like 'serializes diffs with expected arguments' do - let(:headers) { { 'If-None-Match' => response.etag } } - end - end - end + context 'with the different pagination option' do + subject { go(page: 5, per_page: 5) } - context 'with the different expanded option' do - subject { go(page: 0, per_page: 5, expanded: true ) } + let(:collection) { Gitlab::Diff::FileCollection::MergeRequestDiffBatch } + let(:expected_options) { collection_arguments(total_pages: 20) } - let(:collection) { Gitlab::Diff::FileCollection::MergeRequestDiffBatch } - let(:expected_options) { collection_arguments(total_pages: 20) } + it_behaves_like 'serializes diffs with expected arguments' + end - it_behaves_like 'serializes diffs with expected arguments' + context 'with the different diff_view' do + subject { go(page: 0, per_page: 5, view: :parallel) } - context 'when using ETag caching' do - it_behaves_like 'serializes diffs with expected arguments' do - let(:headers) { { 'If-None-Match' => response.etag } } - end - end - end + let(:collection) { Gitlab::Diff::FileCollection::MergeRequestDiffBatch } + let(:expected_options) { collection_arguments(total_pages: 20).merge(diff_view: :parallel) } - context 'with the different ignore_whitespace_change option' do - subject { go(page: 0, per_page: 5, w: 1) } + it_behaves_like 'serializes diffs with expected arguments' + end - let(:collection) { Gitlab::Diff::FileCollection::Compare } - let(:expected_options) { collection_arguments(total_pages: 20) } + context 'with the different expanded option' do + subject { go(page: 0, per_page: 5, expanded: true ) } - it_behaves_like 'serializes diffs with expected arguments' + let(:collection) { Gitlab::Diff::FileCollection::MergeRequestDiffBatch } + let(:expected_options) { collection_arguments(total_pages: 20) } - context 'when using ETag caching' do - it_behaves_like 'serializes diffs with expected arguments' do - let(:headers) { { 'If-None-Match' => response.etag } } - end - end - end + it_behaves_like 'serializes diffs with expected arguments' end - context 'when the paths is given' do - subject { go(headers: headers, page: 0, per_page: 5, paths: %w[README CHANGELOG]) } - - before do - go(page: 0, per_page: 5, paths: %w[README CHANGELOG]) - end + context 'with the different ignore_whitespace_change option' do + subject { go(page: 0, per_page: 5, w: 1) } - context 'when using ETag caching' do - let(:headers) { { 'If-None-Match' => response.etag } } + let(:collection) { Gitlab::Diff::FileCollection::Compare } + let(:expected_options) { collection_arguments(total_pages: 20) } - context 'when etag_merge_request_diff_batches is true' do - it 'does not serialize diffs' do - expect(PaginatedDiffSerializer).not_to receive(:new) + it_behaves_like 'serializes diffs with expected arguments' + end + end - subject + context 'when the paths is given' do + subject { go(headers: headers, page: 0, per_page: 5, paths: %w[README CHANGELOG]) } - expect(response).to have_gitlab_http_status(:not_modified) - end - end + before do + go(page: 0, per_page: 5, paths: %w[README CHANGELOG]) + end - context 'when etag_merge_request_diff_batches is false' do - before do - stub_feature_flags(etag_merge_request_diff_batches: false) - end + context 'when using ETag caching' do + let(:headers) { { 'If-None-Match' => response.etag } } - it 'does not use cache' do - expect(Rails.cache).not_to receive(:fetch).with(/cache:gitlab:PaginatedDiffSerializer/).and_call_original + it 'does not serialize diffs' do + expect(PaginatedDiffSerializer).not_to receive(:new) - subject + subject - expect(response).to have_gitlab_http_status(:success) - end - end + expect(response).to have_gitlab_http_status(:not_modified) end + end - context 'when not using ETag caching' do - it 'does not use cache' do - expect(Rails.cache).not_to receive(:fetch).with(/cache:gitlab:PaginatedDiffSerializer/).and_call_original + context 'when not using ETag caching' do + let(:headers) { {} } - subject + it 'does not use cache' do + expect(Rails.cache).not_to receive(:fetch).with(/cache:gitlab:PaginatedDiffSerializer/).and_call_original - expect(response).to have_gitlab_http_status(:success) - end + subject + + expect(response).to have_gitlab_http_status(:success) end end end diff --git a/spec/requests/projects/merge_requests_discussions_spec.rb b/spec/requests/projects/merge_requests_discussions_spec.rb index 9503dafcf2a..305ca6147be 100644 --- a/spec/requests/projects/merge_requests_discussions_spec.rb +++ b/spec/requests/projects/merge_requests_discussions_spec.rb @@ -37,12 +37,10 @@ RSpec.describe 'merge requests discussions' do it 'avoids N+1 DB queries', :request_store do send_request # warm up - create(:diff_note_on_merge_request, noteable: merge_request, - project: merge_request.project) + create(:diff_note_on_merge_request, noteable: merge_request, project: merge_request.project) control = ActiveRecord::QueryRecorder.new { send_request } - create(:diff_note_on_merge_request, noteable: merge_request, - project: merge_request.project) + create(:diff_note_on_merge_request, noteable: merge_request, project: merge_request.project) expect do send_request @@ -51,8 +49,7 @@ RSpec.describe 'merge requests discussions' do it 'limits Gitaly queries', :request_store do Gitlab::GitalyClient.allow_n_plus_1_calls do - create_list(:diff_note_on_merge_request, 7, noteable: merge_request, - project: merge_request.project) + create_list(:diff_note_on_merge_request, 7, noteable: merge_request, project: merge_request.project) end # The creations above write into the Gitaly counts diff --git a/spec/requests/projects/packages/package_files_controller_spec.rb b/spec/requests/projects/packages/package_files_controller_spec.rb new file mode 100644 index 00000000000..a6daf57f0fa --- /dev/null +++ b/spec/requests/projects/packages/package_files_controller_spec.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Projects::Packages::PackageFilesController do + let_it_be(:project) { create(:project, :public) } + let_it_be(:package) { create(:package, project: project) } + let_it_be(:package_file) { create(:package_file, package: package) } + + let(:filename) { package_file.file_name } + + describe 'GET download' do + subject do + get download_namespace_project_package_file_url( + id: package_file.id, + namespace_id: project.namespace, + project_id: project + ) + end + + it 'sends the package file' do + subject + + expect(response.headers['Content-Disposition']) + .to eq(%Q(attachment; filename="#{filename}"; filename*=UTF-8''#{filename})) + end + + it_behaves_like 'bumping the package last downloaded at field' + end +end diff --git a/spec/requests/projects/settings/integration_hook_logs_controller_spec.rb b/spec/requests/projects/settings/integration_hook_logs_controller_spec.rb new file mode 100644 index 00000000000..77daff901a1 --- /dev/null +++ b/spec/requests/projects/settings/integration_hook_logs_controller_spec.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Projects::Settings::IntegrationHookLogsController do + let_it_be(:user) { create(:user) } + let_it_be(:integration) { create(:datadog_integration) } + let_it_be_with_refind(:web_hook) { integration.service_hook } + let_it_be_with_refind(:web_hook_log) { create(:web_hook_log, web_hook: web_hook) } + + let(:project) { integration.project } + + it_behaves_like WebHooks::HookLogActions do + let(:edit_hook_path) { edit_project_settings_integration_url(project, integration) } + + before do + project.add_owner(user) + end + end +end diff --git a/spec/requests/verifies_with_email_spec.rb b/spec/requests/verifies_with_email_spec.rb index 2f249952455..e8d3e94bd0e 100644 --- a/spec/requests/verifies_with_email_spec.rb +++ b/spec/requests/verifies_with_email_spec.rb @@ -18,7 +18,7 @@ RSpec.describe 'VerifiesWithEmail', :clean_gitlab_redis_sessions, :clean_gitlab_ it 'sends an email' do mail = find_email_for(user) expect(mail.to).to match_array([user.email]) - expect(mail.subject).to eq('Verify your identity') + expect(mail.subject).to eq(s_('IdentityVerification|Verify your identity')) end end @@ -50,7 +50,7 @@ RSpec.describe 'VerifiesWithEmail', :clean_gitlab_redis_sessions, :clean_gitlab_ it 'adds a verification error message' do expect(response.body) .to include("You've reached the maximum amount of tries. "\ - 'Wait 10 minutes or resend a new code and try again.') + 'Wait 10 minutes or send a new code and try again.') end end @@ -62,7 +62,8 @@ RSpec.describe 'VerifiesWithEmail', :clean_gitlab_redis_sessions, :clean_gitlab_ it_behaves_like 'prompt for email verification' it 'adds a verification error message' do - expect(response.body).to include(('The code is incorrect. Enter it again, or resend a new code.')) + expect(response.body) + .to include((s_('IdentityVerification|The code is incorrect. Enter it again, or send a new code.'))) end end @@ -75,7 +76,8 @@ RSpec.describe 'VerifiesWithEmail', :clean_gitlab_redis_sessions, :clean_gitlab_ it_behaves_like 'prompt for email verification' it 'adds a verification error message' do - expect(response.body).to include(('The code has expired. Resend a new code and try again.')) + expect(response.body) + .to include((s_('IdentityVerification|The code has expired. Send a new code and try again.'))) end end @@ -112,7 +114,8 @@ RSpec.describe 'VerifiesWithEmail', :clean_gitlab_redis_sessions, :clean_gitlab_ it 'redirects to the login form and shows an alert message' do expect(response).to redirect_to(new_user_session_path) - expect(flash[:alert]).to eq('Maximum login attempts exceeded. Wait 10 minutes and try again.') + expect(flash[:alert]) + .to eq(s_('IdentityVerification|Maximum login attempts exceeded. Wait 10 minutes and try again.')) end end @@ -217,6 +220,7 @@ RSpec.describe 'VerifiesWithEmail', :clean_gitlab_redis_sessions, :clean_gitlab_ describe 'successful_verification' do before do + allow(user).to receive(:role_required?).and_return(true) # It skips the required signup info before_action sign_in(user) end |