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:rules:internal
script:
- bundle exec rspec
- bundle exec rspec -O .rspec_internal
qa:internal-as-if-foss:
extends:

View File

@ -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])

View File

@ -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

View File

@ -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

View File

@ -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 = []
Gitlab::OptimisticLocking.retry_lock(
stop_action,
name: 'environment_stop_with_action'
) do |build|
build&.play(current_user)
stop_actions.each do |stop_action|
Gitlab::OptimisticLocking.retry_lock(
stop_action,
name: 'environment_stop_with_actions'
) 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

View File

@ -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

View File

@ -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.'))

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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')

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
- 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' }

View File

@ -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

View File

@ -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|

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 = {})
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

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
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

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,
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))
);

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,
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.

View File

@ -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

View File

@ -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

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."
msgstr ""
msgid "Service Ping payload not found in the application cache"
msgstr ""
msgid "Service account generated successfully"
msgstr ""

4
qa/.gitignore vendored
View File

@ -1,6 +1,8 @@
tmp/
reports/
no_of_examples/
.ruby-version
.tool-versions
.ruby-gemset
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
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

View File

@ -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
@ -469,8 +471,8 @@ module QA
return element_when_flag_disabled if has_element?(element_when_flag_disabled)
raise ElementNotFound,
"Could not find the expected element as #{element_when_flag_enabled} or #{element_when_flag_disabled}." \
"The relevant feature flag is #{feature_flag}"
"Could not find the expected element as #{element_when_flag_enabled} or #{element_when_flag_disabled}." \
"The relevant feature flag is #{feature_flag}"
end
end
end

View File

@ -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 }
before do
allow(page).to receive(:wait_for_requests)
end
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)
end
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
it 'does not raise error if page has elements' do
expect { page.visible? }.not_to raise_error
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
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
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 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

View File

@ -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])

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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)

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(: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])

View File

@ -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

View File

@ -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

View File

@ -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(

View File

@ -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' }

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(: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

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(: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')
environment: environment,
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 }

View File

@ -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)

View File

@ -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

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)
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

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