diff options
Diffstat (limited to 'spec/requests/projects')
28 files changed, 825 insertions, 456 deletions
diff --git a/spec/requests/projects/airflow/dags_controller_spec.rb b/spec/requests/projects/airflow/dags_controller_spec.rb deleted file mode 100644 index 2dcedf5f128..00000000000 --- a/spec/requests/projects/airflow/dags_controller_spec.rb +++ /dev/null @@ -1,105 +0,0 @@ -# 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/aws/configuration_controller_spec.rb b/spec/requests/projects/aws/configuration_controller_spec.rb new file mode 100644 index 00000000000..af9460eb76c --- /dev/null +++ b/spec/requests/projects/aws/configuration_controller_spec.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Projects::Aws::ConfigurationController, feature_category: :five_minute_production_app do + let_it_be(:project) { create(:project, :public) } + let_it_be(:url) { project_aws_configuration_path(project) } + + let_it_be(:user_guest) { create(:user) } + let_it_be(:user_developer) { create(:user) } + let_it_be(:user_maintainer) { create(:user) } + + let_it_be(:unauthorized_members) { [user_guest, user_developer] } + let_it_be(:authorized_members) { [user_maintainer] } + + before do + project.add_guest(user_guest) + project.add_developer(user_developer) + project.add_maintainer(user_maintainer) + 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) + + get url + expect_snowplow_event( + category: 'Projects::Aws::ConfigurationController', + action: 'error_invalid_user', + label: nil, + project: project, + user: unauthorized_member + ) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + + context 'when accessed by authorized members' do + it 'returns successful' do + authorized_members.each do |authorized_member| + sign_in(authorized_member) + + get url + + expect(response).to be_successful + expect(response).to render_template('projects/aws/configuration/index') + end + end + + include_examples 'requires feature flag `cloudseed_aws` enabled' do + subject { get url } + + let_it_be(:user) { user_maintainer } + end + end +end diff --git a/spec/requests/projects/ci/promeheus_metrics/histograms_controller_spec.rb b/spec/requests/projects/ci/promeheus_metrics/histograms_controller_spec.rb index b0c7427fa81..11f962e0e96 100644 --- a/spec/requests/projects/ci/promeheus_metrics/histograms_controller_spec.rb +++ b/spec/requests/projects/ci/promeheus_metrics/histograms_controller_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe 'Projects::Ci::PrometheusMetrics::HistogramsController', feature_category: :pipeline_authoring do +RSpec.describe 'Projects::Ci::PrometheusMetrics::HistogramsController', feature_category: :pipeline_composition do let_it_be(:project) { create(:project, :public) } describe 'POST /*namespace_id/:project_id/-/ci/prometheus_metrics/histograms' do diff --git a/spec/requests/projects/cluster_agents_controller_spec.rb b/spec/requests/projects/cluster_agents_controller_spec.rb index d7c791fa0c1..643160ad9f3 100644 --- a/spec/requests/projects/cluster_agents_controller_spec.rb +++ b/spec/requests/projects/cluster_agents_controller_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Projects::ClusterAgentsController, feature_category: :kubernetes_management do +RSpec.describe Projects::ClusterAgentsController, feature_category: :deployment_management do let_it_be(:cluster_agent) { create(:cluster_agent) } let(:project) { cluster_agent.project } diff --git a/spec/requests/projects/cycle_analytics_events_spec.rb b/spec/requests/projects/cycle_analytics_events_spec.rb index 3f9dd74c145..0adf0b525a9 100644 --- a/spec/requests/projects/cycle_analytics_events_spec.rb +++ b/spec/requests/projects/cycle_analytics_events_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe 'value stream analytics events', feature_category: :planning_analytics do +RSpec.describe 'value stream analytics events', feature_category: :team_planning do include CycleAnalyticsHelpers let(:user) { create(:user) } diff --git a/spec/requests/projects/environments_controller_spec.rb b/spec/requests/projects/environments_controller_spec.rb index 41ae2d434fa..5dd83fedf8d 100644 --- a/spec/requests/projects/environments_controller_spec.rb +++ b/spec/requests/projects/environments_controller_spec.rb @@ -18,9 +18,7 @@ RSpec.describe Projects::EnvironmentsController, feature_category: :continuous_d end def environment_params(opts = {}) - opts.reverse_merge(namespace_id: project.namespace, - project_id: project, - id: environment.id) + opts.reverse_merge(namespace_id: project.namespace, project_id: project, id: environment.id) end def create_deployment_with_associations(commit_depth:) diff --git a/spec/requests/projects/google_cloud/configuration_controller_spec.rb b/spec/requests/projects/google_cloud/configuration_controller_spec.rb index 1aa44d1a49a..b807ff7930e 100644 --- a/spec/requests/projects/google_cloud/configuration_controller_spec.rb +++ b/spec/requests/projects/google_cloud/configuration_controller_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Projects::GoogleCloud::ConfigurationController, feature_category: :kubernetes_management do +RSpec.describe Projects::GoogleCloud::ConfigurationController, feature_category: :deployment_management do let_it_be(:project) { create(:project, :public) } let_it_be(:url) { project_google_cloud_configuration_path(project) } diff --git a/spec/requests/projects/google_cloud/databases_controller_spec.rb b/spec/requests/projects/google_cloud/databases_controller_spec.rb index 98e83610600..fa978a3921f 100644 --- a/spec/requests/projects/google_cloud/databases_controller_spec.rb +++ b/spec/requests/projects/google_cloud/databases_controller_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Projects::GoogleCloud::DatabasesController, :snowplow, feature_category: :kubernetes_management do +RSpec.describe Projects::GoogleCloud::DatabasesController, :snowplow, feature_category: :deployment_management do shared_examples 'shared examples for database controller endpoints' do include_examples 'requires `admin_project_google_cloud` role' diff --git a/spec/requests/projects/google_cloud/deployments_controller_spec.rb b/spec/requests/projects/google_cloud/deployments_controller_spec.rb index d564a31f835..e9eac1e7ecd 100644 --- a/spec/requests/projects/google_cloud/deployments_controller_spec.rb +++ b/spec/requests/projects/google_cloud/deployments_controller_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Projects::GoogleCloud::DeploymentsController, feature_category: :kubernetes_management do +RSpec.describe Projects::GoogleCloud::DeploymentsController, feature_category: :deployment_management do let_it_be(:project) { create(:project, :public, :repository) } let_it_be(:repository) { project.repository } @@ -108,66 +108,104 @@ RSpec.describe Projects::GoogleCloud::DeploymentsController, feature_category: : end end - it 'redirects to google cloud deployments on enable service error' do - get url - - 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::DeploymentsController', - action: 'error_enable_services', - label: nil, - project: project, - user: user_maintainer - ) - end + context 'when enable service fails' do + before do + allow_next_instance_of(GoogleCloud::EnableCloudRunService) do |service| + allow(service) + .to receive(:execute) + .and_return( + status: :error, + message: 'No GCP projects found. Configure a service account or GCP_PROJECT_ID ci variable' + ) + end + end - it 'redirects to google cloud deployments with error' do - mock_gcp_error = Google::Apis::ClientError.new('some_error') + it 'redirects to google cloud deployments and tracks event on enable service error' do + get url - allow_next_instance_of(GoogleCloud::EnableCloudRunService) do |service| - allow(service).to receive(:execute).and_raise(mock_gcp_error) + 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::DeploymentsController', + action: 'error_enable_services', + label: nil, + project: project, + user: user_maintainer + ) end - get url + it 'shows a flash alert' do + get url - expect(response).to redirect_to(project_google_cloud_deployments_path(project)) - expect_snowplow_event( - category: 'Projects::GoogleCloud::DeploymentsController', - action: 'error_google_api', - label: nil, - project: project, - user: user_maintainer - ) + expect(flash[:alert]) + .to eq('No GCP projects found. Configure a service account or GCP_PROJECT_ID ci variable') + end end - context 'GCP_PROJECT_IDs are defined' do - it 'redirects to google_cloud deployments on generate pipeline error' do - allow_next_instance_of(GoogleCloud::EnableCloudRunService) do |enable_cloud_run_service| - allow(enable_cloud_run_service).to receive(:execute).and_return({ status: :success }) - end + context 'when enable service raises an error' do + before do + mock_gcp_error = Google::Apis::ClientError.new('some_error') - allow_next_instance_of(GoogleCloud::GeneratePipelineService) do |generate_pipeline_service| - allow(generate_pipeline_service).to receive(:execute).and_return({ status: :error }) + allow_next_instance_of(GoogleCloud::EnableCloudRunService) do |service| + allow(service).to receive(:execute).and_raise(mock_gcp_error) end + end + it 'redirects to google cloud deployments with error' do get url expect(response).to redirect_to(project_google_cloud_deployments_path(project)) expect_snowplow_event( category: 'Projects::GoogleCloud::DeploymentsController', - action: 'error_generate_cloudrun_pipeline', + action: 'error_google_api', label: nil, project: project, user: user_maintainer ) end - it 'redirects to create merge request form' do - allow_next_instance_of(GoogleCloud::EnableCloudRunService) do |service| - allow(service).to receive(:execute).and_return({ status: :success }) + it 'shows a flash warning' do + get url + + expect(flash[:warning]).to eq(format(_('Google Cloud Error - %{error}'), error: 'some_error')) + end + end + + context 'GCP_PROJECT_IDs are defined' do + before do + allow_next_instance_of(GoogleCloud::EnableCloudRunService) do |enable_cloud_run_service| + allow(enable_cloud_run_service).to receive(:execute).and_return({ status: :success }) + end + end + + context 'when generate pipeline service fails' do + before do + allow_next_instance_of(GoogleCloud::GeneratePipelineService) do |generate_pipeline_service| + allow(generate_pipeline_service).to receive(:execute).and_return({ status: :error }) + end + end + + it 'redirects to google_cloud deployments and tracks event on generate pipeline error' do + get url + + expect(response).to redirect_to(project_google_cloud_deployments_path(project)) + expect_snowplow_event( + category: 'Projects::GoogleCloud::DeploymentsController', + action: 'error_generate_cloudrun_pipeline', + label: nil, + project: project, + user: user_maintainer + ) + end + + it 'shows a flash alert' do + get url + + expect(flash[:alert]).to eq('Failed to generate pipeline') end + end + it 'redirects to create merge request form' do allow_next_instance_of(GoogleCloud::GeneratePipelineService) do |service| allow(service).to receive(:execute).and_return({ status: :success }) end 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 de4b96a2e01..da000ec00c0 100644 --- a/spec/requests/projects/google_cloud/gcp_regions_controller_spec.rb +++ b/spec/requests/projects/google_cloud/gcp_regions_controller_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Projects::GoogleCloud::GcpRegionsController, feature_category: :kubernetes_management do +RSpec.describe Projects::GoogleCloud::GcpRegionsController, feature_category: :deployment_management do let_it_be(:project) { create(:project, :public, :repository) } let_it_be(:repository) { project.repository } 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 5965953cf6f..427eff8cd76 100644 --- a/spec/requests/projects/google_cloud/revoke_oauth_controller_spec.rb +++ b/spec/requests/projects/google_cloud/revoke_oauth_controller_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Projects::GoogleCloud::RevokeOauthController, feature_category: :kubernetes_management do +RSpec.describe Projects::GoogleCloud::RevokeOauthController, feature_category: :deployment_management do include SessionHelpers describe 'POST #create', :snowplow, :clean_gitlab_redis_sessions, :aggregate_failures do 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 9b048f814ef..29d4154329f 100644 --- a/spec/requests/projects/google_cloud/service_accounts_controller_spec.rb +++ b/spec/requests/projects/google_cloud/service_accounts_controller_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Projects::GoogleCloud::ServiceAccountsController, feature_category: :kubernetes_management do +RSpec.describe Projects::GoogleCloud::ServiceAccountsController, feature_category: :deployment_management do let_it_be(:project) { create(:project, :public) } describe 'GET index', :snowplow do diff --git a/spec/requests/projects/incident_management/timeline_events_spec.rb b/spec/requests/projects/incident_management/timeline_events_spec.rb index 22a1f654ee2..b827ec07ae1 100644 --- a/spec/requests/projects/incident_management/timeline_events_spec.rb +++ b/spec/requests/projects/incident_management/timeline_events_spec.rb @@ -34,7 +34,7 @@ RSpec.describe 'Timeline Events', feature_category: :incident_management do it 'renders JSON in a correct format' do post preview_markdown_project_incident_management_timeline_events_path(project, format: :json), - params: { text: timeline_text } + params: { text: timeline_text } expect(response).to have_gitlab_http_status(:ok) expect(json_response).to eq({ @@ -51,7 +51,7 @@ RSpec.describe 'Timeline Events', feature_category: :incident_management do context 'when not authorized' do it 'returns 302' do post preview_markdown_project_incident_management_timeline_events_path(project, format: :json), - params: { text: timeline_text } + params: { text: timeline_text } expect(response).to have_gitlab_http_status(:found) end diff --git a/spec/requests/projects/issue_links_controller_spec.rb b/spec/requests/projects/issue_links_controller_spec.rb index 0535156b4b8..c242f762cde 100644 --- a/spec/requests/projects/issue_links_controller_spec.rb +++ b/spec/requests/projects/issue_links_controller_spec.rb @@ -28,28 +28,12 @@ RSpec.describe Projects::IssueLinksController, feature_category: :team_planning context 'when linked issue is a task' do let(:issue_b) { create :issue, :task, project: project } - context 'when the use_iid_in_work_items_path feature flag is disabled' do - before do - stub_feature_flags(use_iid_in_work_items_path: false) - end - - it 'returns a work item path for the linked task' do - get namespace_project_issue_links_path(issue_links_params) - - expect(json_response.count).to eq(1) - expect(json_response.first).to include( - 'path' => project_work_items_path(issue_b.project, issue_b.id), - 'type' => 'TASK' - ) - end - end - it 'returns a work item path for the linked task using the iid in the path' do get namespace_project_issue_links_path(issue_links_params) expect(json_response.count).to eq(1) expect(json_response.first).to include( - 'path' => project_work_items_path(issue_b.project, issue_b.iid, iid_path: true), + 'path' => project_work_items_path(issue_b.project, issue_b.iid), 'type' => 'TASK' ) end @@ -74,8 +58,7 @@ RSpec.describe Projects::IssueLinksController, feature_category: :team_planning list_service_response = IssueLinks::ListService.new(issue, user).execute expect(response).to have_gitlab_http_status(:ok) - expect(json_response).to eq('message' => nil, - 'issuables' => list_service_response.as_json) + expect(json_response).to eq('message' => nil, 'issuables' => list_service_response.as_json) end end @@ -178,9 +161,6 @@ RSpec.describe Projects::IssueLinksController, feature_category: :team_planning end def issue_links_params(opts = {}) - opts.reverse_merge(namespace_id: issue.project.namespace, - project_id: issue.project, - issue_id: issue, - format: :json) + opts.reverse_merge(namespace_id: issue.project.namespace, project_id: issue.project, issue_id: issue, format: :json) end end diff --git a/spec/requests/projects/issues_controller_spec.rb b/spec/requests/projects/issues_controller_spec.rb index 67a73834f2d..583fd5f586e 100644 --- a/spec/requests/projects/issues_controller_spec.rb +++ b/spec/requests/projects/issues_controller_spec.rb @@ -25,33 +25,28 @@ RSpec.describe Projects::IssuesController, feature_category: :team_planning do end describe 'GET #show' do - include_context 'group project issue' + before do + login_as(user) + end it_behaves_like "observability csp policy", described_class do + include_context 'group project issue' let(:tested_path) do project_issue_path(project, issue) end end - end - describe 'GET #index.json' do - let_it_be(:public_project) { create(:project, :public) } + describe 'incident tabs' do + let_it_be(:incident) { create(:incident, project: project) } - it_behaves_like 'rate limited endpoint', rate_limit_key: :search_rate_limit do - let_it_be(:current_user) { create(:user) } - - before do - sign_in current_user - end - - def request - get project_issues_path(public_project, format: :json), params: { scope: 'all', search: 'test' } + it 'redirects to the issues route for non-incidents' do + get incident_issue_project_issue_path(project, issue, 'timeline') + expect(response).to redirect_to project_issue_path(project, issue) end - end - it_behaves_like 'rate limited endpoint', rate_limit_key: :search_rate_limit_unauthenticated do - def request - get project_issues_path(public_project, format: :json), params: { scope: 'all', search: 'test' } + it 'responds with selected tab for incidents' do + get incident_issue_project_issue_path(project, incident, 'timeline') + expect(response.body).to match(/"currentTab":"timeline"/) end end end @@ -119,8 +114,9 @@ RSpec.describe Projects::IssuesController, feature_category: :team_planning do context 'when private project' do let_it_be(:private_project) { create(:project, :private) } - it_behaves_like 'authenticates sessionless user for the request spec', 'index atom', public_resource: false, -ignore_metrics: true do + it_behaves_like 'authenticates sessionless user for the request spec', 'index atom', + public_resource: false, + ignore_metrics: true do let(:url) { project_issues_url(private_project, format: :atom) } before do @@ -128,8 +124,9 @@ ignore_metrics: true do end end - it_behaves_like 'authenticates sessionless user for the request spec', 'calendar ics', public_resource: false, -ignore_metrics: true do + it_behaves_like 'authenticates sessionless user for the request spec', 'calendar ics', + public_resource: false, + ignore_metrics: true do let(:url) { project_issues_url(private_project, format: :ics) } before do diff --git a/spec/requests/projects/merge_requests_controller_spec.rb b/spec/requests/projects/merge_requests_controller_spec.rb index f441438a95a..955e6822211 100644 --- a/spec/requests/projects/merge_requests_controller_spec.rb +++ b/spec/requests/projects/merge_requests_controller_spec.rb @@ -120,8 +120,9 @@ RSpec.describe Projects::MergeRequestsController, feature_category: :source_code context 'when private project' do let_it_be(:private_project) { create(:project, :private) } - it_behaves_like 'authenticates sessionless user for the request spec', 'index atom', public_resource: false, - ignore_metrics: true do + it_behaves_like 'authenticates sessionless user for the request spec', 'index atom', + public_resource: false, + ignore_metrics: true do let(:url) { project_merge_requests_url(private_project, format: :atom) } before do diff --git a/spec/requests/projects/merge_requests_discussions_spec.rb b/spec/requests/projects/merge_requests_discussions_spec.rb index d82fa284a42..caf62c251b6 100644 --- a/spec/requests/projects/merge_requests_discussions_spec.rb +++ b/spec/requests/projects/merge_requests_discussions_spec.rb @@ -27,6 +27,21 @@ RSpec.describe 'merge requests discussions', feature_category: :source_code_mana end # rubocop:enable RSpec/InstanceVariable + shared_examples 'N+1 queries' 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) + control = ActiveRecord::QueryRecorder.new { send_request } + + create(:diff_note_on_merge_request, noteable: merge_request, project: merge_request.project) + + expect do + send_request + end.not_to exceed_query_limit(control).with_threshold(notes_metadata_threshold) + end + end + it 'returns 200' do send_request @@ -34,17 +49,20 @@ RSpec.describe 'merge requests discussions', feature_category: :source_code_mana end # https://docs.gitlab.com/ee/development/query_recorder.html#use-request-specs-instead-of-controller-specs - it 'avoids N+1 DB queries', :request_store do - send_request # warm up + context 'with notes_metadata_threshold' do + let(:notes_metadata_threshold) { 1 } - create(:diff_note_on_merge_request, noteable: merge_request, project: merge_request.project) - control = ActiveRecord::QueryRecorder.new { send_request } + it_behaves_like 'N+1 queries' - create(:diff_note_on_merge_request, noteable: merge_request, project: merge_request.project) + context 'when external_note_author_service_desk feature flag is disabled' do + let(:notes_metadata_threshold) { 0 } - expect do - send_request - end.not_to exceed_query_limit(control) + before do + stub_feature_flags(external_note_author_service_desk: false) + end + + it_behaves_like 'N+1 queries' + end end it 'limits Gitaly queries', :request_store do @@ -59,7 +77,7 @@ RSpec.describe 'merge requests discussions', feature_category: :source_code_mana .to change { Gitlab::GitalyClient.get_request_count }.by_at_most(4) end - context 'caching', :use_clean_rails_memory_store_caching do + context 'caching' do let(:reference) { create(:issue, project: project) } let(:author) { create(:user) } let!(:first_note) { create(:diff_note_on_merge_request, author: author, noteable: merge_request, project: project, note: "reference: #{reference.to_reference}") } @@ -81,193 +99,180 @@ RSpec.describe 'merge requests discussions', feature_category: :source_code_mana shared_examples 'cache hit' do it 'gets cached on subsequent requests' do - expect_next_instance_of(DiscussionSerializer) do |serializer| - expect(serializer).not_to receive(:represent) - end + expect(DiscussionSerializer).not_to receive(:new) send_request end end - context 'when mr_discussions_http_cache and disabled_mr_discussions_redis_cache are enabled' do - before do - send_request - end + before do + send_request + end - it_behaves_like 'cache hit' + it_behaves_like 'cache hit' - context 'when a note in a discussion got updated' do - before do - first_note.update!(updated_at: 1.minute.from_now) - end - - it_behaves_like 'cache miss' do - let(:changed_notes) { [first_note, second_note] } - end + context 'when a note in a discussion got updated' do + before do + first_note.update!(updated_at: 1.minute.from_now) end - context 'when a note in a discussion got its reference state updated' do - before do - reference.close! - end + it_behaves_like 'cache miss' do + let(:changed_notes) { [first_note, second_note] } + end + end - it_behaves_like 'cache miss' do - let(:changed_notes) { [first_note, second_note] } - end + context 'when a note in a discussion got its reference state updated' do + before do + reference.close! end - context 'when a note in a discussion got resolved' do - before do - travel_to(1.minute.from_now) do - first_note.resolve!(user) - end - end + it_behaves_like 'cache miss' do + let(:changed_notes) { [first_note, second_note] } + end + end - it_behaves_like 'cache miss' do - let(:changed_notes) { [first_note, second_note] } + context 'when a note in a discussion got resolved' do + before do + travel_to(1.minute.from_now) do + first_note.resolve!(user) end end - context 'when a note is added to a discussion' do - let!(:third_note) { create(:diff_note_on_merge_request, in_reply_to: first_note, noteable: merge_request, project: project) } - - it_behaves_like 'cache miss' do - let(:changed_notes) { [first_note, second_note, third_note] } - end + it_behaves_like 'cache miss' do + let(:changed_notes) { [first_note, second_note] } end + end - context 'when a note is removed from a discussion' do - before do - second_note.destroy! - end + context 'when a note is added to a discussion' do + let!(:third_note) { create(:diff_note_on_merge_request, in_reply_to: first_note, noteable: merge_request, project: project) } - it_behaves_like 'cache miss' do - let(:changed_notes) { [first_note] } - end + it_behaves_like 'cache miss' do + let(:changed_notes) { [first_note, second_note, third_note] } end + end - context 'when an emoji is awarded to a note in discussion' do - before do - travel_to(1.minute.from_now) do - create(:award_emoji, awardable: first_note) - end - end + context 'when a note is removed from a discussion' do + before do + second_note.destroy! + end - it_behaves_like 'cache miss' do - let(:changed_notes) { [first_note, second_note] } - end + it_behaves_like 'cache miss' do + let(:changed_notes) { [first_note] } end + end - context 'when an award emoji is removed from a note in discussion' do - before do - travel_to(1.minute.from_now) do - award_emoji.destroy! - end + context 'when an emoji is awarded to a note in discussion' do + before do + travel_to(1.minute.from_now) do + create(:award_emoji, awardable: first_note) end + end - it_behaves_like 'cache miss' do - let(:changed_notes) { [first_note, second_note] } - end + it_behaves_like 'cache miss' do + let(:changed_notes) { [first_note, second_note] } end + end - context 'when the diff note position changes' do - before do - # This replicates a position change wherein timestamps aren't updated - # which is why `Gitlab::Timeless.timeless` is utilized. This is the - # same approach being used in Discussions::UpdateDiffPositionService - # which is responsible for updating the positions of diff discussions - # when MR updates. - first_note.position = Gitlab::Diff::Position.new( - old_path: first_note.position.old_path, - new_path: first_note.position.new_path, - old_line: first_note.position.old_line, - new_line: first_note.position.new_line + 1, - diff_refs: first_note.position.diff_refs - ) - - Gitlab::Timeless.timeless(first_note, &:save) + context 'when an award emoji is removed from a note in discussion' do + before do + travel_to(1.minute.from_now) do + award_emoji.destroy! end + end - it_behaves_like 'cache miss' do - let(:changed_notes) { [first_note, second_note] } - end + it_behaves_like 'cache miss' do + let(:changed_notes) { [first_note, second_note] } end + end - context 'when the HEAD diff note position changes' do - before do - # This replicates a DiffNotePosition change. This is the same approach - # being used in Discussions::CaptureDiffNotePositionService which is - # responsible for updating/creating DiffNotePosition of a diff discussions - # in relation to HEAD diff. - new_position = Gitlab::Diff::Position.new( - old_path: first_note.position.old_path, - new_path: first_note.position.new_path, - old_line: first_note.position.old_line, - new_line: first_note.position.new_line + 1, - diff_refs: first_note.position.diff_refs - ) - - DiffNotePosition.create_or_update_for( - first_note, - diff_type: :head, - position: new_position, - line_code: 'bd4b7bfff3a247ccf6e3371c41ec018a55230bcc_534_521' - ) - end + context 'when the diff note position changes' do + before do + # This replicates a position change wherein timestamps aren't updated + # which is why `save(touch: false)` is utilized. This is the same + # approach being used in Discussions::UpdateDiffPositionService which + # is responsible for updating the positions of diff discussions when + # MR updates. + first_note.position = Gitlab::Diff::Position.new( + old_path: first_note.position.old_path, + new_path: first_note.position.new_path, + old_line: first_note.position.old_line, + new_line: first_note.position.new_line + 1, + diff_refs: first_note.position.diff_refs + ) + + first_note.save!(touch: false) + end - it_behaves_like 'cache miss' do - let(:changed_notes) { [first_note, second_note] } - end + it_behaves_like 'cache miss' do + let(:changed_notes) { [first_note, second_note] } end + end - context 'when author detail changes' do - before do - author.update!(name: "#{author.name} (Updated)") - end + context 'when the HEAD diff note position changes' do + before do + # This replicates a DiffNotePosition change. This is the same approach + # being used in Discussions::CaptureDiffNotePositionService which is + # responsible for updating/creating DiffNotePosition of a diff discussions + # in relation to HEAD diff. + new_position = Gitlab::Diff::Position.new( + old_path: first_note.position.old_path, + new_path: first_note.position.new_path, + old_line: first_note.position.old_line, + new_line: first_note.position.new_line + 1, + diff_refs: first_note.position.diff_refs + ) + + DiffNotePosition.create_or_update_for( + first_note, + diff_type: :head, + position: new_position, + line_code: 'bd4b7bfff3a247ccf6e3371c41ec018a55230bcc_534_521' + ) + end - it_behaves_like 'cache miss' do - let(:changed_notes) { [first_note, second_note] } - end + it_behaves_like 'cache miss' do + let(:changed_notes) { [first_note, second_note] } end + end - context 'when author status changes' do - before do - Users::SetStatusService.new(author, message: "updated status").execute - end + context 'when author detail changes' do + before do + author.update!(name: "#{author.name} (Updated)") + end - it_behaves_like 'cache miss' do - let(:changed_notes) { [first_note, second_note] } - end + it_behaves_like 'cache miss' do + let(:changed_notes) { [first_note, second_note] } end + end - context 'when author role changes' do - before do - Members::UpdateService.new(owner, access_level: Gitlab::Access::GUEST).execute(author_membership) - end + context 'when author status changes' do + before do + Users::SetStatusService.new(author, message: "updated status").execute + end - it_behaves_like 'cache miss' do - let(:changed_notes) { [first_note, second_note] } - end + it_behaves_like 'cache miss' do + let(:changed_notes) { [first_note, second_note] } end + end - context 'when current_user role changes' do - before do - Members::UpdateService.new(owner, access_level: Gitlab::Access::GUEST).execute(project.member(user)) - end + context 'when author role changes' do + before do + Members::UpdateService.new(owner, access_level: Gitlab::Access::GUEST).execute(author_membership) + end - it_behaves_like 'cache miss' do - let(:changed_notes) { [first_note, second_note] } - end + it_behaves_like 'cache miss' do + let(:changed_notes) { [first_note, second_note] } end end - context 'when disabled_mr_discussions_redis_cache is disabled' do + context 'when current_user role changes' do before do - stub_feature_flags(disabled_mr_discussions_redis_cache: false) - send_request + Members::UpdateService.new(owner, access_level: Gitlab::Access::GUEST).execute(project.member(user)) end - it_behaves_like 'cache hit' + it_behaves_like 'cache miss' do + let(:changed_notes) { [first_note, second_note] } + end end end end diff --git a/spec/requests/projects/merge_requests_spec.rb b/spec/requests/projects/merge_requests_spec.rb index 9600d1a3656..e57808e6728 100644 --- a/spec/requests/projects/merge_requests_spec.rb +++ b/spec/requests/projects/merge_requests_spec.rb @@ -6,10 +6,13 @@ RSpec.describe 'merge requests actions', feature_category: :source_code_manageme let_it_be(:project) { create(:project, :repository) } let(:merge_request) do - create(:merge_request_with_diffs, target_project: project, - source_project: project, - assignees: [user], - reviewers: [user2]) + create( + :merge_request_with_diffs, + target_project: project, + source_project: project, + assignees: [user], + reviewers: [user2] + ) end let(:user) { project.first_owner } diff --git a/spec/requests/projects/metrics/dashboards/builder_spec.rb b/spec/requests/projects/metrics/dashboards/builder_spec.rb index c929beaed70..8af2d1f1d25 100644 --- a/spec/requests/projects/metrics/dashboards/builder_spec.rb +++ b/spec/requests/projects/metrics/dashboards/builder_spec.rb @@ -49,6 +49,10 @@ RSpec.describe 'Projects::Metrics::Dashboards::BuilderController', feature_categ end describe 'POST /:namespace/:project/-/metrics/dashboards/builder' do + before do + stub_feature_flags(remove_monitor_metrics: false) + end + context 'as anonymous user' do it 'redirects user to sign in page' do send_request @@ -102,6 +106,18 @@ RSpec.describe 'Projects::Metrics::Dashboards::BuilderController', feature_categ expect(json_response['message']).to eq('Invalid configuration format') end end + + context 'when metrics dashboard feature is unavailable' do + before do + stub_feature_flags(remove_monitor_metrics: true) + end + + it 'returns not found' do + send_request + + expect(response).to have_gitlab_http_status(:not_found) + end + end end end end diff --git a/spec/requests/projects/metrics_dashboard_spec.rb b/spec/requests/projects/metrics_dashboard_spec.rb index 01925f8345b..d0181275927 100644 --- a/spec/requests/projects/metrics_dashboard_spec.rb +++ b/spec/requests/projects/metrics_dashboard_spec.rb @@ -11,6 +11,7 @@ RSpec.describe 'Projects::MetricsDashboardController', feature_category: :metric before do project.add_developer(user) login_as(user) + stub_feature_flags(remove_monitor_metrics: false) end describe 'GET /:namespace/:project/-/metrics' do @@ -37,6 +38,17 @@ RSpec.describe 'Projects::MetricsDashboardController', feature_category: :metric expect(response).to redirect_to(dashboard_route(params.merge(environment: environment.id))) end + context 'with remove_monitor_metrics returning true' do + before do + stub_feature_flags(remove_monitor_metrics: true) + end + + it 'renders 404 page' do + send_request + expect(response).to have_gitlab_http_status(:not_found) + end + end + context 'with anonymous user and public dashboard visibility' do let(:anonymous_user) { create(:user) } let(:project) do diff --git a/spec/requests/projects/ml/candidates_controller_spec.rb b/spec/requests/projects/ml/candidates_controller_spec.rb index d3f9d92bc44..78c8e99e3f3 100644 --- a/spec/requests/projects/ml/candidates_controller_spec.rb +++ b/spec/requests/projects/ml/candidates_controller_spec.rb @@ -6,7 +6,7 @@ RSpec.describe Projects::Ml::CandidatesController, feature_category: :mlops do let_it_be(:project) { create(:project, :repository) } let_it_be(:user) { project.first_owner } let_it_be(:experiment) { create(:ml_experiments, project: project, user: user) } - let_it_be(:candidate) { create(:ml_candidates, experiment: experiment, user: user) } + let_it_be(:candidate) { create(:ml_candidates, experiment: experiment, user: user, project: project) } let(:ff_value) { true } let(:candidate_iid) { candidate.iid } @@ -18,19 +18,29 @@ RSpec.describe Projects::Ml::CandidatesController, feature_category: :mlops do sign_in(user) end + shared_examples 'renders 404' do + it 'renders 404' do + expect(response).to have_gitlab_http_status(:not_found) + end + end + + shared_examples '404 if candidate does not exist' do + context 'when experiment does not exist' do + let(:candidate_iid) { non_existing_record_id } + + it_behaves_like 'renders 404' + end + end + shared_examples '404 if feature flag disabled' do context 'when :ml_experiment_tracking disabled' do let(:ff_value) { false } - it 'is 404' do - expect(response).to have_gitlab_http_status(:not_found) - end + it_behaves_like 'renders 404' end end describe 'GET show' do - let(:params) { basic_params.merge(id: experiment.iid) } - before do show_candidate end @@ -48,20 +58,39 @@ RSpec.describe Projects::Ml::CandidatesController, feature_category: :mlops do expect { show_candidate }.not_to exceed_all_query_limit(control_count) end - context 'when candidate does not exist' do - let(:candidate_iid) { non_existing_record_id.to_s } + it_behaves_like '404 if candidate does not exist' + it_behaves_like '404 if feature flag disabled' + end + + describe 'DELETE #destroy' do + let_it_be(:candidate_for_deletion) do + create(:ml_candidates, project: project, experiment: experiment, user: user) + end + + let(:candidate_iid) { candidate_for_deletion.iid } - it 'returns 404' do - expect(response).to have_gitlab_http_status(:not_found) - end + before do + destroy_candidate end + it 'deletes the experiment', :aggregate_failures do + expect(response).to have_gitlab_http_status(:found) + expect(flash[:notice]).to eq('Candidate removed') + expect(response).to redirect_to("/#{project.full_path}/-/ml/experiments/#{experiment.iid}") + expect { Ml::Candidate.find(id: candidate_for_deletion.id) }.to raise_error(ActiveRecord::RecordNotFound) + end + + it_behaves_like '404 if candidate does not exist' it_behaves_like '404 if feature flag disabled' end private def show_candidate - get project_ml_candidate_path(project, candidate_iid) + get project_ml_candidate_path(project, iid: candidate_iid) + end + + def destroy_candidate + delete project_ml_candidate_path(project, candidate_iid) end end diff --git a/spec/requests/projects/ml/experiments_controller_spec.rb b/spec/requests/projects/ml/experiments_controller_spec.rb index 9b071efc1f1..5a8496a250a 100644 --- a/spec/requests/projects/ml/experiments_controller_spec.rb +++ b/spec/requests/projects/ml/experiments_controller_spec.rb @@ -19,6 +19,7 @@ RSpec.describe Projects::Ml::ExperimentsController, feature_category: :mlops do let(:ff_value) { true } let(:project) { project_with_feature } let(:basic_params) { { namespace_id: project.namespace.to_param, project_id: project } } + let(:experiment_iid) { experiment.iid } before do stub_feature_flags(ml_experiment_tracking: false) @@ -27,13 +28,25 @@ RSpec.describe Projects::Ml::ExperimentsController, feature_category: :mlops do sign_in(user) end + shared_examples 'renders 404' do + it 'renders 404' do + expect(response).to have_gitlab_http_status(:not_found) + end + end + + shared_examples '404 if experiment does not exist' do + context 'when experiment does not exist' do + let(:experiment_iid) { non_existing_record_id } + + it_behaves_like 'renders 404' + end + end + shared_examples '404 if feature flag disabled' do context 'when :ml_experiment_tracking disabled' do let(:ff_value) { false } - it 'is 404' do - expect(response).to have_gitlab_http_status(:not_found) - end + it_behaves_like 'renders 404' end end @@ -109,119 +122,184 @@ RSpec.describe Projects::Ml::ExperimentsController, feature_category: :mlops do end describe 'GET show' do - let(:params) { basic_params.merge(id: experiment.iid) } + describe 'html' do + it 'renders the template' do + show_experiment + + expect(response).to render_template('projects/ml/experiments/show') + end - it 'renders the template' do - show_experiment + describe 'pagination' do + 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 - expect(response).to render_template('projects/ml/experiments/show') - end + let(:params) { basic_params.merge(id: experiment.iid) } - describe 'pagination' do - 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) + before do + stub_const("Projects::Ml::ExperimentsController::MAX_CANDIDATES_PER_PAGE", 2) + + show_experiment end - end - let(:params) { basic_params.merge(id: experiment.iid) } + it 'fetches only MAX_CANDIDATES_PER_PAGE candidates' do + expect(assigns(:candidates).size).to eq(2) + end - before do - stub_const("Projects::Ml::ExperimentsController::MAX_CANDIDATES_PER_PAGE", 2) + it 'paginates' do + received = assigns(:page_info) - show_experiment - end + expect(received).to include({ + has_next_page: true, + has_previous_page: false, + start_cursor: nil + }) + end - it 'fetches only MAX_CANDIDATES_PER_PAGE candidates' do - expect(assigns(:candidates).size).to eq(2) - 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) - it 'paginates' do - received = assigns(:page_info) + new_params = params.merge(cursor: assigns(:page_info)[:end_cursor]) - expect(received).to include({ - has_next_page: true, - has_previous_page: false, - start_cursor: nil - }) + show_experiment(new_params: new_params) + + new_page = assigns(:candidates) + + expect(new_page.first).to eq(candidates[1]) + end + end end - context 'when order by metric' do + describe 'search' do let(:params) do - { - order_by: "metric1", - order_by_type: "metric", - sort: "desc" - } + basic_params.merge( + name: 'some_name', + orderBy: 'name', + orderByType: 'metric', + sort: 'asc', + invalid: 'invalid' + ) end - it 'paginates', :aggregate_failures do - page = assigns(:candidates) - - expect(page.first).to eq(candidates.last) - expect(page.last).to eq(candidates.first) + 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 - new_params = params.merge(cursor: assigns(:page_info)[:end_cursor]) + it 'does not perform N+1 sql queries' do + control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) { show_experiment } - show_experiment(new_params) + create_list(:ml_candidates, 2, :with_metrics_and_params, experiment: experiment) - new_page = assigns(:candidates) + expect { show_experiment }.not_to exceed_all_query_limit(control_count) + end - expect(new_page.first).to eq(candidates[1]) + describe '404' do + before do + show_experiment end + + it_behaves_like '404 if experiment does not exist' + it_behaves_like '404 if feature flag disabled' end end - 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 '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' - }) + describe 'csv' do + it 'responds with :ok', :aggregate_failures do + show_experiment_csv + + expect(response).to have_gitlab_http_status(:ok) + expect(response.headers['Content-Type']).to eq('text/csv; charset=utf-8') + end + + it 'calls the presenter' do + allow(::Ml::CandidatesCsvPresenter).to receive(:new).and_call_original + + show_experiment_csv + end + + it 'does not perform N+1 sql queries' do + control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) { show_experiment_csv } + + create_list(:ml_candidates, 2, :with_metrics_and_params, experiment: experiment) + + expect { show_experiment_csv }.not_to exceed_all_query_limit(control_count) + end + + describe '404' do + before do + show_experiment_csv end - show_experiment + it_behaves_like '404 if experiment does not exist' + it_behaves_like '404 if feature flag disabled' end end + end - it 'does not perform N+1 sql queries' do - control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) { show_experiment } + describe 'DELETE #destroy' do + let_it_be(:experiment_for_deletion) do + create(:ml_experiments, project: project_with_feature, user: user).tap do |e| + create(:ml_candidates, experiment: e, user: user) + end + end + + let_it_be(:candidate_for_deletion) { experiment_for_deletion.candidates.first } - create_list(:ml_candidates, 2, :with_metrics_and_params, experiment: experiment) + let(:params) { basic_params.merge(id: experiment.iid) } - expect { show_experiment }.not_to exceed_all_query_limit(control_count) + before do + destroy_experiment end - it_behaves_like '404 if feature flag disabled' do - before do - show_experiment - end + it 'deletes the experiment' do + expect { experiment.reload }.to raise_error(ActiveRecord::RecordNotFound) end + + it_behaves_like '404 if experiment does not exist' + it_behaves_like '404 if feature flag disabled' end private - def show_experiment(new_params = nil) - get project_ml_experiment_path(project, experiment.iid), params: new_params || params + def show_experiment(new_params: nil, format: :html) + get project_ml_experiment_path(project, experiment_iid, format: format), params: new_params || params + end + + def show_experiment_csv + show_experiment(format: :csv) end def list_experiments(new_params = nil) get project_ml_experiments_path(project), params: new_params || params end + + def destroy_experiment + delete project_ml_experiment_path(project, experiment_iid), params: params + end end diff --git a/spec/requests/projects/pipelines_controller_spec.rb b/spec/requests/projects/pipelines_controller_spec.rb index 73e002b63b1..7bdb66755db 100644 --- a/spec/requests/projects/pipelines_controller_spec.rb +++ b/spec/requests/projects/pipelines_controller_spec.rb @@ -23,18 +23,25 @@ RSpec.describe Projects::PipelinesController, feature_category: :continuous_inte it 'does not execute N+1 queries' do get_pipelines_index - control_count = ActiveRecord::QueryRecorder.new do + create_pipelines + + control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do get_pipelines_index end.count - %w[pending running success failed canceled].each do |status| - create(:ci_pipeline, project: project, status: status) - end + create_pipelines # 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 { get_pipelines_index }.not_to exceed_all_query_limit(control_count + 1) expect(response).to have_gitlab_http_status(:ok) - expect(json_response['pipelines'].count).to eq 6 + expect(json_response['pipelines'].count).to eq(11) + end + + def create_pipelines + %w[pending running success failed canceled].each do |status| + pipeline = create(:ci_pipeline, project: project, status: status) + create(:ci_build, :failed, pipeline: pipeline) + end end def get_pipelines_index @@ -49,13 +56,21 @@ RSpec.describe Projects::PipelinesController, feature_category: :continuous_inte it 'does not execute N+1 queries' do request_build_stage - control_count = ActiveRecord::QueryRecorder.new do + control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do request_build_stage end.count create(:ci_build, pipeline: pipeline, stage: 'build') - expect { request_build_stage }.not_to exceed_query_limit(control_count) + 2.times do |i| + create(:ci_build, + name: "test retryable #{i}", + pipeline: pipeline, + stage: 'build', + status: :failed) + end + + expect { request_build_stage }.not_to exceed_all_query_limit(control_count) expect(response).to have_gitlab_http_status(:ok) end @@ -66,13 +81,14 @@ RSpec.describe Projects::PipelinesController, feature_category: :continuous_inte request_build_stage(retried: true) - control_count = ActiveRecord::QueryRecorder.new do + control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do request_build_stage(retried: true) end.count create(:ci_build, :retried, :failed, pipeline: pipeline, stage: 'build') + create(:ci_build, :failed, pipeline: pipeline, stage: 'build') - expect { request_build_stage(retried: true) }.not_to exceed_query_limit(control_count) + expect { request_build_stage(retried: true) }.not_to exceed_all_query_limit(control_count) expect(response).to have_gitlab_http_status(:ok) end diff --git a/spec/requests/projects/settings/access_tokens_controller_spec.rb b/spec/requests/projects/settings/access_tokens_controller_spec.rb index defb35fd496..666dc42bcab 100644 --- a/spec/requests/projects/settings/access_tokens_controller_spec.rb +++ b/spec/requests/projects/settings/access_tokens_controller_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Projects::Settings::AccessTokensController, feature_category: :authentication_and_authorization do +RSpec.describe Projects::Settings::AccessTokensController, feature_category: :system_access do let_it_be(:user) { create(:user) } let_it_be(:group) { create(:group) } let_it_be(:resource) { create(:project, group: group) } diff --git a/spec/requests/projects/uploads_spec.rb b/spec/requests/projects/uploads_spec.rb index aec2636b69c..a591f479763 100644 --- a/spec/requests/projects/uploads_spec.rb +++ b/spec/requests/projects/uploads_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe 'File uploads', feature_category: :not_owned do +RSpec.describe 'File uploads', feature_category: :shared do include WorkhorseHelpers let(:project) { create(:project, :public, :repository) } diff --git a/spec/requests/projects/usage_quotas_spec.rb b/spec/requests/projects/usage_quotas_spec.rb index 60ab64c30c3..33b206c8dc0 100644 --- a/spec/requests/projects/usage_quotas_spec.rb +++ b/spec/requests/projects/usage_quotas_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe 'Project Usage Quotas', feature_category: :subscription_cost_management do +RSpec.describe 'Project Usage Quotas', feature_category: :consumables_cost_management do let_it_be(:project) { create(:project) } let_it_be(:role) { :maintainer } let_it_be(:user) { create(:user) } diff --git a/spec/requests/projects/wikis_controller_spec.rb b/spec/requests/projects/wikis_controller_spec.rb new file mode 100644 index 00000000000..3c434b36b21 --- /dev/null +++ b/spec/requests/projects/wikis_controller_spec.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Projects::WikisController, feature_category: :wiki do + using RSpec::Parameterized::TableSyntax + + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project, :wiki_repo, namespace: user.namespace) } + let_it_be(:project_wiki) { create(:project_wiki, project: project, user: user) } + let_it_be(:wiki_page) do + create(:wiki_page, + wiki: project_wiki, + title: 'home', content: "Look at this [image](#{path})\n\n ![alt text](#{path})") + end + + let_it_be(:csp_nonce) { 'just=some=noncense' } + + before do + sign_in(user) + + allow_next_instance_of(described_class) do |instance| + allow(instance).to receive(:content_security_policy_nonce).and_return(csp_nonce) + end + end + + shared_examples 'embed.diagrams.net frame-src directive' do + it 'adds drawio frame-src directive to the Content Security Policy header' do + frame_src = response.headers['Content-Security-Policy'].split(';') + .map(&:strip) + .find { |entry| entry.starts_with?('frame-src') } + + expect(frame_src).to include('https://embed.diagrams.net') + end + end + + describe 'CSP policy' do + describe '#new' do + before do + get wiki_path(project_wiki, action: :new) + end + + it_behaves_like 'embed.diagrams.net frame-src directive' + end + + describe '#edit' do + before do + get wiki_page_path(project_wiki, wiki_page, action: 'edit') + end + + it_behaves_like 'embed.diagrams.net frame-src directive' + end + + describe '#create' do + before do + # Creating a page with an invalid title to render edit page + post wiki_path(project_wiki, action: 'create'), params: { wiki: { title: 'home' } } + end + + it_behaves_like 'embed.diagrams.net frame-src directive' + end + + describe '#update' do + before do + # Setting an invalid page title to render edit page + put wiki_page_path(project_wiki, wiki_page), params: { wiki: { title: '' } } + end + + it_behaves_like 'embed.diagrams.net frame-src directive' + end + end +end diff --git a/spec/requests/projects/work_items_spec.rb b/spec/requests/projects/work_items_spec.rb index 056416d380d..c02f76d2c65 100644 --- a/spec/requests/projects/work_items_spec.rb +++ b/spec/requests/projects/work_items_spec.rb @@ -3,22 +3,192 @@ require 'spec_helper' RSpec.describe 'Work Items', feature_category: :team_planning do + include WorkhorseHelpers + + include_context 'workhorse headers' + let_it_be(:work_item) { create(:work_item) } - let_it_be(:developer) { create(:user) } + let_it_be(:current_user) { create(:user) } + let_it_be(:project) { create(:project) } + + let(:file) { fixture_file_upload("spec/fixtures/#{filename}") } before_all do - work_item.project.add_developer(developer) + work_item.project.add_developer(current_user) + end + + shared_examples 'response with 404 status' do + it 'returns 404' do + subject + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + shared_examples 'safely handles uploaded files' do + it 'ensures the upload is handled safely', :aggregate_failures do + allow(Gitlab::Utils).to receive(:check_path_traversal!).and_call_original + expect(Gitlab::Utils).to receive(:check_path_traversal!).with(filename).at_least(:once) + expect(FileUploader).not_to receive(:cache) + + subject + end end describe 'GET /:namespace/:project/work_items/:id' do before do - sign_in(developer) + sign_in(current_user) end it 'renders index' do - get project_work_items_url(work_item.project, work_items_path: work_item.id) + get project_work_items_url(work_item.project, work_items_path: work_item.iid) expect(response).to have_gitlab_http_status(:ok) end end + + describe 'POST /:namespace/:project/work_items/import_csv' do + let(:filename) { 'work_items_valid_types.csv' } + let(:params) { { namespace_id: project.namespace.id, path: 'test' } } + + subject { upload_file(file, workhorse_headers, params) } + + shared_examples 'handles authorisation' do + context 'when unauthorized' do + context 'with non-member' do + let_it_be(:current_user) { create(:user) } + + before do + sign_in(current_user) + end + + it 'responds with error' do + subject + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'with anonymous user' do + it 'responds with error' do + subject + + expect(response).to have_gitlab_http_status(:found) + expect(response).to be_redirect + end + end + end + + context 'when authorized' do + before do + sign_in(current_user) + project.add_reporter(current_user) + end + + context 'when import/export work items feature is available and member is a reporter' do + shared_examples 'response with success status' do + it 'returns 200 status and success message' do + subject + + expect(response).to have_gitlab_http_status(:success) + expect(json_response).to eq( + 'message' => "Your work items are being imported. Once finished, you'll receive a confirmation email.") + end + end + + it_behaves_like 'response with success status' + it_behaves_like 'safely handles uploaded files' + + it 'shows error when upload fails' do + expect_next_instance_of(UploadService) do |upload_service| + expect(upload_service).to receive(:execute).and_return(nil) + end + + subject + + expect(json_response).to eq('errors' => 'File upload error.') + end + + context 'when file extension is not csv' do + let(:filename) { 'sample_doc.md' } + + it 'returns error message' do + subject + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response).to eq( + 'errors' => "The uploaded file was invalid. Supported file extensions are .csv.") + end + end + end + + context 'when work items import/export feature is not available' do + before do + stub_feature_flags(import_export_work_items_csv: false) + end + + it_behaves_like 'response with 404 status' + end + end + end + + context 'with public project' do + let_it_be(:project) { create(:project, :public) } + + it_behaves_like 'handles authorisation' + end + + context 'with private project' do + it_behaves_like 'handles authorisation' + end + + def upload_file(file, headers = {}, params = {}) + workhorse_finalize( + import_csv_project_work_items_path(project), + method: :post, + file_key: :file, + params: params.merge(file: file), + headers: headers, + send_rewritten_field: true + ) + end + end + + describe 'POST #authorize' do + subject do + post import_csv_authorize_project_work_items_path(project), + headers: workhorse_headers + end + + before do + sign_in(current_user) + end + + context 'with authorized user' do + before do + project.add_reporter(current_user) + end + + context 'when work items import/export feature is enabled' do + let(:user) { current_user } + + it_behaves_like 'handle uploads authorize request' do + let(:uploader_class) { FileUploader } + let(:maximum_size) { Gitlab::CurrentSettings.max_attachment_size.megabytes } + end + end + + context 'when work items import/export feature is disabled' do + before do + stub_feature_flags(import_export_work_items_csv: false) + end + + it_behaves_like 'response with 404 status' + end + end + + context 'with unauthorized user' do + it_behaves_like 'response with 404 status' + end + end end |