From 71786ddc8e28fbd3cb3fcc4b3ff15e5962a1c82e Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Mon, 20 Feb 2023 13:49:51 +0000 Subject: Add latest changes from gitlab-org/gitlab@15-9-stable-ee --- .../projects/airflow/dags_controller_spec.rb | 105 ++++++++++++++ spec/requests/projects/blob_spec.rb | 87 ++++++++++++ .../google_cloud/databases_controller_spec.rb | 84 ++++++++---- .../projects/ml/experiments_controller_spec.rb | 152 +++++++++++++++++---- spec/requests/projects/network_controller_spec.rb | 11 -- spec/requests/projects/noteable_notes_spec.rb | 36 +++++ .../requests/projects/pipelines_controller_spec.rb | 26 ++++ spec/requests/projects/releases_controller_spec.rb | 40 ++++-- 8 files changed, 463 insertions(+), 78 deletions(-) create mode 100644 spec/requests/projects/airflow/dags_controller_spec.rb create mode 100644 spec/requests/projects/blob_spec.rb (limited to 'spec/requests/projects') diff --git a/spec/requests/projects/airflow/dags_controller_spec.rb b/spec/requests/projects/airflow/dags_controller_spec.rb new file mode 100644 index 00000000000..2dcedf5f128 --- /dev/null +++ b/spec/requests/projects/airflow/dags_controller_spec.rb @@ -0,0 +1,105 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Projects::Airflow::DagsController, feature_category: :dataops do + let_it_be(:non_member) { create(:user) } + let_it_be(:user) { create(:user) } + let_it_be(:group) { create(:group).tap { |p| p.add_developer(user) } } + let_it_be(:project) { create(:project, group: group).tap { |p| p.add_developer(user) } } + + let(:current_user) { user } + let(:feature_flag) { true } + + let_it_be(:dags) do + create_list(:airflow_dags, 5, project: project) + end + + let(:params) { { namespace_id: project.namespace.to_param, project_id: project } } + let(:extra_params) { {} } + + before do + sign_in(current_user) if current_user + stub_feature_flags(airflow_dags: false) + stub_feature_flags(airflow_dags: project) if feature_flag + list_dags + end + + shared_examples 'returns a 404 if feature flag disabled' do + context 'when :airflow_dags disabled' do + let(:feature_flag) { false } + + it 'is 404' do + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + + describe 'GET index' do + it 'renders the template' do + expect(response).to render_template('projects/airflow/dags/index') + end + + describe 'pagination' do + before do + stub_const("Projects::Airflow::DagsController::MAX_DAGS_PER_PAGE", 2) + dags + + list_dags + end + + context 'when out of bounds' do + let(:params) { extra_params.merge(page: 10000) } + + it 'redirects to last page' do + last_page = (dags.size + 1) / 2 + expect(response).to redirect_to(project_airflow_dags_path(project, page: last_page)) + end + end + + context 'when bad page' do + let(:params) { extra_params.merge(page: 's') } + + it 'uses first page' do + expect(assigns(:pagination)).to include( + page: 1, + is_last_page: false, + per_page: 2, + total_items: dags.size) + end + end + end + + it 'does not perform N+1 sql queries' do + control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) { list_dags } + + create_list(:airflow_dags, 1, project: project) + + expect { list_dags }.not_to exceed_all_query_limit(control_count) + end + + context 'when user is not logged in' do + let(:current_user) { nil } + + it 'redirects to login' do + expect(response).to redirect_to(new_user_session_path) + end + end + + context 'when user is not a member' do + let(:current_user) { non_member } + + it 'returns a 404' do + expect(response).to have_gitlab_http_status(:not_found) + end + end + + it_behaves_like 'returns a 404 if feature flag disabled' + end + + private + + def list_dags + get project_airflow_dags_path(project), params: params + end +end diff --git a/spec/requests/projects/blob_spec.rb b/spec/requests/projects/blob_spec.rb new file mode 100644 index 00000000000..7d62619e76a --- /dev/null +++ b/spec/requests/projects/blob_spec.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Blobs', feature_category: :source_code_management do + let_it_be(:project) { create(:project, :public, :repository, lfs: true) } + + describe 'GET /:namespace_id/:project_id/-/blob/:id' do + subject(:request) do + get namespace_project_blob_path(namespace_id: project.namespace, project_id: project, id: id) + end + + context 'with LFS file' do + let(:id) { 'master/files/lfs/lfs_object.iso' } + let(:object_store_host) { 'http://127.0.0.1:9000' } + let(:connect_src) do + csp = response.headers['Content-Security-Policy'] + csp.split('; ').find { |src| src.starts_with?('connect-src') } + end + + let(:gitlab_config) do + Gitlab.config.gitlab.deep_merge( + 'content_security_policy' => { + 'enabled' => content_security_policy_enabled + } + ) + end + + let(:lfs_config) do + Gitlab.config.lfs.deep_merge( + 'enabled' => lfs_enabled, + 'object_store' => { + 'remote_directory' => 'lfs-objects', + 'enabled' => true, + 'proxy_download' => proxy_download, + 'connection' => { + 'endpoint' => object_store_host, + 'path_style' => true + } + } + ) + end + + before do + stub_config_setting(gitlab_config) + stub_lfs_setting(lfs_config) + stub_lfs_object_storage(proxy_download: proxy_download) + + request + end + + describe 'directly downloading lfs file' do + let(:lfs_enabled) { true } + let(:proxy_download) { false } + let(:content_security_policy_enabled) { true } + + it { expect(response).to have_gitlab_http_status(:success) } + + it { expect(connect_src).to include(object_store_host) } + + context 'when lfs is disabled' do + let(:lfs_enabled) { false } + + it { expect(response).to have_gitlab_http_status(:success) } + + it { expect(connect_src).not_to include(object_store_host) } + end + + context 'when content_security_policy is disabled' do + let(:content_security_policy_enabled) { false } + + it { expect(response).to have_gitlab_http_status(:success) } + + it { expect(connect_src).not_to include(object_store_host) } + end + + context 'when proxy download is enabled' do + let(:proxy_download) { true } + + it { expect(response).to have_gitlab_http_status(:success) } + + it { expect(connect_src).not_to include(object_store_host) } + end + end + end + end +end diff --git a/spec/requests/projects/google_cloud/databases_controller_spec.rb b/spec/requests/projects/google_cloud/databases_controller_spec.rb index e91a51ce2ef..98e83610600 100644 --- a/spec/requests/projects/google_cloud/databases_controller_spec.rb +++ b/spec/requests/projects/google_cloud/databases_controller_spec.rb @@ -94,23 +94,33 @@ RSpec.describe Projects::GoogleCloud::DatabasesController, :snowplow, feature_ca post project_google_cloud_databases_path(project) end - 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' }) + context 'when EnableCloudsqlService fails' do + before do + allow_next_instance_of(::GoogleCloud::EnableCloudsqlService) do |service| + allow(service).to receive(:execute) + .and_return({ status: :error, message: 'error' }) + end end - subject + it 'redirects and track event on error' do + subject + + 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 - expect(response).to redirect_to(project_google_cloud_databases_path(project)) + it 'shows a flash alert' do + subject - expect_snowplow_event( - category: 'Projects::GoogleCloud::DatabasesController', - action: 'error_enable_cloudsql_services', - label: nil, - project: project, - user: user - ) + expect(flash[:alert]).to eq(s_('CloudSeed|Google Cloud Error - error')) + end end context 'when EnableCloudsqlService is successful' do @@ -121,23 +131,33 @@ RSpec.describe Projects::GoogleCloud::DatabasesController, :snowplow, feature_ca end end - 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' }) + context 'when CreateCloudsqlInstanceService fails' do + before do + allow_next_instance_of(::GoogleCloud::CreateCloudsqlInstanceService) do |service| + allow(service).to receive(:execute) + .and_return({ status: :error, message: 'error' }) + end end - subject + it 'redirects and track event on error' do + subject - expect(response).to redirect_to(project_google_cloud_databases_path(project)) + expect(response).to redirect_to(project_google_cloud_databases_path(project)) - expect_snowplow_event( - category: 'Projects::GoogleCloud::DatabasesController', - action: 'error_create_cloudsql_instance', - label: nil, - project: project, - user: user - ) + expect_snowplow_event( + category: 'Projects::GoogleCloud::DatabasesController', + action: 'error_create_cloudsql_instance', + label: nil, + project: project, + user: user + ) + end + + it 'shows a flash warning' do + subject + + expect(flash[:warning]).to eq(s_('CloudSeed|Google Cloud Error - error')) + end end context 'when CreateCloudsqlInstanceService is successful' do @@ -161,6 +181,18 @@ RSpec.describe Projects::GoogleCloud::DatabasesController, :snowplow, feature_ca user: user ) end + + it 'shows a flash notice' do + subject + + expect(flash[:notice]) + .to eq( + s_( + 'CloudSeed|Cloud SQL instance creation request successful. ' \ + 'Expected resolution time is ~5 minutes.' + ) + ) + end end end end diff --git a/spec/requests/projects/ml/experiments_controller_spec.rb b/spec/requests/projects/ml/experiments_controller_spec.rb index e8b6f806251..9b071efc1f1 100644 --- a/spec/requests/projects/ml/experiments_controller_spec.rb +++ b/spec/requests/projects/ml/experiments_controller_spec.rb @@ -38,31 +38,74 @@ RSpec.describe Projects::Ml::ExperimentsController, feature_category: :mlops do end describe 'GET index' do - before do - list_experiments - end + describe 'renderering' do + before do + list_experiments + end - it 'renders the template' do - expect(response).to render_template('projects/ml/experiments/index') + it 'renders the template' do + expect(response).to render_template('projects/ml/experiments/index') + end + + it 'does not perform N+1 sql queries' do + control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) { list_experiments } + + create_list(:ml_experiments, 2, project: project, user: user) + + expect { list_experiments }.not_to exceed_all_query_limit(control_count) + end end - it 'does not perform N+1 sql queries' do - control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) { list_experiments } + describe 'pagination' do + let_it_be(:experiments) do + create_list(:ml_experiments, 3, project: project_with_feature) + end - create_list(:ml_experiments, 2, project: project, user: user) + let(:params) { basic_params.merge(id: experiment.iid) } - expect { list_experiments }.not_to exceed_all_query_limit(control_count) + before do + stub_const("Projects::Ml::ExperimentsController::MAX_EXPERIMENTS_PER_PAGE", 2) + + list_experiments + end + + it 'fetches only MAX_CANDIDATES_PER_PAGE candidates' do + expect(assigns(:experiments).size).to eq(2) + end + + it 'paginates', :aggregate_failures do + page = assigns(:experiments) + + expect(page.first).to eq(experiments.last) + expect(page.last).to eq(experiments[1]) + + new_params = params.merge(cursor: assigns(:page_info)[:end_cursor]) + + list_experiments(new_params) + + new_page = assigns(:experiments) + + expect(new_page.first).to eq(experiments.first) + end end context 'when :ml_experiment_tracking is disabled for the project' do let(:project) { project_without_feature } + before do + list_experiments + end + it 'responds with a 404' do expect(response).to have_gitlab_http_status(:not_found) end end - it_behaves_like '404 if feature flag disabled' + it_behaves_like '404 if feature flag disabled' do + before do + list_experiments + end + end end describe 'GET show' do @@ -75,36 +118,85 @@ RSpec.describe Projects::Ml::ExperimentsController, feature_category: :mlops do end describe 'pagination' do - let_it_be(:candidates) { create_list(:ml_candidates, 5, experiment: experiment) } + let_it_be(:candidates) do + create_list(:ml_candidates, 5, experiment: experiment).tap do |c| + c.first.metrics.create!(name: 'metric1', value: 0.3) + c[1].metrics.create!(name: 'metric1', value: 0.2) + c.last.metrics.create!(name: 'metric1', value: 0.6) + end + end + + let(:params) { basic_params.merge(id: experiment.iid) } before do stub_const("Projects::Ml::ExperimentsController::MAX_CANDIDATES_PER_PAGE", 2) - candidates show_experiment end - context 'when out of bounds' do - let(:params) { basic_params.merge(id: experiment.iid, page: 10000) } + it 'fetches only MAX_CANDIDATES_PER_PAGE candidates' do + expect(assigns(:candidates).size).to eq(2) + end + + it 'paginates' do + received = assigns(:page_info) - it 'redirects to last page' do - last_page = (experiment.candidates.size + 1) / 2 + expect(received).to include({ + has_next_page: true, + has_previous_page: false, + start_cursor: nil + }) + end + + context 'when order by metric' do + let(:params) do + { + order_by: "metric1", + order_by_type: "metric", + sort: "desc" + } + end + + it 'paginates', :aggregate_failures do + page = assigns(:candidates) + + expect(page.first).to eq(candidates.last) + expect(page.last).to eq(candidates.first) + + new_params = params.merge(cursor: assigns(:page_info)[:end_cursor]) - expect(response).to redirect_to(project_ml_experiment_path(project, experiment.iid, page: last_page)) + show_experiment(new_params) + + new_page = assigns(:candidates) + + expect(new_page.first).to eq(candidates[1]) end end + end - context 'when bad page' do - let(:params) { basic_params.merge(id: experiment.iid, page: 's') } + describe 'search' do + let(:params) do + basic_params.merge( + id: experiment.iid, + name: 'some_name', + orderBy: 'name', + orderByType: 'metric', + sort: 'asc', + invalid: 'invalid' + ) + end - it 'uses first page' do - expect(assigns(:pagination)).to include( - page: 1, - is_last_page: false, - per_page: 2, - total_items: experiment.candidates&.size - ) + it 'formats and filters the parameters' do + expect(Projects::Ml::CandidateFinder).to receive(:new).and_call_original do |exp, params| + expect(params.to_h).to include({ + name: 'some_name', + order_by: 'name', + order_by_type: 'metric', + sort: 'asc' + }) end + + show_experiment end end @@ -125,11 +217,11 @@ RSpec.describe Projects::Ml::ExperimentsController, feature_category: :mlops do private - def show_experiment - get project_ml_experiment_path(project, experiment.iid), params: params + def show_experiment(new_params = nil) + get project_ml_experiment_path(project, experiment.iid), params: new_params || params end - def list_experiments - get project_ml_experiments_path(project), params: params + def list_experiments(new_params = nil) + get project_ml_experiments_path(project), params: new_params || params end end diff --git a/spec/requests/projects/network_controller_spec.rb b/spec/requests/projects/network_controller_spec.rb index 954f9655558..dee95c6e70e 100644 --- a/spec/requests/projects/network_controller_spec.rb +++ b/spec/requests/projects/network_controller_spec.rb @@ -35,17 +35,6 @@ RSpec.describe Projects::NetworkController, feature_category: :source_code_manag subject expect(assigns(:url)).to eq(project_network_path(project, ref, format: :json, ref_type: 'heads')) end - - context 'when the use_ref_type_parameter flag is disabled' do - before do - stub_feature_flags(use_ref_type_parameter: false) - end - - it 'assigns url without ref_type' do - subject - expect(assigns(:url)).to eq(project_network_path(project, ref, format: :json)) - end - end end it 'assigns url' do diff --git a/spec/requests/projects/noteable_notes_spec.rb b/spec/requests/projects/noteable_notes_spec.rb index 5699bf17b80..55540447da0 100644 --- a/spec/requests/projects/noteable_notes_spec.rb +++ b/spec/requests/projects/noteable_notes_spec.rb @@ -36,5 +36,41 @@ RSpec.describe 'Project noteable notes', feature_category: :team_planning do expect(response).to have_gitlab_http_status(:ok) expect(response_etag).to eq(stored_etag) end + + it "instruments cache hits correctly" do + etag_store.touch(notes_path) + + expect(Gitlab::Metrics::RailsSlis.request_apdex).to( + receive(:increment).with( + labels: { + request_urgency: :medium, + feature_category: "team_planning", + endpoint_id: "Projects::NotesController#index" + }, + success: be_in([true, false]) + ) + ) + allow(ActiveSupport::Notifications).to receive(:instrument).and_call_original + + expect(ActiveSupport::Notifications).to( + receive(:instrument).with( + 'process_action.action_controller', + a_hash_including( + { + request_urgency: :medium, + target_duration_s: 0.5, + metadata: a_hash_including({ + 'meta.feature_category' => 'team_planning', + 'meta.caller_id' => "Projects::NotesController#index" + }) + } + ) + ) + ) + + get notes_path, headers: { "if-none-match": stored_etag } + + expect(response).to have_gitlab_http_status(:not_modified) + end end end diff --git a/spec/requests/projects/pipelines_controller_spec.rb b/spec/requests/projects/pipelines_controller_spec.rb index 7f185ade339..73e002b63b1 100644 --- a/spec/requests/projects/pipelines_controller_spec.rb +++ b/spec/requests/projects/pipelines_controller_spec.rb @@ -19,6 +19,32 @@ RSpec.describe Projects::PipelinesController, feature_category: :continuous_inte login_as(user) end + describe "GET index.json" do + it 'does not execute N+1 queries' do + get_pipelines_index + + control_count = ActiveRecord::QueryRecorder.new do + get_pipelines_index + end.count + + %w[pending running success failed canceled].each do |status| + create(:ci_pipeline, project: project, status: status) + end + + # There appears to be one extra query for Pipelines#has_warnings? for some reason + expect { get_pipelines_index }.not_to exceed_query_limit(control_count + 1) + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['pipelines'].count).to eq 6 + end + + def get_pipelines_index + get namespace_project_pipelines_path( + namespace_id: project.namespace.to_param, + project_id: project.to_param, + format: :json) + end + end + describe "GET stages.json" do it 'does not execute N+1 queries' do request_build_stage diff --git a/spec/requests/projects/releases_controller_spec.rb b/spec/requests/projects/releases_controller_spec.rb index d331142583d..42fd55b5a43 100644 --- a/spec/requests/projects/releases_controller_spec.rb +++ b/spec/requests/projects/releases_controller_spec.rb @@ -8,17 +8,20 @@ RSpec.describe 'Projects::ReleasesController', feature_category: :release_orches before do project.add_developer(user) - login_as(user) end # Added as a request spec because of https://gitlab.com/gitlab-org/gitlab/-/issues/232386 describe 'GET #downloads' do - context 'filepath redirection' do - let_it_be(:release) { create(:release, project: project, tag: 'v11.9.0-rc2' ) } - let!(:link) { create(:release_link, release: release, name: 'linux-amd64 binaries', filepath: filepath, url: 'https://aws.example.com/s3/project/bin/hello-darwin-amd64') } - let_it_be(:url) { "#{project_releases_path(project)}/#{release.tag}/downloads/bin/darwin-amd64" } + let_it_be(:release) { create(:release, project: project, tag: 'v11.9.0-rc2' ) } + let!(:link) { create(:release_link, release: release, name: 'linux-amd64 binaries', filepath: filepath, url: 'https://aws.example.com/s3/project/bin/hello-darwin-amd64') } + let_it_be(:url) { "#{project_releases_path(project)}/#{release.tag}/downloads/bin/darwin-amd64" } - let(:subject) { get url } + let(:subject) { get url } + + context 'filepath redirection' do + before do + login_as(user) + end context 'valid filepath' do let(:filepath) { '/bin/darwin-amd64' } @@ -47,14 +50,29 @@ RSpec.describe 'Projects::ReleasesController', feature_category: :release_orches end end - context 'invalid filepath' do - let(:invalid_filepath) { 'bin/darwin-amd64' } + context 'sessionless download authentication' do + let(:personal_access_token) { create(:personal_access_token, user: user) } + let(:filepath) { '/bin/darwin-amd64' } + + subject { get url, params: { private_token: personal_access_token.token } } - let(:subject) { create(:release_link, name: 'linux-amd64 binaries', filepath: invalid_filepath, url: 'https://aws.example.com/s3/project/bin/hello-darwin-amd64') } + it 'will allow sessionless users to download the file' do + subject - it 'cannot create an invalid filepath' do - expect { subject }.to raise_error(ActiveRecord::RecordInvalid) + expect(controller.current_user).to eq(user) + expect(response).to have_gitlab_http_status(:redirect) + expect(response).to redirect_to(link.url) end end end + + context 'invalid filepath' do + let(:invalid_filepath) { 'bin/darwin-amd64' } + + let(:subject) { create(:release_link, name: 'linux-amd64 binaries', filepath: invalid_filepath, url: 'https://aws.example.com/s3/project/bin/hello-darwin-amd64') } + + it 'cannot create an invalid filepath' do + expect { subject }.to raise_error(ActiveRecord::RecordInvalid) + end + end end -- cgit v1.2.3