Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-04-14 06:08:29 +00:00
parent 7046de6ada
commit 8952851661
55 changed files with 723 additions and 324 deletions

View File

@ -18,7 +18,7 @@ qa:internal:
- .qa-job-base - .qa-job-base
- .qa:rules:internal - .qa:rules:internal
script: script:
- bundle exec rspec - bundle exec rspec -O .rspec_internal
qa:internal-as-if-foss: qa:internal-as-if-foss:
extends: extends:

View File

@ -4,6 +4,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
skip_before_action :check_two_factor_requirement skip_before_action :check_two_factor_requirement
before_action :ensure_verified_primary_email, only: [:show, :create] before_action :ensure_verified_primary_email, only: [:show, :create]
before_action :validate_current_password, only: [:create, :codes, :destroy], if: :current_password_required? before_action :validate_current_password, only: [:create, :codes, :destroy], if: :current_password_required?
before_action :update_current_user_otp!, only: [:show]
helper_method :current_password_required? helper_method :current_password_required?
@ -14,16 +15,6 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
feature_category :authentication_and_authorization feature_category :authentication_and_authorization
def show def show
unless current_user.two_factor_enabled?
current_user.otp_secret = User.generate_otp_secret(32)
end
unless current_user.otp_grace_period_started_at && two_factor_grace_period
current_user.otp_grace_period_started_at = Time.current
end
Users::UpdateService.new(current_user, user: current_user).execute!
if two_factor_authentication_required? && !current_user.two_factor_enabled? if two_factor_authentication_required? && !current_user.two_factor_enabled?
two_factor_authentication_reason( two_factor_authentication_reason(
global: lambda do global: lambda do
@ -139,6 +130,18 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
private private
def update_current_user_otp!
if current_user.needs_new_otp_secret?
current_user.update_otp_secret!
end
unless current_user.otp_grace_period_started_at && two_factor_grace_period
current_user.otp_grace_period_started_at = Time.current
end
Users::UpdateService.new(current_user, user: current_user).execute!
end
def validate_current_password def validate_current_password
return if current_user.valid_password?(params[:current_password]) return if current_user.valid_password?(params[:current_password])

View File

@ -104,11 +104,11 @@ class Projects::EnvironmentsController < Projects::ApplicationController
def stop def stop
return render_404 unless @environment.available? return render_404 unless @environment.available?
stop_action = @environment.stop_with_action!(current_user) stop_actions = @environment.stop_with_actions!(current_user)
action_or_env_url = action_or_env_url =
if stop_action if stop_actions&.count == 1
polymorphic_url([project, stop_action]) polymorphic_url([project, stop_actions.first])
else else
project_environment_url(project, @environment) project_environment_url(project, @environment)
end end

View File

@ -959,7 +959,7 @@ module Ci
Ci::Build.latest.where(pipeline: self_and_descendants) Ci::Build.latest.where(pipeline: self_and_descendants)
end end
def environments_in_self_and_descendants def environments_in_self_and_descendants(deployment_status: nil)
# We limit to 100 unique environments for application safety. # We limit to 100 unique environments for application safety.
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/340781#note_699114700 # See: https://gitlab.com/gitlab-org/gitlab/-/issues/340781#note_699114700
expanded_environment_names = expanded_environment_names =
@ -969,7 +969,7 @@ module Ci
.limit(100) .limit(100)
.pluck(:expanded_environment_name) .pluck(:expanded_environment_name)
Environment.where(project: project, name: expanded_environment_names).with_deployment(sha) Environment.where(project: project, name: expanded_environment_names).with_deployment(sha, status: deployment_status)
end end
# With multi-project and parent-child pipelines # With multi-project and parent-child pipelines

View File

@ -59,7 +59,7 @@ class Environment < ApplicationRecord
allow_nil: true, allow_nil: true,
addressable_url: true addressable_url: true
delegate :stop_action, :manual_actions, to: :last_deployment, allow_nil: true delegate :manual_actions, to: :last_deployment, allow_nil: true
delegate :auto_rollback_enabled?, to: :project delegate :auto_rollback_enabled?, to: :project
scope :available, -> { with_state(:available) } scope :available, -> { with_state(:available) }
@ -89,13 +89,19 @@ class Environment < ApplicationRecord
scope :for_project, -> (project) { where(project_id: project) } scope :for_project, -> (project) { where(project_id: project) }
scope :for_tier, -> (tier) { where(tier: tier).where.not(tier: nil) } scope :for_tier, -> (tier) { where(tier: tier).where.not(tier: nil) }
scope :with_deployment, -> (sha) { where('EXISTS (?)', Deployment.select(1).where('deployments.environment_id = environments.id').where(sha: sha)) }
scope :unfoldered, -> { where(environment_type: nil) } scope :unfoldered, -> { where(environment_type: nil) }
scope :with_rank, -> do scope :with_rank, -> do
select('environments.*, rank() OVER (PARTITION BY project_id ORDER BY id DESC)') select('environments.*, rank() OVER (PARTITION BY project_id ORDER BY id DESC)')
end end
scope :for_id, -> (id) { where(id: id) } scope :for_id, -> (id) { where(id: id) }
scope :with_deployment, -> (sha, status: nil) do
deployments = Deployment.select(1).where('deployments.environment_id = environments.id').where(sha: sha)
deployments = deployments.where(status: status) if status
where('EXISTS (?)', deployments)
end
scope :stopped_review_apps, -> (before, limit) do scope :stopped_review_apps, -> (before, limit) do
stopped stopped
.in_review_folder .in_review_folder
@ -185,6 +191,23 @@ class Environment < ApplicationRecord
last_deployment&.deployable last_deployment&.deployable
end end
def last_deployment_pipeline
last_deployable&.pipeline
end
# This method returns the deployment records of the last deployment pipeline, that successfully executed to this environment.
# e.g.
# A pipeline contains
# - deploy job A => production environment
# - deploy job B => production environment
# In this case, `last_deployment_group` returns both deployments, whereas `last_deployable` returns only B.
def last_deployment_group
return Deployment.none unless last_deployment_pipeline
successful_deployments.where(
deployable_id: last_deployment_pipeline.latest_builds.pluck(:id))
end
# NOTE: Below assocation overrides is a workaround for issue https://gitlab.com/gitlab-org/gitlab/-/issues/339908 # NOTE: Below assocation overrides is a workaround for issue https://gitlab.com/gitlab-org/gitlab/-/issues/339908
# It helps to avoid cross joins with the CI database. # It helps to avoid cross joins with the CI database.
# Caveat: It also overrides and losses the default AR caching mechanism. # Caveat: It also overrides and losses the default AR caching mechanism.
@ -255,8 +278,8 @@ class Environment < ApplicationRecord
external_url.gsub(%r{\A.*?://}, '') external_url.gsub(%r{\A.*?://}, '')
end end
def stop_action_available? def stop_actions_available?
available? && stop_action.present? available? && stop_actions.present?
end end
def cancel_deployment_jobs! def cancel_deployment_jobs!
@ -269,18 +292,34 @@ class Environment < ApplicationRecord
end end
end end
def stop_with_action!(current_user) def stop_with_actions!(current_user)
return unless available? return unless available?
stop! stop!
return unless stop_action actions = []
Gitlab::OptimisticLocking.retry_lock( stop_actions.each do |stop_action|
stop_action, Gitlab::OptimisticLocking.retry_lock(
name: 'environment_stop_with_action' stop_action,
) do |build| name: 'environment_stop_with_actions'
build&.play(current_user) ) do |build|
actions << build.play(current_user)
end
end
actions
end
def stop_actions
strong_memoize(:stop_actions) do
if ::Feature.enabled?(:environment_multiple_stop_actions, project, default_enabled: :yaml)
# Fix N+1 queries it brings to the serializer.
# Tracked in https://gitlab.com/gitlab-org/gitlab/-/issues/358780
last_deployment_group.map(&:stop_action).compact
else
[last_deployment&.stop_action].compact
end
end end
end end

View File

@ -1456,9 +1456,9 @@ class MergeRequest < ApplicationRecord
Environment.where(project: project, name: environments) Environment.where(project: project, name: environments)
end end
def environments_in_head_pipeline def environments_in_head_pipeline(deployment_status: nil)
if ::Feature.enabled?(:fix_related_environments_for_merge_requests, target_project, default_enabled: :yaml) if ::Feature.enabled?(:fix_related_environments_for_merge_requests, target_project, default_enabled: :yaml)
actual_head_pipeline&.environments_in_self_and_descendants || Environment.none actual_head_pipeline&.environments_in_self_and_descendants(deployment_status: deployment_status) || Environment.none
else else
legacy_environments legacy_environments
end end

View File

@ -37,6 +37,9 @@ class User < ApplicationRecord
COUNT_CACHE_VALIDITY_PERIOD = 24.hours COUNT_CACHE_VALIDITY_PERIOD = 24.hours
OTP_SECRET_LENGTH = 32
OTP_SECRET_TTL = 2.minutes
MAX_USERNAME_LENGTH = 255 MAX_USERNAME_LENGTH = 255
MIN_USERNAME_LENGTH = 2 MIN_USERNAME_LENGTH = 2
@ -954,6 +957,21 @@ class User < ApplicationRecord
(webauthn_registrations.loaded? && webauthn_registrations.any?) || (!webauthn_registrations.loaded? && webauthn_registrations.exists?) (webauthn_registrations.loaded? && webauthn_registrations.any?) || (!webauthn_registrations.loaded? && webauthn_registrations.exists?)
end end
def needs_new_otp_secret?
!two_factor_enabled? && otp_secret_expired?
end
def otp_secret_expired?
return true unless otp_secret_expires_at
otp_secret_expires_at < Time.current
end
def update_otp_secret!
self.otp_secret = User.generate_otp_secret(OTP_SECRET_LENGTH)
self.otp_secret_expires_at = Time.current + OTP_SECRET_TTL
end
def namespace_move_dir_allowed def namespace_move_dir_allowed
if namespace&.any_project_has_container_registry_tags? if namespace&.any_project_has_container_registry_tags?
errors.add(:username, _('cannot be changed if a personal project has container registry tags.')) errors.add(:username, _('cannot be changed if a personal project has container registry tags.'))

View File

@ -4,12 +4,12 @@ class EnvironmentPolicy < BasePolicy
delegate { @subject.project } delegate { @subject.project }
condition(:stop_with_deployment_allowed) do condition(:stop_with_deployment_allowed) do
@subject.stop_action_available? && @subject.stop_actions_available? &&
can?(:create_deployment) && can?(:update_build, @subject.stop_action) can?(:create_deployment) && can?(:update_build, @subject.stop_actions.last)
end end
condition(:stop_with_update_allowed) do condition(:stop_with_update_allowed) do
!@subject.stop_action_available? && can?(:update_environment, @subject) !@subject.stop_actions_available? && can?(:update_environment, @subject)
end end
condition(:stopped) do condition(:stopped) do

View File

@ -18,7 +18,7 @@ class EnvironmentEntity < Grape::Entity
expose :environment_type expose :environment_type
expose :name_without_type expose :name_without_type
expose :last_deployment, using: DeploymentEntity expose :last_deployment, using: DeploymentEntity
expose :stop_action_available?, as: :has_stop_action expose :stop_actions_available?, as: :has_stop_action
expose :rollout_status, if: -> (*) { can_read_deploy_board? }, using: RolloutStatusEntity expose :rollout_status, if: -> (*) { can_read_deploy_board? }, using: RolloutStatusEntity
expose :tier expose :tier

View File

@ -7,7 +7,7 @@ module Environments
def execute(environment) def execute(environment)
return unless can?(current_user, :stop_environment, environment) return unless can?(current_user, :stop_environment, environment)
environment.stop_with_action!(current_user) environment.stop_with_actions!(current_user)
end end
def execute_for_branch(branch_name) def execute_for_branch(branch_name)
@ -19,7 +19,9 @@ module Environments
end end
def execute_for_merge_request(merge_request) def execute_for_merge_request(merge_request)
merge_request.environments_in_head_pipeline.each { |environment| execute(environment) } merge_request.environments_in_head_pipeline(deployment_status: :success).each do |environment|
execute(environment)
end
end end
private private

View File

@ -16,10 +16,9 @@
.js-text.d-inline= _('Download payload') .js-text.d-inline= _('Download payload')
%pre.js-syntax-highlight.code.highlight.gl-mt-2.gl-display-none{ class: payload_class, data: { endpoint: usage_data_admin_application_settings_path(format: :html) } } %pre.js-syntax-highlight.code.highlight.gl-mt-2.gl-display-none{ class: payload_class, data: { endpoint: usage_data_admin_application_settings_path(format: :html) } }
- else - else
= render 'shared/global_alert', = render Pajamas::AlertComponent.new(variant: :warning,
variant: :warning,
dismissible: false, dismissible: false,
title: 'Service Ping payload not found in the application cache' do title: _('Service Ping payload not found in the application cache')) do
.gl-alert-body .gl-alert-body
- enable_service_ping_link_url = help_page_path('user/admin_area/settings/usage_statistics', anchor: 'enable-or-disable-usage-statistics') - enable_service_ping_link_url = help_page_path('user/admin_area/settings/usage_statistics', anchor: 'enable-or-disable-usage-statistics')

View File

@ -1,4 +1,4 @@
= render 'shared/global_alert', variant: :warning, dismissible: false, alert_class: 'gl-mt-6 gl-mb-3' do = render Pajamas::AlertComponent.new(variant: :warning, dismissible: false, alert_class: 'gl-mt-6 gl-mb-3') do
.gl-alert-body .gl-alert-body
- link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe - link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe
- issue_link_start = link_start % { url: 'https://gitlab.com/gitlab-org/configure/general/-/issues/199' } - issue_link_start = link_start % { url: 'https://gitlab.com/gitlab-org/configure/general/-/issues/199' }

View File

@ -6,9 +6,8 @@
.gl-border-l-solid.gl-border-r-solid.gl-border-gray-100.gl-border-1.gl-p-5 .gl-border-l-solid.gl-border-r-solid.gl-border-gray-100.gl-border-1.gl-p-5
%h4 %h4
= _('Import group from file') = _('Import group from file')
= render 'shared/global_alert', = render Pajamas::AlertComponent.new(variant: :warning,
variant: :warning, dismissible: false) do
dismissible: false do
.gl-alert-body .gl-alert-body
- docs_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/group/import/index.md') } - docs_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/group/import/index.md') }
- link_end = '</a>'.html_safe - link_end = '</a>'.html_safe

View File

@ -3,8 +3,7 @@
%div %div
- if @user.errors.any? - if @user.errors.any?
= render 'shared/global_alert', = render Pajamas::AlertComponent.new(variant: :danger) do
variant: :danger do
.gl-alert-body .gl-alert-body
%ul %ul
- @user.errors.full_messages.each do |msg| - @user.errors.full_messages.each do |msg|

View File

@ -1,21 +0,0 @@
- icons = { info: 'information-o', warning: 'warning', success: 'check-circle', danger: 'error', tip: 'bulb' }
- title = local_assigns.fetch(:title, nil)
- variant = local_assigns.fetch(:variant, :info)
- dismissible = local_assigns.fetch(:dismissible, true)
- alert_class = local_assigns.fetch(:alert_class, nil)
- alert_data = local_assigns.fetch(:alert_data, nil)
- close_button_class = local_assigns.fetch(:close_button_class, nil)
- close_button_data = local_assigns.fetch(:close_button_data, nil)
- icon = icons[variant]
%div{ role: 'alert', class: ['gl-alert', "gl-alert-#{variant}", alert_class], data: alert_data }
= sprite_icon(icon, css_class: "gl-alert-icon#{' gl-alert-icon-no-title' if title.nil?}")
- if dismissible
%button.btn.gl-dismiss-btn.btn-default.btn-sm.gl-button.btn-default-tertiary.btn-icon.js-close{ type: 'button', aria: { label: _('Dismiss') }, class: close_button_class, data: close_button_data }
= sprite_icon('close')
.gl-alert-content{ role: 'alert' }
- if title
%h4.gl-alert-title
= title
= yield

View File

@ -10,8 +10,10 @@ module Environments
def perform(environment_id, params = {}) def perform(environment_id, params = {})
Environment.find_by_id(environment_id).try do |environment| Environment.find_by_id(environment_id).try do |environment|
user = environment.stop_action&.user stop_actions = environment.stop_actions
environment.stop_with_action!(user)
user = stop_actions.last&.user
environment.stop_with_actions!(user)
end end
end end
end end

View File

@ -0,0 +1,8 @@
---
name: environment_multiple_stop_actions
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/84922
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/358911
milestone: '14.10'
type: development
group: group::release
default_enabled: false

View File

@ -0,0 +1,30 @@
# frozen_string_literal: true
class RemoveAllIssuableEscalationStatuses < Gitlab::Database::Migration[1.0]
BATCH_SIZE = 5_000
disable_ddl_transaction!
# Removes records from previous backfill. Records for
# existing incidents will be created entirely as-needed.
#
# See db/post_migrate/20211214012507_backfill_incident_issue_escalation_statuses.rb,
# & IncidentManagement::IssuableEscalationStatuses::[BuildService,PrepareUpdateService]
def up
each_batch_range('incident_management_issuable_escalation_statuses', of: BATCH_SIZE) do |min, max|
execute <<~SQL
DELETE FROM incident_management_issuable_escalation_statuses
WHERE id BETWEEN #{min} AND #{max}
SQL
end
end
def down
# no-op
#
# Potential rollback/re-run should not have impact, as these
# records are not required to be present in the application.
# The corresponding feature flag is also disabled,
# preventing any user-facing access to the records.
end
end

View File

@ -0,0 +1,18 @@
# frozen_string_literal: true
class AddEpicsRelativePosition < Gitlab::Database::Migration[1.0]
DOWNTIME = false
def up
return unless table_exists?(:epics)
return if column_exists?(:epics, :relative_position)
add_column :epics, :relative_position, :integer
execute('UPDATE epics SET relative_position=id*500')
end
def down
# no-op - this column should normally exist if epics table exists too
end
end

View File

@ -0,0 +1,11 @@
# frozen_string_literal: true
class AddOtpSecretExpiresAt < Gitlab::Database::Migration[1.0]
enable_lock_retries!
def change
# rubocop: disable Migration/AddColumnsToWideTables
add_column :users, :otp_secret_expires_at, :datetime_with_timezone
# rubocop: enable Migration/AddColumnsToWideTables
end
end

View File

@ -1,26 +1,9 @@
# frozen_string_literal: true # frozen_string_literal: true
class BackfillIncidentIssueEscalationStatuses < Gitlab::Database::Migration[1.0] class BackfillIncidentIssueEscalationStatuses < Gitlab::Database::Migration[1.0]
MIGRATION = 'BackfillIncidentIssueEscalationStatuses' # Removed in favor of creating records for existing incidents
DELAY_INTERVAL = 2.minutes # as-needed. See db/migrate/20220321234317_remove_all_issuable_escalation_statuses.rb.
BATCH_SIZE = 20_000 def change
disable_ddl_transaction!
class Issue < ActiveRecord::Base
include EachBatch
self.table_name = 'issues'
end
def up
relation = Issue.all
queue_background_migration_jobs_by_range_at_intervals(
relation, MIGRATION, DELAY_INTERVAL, batch_size: BATCH_SIZE, track_jobs: true)
end
def down
# no-op # no-op
end end
end end

View File

@ -0,0 +1 @@
ba5c1738b7c368ee8e10e390c959538c4d74055b8bc57f652b06ffe3a1c3becf

View File

@ -0,0 +1 @@
ab7bb319a7099714d9863ec16b7dcf8c1aeab495b8635a01dff4a51fab876b6b

View File

@ -0,0 +1 @@
efba00e36821c5ebe92ba39ad40dd165ab46c97b1b18becdec0d192470c2e8ca

View File

@ -21480,6 +21480,7 @@ CREATE TABLE users (
role smallint, role smallint,
user_type smallint, user_type smallint,
static_object_token_encrypted text, static_object_token_encrypted text,
otp_secret_expires_at timestamp with time zone,
CONSTRAINT check_7bde697e8e CHECK ((char_length(static_object_token_encrypted) <= 255)) CONSTRAINT check_7bde697e8e CHECK ((char_length(static_object_token_encrypted) <= 255))
); );

View File

@ -558,6 +558,55 @@ Because `stop_review_app` is set to `auto_stop_in: 1 week`,
if a merge request is inactive for more than a week, if a merge request is inactive for more than a week,
GitLab automatically triggers the `stop_review_app` job to stop the environment. GitLab automatically triggers the `stop_review_app` job to stop the environment.
#### Multiple stop actions for an environment
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/22456) in GitLab 14.10 [with a flag](../../administration/feature_flags.md) named `environment_multiple_stop_actions`. Disabled by default.
FLAG:
On self-managed GitLab, by default this feature is not available. To make it available, ask an administrator to [enable the feature flag](../../administration/feature_flags.md) named `environment_multiple_stop_actions`.
On GitLab.com, this feature is not available. We are enabling in phases and the status can be tracked in [issue 358911](https://gitlab.com/gitlab-org/gitlab/-/issues/358911).
This feature is useful when you need to perform multiple **parallel** stop actions on an environment.
To configure multiple stop actions on an environment, specify the [`on_stop`](../yaml/index.md#environmenton_stop)
keyword across multiple [deployment jobs](../jobs/index.md#deployment-jobs) for the same `environment`, as defined in the `.gitlab-ci.yml` file.
When an environment is stopped, the matching `on_stop` actions from *successful deployment jobs* alone are run in parallel in no particular order.
In the following example, for the `test` environment there are two deployment jobs `deploy-to-cloud-a`
and `deploy-to-cloud-b`.
```yaml
deploy-to-cloud-a:
script: echo "Deploy to cloud a"
environment:
name: test
on_stop: teardown-cloud-a
deploy-to-cloud-b:
script: echo "Deploy to cloud b"
environment:
name: test
on_stop: teardown-cloud-b
teardown-cloud-a:
script: echo "Delete the resources in cloud a"
environment:
name: test
action: stop
when: manual
teardown-cloud-b:
script: echo "Delete the resources in cloud b"
environment:
name: test
action: stop
when: manual
```
When the environment is stopped, the system runs `on_stop` actions
`teardown-cloud-a` and `teardown-cloud-b` in parallel.
#### View a deployment's scheduled stop time #### View a deployment's scheduled stop time
You can view a deployment's expiration date in the GitLab UI. You can view a deployment's expiration date in the GitLab UI.

View File

@ -89,15 +89,11 @@ read-only view to discourage this behavior.
Compliance framework pipelines allow group owners to define Compliance framework pipelines allow group owners to define
a compliance pipeline in a separate repository that gets a compliance pipeline in a separate repository that gets
executed in place of the local project's `gitlab-ci.yml` file. As part of this pipeline, an executed in place of the local project's `gitlab-ci.yml` file. As part of this pipeline, an
`include` statement can reference the local project's `gitlab-ci.yml` file. This way, the two CI `include` statement can reference the local project's `gitlab-ci.yml` file. This way, the compliance
files are merged together any time the pipeline runs. Jobs and variables defined in the compliance pipeline jobs can run alongside the project-specific jobs any time the pipeline runs.
Jobs and variables defined in the compliance
pipeline can't be changed by variables in the local project's `gitlab-ci.yml` file. pipeline can't be changed by variables in the local project's `gitlab-ci.yml` file.
When used to enforce scan execution, this feature has some overlap with [scan execution policies](../../application_security/policies/scan-execution-policies.md),
as we have not [unified the user experience for these two features](https://gitlab.com/groups/gitlab-org/-/epics/7312).
For details on the similarities and differences between these features, see
[Enforce scan execution](../../application_security/#enforce-scan-execution).
When you set up the compliance framework, use the **Compliance pipeline configuration** box to link When you set up the compliance framework, use the **Compliance pipeline configuration** box to link
the compliance framework to specific CI/CD configuration. Use the the compliance framework to specific CI/CD configuration. Use the
`path/file.y[a]ml@group-name/project-name` format. For example: `path/file.y[a]ml@group-name/project-name` format. For example:
@ -185,6 +181,11 @@ include: # Execute individual project's configuration (if project contains .git
ref: '$CI_COMMIT_REF_NAME' # Must be defined or MR pipelines always use the use default branch ref: '$CI_COMMIT_REF_NAME' # Must be defined or MR pipelines always use the use default branch
``` ```
When used to enforce scan execution, this feature has some overlap with [scan execution policies](../../application_security/policies/scan-execution-policies.md),
as we have not [unified the user experience for these two features](https://gitlab.com/groups/gitlab-org/-/epics/7312).
For details on the similarities and differences between these features, see
[Enforce scan execution](../../application_security/#enforce-scan-execution).
##### Ensure compliance jobs are always run ##### Ensure compliance jobs are always run
Compliance pipelines use GitLab CI/CD to give you an incredible amount of flexibility Compliance pipelines use GitLab CI/CD to give you an incredible amount of flexibility

View File

@ -131,7 +131,7 @@ module API
environment = user_project.environments.find(params[:environment_id]) environment = user_project.environments.find(params[:environment_id])
authorize! :stop_environment, environment authorize! :stop_environment, environment
environment.stop_with_action!(current_user) environment.stop_with_actions!(current_user)
status 200 status 200
present environment, with: Entities::Environment, current_user: current_user present environment, with: Entities::Environment, current_user: current_user

View File

@ -1,32 +0,0 @@
# frozen_string_literal: true
module Gitlab
module BackgroundMigration
# BackfillIncidentIssueEscalationStatuses adds
# IncidentManagement::IssuableEscalationStatus records for existing Incident issues.
# They will be added with no policy, and escalations_started_at as nil.
class BackfillIncidentIssueEscalationStatuses
def perform(start_id, stop_id)
ActiveRecord::Base.connection.execute <<~SQL
INSERT INTO incident_management_issuable_escalation_statuses (issue_id, created_at, updated_at)
SELECT issues.id, current_timestamp, current_timestamp
FROM issues
WHERE issues.issue_type = 1
AND issues.id BETWEEN #{start_id} AND #{stop_id}
ON CONFLICT (issue_id) DO NOTHING;
SQL
mark_job_as_succeeded(start_id, stop_id)
end
private
def mark_job_as_succeeded(*arguments)
::Gitlab::Database::BackgroundMigrationJob.mark_all_as_succeeded(
self.class.name.demodulize,
arguments
)
end
end
end
end

View File

@ -34469,6 +34469,9 @@ msgstr ""
msgid "Service Desk allows people to create issues in your GitLab instance without their own user account. It provides a unique email address for end users to create issues in a project. Replies can be sent either through the GitLab interface or by email. End users only see threads through email." msgid "Service Desk allows people to create issues in your GitLab instance without their own user account. It provides a unique email address for end users to create issues in a project. Replies can be sent either through the GitLab interface or by email. End users only see threads through email."
msgstr "" msgstr ""
msgid "Service Ping payload not found in the application cache"
msgstr ""
msgid "Service account generated successfully" msgid "Service account generated successfully"
msgstr "" msgstr ""

4
qa/.gitignore vendored
View File

@ -1,6 +1,8 @@
tmp/ tmp/
reports/
no_of_examples/
.ruby-version .ruby-version
.tool-versions .tool-versions
.ruby-gemset .ruby-gemset
urls.yml urls.yml
reports/

4
qa/.rspec_internal Normal file
View File

@ -0,0 +1,4 @@
--force-color
--order random
--format documentation
--require specs/spec_helper

View File

@ -11,7 +11,7 @@ module QA
end end
def has_disabled_usage_data_checkbox? def has_disabled_usage_data_checkbox?
has_element?(:enable_usage_data_checkbox, disabled: true) has_element?(:enable_usage_data_checkbox, disabled: true, visible: false)
end end
end end
end end

View File

@ -133,7 +133,9 @@ module QA
end end
def all_elements(name, **kwargs) def all_elements(name, **kwargs)
if kwargs.keys.none? { |key| [:minimum, :maximum, :count, :between].include?(key) } all_args = [:minimum, :maximum, :count, :between]
if kwargs.keys.none? { |key| all_args.include?(key) }
raise ArgumentError, "Please use :minimum, :maximum, :count, or :between so that all is more reliable" raise ArgumentError, "Please use :minimum, :maximum, :count, or :between so that all is more reliable"
end end
@ -469,8 +471,8 @@ module QA
return element_when_flag_disabled if has_element?(element_when_flag_disabled) return element_when_flag_disabled if has_element?(element_when_flag_disabled)
raise ElementNotFound, raise ElementNotFound,
"Could not find the expected element as #{element_when_flag_enabled} or #{element_when_flag_disabled}." \ "Could not find the expected element as #{element_when_flag_enabled} or #{element_when_flag_disabled}." \
"The relevant feature flag is #{feature_flag}" "The relevant feature flag is #{feature_flag}"
end end
end end
end end

View File

@ -1,5 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
# rubocop:disable QA/ElementWithPattern
RSpec.describe QA::Page::Base do RSpec.describe QA::Page::Base do
describe 'page helpers' do describe 'page helpers' do
it 'exposes helpful page helpers' do it 'exposes helpful page helpers' do
@ -11,12 +12,12 @@ RSpec.describe QA::Page::Base do
subject do subject do
Class.new(described_class) do Class.new(described_class) do
view 'path/to/some/view.html.haml' do view 'path/to/some/view.html.haml' do
element :something, 'string pattern' # rubocop:disable QA/ElementWithPattern element :something, 'string pattern'
element :something_else, /regexp pattern/ # rubocop:disable QA/ElementWithPattern element :something_else, /regexp pattern/
end end
view 'path/to/some/_partial.html.haml' do view 'path/to/some/_partial.html.haml' do
element :another_element, 'string pattern' # rubocop:disable QA/ElementWithPattern element :another_element, 'string pattern'
end end
end end
end end
@ -95,6 +96,7 @@ RSpec.describe QA::Page::Base do
describe '#all_elements' do describe '#all_elements' do
before do before do
allow(subject).to receive(:all) allow(subject).to receive(:all)
allow(subject).to receive(:wait_for_requests)
end end
it 'raises an error if count or minimum are not specified' do it 'raises an error if count or minimum are not specified' do
@ -108,7 +110,7 @@ RSpec.describe QA::Page::Base do
end end
end end
context 'elements' do describe 'elements' do
subject do subject do
Class.new(described_class) do Class.new(described_class) do
view 'path/to/some/view.html.haml' do view 'path/to/some/view.html.haml' do
@ -133,35 +135,37 @@ RSpec.describe QA::Page::Base do
describe '#visible?', 'Page is currently visible' do describe '#visible?', 'Page is currently visible' do
let(:page) { subject.new } let(:page) { subject.new }
before do
allow(page).to receive(:wait_for_requests)
end
context 'with elements' do context 'with elements' do
context 'on the page' do before do
before do allow(page).to receive(:has_no_element?).and_return(has_no_element)
# required elements not there, meaning not on page end
allow(page).to receive(:has_no_element?).and_return(false)
end context 'with element on the page' do
let(:has_no_element) { false }
it 'is visible' do it 'is visible' do
expect(page).to be_visible expect(page).to be_visible
end end
it 'does not raise error if page has elements' do
expect { page.visible? }.not_to raise_error
end
end end
context 'not on the page' do context 'with element not on the page' do
before do let(:has_no_element) { true }
# required elements are not on the page
allow(page).to receive(:has_no_element?).and_return(true)
end
it 'is not visible' do it 'is not visible' do
expect(page).not_to be_visible expect(page).not_to be_visible
end end
end end
it 'does not raise error if page has elements' do
expect { page.visible? }.not_to raise_error
end
end end
context 'no elements' do context 'with no elements' do
subject do subject do
Class.new(described_class) do Class.new(described_class) do
view 'path/to/some/view.html.haml' do view 'path/to/some/view.html.haml' do
@ -180,3 +184,4 @@ RSpec.describe QA::Page::Base do
end end
end end
end end
# rubocop:enable QA/ElementWithPattern

View File

@ -72,41 +72,47 @@ RSpec.describe QA::Support::Page::Logging do
end end
it 'logs has_element?' do it 'logs has_element?' do
expect { subject.has_element?(:element) } expect { subject.has_element?(:element) }.to output(
.to output(/has_element\? :element \(wait: #{QA::Runtime::Browser::CAPYBARA_MAX_WAIT_TIME}\) returned: true/o).to_stdout_from_any_process /has_element\? :element \(wait: #{Capybara.default_max_wait_time}\) returned: true/o
).to_stdout_from_any_process
end end
it 'logs has_element? with text' do it 'logs has_element? with text' do
expect { subject.has_element?(:element, text: "some text") } expect { subject.has_element?(:element, text: "some text") }.to output(
.to output(/has_element\? :element with text "some text" \(wait: #{QA::Runtime::Browser::CAPYBARA_MAX_WAIT_TIME}\) returned: true/o).to_stdout_from_any_process /has_element\? :element with text "some text" \(wait: #{Capybara.default_max_wait_time}\) returned: true/o
).to_stdout_from_any_process
end end
it 'logs has_no_element?' do it 'logs has_no_element?' do
allow(page).to receive(:has_no_css?).and_return(true) allow(page).to receive(:has_no_css?).and_return(true)
expect { subject.has_no_element?(:element) } expect { subject.has_no_element?(:element) }.to output(
.to output(/has_no_element\? :element \(wait: #{QA::Runtime::Browser::CAPYBARA_MAX_WAIT_TIME}\) returned: true/o).to_stdout_from_any_process /has_no_element\? :element \(wait: #{Capybara.default_max_wait_time}\) returned: true/o
).to_stdout_from_any_process
end end
it 'logs has_no_element? with text' do it 'logs has_no_element? with text' do
allow(page).to receive(:has_no_css?).and_return(true) allow(page).to receive(:has_no_css?).and_return(true)
expect { subject.has_no_element?(:element, text: "more text") } expect { subject.has_no_element?(:element, text: "more text") }.to output(
.to output(/has_no_element\? :element with text "more text" \(wait: #{QA::Runtime::Browser::CAPYBARA_MAX_WAIT_TIME}\) returned: true/o).to_stdout_from_any_process /has_no_element\? :element with text "more text" \(wait: #{Capybara.default_max_wait_time}\) returned: true/o
).to_stdout_from_any_process
end end
it 'logs has_text?' do it 'logs has_text?' do
allow(page).to receive(:has_text?).and_return(true) allow(page).to receive(:has_text?).and_return(true)
expect { subject.has_text? 'foo' } expect { subject.has_text? 'foo' }.to output(
.to output(/has_text\?\('foo', wait: #{QA::Runtime::Browser::CAPYBARA_MAX_WAIT_TIME}\) returned true/o).to_stdout_from_any_process /has_text\?\('foo', wait: #{Capybara.default_max_wait_time}\) returned true/o
).to_stdout_from_any_process
end end
it 'logs has_no_text?' do it 'logs has_no_text?' do
allow(page).to receive(:has_no_text?).with('foo', any_args).and_return(true) allow(page).to receive(:has_no_text?).with('foo', any_args).and_return(true)
expect { subject.has_no_text? 'foo' } expect { subject.has_no_text? 'foo' }.to output(
.to output(/has_no_text\?\('foo', wait: #{QA::Runtime::Browser::CAPYBARA_MAX_WAIT_TIME}\) returned true/o).to_stdout_from_any_process /has_no_text\?\('foo', wait: #{Capybara.default_max_wait_time}\) returned true/o
).to_stdout_from_any_process
end end
it 'logs finished_loading?' do it 'logs finished_loading?' do
@ -123,7 +129,7 @@ RSpec.describe QA::Support::Page::Logging do
.to output(/end within element :element/).to_stdout_from_any_process .to output(/end within element :element/).to_stdout_from_any_process
end end
context 'all_elements' do context 'with all_elements' do
it 'logs the number of elements found' do it 'logs the number of elements found' do
allow(page).to receive(:all).and_return([1, 2]) allow(page).to receive(:all).and_return([1, 2])

View File

@ -4,6 +4,7 @@ RSpec.describe QA::Resource::Base do
include QA::Support::Helpers::StubEnv include QA::Support::Helpers::StubEnv
let(:resource) { spy('resource') } let(:resource) { spy('resource') }
let(:api_client) { instance_double('Runtime::API::Client') }
let(:location) { 'http://location' } let(:location) { 'http://location' }
let(:log_regex) { %r{==> Built a MyResource with username 'qa' via #{method} in [\d.\-e]+ seconds+} } let(:log_regex) { %r{==> Built a MyResource with username 'qa' via #{method} in [\d.\-e]+ seconds+} }
@ -114,6 +115,7 @@ RSpec.describe QA::Resource::Base do
allow(QA::Runtime::Logger).to receive(:debug) allow(QA::Runtime::Logger).to receive(:debug)
allow(resource).to receive(:api_support?).and_return(true) allow(resource).to receive(:api_support?).and_return(true)
allow(resource).to receive(:fabricate_via_api!) allow(resource).to receive(:fabricate_via_api!)
allow(resource).to receive(:api_client) { api_client }
end end
it 'logs the resource and build method' do it 'logs the resource and build method' do
@ -154,7 +156,6 @@ RSpec.describe QA::Resource::Base do
before do before do
allow(QA::Runtime::Logger).to receive(:debug) allow(QA::Runtime::Logger).to receive(:debug)
# allow(resource).to receive(:fabricate!)
end end
it 'logs the resource and build method' do it 'logs the resource and build method' do

View File

@ -8,6 +8,7 @@ RSpec.describe 'Interceptor' do
before(:context) do before(:context) do
skip 'Only can test for chrome' unless QA::Runtime::Env.can_intercept? skip 'Only can test for chrome' unless QA::Runtime::Env.can_intercept?
QA::Runtime::Browser.configure!
QA::Runtime::Browser::Session.enable_interception QA::Runtime::Browser::Session.enable_interception
end end
@ -26,7 +27,7 @@ RSpec.describe 'Interceptor' do
end end
context 'with Interceptor' do context 'with Interceptor' do
context 'caching' do context 'with caching' do
it 'checks the cache' do it 'checks the cache' do
expect(check_cache).to be(true) expect(check_cache).to be(true)
end end

View File

@ -3,7 +3,7 @@
describe QA::Runtime::AllureReport do describe QA::Runtime::AllureReport do
include QA::Support::Helpers::StubEnv include QA::Support::Helpers::StubEnv
let(:rspec_config) { double('RSpec::Core::Configuration', 'add_formatter': nil, append_after: nil) } let(:rspec_config) { instance_double('RSpec::Core::Configuration', 'add_formatter': nil, append_after: nil) }
let(:png_path) { 'png_path' } let(:png_path) { 'png_path' }
let(:html_path) { 'html_path' } let(:html_path) { 'html_path' }
@ -42,11 +42,14 @@ describe QA::Runtime::AllureReport do
context 'with report generation enabled' do context 'with report generation enabled' do
let(:generate_report) { 'true' } let(:generate_report) { 'true' }
let(:session) { instance_double('Capybara::Session') }
let(:attributes) { class_spy('Runtime::Scenario') }
let(:version_response) { instance_double('HTTPResponse', code: 200, body: versions.to_json) }
let(:png_file) { 'png-file' } let(:png_file) { 'png-file' }
let(:html_file) { 'html-file' } let(:html_file) { 'html-file' }
let(:ci_job) { 'ee:relative 5' } let(:ci_job) { 'ee:relative 5' }
let(:versions) { { version: '14', revision: '6ced31db947' } } let(:versions) { { version: '14', revision: '6ced31db947' } }
let(:session) { double('session') }
let(:browser_log) { ['log message 1', 'log message 2'] } let(:browser_log) { ['log message 1', 'log message 2'] }
before do before do
@ -54,11 +57,13 @@ describe QA::Runtime::AllureReport do
stub_env('CI_JOB_NAME', ci_job) stub_env('CI_JOB_NAME', ci_job)
stub_env('GITLAB_QA_ADMIN_ACCESS_TOKEN', 'token') stub_env('GITLAB_QA_ADMIN_ACCESS_TOKEN', 'token')
stub_const('QA::Runtime::Scenario', attributes)
allow(Allure).to receive(:add_attachment) allow(Allure).to receive(:add_attachment)
allow(File).to receive(:open).with(png_path) { png_file } allow(File).to receive(:open).with(png_path) { png_file }
allow(File).to receive(:open).with(html_path) { html_file } allow(File).to receive(:open).with(html_path) { html_file }
allow(RestClient::Request).to receive(:execute) { double('response', code: 200, body: versions.to_json) } allow(RestClient::Request).to receive(:execute) { version_response }
allow(QA::Runtime::Scenario).to receive(:method_missing).with(:gitlab_address).and_return('gitlab.com') allow(attributes).to receive(:gitlab_address).and_return("https://gitlab.com")
allow(Capybara).to receive(:current_session).and_return(session) allow(Capybara).to receive(:current_session).and_return(session)
allow(session).to receive_message_chain('driver.browser.logs.get').and_return(browser_log) allow(session).to receive_message_chain('driver.browser.logs.get').and_return(browser_log)
@ -66,7 +71,7 @@ describe QA::Runtime::AllureReport do
described_class.configure! described_class.configure!
end end
it 'configures Allure options', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/357816' do it 'configures Allure options' do
aggregate_failures do aggregate_failures do
expect(allure_config.results_directory).to eq('tmp/allure-results') expect(allure_config.results_directory).to eq('tmp/allure-results')
expect(allure_config.clean_results_directory).to eq(true) expect(allure_config.clean_results_directory).to eq(true)

View File

@ -2,10 +2,10 @@
module QA module QA
RSpec.shared_examples 'a QA scenario class' do RSpec.shared_examples 'a QA scenario class' do
let(:attributes) { spy('Runtime::Scenario') } let(:attributes) { class_spy('Runtime::Scenario') }
let(:runner) { spy('Specs::Runner') } let(:runner) { class_spy('Specs::Runner') }
let(:release) { spy('Runtime::Release') } let(:release) { class_spy('Runtime::Release') }
let(:feature) { spy('Runtime::Feature') } let(:feature) { class_spy('Runtime::Feature') }
let(:args) { { gitlab_address: 'http://gitlab_address' } } let(:args) { { gitlab_address: 'http://gitlab_address' } }
let(:named_options) { %w[--address http://gitlab_address] } let(:named_options) { %w[--address http://gitlab_address] }
@ -45,7 +45,7 @@ module QA
expect(runner).to have_received(:tags=).with(tags) expect(runner).to have_received(:tags=).with(tags)
end end
context 'specifying RSpec options' do context 'with RSpec options' do
it 'sets options on runner' do it 'sets options on runner' do
subject.perform(args, *options) subject.perform(args, *options)

View File

@ -0,0 +1,5 @@
# frozen_string_literal: true
require_relative '../../qa'
require_relative 'scenario_shared_examples'

View File

@ -26,6 +26,7 @@ describe QA::Support::Formatters::TestStatsFormatter do
let(:ui_fabrication) { 0 } let(:ui_fabrication) { 0 }
let(:api_fabrication) { 0 } let(:api_fabrication) { 0 }
let(:fabrication_resources) { {} } let(:fabrication_resources) { {} }
let(:testcase) { 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/1234' }
let(:influx_client_args) do let(:influx_client_args) do
{ {
@ -51,7 +52,7 @@ describe QA::Support::Formatters::TestStatsFormatter do
merge_request: 'false', merge_request: 'false',
run_type: run_type, run_type: run_type,
stage: stage.match(%r{\d{1,2}_(\w+)}).captures.first, stage: stage.match(%r{\d{1,2}_(\w+)}).captures.first,
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/1234' testcase: testcase
}, },
fields: { fields: {
id: './spec/support/formatters/test_stats_formatter_spec.rb[1:1]', id: './spec/support/formatters/test_stats_formatter_spec.rb[1:1]',
@ -80,12 +81,6 @@ describe QA::Support::Formatters::TestStatsFormatter do
around do |example| around do |example|
RSpec::Core::Sandbox.sandboxed do |config| RSpec::Core::Sandbox.sandboxed do |config|
config.formatter = QA::Support::Formatters::TestStatsFormatter config.formatter = QA::Support::Formatters::TestStatsFormatter
config.append_after do |example|
example.metadata[:api_fabrication] = Thread.current[:api_fabrication]
example.metadata[:browser_ui_fabrication] = Thread.current[:browser_ui_fabrication]
end
config.before(:context) { RSpec.current_example = nil } config.before(:context) { RSpec.current_example = nil }
example.run example.run
@ -226,16 +221,18 @@ describe QA::Support::Formatters::TestStatsFormatter do
end end
context 'with fabrication runtimes' do context 'with fabrication runtimes' do
let(:ui_fabrication) { 10 }
let(:api_fabrication) { 4 } let(:api_fabrication) { 4 }
let(:ui_fabrication) { 10 }
before do let(:testcase) { nil }
Thread.current[:api_fabrication] = api_fabrication
Thread.current[:browser_ui_fabrication] = ui_fabrication
end
it 'exports data to influxdb with fabrication times' do it 'exports data to influxdb with fabrication times' do
run_spec run_spec do
# Main logic tracks fabrication time in thread local variable and injects it as metadata from
# global after hook defined in main spec_helper.
#
# Inject the values directly since we do not load e2e test spec_helper in unit tests
it('spec', api_fabrication: 4, browser_ui_fabrication: 10) {}
end
expect(influx_write_api).to have_received(:write).once expect(influx_write_api).to have_received(:write).once
expect(influx_write_api).to have_received(:write).with(data: [data]) expect(influx_write_api).to have_received(:write).with(data: [data])

View File

@ -5,37 +5,38 @@ RSpec.describe QA::Support::WaitForRequests do
before do before do
allow(subject).to receive(:finished_all_ajax_requests?).and_return(true) allow(subject).to receive(:finished_all_ajax_requests?).and_return(true)
allow(subject).to receive(:finished_loading?).and_return(true) allow(subject).to receive(:finished_loading?).and_return(true)
allow(QA::Support::PageErrorChecker).to receive(:check_page_for_error_code)
end end
context 'when skip_finished_loading_check is defaulted to false' do context 'when skip_finished_loading_check is defaulted to false' do
it 'calls finished_loading?' do it 'calls finished_loading?' do
expect(subject).to receive(:finished_loading?).with(hash_including(wait: 1))
subject.wait_for_requests subject.wait_for_requests
expect(subject).to have_received(:finished_loading?).with(hash_including(wait: 1))
end end
end end
context 'when skip_finished_loading_check is true' do context 'when skip_finished_loading_check is true' do
it 'does not call finished_loading?' do it 'does not call finished_loading?' do
expect(subject).not_to receive(:finished_loading?)
subject.wait_for_requests(skip_finished_loading_check: true) subject.wait_for_requests(skip_finished_loading_check: true)
expect(subject).not_to have_received(:finished_loading?)
end end
end end
context 'when skip_resp_code_check is defaulted to false' do context 'when skip_resp_code_check is defaulted to false' do
it 'call report' do it 'call report' do
allow(QA::Support::PageErrorChecker).to receive(:check_page_for_error_code).with(Capybara.page)
subject.wait_for_requests subject.wait_for_requests
expect(QA::Support::PageErrorChecker).to have_received(:check_page_for_error_code).with(Capybara.page)
end end
end end
context 'when skip_resp_code_check is true' do context 'when skip_resp_code_check is true' do
it 'does not parse for an error code' do it 'does not parse for an error code' do
expect(QA::Support::PageErrorChecker).not_to receive(:check_page_for_error_code)
subject.wait_for_requests(skip_resp_code_check: true) subject.wait_for_requests(skip_resp_code_check: true)
expect(QA::Support::PageErrorChecker).not_to have_received(:check_page_for_error_code)
end end
end end
end end

View File

@ -107,14 +107,26 @@ RSpec.describe Profiles::TwoFactorAuthsController do
expect(assigns[:qr_code]).to eq(code) expect(assigns[:qr_code]).to eq(code)
end end
it 'generates a unique otp_secret every time the page is loaded' do it 'generates a single otp_secret with multiple page loads', :freeze_time do
expect(User).to receive(:generate_otp_secret).with(32).and_call_original.twice expect(User).to receive(:generate_otp_secret).with(32).and_call_original.once
user.update!(otp_secret: nil, otp_secret_expires_at: nil)
2.times do 2.times do
get :show get :show
end end
end end
it 'generates a new otp_secret once the ttl has expired' do
expect(User).to receive(:generate_otp_secret).with(32).and_call_original.once
user.update!(otp_secret: "FT7KAVNU63YZH7PBRVPVL7CPSAENXY25", otp_secret_expires_at: 2.minutes.from_now)
travel_to(10.minutes.from_now) do
get :show
end
end
it_behaves_like 'user must first verify their primary email address' do it_behaves_like 'user must first verify their primary email address' do
let(:go) { get :show } let(:go) { get :show }
end end

View File

@ -254,38 +254,54 @@ RSpec.describe Projects::EnvironmentsController do
end end
describe 'PATCH #stop' do describe 'PATCH #stop' do
subject { patch :stop, params: environment_params(format: :json) }
context 'when env not available' do context 'when env not available' do
it 'returns 404' do it 'returns 404' do
allow_any_instance_of(Environment).to receive(:available?) { false } allow_any_instance_of(Environment).to receive(:available?) { false }
patch :stop, params: environment_params(format: :json) subject
expect(response).to have_gitlab_http_status(:not_found) expect(response).to have_gitlab_http_status(:not_found)
end end
end end
context 'when stop action' do context 'when stop action' do
it 'returns action url' do it 'returns action url for single stop action' do
action = create(:ci_build, :manual) action = create(:ci_build, :manual)
allow_any_instance_of(Environment) allow_any_instance_of(Environment)
.to receive_messages(available?: true, stop_with_action!: action) .to receive_messages(available?: true, stop_with_actions!: [action])
patch :stop, params: environment_params(format: :json) subject
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to eq( expect(json_response).to eq(
{ 'redirect_url' => { 'redirect_url' =>
project_job_url(project, action) }) project_job_url(project, action) })
end end
it 'returns environment url for multiple stop actions' do
actions = create_list(:ci_build, 2, :manual)
allow_any_instance_of(Environment)
.to receive_messages(available?: true, stop_with_actions!: actions)
subject
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to eq(
{ 'redirect_url' =>
project_environment_url(project, environment) })
end
end end
context 'when no stop action' do context 'when no stop action' do
it 'returns env url' do it 'returns env url' do
allow_any_instance_of(Environment) allow_any_instance_of(Environment)
.to receive_messages(available?: true, stop_with_action!: nil) .to receive_messages(available?: true, stop_with_actions!: nil)
patch :stop, params: environment_params(format: :json) subject
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to eq( expect(json_response).to eq(

View File

@ -189,6 +189,20 @@ FactoryBot.define do
set_expanded_environment_name set_expanded_environment_name
end end
trait :start_staging do
name { 'start staging' }
environment { 'staging' }
options do
{
script: %w(ls),
environment: { name: 'staging', action: 'start' }
}
end
set_expanded_environment_name
end
trait :stop_staging do trait :stop_staging do
name { 'stop staging' } name { 'stop staging' }
environment { 'staging' } environment { 'staging' }

View File

@ -1,27 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration::BackfillIncidentIssueEscalationStatuses, schema: 20211214012507 do
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
let(:issues) { table(:issues) }
let(:issuable_escalation_statuses) { table(:incident_management_issuable_escalation_statuses) }
subject(:migration) { described_class.new }
it 'correctly backfills issuable escalation status records' do
namespace = namespaces.create!(name: 'foo', path: 'foo')
project = projects.create!(namespace_id: namespace.id)
issues.create!(project_id: project.id, title: 'issue 1', issue_type: 0) # non-incident issue
issues.create!(project_id: project.id, title: 'incident 1', issue_type: 1)
issues.create!(project_id: project.id, title: 'incident 2', issue_type: 1)
incident_issue_existing_status = issues.create!(project_id: project.id, title: 'incident 3', issue_type: 1)
issuable_escalation_statuses.create!(issue_id: incident_issue_existing_status.id)
migration.perform(1, incident_issue_existing_status.id)
expect(issuable_escalation_statuses.count).to eq(3)
end
end

View File

@ -10,27 +10,10 @@ RSpec.describe BackfillIncidentIssueEscalationStatuses do
let(:namespace) { namespaces.create!(name: 'foo', path: 'foo') } let(:namespace) { namespaces.create!(name: 'foo', path: 'foo') }
let(:project) { projects.create!(namespace_id: namespace.id) } let(:project) { projects.create!(namespace_id: namespace.id) }
before do # Backfill removed - see db/migrate/20220321234317_remove_all_issuable_escalation_statuses.rb.
stub_const("#{described_class.name}::BATCH_SIZE", 1) it 'does nothing' do
end issues.create!(project_id: project.id, issue_type: 1)
it 'schedules jobs for incident issues' do expect { migrate! }.not_to change { BackgroundMigrationWorker.jobs.size }
issue_1 = issues.create!(project_id: project.id) # non-incident issue
incident_1 = issues.create!(project_id: project.id, issue_type: 1)
incident_2 = issues.create!(project_id: project.id, issue_type: 1)
Sidekiq::Testing.fake! do
freeze_time do
migrate!
expect(described_class::MIGRATION).to be_scheduled_delayed_migration(
2.minutes, issue_1.id, issue_1.id)
expect(described_class::MIGRATION).to be_scheduled_delayed_migration(
4.minutes, incident_1.id, incident_1.id)
expect(described_class::MIGRATION).to be_scheduled_delayed_migration(
6.minutes, incident_2.id, incident_2.id)
expect(BackgroundMigrationWorker.jobs.size).to eq(3)
end
end
end end
end end

View File

@ -0,0 +1,20 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!
RSpec.describe RemoveAllIssuableEscalationStatuses do
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
let(:issues) { table(:issues) }
let(:statuses) { table(:incident_management_issuable_escalation_statuses) }
let(:namespace) { namespaces.create!(name: 'foo', path: 'foo') }
let(:project) { projects.create!(namespace_id: namespace.id) }
it 'removes all escalation status records' do
issue = issues.create!(project_id: project.id, issue_type: 1)
statuses.create!(issue_id: issue.id)
expect { migrate! }.to change(statuses, :count).from(1).to(0)
end
end

View File

@ -0,0 +1,29 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!
RSpec.describe AddEpicsRelativePosition, :migration do
let(:groups) { table(:namespaces) }
let(:epics) { table(:epics) }
let(:users) { table(:users) }
let(:user) { users.create!(name: 'user', email: 'email@example.org', projects_limit: 100) }
let(:group) { groups.create!(name: 'gitlab', path: 'gitlab-org', type: 'Group') }
let!(:epic1) { epics.create!(title: 'epic 1', title_html: 'epic 1', author_id: user.id, group_id: group.id, iid: 1) }
let!(:epic2) { epics.create!(title: 'epic 2', title_html: 'epic 2', author_id: user.id, group_id: group.id, iid: 2) }
let!(:epic3) { epics.create!(title: 'epic 3', title_html: 'epic 3', author_id: user.id, group_id: group.id, iid: 3) }
it 'does nothing if epics table contains relative_position' do
expect { migrate! }.not_to change { epics.pluck(:relative_position) }
end
it 'adds relative_position if missing and backfills it with ID value', :aggregate_failures do
ActiveRecord::Base.connection.execute('ALTER TABLE epics DROP relative_position')
migrate!
expect(epics.pluck(:relative_position)).to match_array([epic1.id * 500, epic2.id * 500, epic3.id * 500])
end
end

View File

@ -23,7 +23,6 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do
it { is_expected.to have_one(:upcoming_deployment) } it { is_expected.to have_one(:upcoming_deployment) }
it { is_expected.to have_one(:latest_opened_most_severe_alert) } it { is_expected.to have_one(:latest_opened_most_severe_alert) }
it { is_expected.to delegate_method(:stop_action).to(:last_deployment) }
it { is_expected.to delegate_method(:manual_actions).to(:last_deployment) } it { is_expected.to delegate_method(:manual_actions).to(:last_deployment) }
it { is_expected.to validate_presence_of(:name) } it { is_expected.to validate_presence_of(:name) }
@ -349,15 +348,28 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do
end end
describe '.with_deployment' do describe '.with_deployment' do
subject { described_class.with_deployment(sha) } subject { described_class.with_deployment(sha, status: status) }
let(:environment) { create(:environment, project: project) } let(:environment) { create(:environment, project: project) }
let(:sha) { 'b83d6e391c22777fca1ed3012fce84f633d7fed0' } let(:sha) { 'b83d6e391c22777fca1ed3012fce84f633d7fed0' }
let(:status) { nil }
context 'when deployment has the specified sha' do context 'when deployment has the specified sha' do
let!(:deployment) { create(:deployment, environment: environment, sha: sha) } let!(:deployment) { create(:deployment, environment: environment, sha: sha) }
it { is_expected.to eq([environment]) } it { is_expected.to eq([environment]) }
context 'with success status filter' do
let(:status) { :success }
it { is_expected.to be_empty }
end
context 'with created status filter' do
let(:status) { :created }
it { is_expected.to contain_exactly(environment) }
end
end end
context 'when deployment does not have the specified sha' do context 'when deployment does not have the specified sha' do
@ -459,8 +471,8 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do
end end
end end
describe '#stop_action_available?' do describe '#stop_actions_available?' do
subject { environment.stop_action_available? } subject { environment.stop_actions_available? }
context 'when no other actions' do context 'when no other actions' do
it { is_expected.to be_falsey } it { is_expected.to be_falsey }
@ -499,10 +511,10 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do
end end
end end
describe '#stop_with_action!' do describe '#stop_with_actions!' do
let(:user) { create(:user) } let(:user) { create(:user) }
subject { environment.stop_with_action!(user) } subject { environment.stop_with_actions!(user) }
before do before do
expect(environment).to receive(:available?).and_call_original expect(environment).to receive(:available?).and_call_original
@ -515,9 +527,10 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do
end end
it do it do
subject actions = subject
expect(environment).to be_stopped expect(environment).to be_stopped
expect(actions).to match_array([])
end end
end end
@ -536,18 +549,18 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do
context 'when matching action is defined' do context 'when matching action is defined' do
let(:pipeline) { create(:ci_pipeline, project: project) } let(:pipeline) { create(:ci_pipeline, project: project) }
let(:build) { create(:ci_build, pipeline: pipeline) } let(:build_a) { create(:ci_build, pipeline: pipeline) }
let!(:deployment) do before do
create(:deployment, :success, create(:deployment, :success,
environment: environment, environment: environment,
deployable: build, deployable: build_a,
on_stop: 'close_app') on_stop: 'close_app_a')
end end
context 'when user is not allowed to stop environment' do context 'when user is not allowed to stop environment' do
let!(:close_action) do let!(:close_action) do
create(:ci_build, :manual, pipeline: pipeline, name: 'close_app') create(:ci_build, :manual, pipeline: pipeline, name: 'close_app_a')
end end
it 'raises an exception' do it 'raises an exception' do
@ -565,36 +578,39 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do
context 'when action did not yet finish' do context 'when action did not yet finish' do
let!(:close_action) do let!(:close_action) do
create(:ci_build, :manual, pipeline: pipeline, name: 'close_app') create(:ci_build, :manual, pipeline: pipeline, name: 'close_app_a')
end end
it 'returns the same action' do it 'returns the same action' do
expect(subject).to eq(close_action) action = subject.first
expect(subject.user).to eq(user) expect(action).to eq(close_action)
expect(action.user).to eq(user)
end end
end end
context 'if action did finish' do context 'if action did finish' do
let!(:close_action) do let!(:close_action) do
create(:ci_build, :manual, :success, create(:ci_build, :manual, :success,
pipeline: pipeline, name: 'close_app') pipeline: pipeline, name: 'close_app_a')
end end
it 'returns a new action of the same type' do it 'returns a new action of the same type' do
expect(subject).to be_persisted action = subject.first
expect(subject.name).to eq(close_action.name)
expect(subject.user).to eq(user) expect(action).to be_persisted
expect(action.name).to eq(close_action.name)
expect(action.user).to eq(user)
end end
end end
context 'close action does not raise ActiveRecord::StaleObjectError' do context 'close action does not raise ActiveRecord::StaleObjectError' do
let!(:close_action) do let!(:close_action) do
create(:ci_build, :manual, pipeline: pipeline, name: 'close_app') create(:ci_build, :manual, pipeline: pipeline, name: 'close_app_a')
end end
before do before do
# preload the build # preload the build
environment.stop_action environment.stop_actions
# Update record as the other process. This makes `environment.stop_action` stale. # Update record as the other process. This makes `environment.stop_action` stale.
close_action.drop! close_action.drop!
@ -613,6 +629,147 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do
end end
end end
end end
context 'when there are more then one stop action for the environment' do
let(:pipeline) { create(:ci_pipeline, project: project) }
let(:build_a) { create(:ci_build, pipeline: pipeline) }
let(:build_b) { create(:ci_build, pipeline: pipeline) }
let!(:close_actions) do
[
create(:ci_build, :manual, pipeline: pipeline, name: 'close_app_a'),
create(:ci_build, :manual, pipeline: pipeline, name: 'close_app_b')
]
end
before do
project.add_developer(user)
create(:deployment, :success,
environment: environment,
deployable: build_a,
finished_at: 5.minutes.ago,
on_stop: 'close_app_a')
create(:deployment, :success,
environment: environment,
deployable: build_b,
finished_at: 1.second.ago,
on_stop: 'close_app_b')
end
it 'returns the same actions' do
actions = subject
expect(actions.count).to eq(close_actions.count)
expect(actions.pluck(:id)).to match_array(close_actions.pluck(:id))
expect(actions.pluck(:user)).to match_array(close_actions.pluck(:user))
end
context 'when there are failed deployment jobs' do
before do
create(:ci_build, pipeline: pipeline, name: 'close_app_c')
create(:deployment, :failed,
environment: environment,
deployable: create(:ci_build, pipeline: pipeline),
on_stop: 'close_app_c')
end
it 'returns only stop actions from successful deployment jobs' do
actions = subject
expect(actions).to match_array(close_actions)
expect(actions.count).to eq(environment.successful_deployments.count)
end
end
context 'when the feature is disabled' do
before do
stub_feature_flags(environment_multiple_stop_actions: false)
end
it 'returns the last deployment job stop action' do
stop_actions = subject
expect(stop_actions.first).to eq(close_actions[1])
expect(stop_actions.count).to eq(1)
end
end
end
end
describe '#stop_actions' do
subject { environment.stop_actions }
context 'when there are no deployments and builds' do
it 'returns empty array' do
is_expected.to match_array([])
end
end
context 'when there are multiple deployments with actions' do
let(:pipeline) { create(:ci_pipeline, project: project) }
let(:ci_build_a) { create(:ci_build, project: project, pipeline: pipeline) }
let(:ci_build_b) { create(:ci_build, project: project, pipeline: pipeline) }
let!(:ci_build_c) { create(:ci_build, :manual, project: project, pipeline: pipeline, name: 'close_app_a') }
let!(:ci_build_d) { create(:ci_build, :manual, project: project, pipeline: pipeline, name: 'close_app_b') }
let!(:deployment_a) do
create(:deployment,
:success, project: project, environment: environment, deployable: ci_build_a, on_stop: 'close_app_a')
end
let!(:deployment_b) do
create(:deployment,
:success, project: project, environment: environment, deployable: ci_build_b, on_stop: 'close_app_b')
end
before do
# Create failed deployment without stop_action.
build = create(:ci_build, project: project, pipeline: pipeline)
create(:deployment, :failed, project: project, environment: environment, deployable: build)
end
it 'returns only the stop actions' do
expect(subject.pluck(:id)).to contain_exactly(ci_build_c.id, ci_build_d.id)
end
end
end
describe '#last_deployment_group' do
subject { environment.last_deployment_group }
context 'when there are no deployments and builds' do
it do
is_expected.to eq(Deployment.none)
end
end
context 'when there are deployments for multiple pipelines' do
let(:pipeline_a) { create(:ci_pipeline, project: project) }
let(:pipeline_b) { create(:ci_pipeline, project: project) }
let(:ci_build_a) { create(:ci_build, project: project, pipeline: pipeline_a) }
let(:ci_build_b) { create(:ci_build, project: project, pipeline: pipeline_b) }
let(:ci_build_c) { create(:ci_build, project: project, pipeline: pipeline_a) }
let(:ci_build_d) { create(:ci_build, project: project, pipeline: pipeline_a) }
# Successful deployments for pipeline_a
let!(:deployment_a) { create(:deployment, :success, project: project, environment: environment, deployable: ci_build_a) }
let!(:deployment_b) { create(:deployment, :success, project: project, environment: environment, deployable: ci_build_c) }
before do
# Failed deployment for pipeline_a
create(:deployment, :failed, project: project, environment: environment, deployable: ci_build_d)
# Failed deployment for pipeline_b
create(:deployment, :failed, project: project, environment: environment, deployable: ci_build_b)
end
it 'returns the successful deployment jobs for the last deployment pipeline' do
expect(subject.pluck(:id)).to contain_exactly(deployment_a.id, deployment_b.id)
end
end
end end
describe 'recently_updated_on_branch?' do describe 'recently_updated_on_branch?' do
@ -772,6 +929,26 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do
end end
end end
describe '#last_deployment_pipeline' do
subject { environment.last_deployment_pipeline }
let(:pipeline_a) { create(:ci_pipeline, project: project) }
let(:pipeline_b) { create(:ci_pipeline, project: project) }
let(:ci_build_a) { create(:ci_build, project: project, pipeline: pipeline_a) }
let(:ci_build_b) { create(:ci_build, project: project, pipeline: pipeline_b) }
before do
create(:deployment, :success, project: project, environment: environment, deployable: ci_build_a)
create(:deployment, :failed, project: project, environment: environment, deployable: ci_build_b)
end
it 'does not join across databases' do
with_cross_joins_prevented do
expect(subject.id).to eq(pipeline_a.id)
end
end
end
describe '#last_visible_deployment' do describe '#last_visible_deployment' do
subject { environment.last_visible_deployment } subject { environment.last_visible_deployment }

View File

@ -2089,6 +2089,74 @@ RSpec.describe User do
end end
end end
describe 'needs_new_otp_secret?', :freeze_time do
let(:user) { create(:user) }
context 'when two-factor is not enabled' do
it 'returns true if otp_secret_expires_at is nil' do
expect(user.needs_new_otp_secret?).to eq(true)
end
it 'returns true if the otp_secret_expires_at has passed' do
user.update!(otp_secret_expires_at: 10.minutes.ago)
expect(user.reload.needs_new_otp_secret?).to eq(true)
end
it 'returns false if the otp_secret_expires_at has not passed' do
user.update!(otp_secret_expires_at: 10.minutes.from_now)
expect(user.reload.needs_new_otp_secret?).to eq(false)
end
end
context 'when two-factor is enabled' do
let(:user) { create(:user, :two_factor) }
it 'returns false even if ttl is expired' do
user.otp_secret_expires_at = 10.minutes.ago
expect(user.needs_new_otp_secret?).to eq(false)
end
end
end
describe 'otp_secret_expired?', :freeze_time do
let(:user) { create(:user) }
it 'returns true if otp_secret_expires_at is nil' do
expect(user.otp_secret_expired?).to eq(true)
end
it 'returns true if the otp_secret_expires_at has passed' do
user.otp_secret_expires_at = 10.minutes.ago
expect(user.otp_secret_expired?).to eq(true)
end
it 'returns false if the otp_secret_expires_at has not passed' do
user.otp_secret_expires_at = 20.minutes.from_now
expect(user.otp_secret_expired?).to eq(false)
end
end
describe 'update_otp_secret!', :freeze_time do
let(:user) { create(:user) }
before do
user.update_otp_secret!
end
it 'sets the otp_secret' do
expect(user.otp_secret).to have_attributes(length: described_class::OTP_SECRET_LENGTH)
end
it 'updates the otp_secret_expires_at' do
expect(user.otp_secret_expires_at).to eq(Time.current + described_class::OTP_SECRET_TTL)
end
end
describe 'projects' do describe 'projects' do
before do before do
@user = create(:user) @user = create(:user)

View File

@ -202,6 +202,7 @@ RSpec.describe Environments::StopService do
context 'with environment related jobs ' do context 'with environment related jobs ' do
let!(:environment) { create(:environment, :available, name: 'staging', project: project) } let!(:environment) { create(:environment, :available, name: 'staging', project: project) }
let!(:prepare_staging_job) { create(:ci_build, :prepare_staging, pipeline: pipeline, project: project) } let!(:prepare_staging_job) { create(:ci_build, :prepare_staging, pipeline: pipeline, project: project) }
let!(:start_staging_job) { create(:ci_build, :start_staging, :with_deployment, :manual, pipeline: pipeline, project: project) }
let!(:stop_staging_job) { create(:ci_build, :stop_staging, :manual, pipeline: pipeline, project: project) } let!(:stop_staging_job) { create(:ci_build, :stop_staging, :manual, pipeline: pipeline, project: project) }
it 'does not stop environments that was not started by the merge request' do it 'does not stop environments that was not started by the merge request' do

View File

@ -8,7 +8,11 @@ RSpec.shared_examples 'avoid N+1 on environments serialization' do |ee: false|
create_environment_with_associations(project) create_environment_with_associations(project)
create_environment_with_associations(project) create_environment_with_associations(project)
expect { serialize(grouping: true) }.not_to exceed_query_limit(control.count) # Fix N+1 queries introduced by multi stop_actions for environment.
# Tracked in https://gitlab.com/gitlab-org/gitlab/-/issues/358780
relax_count = 14
expect { serialize(grouping: true) }.not_to exceed_query_limit(control.count + relax_count)
end end
it 'avoids N+1 database queries without grouping', :request_store do it 'avoids N+1 database queries without grouping', :request_store do
@ -19,7 +23,11 @@ RSpec.shared_examples 'avoid N+1 on environments serialization' do |ee: false|
create_environment_with_associations(project) create_environment_with_associations(project)
create_environment_with_associations(project) create_environment_with_associations(project)
expect { serialize(grouping: false) }.not_to exceed_query_limit(control.count) # Fix N+1 queries introduced by multi stop_actions for environment.
# Tracked in https://gitlab.com/gitlab-org/gitlab/-/issues/358780
relax_count = 14
expect { serialize(grouping: false) }.not_to exceed_query_limit(control.count + relax_count)
end end
it 'does not preload for environments that does not exist in the page', :request_store do it 'does not preload for environments that does not exist in the page', :request_store do

View File

@ -1,46 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'shared/_global_alert.html.haml' do
before do
allow(view).to receive(:sprite_icon).and_return('<span class="icon"></span>'.html_safe)
end
it 'renders the title' do
title = "The alert's title"
render partial: 'shared/global_alert', locals: { title: title }
expect(rendered).to have_text(title)
end
context 'variants' do
it 'renders an info alert by default' do
render
expect(rendered).to have_selector(".gl-alert-info")
end
%w[warning success danger tip].each do |variant|
it "renders a #{variant} variant" do
allow(view).to receive(:variant).and_return(variant)
render partial: 'shared/global_alert', locals: { variant: variant }
expect(rendered).to have_selector(".gl-alert-#{variant}")
end
end
end
context 'dismissible option' do
it 'shows the dismiss button by default' do
render
expect(rendered).to have_selector('.gl-dismiss-btn')
end
it 'does not show the dismiss button when dismissible is false' do
render partial: 'shared/global_alert', locals: { dismissible: false }
expect(rendered).not_to have_selector('.gl-dismiss-btn')
end
end
end