Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
7046de6ada
commit
8952851661
|
@ -18,7 +18,7 @@ qa:internal:
|
|||
- .qa-job-base
|
||||
- .qa:rules:internal
|
||||
script:
|
||||
- bundle exec rspec
|
||||
- bundle exec rspec -O .rspec_internal
|
||||
|
||||
qa:internal-as-if-foss:
|
||||
extends:
|
||||
|
|
|
@ -4,6 +4,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
|
|||
skip_before_action :check_two_factor_requirement
|
||||
before_action :ensure_verified_primary_email, only: [:show, :create]
|
||||
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?
|
||||
|
||||
|
@ -14,16 +15,6 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
|
|||
feature_category :authentication_and_authorization
|
||||
|
||||
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?
|
||||
two_factor_authentication_reason(
|
||||
global: lambda do
|
||||
|
@ -139,6 +130,18 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
|
|||
|
||||
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
|
||||
return if current_user.valid_password?(params[:current_password])
|
||||
|
||||
|
|
|
@ -104,11 +104,11 @@ class Projects::EnvironmentsController < Projects::ApplicationController
|
|||
def stop
|
||||
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 =
|
||||
if stop_action
|
||||
polymorphic_url([project, stop_action])
|
||||
if stop_actions&.count == 1
|
||||
polymorphic_url([project, stop_actions.first])
|
||||
else
|
||||
project_environment_url(project, @environment)
|
||||
end
|
||||
|
|
|
@ -959,7 +959,7 @@ module Ci
|
|||
Ci::Build.latest.where(pipeline: self_and_descendants)
|
||||
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.
|
||||
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/340781#note_699114700
|
||||
expanded_environment_names =
|
||||
|
@ -969,7 +969,7 @@ module Ci
|
|||
.limit(100)
|
||||
.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
|
||||
|
||||
# With multi-project and parent-child pipelines
|
||||
|
|
|
@ -59,7 +59,7 @@ class Environment < ApplicationRecord
|
|||
allow_nil: 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
|
||||
|
||||
scope :available, -> { with_state(:available) }
|
||||
|
@ -89,13 +89,19 @@ class Environment < ApplicationRecord
|
|||
|
||||
scope :for_project, -> (project) { where(project_id: project) }
|
||||
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 :with_rank, -> do
|
||||
select('environments.*, rank() OVER (PARTITION BY project_id ORDER BY id DESC)')
|
||||
end
|
||||
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
|
||||
stopped
|
||||
.in_review_folder
|
||||
|
@ -185,6 +191,23 @@ class Environment < ApplicationRecord
|
|||
last_deployment&.deployable
|
||||
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
|
||||
# It helps to avoid cross joins with the CI database.
|
||||
# Caveat: It also overrides and losses the default AR caching mechanism.
|
||||
|
@ -255,8 +278,8 @@ class Environment < ApplicationRecord
|
|||
external_url.gsub(%r{\A.*?://}, '')
|
||||
end
|
||||
|
||||
def stop_action_available?
|
||||
available? && stop_action.present?
|
||||
def stop_actions_available?
|
||||
available? && stop_actions.present?
|
||||
end
|
||||
|
||||
def cancel_deployment_jobs!
|
||||
|
@ -269,18 +292,34 @@ class Environment < ApplicationRecord
|
|||
end
|
||||
end
|
||||
|
||||
def stop_with_action!(current_user)
|
||||
def stop_with_actions!(current_user)
|
||||
return unless available?
|
||||
|
||||
stop!
|
||||
|
||||
return unless stop_action
|
||||
actions = []
|
||||
|
||||
stop_actions.each do |stop_action|
|
||||
Gitlab::OptimisticLocking.retry_lock(
|
||||
stop_action,
|
||||
name: 'environment_stop_with_action'
|
||||
name: 'environment_stop_with_actions'
|
||||
) do |build|
|
||||
build&.play(current_user)
|
||||
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
|
||||
|
||||
|
|
|
@ -1456,9 +1456,9 @@ class MergeRequest < ApplicationRecord
|
|||
Environment.where(project: project, name: environments)
|
||||
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)
|
||||
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
|
||||
legacy_environments
|
||||
end
|
||||
|
|
|
@ -37,6 +37,9 @@ class User < ApplicationRecord
|
|||
|
||||
COUNT_CACHE_VALIDITY_PERIOD = 24.hours
|
||||
|
||||
OTP_SECRET_LENGTH = 32
|
||||
OTP_SECRET_TTL = 2.minutes
|
||||
|
||||
MAX_USERNAME_LENGTH = 255
|
||||
MIN_USERNAME_LENGTH = 2
|
||||
|
||||
|
@ -954,6 +957,21 @@ class User < ApplicationRecord
|
|||
(webauthn_registrations.loaded? && webauthn_registrations.any?) || (!webauthn_registrations.loaded? && webauthn_registrations.exists?)
|
||||
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
|
||||
if namespace&.any_project_has_container_registry_tags?
|
||||
errors.add(:username, _('cannot be changed if a personal project has container registry tags.'))
|
||||
|
|
|
@ -4,12 +4,12 @@ class EnvironmentPolicy < BasePolicy
|
|||
delegate { @subject.project }
|
||||
|
||||
condition(:stop_with_deployment_allowed) do
|
||||
@subject.stop_action_available? &&
|
||||
can?(:create_deployment) && can?(:update_build, @subject.stop_action)
|
||||
@subject.stop_actions_available? &&
|
||||
can?(:create_deployment) && can?(:update_build, @subject.stop_actions.last)
|
||||
end
|
||||
|
||||
condition(:stop_with_update_allowed) do
|
||||
!@subject.stop_action_available? && can?(:update_environment, @subject)
|
||||
!@subject.stop_actions_available? && can?(:update_environment, @subject)
|
||||
end
|
||||
|
||||
condition(:stopped) do
|
||||
|
|
|
@ -18,7 +18,7 @@ class EnvironmentEntity < Grape::Entity
|
|||
expose :environment_type
|
||||
expose :name_without_type
|
||||
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 :tier
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ module Environments
|
|||
def execute(environment)
|
||||
return unless can?(current_user, :stop_environment, environment)
|
||||
|
||||
environment.stop_with_action!(current_user)
|
||||
environment.stop_with_actions!(current_user)
|
||||
end
|
||||
|
||||
def execute_for_branch(branch_name)
|
||||
|
@ -19,7 +19,9 @@ module Environments
|
|||
end
|
||||
|
||||
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
|
||||
|
||||
private
|
||||
|
|
|
@ -16,10 +16,9 @@
|
|||
.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) } }
|
||||
- else
|
||||
= render 'shared/global_alert',
|
||||
variant: :warning,
|
||||
= render Pajamas::AlertComponent.new(variant: :warning,
|
||||
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
|
||||
- enable_service_ping_link_url = help_page_path('user/admin_area/settings/usage_statistics', anchor: 'enable-or-disable-usage-statistics')
|
||||
|
|
|
@ -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
|
||||
- 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' }
|
||||
|
|
|
@ -6,9 +6,8 @@
|
|||
.gl-border-l-solid.gl-border-r-solid.gl-border-gray-100.gl-border-1.gl-p-5
|
||||
%h4
|
||||
= _('Import group from file')
|
||||
= render 'shared/global_alert',
|
||||
variant: :warning,
|
||||
dismissible: false do
|
||||
= render Pajamas::AlertComponent.new(variant: :warning,
|
||||
dismissible: false) do
|
||||
.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') }
|
||||
- link_end = '</a>'.html_safe
|
||||
|
|
|
@ -3,8 +3,7 @@
|
|||
|
||||
%div
|
||||
- if @user.errors.any?
|
||||
= render 'shared/global_alert',
|
||||
variant: :danger do
|
||||
= render Pajamas::AlertComponent.new(variant: :danger) do
|
||||
.gl-alert-body
|
||||
%ul
|
||||
- @user.errors.full_messages.each do |msg|
|
||||
|
|
|
@ -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
|
|
@ -10,8 +10,10 @@ module Environments
|
|||
|
||||
def perform(environment_id, params = {})
|
||||
Environment.find_by_id(environment_id).try do |environment|
|
||||
user = environment.stop_action&.user
|
||||
environment.stop_with_action!(user)
|
||||
stop_actions = environment.stop_actions
|
||||
|
||||
user = stop_actions.last&.user
|
||||
environment.stop_with_actions!(user)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -1,26 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class BackfillIncidentIssueEscalationStatuses < Gitlab::Database::Migration[1.0]
|
||||
MIGRATION = 'BackfillIncidentIssueEscalationStatuses'
|
||||
DELAY_INTERVAL = 2.minutes
|
||||
BATCH_SIZE = 20_000
|
||||
|
||||
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
|
||||
# Removed in favor of creating records for existing incidents
|
||||
# as-needed. See db/migrate/20220321234317_remove_all_issuable_escalation_statuses.rb.
|
||||
def change
|
||||
# no-op
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
ba5c1738b7c368ee8e10e390c959538c4d74055b8bc57f652b06ffe3a1c3becf
|
|
@ -0,0 +1 @@
|
|||
ab7bb319a7099714d9863ec16b7dcf8c1aeab495b8635a01dff4a51fab876b6b
|
|
@ -0,0 +1 @@
|
|||
efba00e36821c5ebe92ba39ad40dd165ab46c97b1b18becdec0d192470c2e8ca
|
|
@ -21480,6 +21480,7 @@ CREATE TABLE users (
|
|||
role smallint,
|
||||
user_type smallint,
|
||||
static_object_token_encrypted text,
|
||||
otp_secret_expires_at timestamp with time zone,
|
||||
CONSTRAINT check_7bde697e8e CHECK ((char_length(static_object_token_encrypted) <= 255))
|
||||
);
|
||||
|
||||
|
|
|
@ -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,
|
||||
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
|
||||
|
||||
You can view a deployment's expiration date in the GitLab UI.
|
||||
|
|
|
@ -89,15 +89,11 @@ read-only view to discourage this behavior.
|
|||
Compliance framework pipelines allow group owners to define
|
||||
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
|
||||
`include` statement can reference the local project's `gitlab-ci.yml` file. This way, the two CI
|
||||
files are merged together any time the pipeline runs. Jobs and variables defined in the compliance
|
||||
`include` statement can reference the local project's `gitlab-ci.yml` file. This way, 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.
|
||||
|
||||
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
|
||||
the compliance framework to specific CI/CD configuration. Use the
|
||||
`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
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
Compliance pipelines use GitLab CI/CD to give you an incredible amount of flexibility
|
||||
|
|
|
@ -131,7 +131,7 @@ module API
|
|||
environment = user_project.environments.find(params[:environment_id])
|
||||
authorize! :stop_environment, environment
|
||||
|
||||
environment.stop_with_action!(current_user)
|
||||
environment.stop_with_actions!(current_user)
|
||||
|
||||
status 200
|
||||
present environment, with: Entities::Environment, current_user: current_user
|
||||
|
|
|
@ -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
|
|
@ -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."
|
||||
msgstr ""
|
||||
|
||||
msgid "Service Ping payload not found in the application cache"
|
||||
msgstr ""
|
||||
|
||||
msgid "Service account generated successfully"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
tmp/
|
||||
reports/
|
||||
no_of_examples/
|
||||
|
||||
.ruby-version
|
||||
.tool-versions
|
||||
.ruby-gemset
|
||||
urls.yml
|
||||
reports/
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
--force-color
|
||||
--order random
|
||||
--format documentation
|
||||
--require specs/spec_helper
|
|
@ -11,7 +11,7 @@ module QA
|
|||
end
|
||||
|
||||
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
|
||||
|
|
|
@ -133,7 +133,9 @@ module QA
|
|||
end
|
||||
|
||||
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"
|
||||
end
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# rubocop:disable QA/ElementWithPattern
|
||||
RSpec.describe QA::Page::Base do
|
||||
describe 'page helpers' do
|
||||
it 'exposes helpful page helpers' do
|
||||
|
@ -11,12 +12,12 @@ RSpec.describe QA::Page::Base do
|
|||
subject do
|
||||
Class.new(described_class) do
|
||||
view 'path/to/some/view.html.haml' do
|
||||
element :something, 'string pattern' # rubocop:disable QA/ElementWithPattern
|
||||
element :something_else, /regexp pattern/ # rubocop:disable QA/ElementWithPattern
|
||||
element :something, 'string pattern'
|
||||
element :something_else, /regexp pattern/
|
||||
end
|
||||
|
||||
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
|
||||
|
@ -95,6 +96,7 @@ RSpec.describe QA::Page::Base do
|
|||
describe '#all_elements' do
|
||||
before do
|
||||
allow(subject).to receive(:all)
|
||||
allow(subject).to receive(:wait_for_requests)
|
||||
end
|
||||
|
||||
it 'raises an error if count or minimum are not specified' do
|
||||
|
@ -108,7 +110,7 @@ RSpec.describe QA::Page::Base do
|
|||
end
|
||||
end
|
||||
|
||||
context 'elements' do
|
||||
describe 'elements' do
|
||||
subject do
|
||||
Class.new(described_class) 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
|
||||
let(:page) { subject.new }
|
||||
|
||||
context 'with elements' do
|
||||
context 'on the page' do
|
||||
before do
|
||||
# required elements not there, meaning not on page
|
||||
allow(page).to receive(:has_no_element?).and_return(false)
|
||||
allow(page).to receive(:wait_for_requests)
|
||||
end
|
||||
|
||||
context 'with elements' do
|
||||
before do
|
||||
allow(page).to receive(:has_no_element?).and_return(has_no_element)
|
||||
end
|
||||
|
||||
context 'with element on the page' do
|
||||
let(:has_no_element) { false }
|
||||
|
||||
it 'is visible' do
|
||||
expect(page).to be_visible
|
||||
end
|
||||
end
|
||||
|
||||
context 'not on the page' do
|
||||
before do
|
||||
# required elements are not on the page
|
||||
allow(page).to receive(:has_no_element?).and_return(true)
|
||||
end
|
||||
|
||||
it 'is not visible' do
|
||||
expect(page).not_to be_visible
|
||||
end
|
||||
end
|
||||
|
||||
it 'does not raise error if page has elements' do
|
||||
expect { page.visible? }.not_to raise_error
|
||||
end
|
||||
end
|
||||
|
||||
context 'no elements' do
|
||||
context 'with element not on the page' do
|
||||
let(:has_no_element) { true }
|
||||
|
||||
it 'is not visible' do
|
||||
expect(page).not_to be_visible
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with no elements' do
|
||||
subject do
|
||||
Class.new(described_class) do
|
||||
view 'path/to/some/view.html.haml' do
|
||||
|
@ -180,3 +184,4 @@ RSpec.describe QA::Page::Base do
|
|||
end
|
||||
end
|
||||
end
|
||||
# rubocop:enable QA/ElementWithPattern
|
||||
|
|
|
@ -72,41 +72,47 @@ RSpec.describe QA::Support::Page::Logging do
|
|||
end
|
||||
|
||||
it 'logs has_element?' do
|
||||
expect { subject.has_element?(:element) }
|
||||
.to output(/has_element\? :element \(wait: #{QA::Runtime::Browser::CAPYBARA_MAX_WAIT_TIME}\) returned: true/o).to_stdout_from_any_process
|
||||
expect { subject.has_element?(:element) }.to output(
|
||||
/has_element\? :element \(wait: #{Capybara.default_max_wait_time}\) returned: true/o
|
||||
).to_stdout_from_any_process
|
||||
end
|
||||
|
||||
it 'logs has_element? with text' do
|
||||
expect { subject.has_element?(:element, text: "some text") }
|
||||
.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
|
||||
expect { subject.has_element?(:element, text: "some text") }.to output(
|
||||
/has_element\? :element with text "some text" \(wait: #{Capybara.default_max_wait_time}\) returned: true/o
|
||||
).to_stdout_from_any_process
|
||||
end
|
||||
|
||||
it 'logs has_no_element?' do
|
||||
allow(page).to receive(:has_no_css?).and_return(true)
|
||||
|
||||
expect { subject.has_no_element?(:element) }
|
||||
.to output(/has_no_element\? :element \(wait: #{QA::Runtime::Browser::CAPYBARA_MAX_WAIT_TIME}\) returned: true/o).to_stdout_from_any_process
|
||||
expect { subject.has_no_element?(:element) }.to output(
|
||||
/has_no_element\? :element \(wait: #{Capybara.default_max_wait_time}\) returned: true/o
|
||||
).to_stdout_from_any_process
|
||||
end
|
||||
|
||||
it 'logs has_no_element? with text' do
|
||||
allow(page).to receive(:has_no_css?).and_return(true)
|
||||
|
||||
expect { subject.has_no_element?(:element, text: "more text") }
|
||||
.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
|
||||
expect { subject.has_no_element?(:element, text: "more text") }.to output(
|
||||
/has_no_element\? :element with text "more text" \(wait: #{Capybara.default_max_wait_time}\) returned: true/o
|
||||
).to_stdout_from_any_process
|
||||
end
|
||||
|
||||
it 'logs has_text?' do
|
||||
allow(page).to receive(:has_text?).and_return(true)
|
||||
|
||||
expect { subject.has_text? 'foo' }
|
||||
.to output(/has_text\?\('foo', wait: #{QA::Runtime::Browser::CAPYBARA_MAX_WAIT_TIME}\) returned true/o).to_stdout_from_any_process
|
||||
expect { subject.has_text? 'foo' }.to output(
|
||||
/has_text\?\('foo', wait: #{Capybara.default_max_wait_time}\) returned true/o
|
||||
).to_stdout_from_any_process
|
||||
end
|
||||
|
||||
it 'logs has_no_text?' do
|
||||
allow(page).to receive(:has_no_text?).with('foo', any_args).and_return(true)
|
||||
|
||||
expect { subject.has_no_text? 'foo' }
|
||||
.to output(/has_no_text\?\('foo', wait: #{QA::Runtime::Browser::CAPYBARA_MAX_WAIT_TIME}\) returned true/o).to_stdout_from_any_process
|
||||
expect { subject.has_no_text? 'foo' }.to output(
|
||||
/has_no_text\?\('foo', wait: #{Capybara.default_max_wait_time}\) returned true/o
|
||||
).to_stdout_from_any_process
|
||||
end
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
context 'all_elements' do
|
||||
context 'with all_elements' do
|
||||
it 'logs the number of elements found' do
|
||||
allow(page).to receive(:all).and_return([1, 2])
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ RSpec.describe QA::Resource::Base do
|
|||
include QA::Support::Helpers::StubEnv
|
||||
|
||||
let(:resource) { spy('resource') }
|
||||
let(:api_client) { instance_double('Runtime::API::Client') }
|
||||
let(:location) { 'http://location' }
|
||||
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(resource).to receive(:api_support?).and_return(true)
|
||||
allow(resource).to receive(:fabricate_via_api!)
|
||||
allow(resource).to receive(:api_client) { api_client }
|
||||
end
|
||||
|
||||
it 'logs the resource and build method' do
|
||||
|
@ -154,7 +156,6 @@ RSpec.describe QA::Resource::Base do
|
|||
|
||||
before do
|
||||
allow(QA::Runtime::Logger).to receive(:debug)
|
||||
# allow(resource).to receive(:fabricate!)
|
||||
end
|
||||
|
||||
it 'logs the resource and build method' do
|
||||
|
|
|
@ -8,6 +8,7 @@ RSpec.describe 'Interceptor' do
|
|||
before(:context) do
|
||||
skip 'Only can test for chrome' unless QA::Runtime::Env.can_intercept?
|
||||
|
||||
QA::Runtime::Browser.configure!
|
||||
QA::Runtime::Browser::Session.enable_interception
|
||||
end
|
||||
|
||||
|
@ -26,7 +27,7 @@ RSpec.describe 'Interceptor' do
|
|||
end
|
||||
|
||||
context 'with Interceptor' do
|
||||
context 'caching' do
|
||||
context 'with caching' do
|
||||
it 'checks the cache' do
|
||||
expect(check_cache).to be(true)
|
||||
end
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
describe QA::Runtime::AllureReport do
|
||||
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(:html_path) { 'html_path' }
|
||||
|
@ -42,11 +42,14 @@ describe QA::Runtime::AllureReport do
|
|||
context 'with report generation enabled' do
|
||||
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(:html_file) { 'html-file' }
|
||||
let(:ci_job) { 'ee:relative 5' }
|
||||
let(:versions) { { version: '14', revision: '6ced31db947' } }
|
||||
let(:session) { double('session') }
|
||||
let(:browser_log) { ['log message 1', 'log message 2'] }
|
||||
|
||||
before do
|
||||
|
@ -54,11 +57,13 @@ describe QA::Runtime::AllureReport do
|
|||
stub_env('CI_JOB_NAME', ci_job)
|
||||
stub_env('GITLAB_QA_ADMIN_ACCESS_TOKEN', 'token')
|
||||
|
||||
stub_const('QA::Runtime::Scenario', attributes)
|
||||
|
||||
allow(Allure).to receive(:add_attachment)
|
||||
allow(File).to receive(:open).with(png_path) { png_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(QA::Runtime::Scenario).to receive(:method_missing).with(:gitlab_address).and_return('gitlab.com')
|
||||
allow(RestClient::Request).to receive(:execute) { version_response }
|
||||
allow(attributes).to receive(:gitlab_address).and_return("https://gitlab.com")
|
||||
|
||||
allow(Capybara).to receive(:current_session).and_return(session)
|
||||
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!
|
||||
end
|
||||
|
||||
it 'configures Allure options', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/357816' do
|
||||
it 'configures Allure options' do
|
||||
aggregate_failures do
|
||||
expect(allure_config.results_directory).to eq('tmp/allure-results')
|
||||
expect(allure_config.clean_results_directory).to eq(true)
|
||||
|
|
|
@ -2,10 +2,10 @@
|
|||
|
||||
module QA
|
||||
RSpec.shared_examples 'a QA scenario class' do
|
||||
let(:attributes) { spy('Runtime::Scenario') }
|
||||
let(:runner) { spy('Specs::Runner') }
|
||||
let(:release) { spy('Runtime::Release') }
|
||||
let(:feature) { spy('Runtime::Feature') }
|
||||
let(:attributes) { class_spy('Runtime::Scenario') }
|
||||
let(:runner) { class_spy('Specs::Runner') }
|
||||
let(:release) { class_spy('Runtime::Release') }
|
||||
let(:feature) { class_spy('Runtime::Feature') }
|
||||
|
||||
let(:args) { { gitlab_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)
|
||||
end
|
||||
|
||||
context 'specifying RSpec options' do
|
||||
context 'with RSpec options' do
|
||||
it 'sets options on runner' do
|
||||
subject.perform(args, *options)
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require_relative '../../qa'
|
||||
|
||||
require_relative 'scenario_shared_examples'
|
|
@ -26,6 +26,7 @@ describe QA::Support::Formatters::TestStatsFormatter do
|
|||
let(:ui_fabrication) { 0 }
|
||||
let(:api_fabrication) { 0 }
|
||||
let(:fabrication_resources) { {} }
|
||||
let(:testcase) { 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/1234' }
|
||||
|
||||
let(:influx_client_args) do
|
||||
{
|
||||
|
@ -51,7 +52,7 @@ describe QA::Support::Formatters::TestStatsFormatter do
|
|||
merge_request: 'false',
|
||||
run_type: run_type,
|
||||
stage: stage.match(%r{\d{1,2}_(\w+)}).captures.first,
|
||||
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/1234'
|
||||
testcase: testcase
|
||||
},
|
||||
fields: {
|
||||
id: './spec/support/formatters/test_stats_formatter_spec.rb[1:1]',
|
||||
|
@ -80,12 +81,6 @@ describe QA::Support::Formatters::TestStatsFormatter do
|
|||
around do |example|
|
||||
RSpec::Core::Sandbox.sandboxed do |config|
|
||||
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 }
|
||||
|
||||
example.run
|
||||
|
@ -226,16 +221,18 @@ describe QA::Support::Formatters::TestStatsFormatter do
|
|||
end
|
||||
|
||||
context 'with fabrication runtimes' do
|
||||
let(:ui_fabrication) { 10 }
|
||||
let(:api_fabrication) { 4 }
|
||||
|
||||
before do
|
||||
Thread.current[:api_fabrication] = api_fabrication
|
||||
Thread.current[:browser_ui_fabrication] = ui_fabrication
|
||||
end
|
||||
let(:ui_fabrication) { 10 }
|
||||
let(:testcase) { nil }
|
||||
|
||||
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).with(data: [data])
|
||||
|
|
|
@ -5,37 +5,38 @@ RSpec.describe QA::Support::WaitForRequests do
|
|||
before do
|
||||
allow(subject).to receive(:finished_all_ajax_requests?).and_return(true)
|
||||
allow(subject).to receive(:finished_loading?).and_return(true)
|
||||
allow(QA::Support::PageErrorChecker).to receive(:check_page_for_error_code)
|
||||
end
|
||||
|
||||
context 'when skip_finished_loading_check is defaulted to false' do
|
||||
it 'calls finished_loading?' do
|
||||
expect(subject).to receive(:finished_loading?).with(hash_including(wait: 1))
|
||||
|
||||
subject.wait_for_requests
|
||||
|
||||
expect(subject).to have_received(:finished_loading?).with(hash_including(wait: 1))
|
||||
end
|
||||
end
|
||||
|
||||
context 'when skip_finished_loading_check is true' do
|
||||
it 'does not call finished_loading?' do
|
||||
expect(subject).not_to receive(:finished_loading?)
|
||||
|
||||
subject.wait_for_requests(skip_finished_loading_check: true)
|
||||
|
||||
expect(subject).not_to have_received(:finished_loading?)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when skip_resp_code_check is defaulted to false' do
|
||||
it 'call report' do
|
||||
allow(QA::Support::PageErrorChecker).to receive(:check_page_for_error_code).with(Capybara.page)
|
||||
|
||||
subject.wait_for_requests
|
||||
|
||||
expect(QA::Support::PageErrorChecker).to have_received(:check_page_for_error_code).with(Capybara.page)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when skip_resp_code_check is true' 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)
|
||||
|
||||
expect(QA::Support::PageErrorChecker).not_to have_received(:check_page_for_error_code)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -107,14 +107,26 @@ RSpec.describe Profiles::TwoFactorAuthsController do
|
|||
expect(assigns[:qr_code]).to eq(code)
|
||||
end
|
||||
|
||||
it 'generates a unique otp_secret every time the page is loaded' do
|
||||
expect(User).to receive(:generate_otp_secret).with(32).and_call_original.twice
|
||||
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.once
|
||||
|
||||
user.update!(otp_secret: nil, otp_secret_expires_at: nil)
|
||||
|
||||
2.times do
|
||||
get :show
|
||||
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
|
||||
let(:go) { get :show }
|
||||
end
|
||||
|
|
|
@ -254,38 +254,54 @@ RSpec.describe Projects::EnvironmentsController do
|
|||
end
|
||||
|
||||
describe 'PATCH #stop' do
|
||||
subject { patch :stop, params: environment_params(format: :json) }
|
||||
|
||||
context 'when env not available' do
|
||||
it 'returns 404' do
|
||||
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)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when stop action' do
|
||||
it 'returns action url' do
|
||||
it 'returns action url for single stop action' do
|
||||
action = create(:ci_build, :manual)
|
||||
|
||||
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(json_response).to eq(
|
||||
{ 'redirect_url' =>
|
||||
project_job_url(project, action) })
|
||||
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
|
||||
|
||||
context 'when no stop action' do
|
||||
it 'returns env url' do
|
||||
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(json_response).to eq(
|
||||
|
|
|
@ -189,6 +189,20 @@ FactoryBot.define do
|
|||
set_expanded_environment_name
|
||||
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
|
||||
name { 'stop staging' }
|
||||
environment { 'staging' }
|
||||
|
|
|
@ -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
|
|
@ -10,27 +10,10 @@ RSpec.describe BackfillIncidentIssueEscalationStatuses do
|
|||
let(:namespace) { namespaces.create!(name: 'foo', path: 'foo') }
|
||||
let(:project) { projects.create!(namespace_id: namespace.id) }
|
||||
|
||||
before do
|
||||
stub_const("#{described_class.name}::BATCH_SIZE", 1)
|
||||
end
|
||||
# Backfill removed - see db/migrate/20220321234317_remove_all_issuable_escalation_statuses.rb.
|
||||
it 'does nothing' do
|
||||
issues.create!(project_id: project.id, issue_type: 1)
|
||||
|
||||
it 'schedules jobs for incident issues' do
|
||||
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
|
||||
expect { migrate! }.not_to change { BackgroundMigrationWorker.jobs.size }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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(: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 validate_presence_of(:name) }
|
||||
|
@ -349,15 +348,28 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do
|
|||
end
|
||||
|
||||
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(:sha) { 'b83d6e391c22777fca1ed3012fce84f633d7fed0' }
|
||||
let(:status) { nil }
|
||||
|
||||
context 'when deployment has the specified sha' do
|
||||
let!(:deployment) { create(:deployment, environment: environment, sha: sha) }
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
describe '#stop_action_available?' do
|
||||
subject { environment.stop_action_available? }
|
||||
describe '#stop_actions_available?' do
|
||||
subject { environment.stop_actions_available? }
|
||||
|
||||
context 'when no other actions' do
|
||||
it { is_expected.to be_falsey }
|
||||
|
@ -499,10 +511,10 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#stop_with_action!' do
|
||||
describe '#stop_with_actions!' do
|
||||
let(:user) { create(:user) }
|
||||
|
||||
subject { environment.stop_with_action!(user) }
|
||||
subject { environment.stop_with_actions!(user) }
|
||||
|
||||
before do
|
||||
expect(environment).to receive(:available?).and_call_original
|
||||
|
@ -515,9 +527,10 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do
|
|||
end
|
||||
|
||||
it do
|
||||
subject
|
||||
actions = subject
|
||||
|
||||
expect(environment).to be_stopped
|
||||
expect(actions).to match_array([])
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -536,18 +549,18 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do
|
|||
|
||||
context 'when matching action is defined' do
|
||||
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,
|
||||
environment: environment,
|
||||
deployable: build,
|
||||
on_stop: 'close_app')
|
||||
deployable: build_a,
|
||||
on_stop: 'close_app_a')
|
||||
end
|
||||
|
||||
context 'when user is not allowed to stop environment' 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
|
||||
|
||||
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
|
||||
let!(:close_action) do
|
||||
create(:ci_build, :manual, pipeline: pipeline, name: 'close_app')
|
||||
create(:ci_build, :manual, pipeline: pipeline, name: 'close_app_a')
|
||||
end
|
||||
|
||||
it 'returns the same action' do
|
||||
expect(subject).to eq(close_action)
|
||||
expect(subject.user).to eq(user)
|
||||
action = subject.first
|
||||
expect(action).to eq(close_action)
|
||||
expect(action.user).to eq(user)
|
||||
end
|
||||
end
|
||||
|
||||
context 'if action did finish' do
|
||||
let!(:close_action) do
|
||||
create(:ci_build, :manual, :success,
|
||||
pipeline: pipeline, name: 'close_app')
|
||||
pipeline: pipeline, name: 'close_app_a')
|
||||
end
|
||||
|
||||
it 'returns a new action of the same type' do
|
||||
expect(subject).to be_persisted
|
||||
expect(subject.name).to eq(close_action.name)
|
||||
expect(subject.user).to eq(user)
|
||||
action = subject.first
|
||||
|
||||
expect(action).to be_persisted
|
||||
expect(action.name).to eq(close_action.name)
|
||||
expect(action.user).to eq(user)
|
||||
end
|
||||
end
|
||||
|
||||
context 'close action does not raise ActiveRecord::StaleObjectError' 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
|
||||
|
||||
before do
|
||||
# preload the build
|
||||
environment.stop_action
|
||||
environment.stop_actions
|
||||
|
||||
# Update record as the other process. This makes `environment.stop_action` stale.
|
||||
close_action.drop!
|
||||
|
@ -613,6 +629,147 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do
|
|||
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
|
||||
|
||||
describe 'recently_updated_on_branch?' do
|
||||
|
@ -772,6 +929,26 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do
|
|||
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
|
||||
subject { environment.last_visible_deployment }
|
||||
|
||||
|
|
|
@ -2089,6 +2089,74 @@ RSpec.describe User do
|
|||
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
|
||||
before do
|
||||
@user = create(:user)
|
||||
|
|
|
@ -202,6 +202,7 @@ RSpec.describe Environments::StopService do
|
|||
context 'with environment related jobs ' do
|
||||
let!(:environment) { create(:environment, :available, name: 'staging', 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) }
|
||||
|
||||
it 'does not stop environments that was not started by the merge request' do
|
||||
|
|
|
@ -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)
|
||||
|
||||
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
|
||||
|
||||
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)
|
||||
|
||||
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
|
||||
|
||||
it 'does not preload for environments that does not exist in the page', :request_store do
|
||||
|
|
|
@ -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
|
Loading…
Reference in New Issue