Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-05-17 19:05:49 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-05-17 19:05:49 +0300
commit43a25d93ebdabea52f99b05e15b06250cd8f07d7 (patch)
treedceebdc68925362117480a5d672bcff122fb625b /spec/requests/projects
parent20c84b99005abd1c82101dfeff264ac50d2df211 (diff)
Add latest changes from gitlab-org/gitlab@16-0-stable-eev16.0.0-rc42
Diffstat (limited to 'spec/requests/projects')
-rw-r--r--spec/requests/projects/airflow/dags_controller_spec.rb105
-rw-r--r--spec/requests/projects/aws/configuration_controller_spec.rb59
-rw-r--r--spec/requests/projects/ci/promeheus_metrics/histograms_controller_spec.rb2
-rw-r--r--spec/requests/projects/cluster_agents_controller_spec.rb2
-rw-r--r--spec/requests/projects/cycle_analytics_events_spec.rb2
-rw-r--r--spec/requests/projects/environments_controller_spec.rb4
-rw-r--r--spec/requests/projects/google_cloud/configuration_controller_spec.rb2
-rw-r--r--spec/requests/projects/google_cloud/databases_controller_spec.rb2
-rw-r--r--spec/requests/projects/google_cloud/deployments_controller_spec.rb114
-rw-r--r--spec/requests/projects/google_cloud/gcp_regions_controller_spec.rb2
-rw-r--r--spec/requests/projects/google_cloud/revoke_oauth_controller_spec.rb2
-rw-r--r--spec/requests/projects/google_cloud/service_accounts_controller_spec.rb2
-rw-r--r--spec/requests/projects/incident_management/timeline_events_spec.rb4
-rw-r--r--spec/requests/projects/issue_links_controller_spec.rb26
-rw-r--r--spec/requests/projects/issues_controller_spec.rb39
-rw-r--r--spec/requests/projects/merge_requests_controller_spec.rb5
-rw-r--r--spec/requests/projects/merge_requests_discussions_spec.rb295
-rw-r--r--spec/requests/projects/merge_requests_spec.rb11
-rw-r--r--spec/requests/projects/metrics/dashboards/builder_spec.rb16
-rw-r--r--spec/requests/projects/metrics_dashboard_spec.rb12
-rw-r--r--spec/requests/projects/ml/candidates_controller_spec.rb53
-rw-r--r--spec/requests/projects/ml/experiments_controller_spec.rb230
-rw-r--r--spec/requests/projects/pipelines_controller_spec.rb36
-rw-r--r--spec/requests/projects/settings/access_tokens_controller_spec.rb2
-rw-r--r--spec/requests/projects/uploads_spec.rb2
-rw-r--r--spec/requests/projects/usage_quotas_spec.rb2
-rw-r--r--spec/requests/projects/wikis_controller_spec.rb72
-rw-r--r--spec/requests/projects/work_items_spec.rb178
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(/&quot;currentTab&quot;:&quot;timeline&quot;/)
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