From 89528516610699b580e6ea925312f08a9fdb44ce Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Thu, 14 Apr 2022 06:08:29 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- .gitlab/ci/qa.gitlab-ci.yml | 2 +- .../profiles/two_factor_auths_controller.rb | 23 +- .../projects/environments_controller.rb | 6 +- app/models/ci/pipeline.rb | 4 +- app/models/environment.rb | 61 ++++- app/models/merge_request.rb | 4 +- app/models/user.rb | 18 ++ app/policies/environment_policy.rb | 6 +- app/serializers/environment_entity.rb | 2 +- app/services/environments/stop_service.rb | 6 +- .../service_usage_data.html.haml | 5 +- .../clusters/_deprecation_alert.html.haml | 2 +- .../_import_group_from_file_panel.html.haml | 5 +- .../profiles/notifications/show.html.haml | 3 +- app/views/shared/_global_alert.html.haml | 21 -- app/workers/environments/auto_stop_worker.rb | 6 +- .../environment_multiple_stop_actions.yml | 8 + ...remove_all_issuable_escalation_statuses.rb | 30 +++ ...20407135820_add_epics_relative_position.rb | 18 ++ ...0220412171810_add_otp_secret_expires_at.rb | 11 + ...fill_incident_issue_escalation_statuses.rb | 23 +- db/schema_migrations/20220321234317 | 1 + db/schema_migrations/20220407135820 | 1 + db/schema_migrations/20220412171810 | 1 + db/structure.sql | 1 + doc/ci/environments/index.md | 49 ++++ doc/user/project/settings/index.md | 15 +- lib/api/environments.rb | 2 +- ...fill_incident_issue_escalation_statuses.rb | 32 --- locale/gitlab.pot | 3 + qa/.gitignore | 4 +- qa/.rspec_internal | 4 + .../settings/component/usage_statistics.rb | 2 +- qa/qa/page/base.rb | 8 +- qa/spec/page/base_spec.rb | 43 ++-- qa/spec/page/logging_spec.rb | 32 +-- qa/spec/resource/base_spec.rb | 3 +- .../script_extensions/interceptor_spec.rb | 3 +- qa/spec/specs/allure_report_spec.rb | 15 +- .../scenario_shared_examples.rb | 10 +- qa/spec/specs/spec_helper.rb | 5 + .../formatters/test_stats_formatter_spec.rb | 25 +- qa/spec/support/wait_for_requests_spec.rb | 17 +- .../two_factor_auths_controller_spec.rb | 16 +- .../projects/environments_controller_spec.rb | 28 ++- spec/factories/ci/builds.rb | 14 ++ ...incident_issue_escalation_statuses_spec.rb | 27 --- ...incident_issue_escalation_statuses_spec.rb | 25 +- ...e_all_issuable_escalation_statuses_spec.rb | 20 ++ .../add_epics_relative_position_spec.rb | 29 +++ spec/models/environment_spec.rb | 221 ++++++++++++++++-- spec/models/user_spec.rb | 68 ++++++ .../environments/stop_service_spec.rb | 1 + .../environment_serializer_shared_examples.rb | 12 +- .../shared/_global_alert.html.haml_spec.rb | 46 ---- 55 files changed, 723 insertions(+), 324 deletions(-) delete mode 100644 app/views/shared/_global_alert.html.haml create mode 100644 config/feature_flags/development/environment_multiple_stop_actions.yml create mode 100644 db/migrate/20220321234317_remove_all_issuable_escalation_statuses.rb create mode 100644 db/migrate/20220407135820_add_epics_relative_position.rb create mode 100644 db/migrate/20220412171810_add_otp_secret_expires_at.rb create mode 100644 db/schema_migrations/20220321234317 create mode 100644 db/schema_migrations/20220407135820 create mode 100644 db/schema_migrations/20220412171810 delete mode 100644 lib/gitlab/background_migration/backfill_incident_issue_escalation_statuses.rb create mode 100644 qa/.rspec_internal rename qa/spec/{support/shared_examples => specs}/scenario_shared_examples.rb (89%) create mode 100644 qa/spec/specs/spec_helper.rb delete mode 100644 spec/lib/gitlab/background_migration/backfill_incident_issue_escalation_statuses_spec.rb create mode 100644 spec/migrations/20220321234317_remove_all_issuable_escalation_statuses_spec.rb create mode 100644 spec/migrations/add_epics_relative_position_spec.rb delete mode 100644 spec/views/shared/_global_alert.html.haml_spec.rb diff --git a/.gitlab/ci/qa.gitlab-ci.yml b/.gitlab/ci/qa.gitlab-ci.yml index 877d3275edb..8881a4c486d 100644 --- a/.gitlab/ci/qa.gitlab-ci.yml +++ b/.gitlab/ci/qa.gitlab-ci.yml @@ -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: diff --git a/app/controllers/profiles/two_factor_auths_controller.rb b/app/controllers/profiles/two_factor_auths_controller.rb index 27a36a00a6d..48b0d313d3c 100644 --- a/app/controllers/profiles/two_factor_auths_controller.rb +++ b/app/controllers/profiles/two_factor_auths_controller.rb @@ -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]) diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb index eabc048e341..8e81e75ad13 100644 --- a/app/controllers/projects/environments_controller.rb +++ b/app/controllers/projects/environments_controller.rb @@ -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 diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 11814d98b5f..2d0479e02a3 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -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 diff --git a/app/models/environment.rb b/app/models/environment.rb index 54323c8bbd8..a3fb35917ba 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -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 diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index f4cac56fdd7..512fa294128 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -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 diff --git a/app/models/user.rb b/app/models/user.rb index 743ba4d229c..f229ffef18c 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -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.')) diff --git a/app/policies/environment_policy.rb b/app/policies/environment_policy.rb index e9e3517b3da..72db6d31764 100644 --- a/app/policies/environment_policy.rb +++ b/app/policies/environment_policy.rb @@ -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 diff --git a/app/serializers/environment_entity.rb b/app/serializers/environment_entity.rb index d484f60ed8f..634be365a9d 100644 --- a/app/serializers/environment_entity.rb +++ b/app/serializers/environment_entity.rb @@ -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 diff --git a/app/services/environments/stop_service.rb b/app/services/environments/stop_service.rb index 8e66155f400..24ae658d3d6 100644 --- a/app/services/environments/stop_service.rb +++ b/app/services/environments/stop_service.rb @@ -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 diff --git a/app/views/admin/application_settings/service_usage_data.html.haml b/app/views/admin/application_settings/service_usage_data.html.haml index d9fbc75e58f..ec084c05cf7 100644 --- a/app/views/admin/application_settings/service_usage_data.html.haml +++ b/app/views/admin/application_settings/service_usage_data.html.haml @@ -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') diff --git a/app/views/clusters/clusters/_deprecation_alert.html.haml b/app/views/clusters/clusters/_deprecation_alert.html.haml index 6ef39766906..202e2c14d3f 100644 --- a/app/views/clusters/clusters/_deprecation_alert.html.haml +++ b/app/views/clusters/clusters/_deprecation_alert.html.haml @@ -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 = ''.html_safe - issue_link_start = link_start % { url: 'https://gitlab.com/gitlab-org/configure/general/-/issues/199' } diff --git a/app/views/groups/_import_group_from_file_panel.html.haml b/app/views/groups/_import_group_from_file_panel.html.haml index 9d7a326d74a..ddd7481e0bd 100644 --- a/app/views/groups/_import_group_from_file_panel.html.haml +++ b/app/views/groups/_import_group_from_file_panel.html.haml @@ -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 = ''.html_safe % { url: help_page_path('user/group/import/index.md') } - link_end = ''.html_safe diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml index e958168bb8e..5d74bbe9971 100644 --- a/app/views/profiles/notifications/show.html.haml +++ b/app/views/profiles/notifications/show.html.haml @@ -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| diff --git a/app/views/shared/_global_alert.html.haml b/app/views/shared/_global_alert.html.haml deleted file mode 100644 index cb7ad32e474..00000000000 --- a/app/views/shared/_global_alert.html.haml +++ /dev/null @@ -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 diff --git a/app/workers/environments/auto_stop_worker.rb b/app/workers/environments/auto_stop_worker.rb index 672a4f4121e..aee6e977550 100644 --- a/app/workers/environments/auto_stop_worker.rb +++ b/app/workers/environments/auto_stop_worker.rb @@ -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 diff --git a/config/feature_flags/development/environment_multiple_stop_actions.yml b/config/feature_flags/development/environment_multiple_stop_actions.yml new file mode 100644 index 00000000000..514d5e8cf52 --- /dev/null +++ b/config/feature_flags/development/environment_multiple_stop_actions.yml @@ -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 diff --git a/db/migrate/20220321234317_remove_all_issuable_escalation_statuses.rb b/db/migrate/20220321234317_remove_all_issuable_escalation_statuses.rb new file mode 100644 index 00000000000..a6ea1820160 --- /dev/null +++ b/db/migrate/20220321234317_remove_all_issuable_escalation_statuses.rb @@ -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 diff --git a/db/migrate/20220407135820_add_epics_relative_position.rb b/db/migrate/20220407135820_add_epics_relative_position.rb new file mode 100644 index 00000000000..8ab62667e1e --- /dev/null +++ b/db/migrate/20220407135820_add_epics_relative_position.rb @@ -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 diff --git a/db/migrate/20220412171810_add_otp_secret_expires_at.rb b/db/migrate/20220412171810_add_otp_secret_expires_at.rb new file mode 100644 index 00000000000..883293c87f8 --- /dev/null +++ b/db/migrate/20220412171810_add_otp_secret_expires_at.rb @@ -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 diff --git a/db/post_migrate/20211214012507_backfill_incident_issue_escalation_statuses.rb b/db/post_migrate/20211214012507_backfill_incident_issue_escalation_statuses.rb index 7f0168be1a4..f8239b6e0cd 100644 --- a/db/post_migrate/20211214012507_backfill_incident_issue_escalation_statuses.rb +++ b/db/post_migrate/20211214012507_backfill_incident_issue_escalation_statuses.rb @@ -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 diff --git a/db/schema_migrations/20220321234317 b/db/schema_migrations/20220321234317 new file mode 100644 index 00000000000..7b24f81ca1c --- /dev/null +++ b/db/schema_migrations/20220321234317 @@ -0,0 +1 @@ +ba5c1738b7c368ee8e10e390c959538c4d74055b8bc57f652b06ffe3a1c3becf \ No newline at end of file diff --git a/db/schema_migrations/20220407135820 b/db/schema_migrations/20220407135820 new file mode 100644 index 00000000000..c1d1f8a5891 --- /dev/null +++ b/db/schema_migrations/20220407135820 @@ -0,0 +1 @@ +ab7bb319a7099714d9863ec16b7dcf8c1aeab495b8635a01dff4a51fab876b6b \ No newline at end of file diff --git a/db/schema_migrations/20220412171810 b/db/schema_migrations/20220412171810 new file mode 100644 index 00000000000..377f268f697 --- /dev/null +++ b/db/schema_migrations/20220412171810 @@ -0,0 +1 @@ +efba00e36821c5ebe92ba39ad40dd165ab46c97b1b18becdec0d192470c2e8ca \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index b1b2c7fbcdc..c09cbdebe9b 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -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)) ); diff --git a/doc/ci/environments/index.md b/doc/ci/environments/index.md index c2612b6ff16..3822b32181f 100644 --- a/doc/ci/environments/index.md +++ b/doc/ci/environments/index.md @@ -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. diff --git a/doc/user/project/settings/index.md b/doc/user/project/settings/index.md index 9ef6dcdd9a2..31cda756a78 100644 --- a/doc/user/project/settings/index.md +++ b/doc/user/project/settings/index.md @@ -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 diff --git a/lib/api/environments.rb b/lib/api/environments.rb index c032b80e39b..19b48c1e3cf 100644 --- a/lib/api/environments.rb +++ b/lib/api/environments.rb @@ -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 diff --git a/lib/gitlab/background_migration/backfill_incident_issue_escalation_statuses.rb b/lib/gitlab/background_migration/backfill_incident_issue_escalation_statuses.rb deleted file mode 100644 index 2d46ff6b933..00000000000 --- a/lib/gitlab/background_migration/backfill_incident_issue_escalation_statuses.rb +++ /dev/null @@ -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 diff --git a/locale/gitlab.pot b/locale/gitlab.pot index a5a043bf743..2f9ea5beebf 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -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 "" diff --git a/qa/.gitignore b/qa/.gitignore index b54b8666e28..3c5db4b565e 100644 --- a/qa/.gitignore +++ b/qa/.gitignore @@ -1,6 +1,8 @@ tmp/ +reports/ +no_of_examples/ + .ruby-version .tool-versions .ruby-gemset urls.yml -reports/ diff --git a/qa/.rspec_internal b/qa/.rspec_internal new file mode 100644 index 00000000000..ea32ca1e093 --- /dev/null +++ b/qa/.rspec_internal @@ -0,0 +1,4 @@ +--force-color +--order random +--format documentation +--require specs/spec_helper diff --git a/qa/qa/page/admin/settings/component/usage_statistics.rb b/qa/qa/page/admin/settings/component/usage_statistics.rb index 0275b7ae926..c296e63e28e 100644 --- a/qa/qa/page/admin/settings/component/usage_statistics.rb +++ b/qa/qa/page/admin/settings/component/usage_statistics.rb @@ -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 diff --git a/qa/qa/page/base.rb b/qa/qa/page/base.rb index 256868caebf..83db8bc0fd6 100644 --- a/qa/qa/page/base.rb +++ b/qa/qa/page/base.rb @@ -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 diff --git a/qa/spec/page/base_spec.rb b/qa/spec/page/base_spec.rb index 52345876149..146e71da933 100644 --- a/qa/spec/page/base_spec.rb +++ b/qa/spec/page/base_spec.rb @@ -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 diff --git a/qa/spec/page/logging_spec.rb b/qa/spec/page/logging_spec.rb index 054332eea29..93a08108787 100644 --- a/qa/spec/page/logging_spec.rb +++ b/qa/spec/page/logging_spec.rb @@ -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]) diff --git a/qa/spec/resource/base_spec.rb b/qa/spec/resource/base_spec.rb index eab205ec5d1..6dac8e0e3ee 100644 --- a/qa/spec/resource/base_spec.rb +++ b/qa/spec/resource/base_spec.rb @@ -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 diff --git a/qa/spec/runtime/script_extensions/interceptor_spec.rb b/qa/spec/runtime/script_extensions/interceptor_spec.rb index 28a368b2d99..28e8007973c 100644 --- a/qa/spec/runtime/script_extensions/interceptor_spec.rb +++ b/qa/spec/runtime/script_extensions/interceptor_spec.rb @@ -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 diff --git a/qa/spec/specs/allure_report_spec.rb b/qa/spec/specs/allure_report_spec.rb index 09f0f752731..85befb2f602 100644 --- a/qa/spec/specs/allure_report_spec.rb +++ b/qa/spec/specs/allure_report_spec.rb @@ -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) diff --git a/qa/spec/support/shared_examples/scenario_shared_examples.rb b/qa/spec/specs/scenario_shared_examples.rb similarity index 89% rename from qa/spec/support/shared_examples/scenario_shared_examples.rb rename to qa/spec/specs/scenario_shared_examples.rb index 5e448349cf9..7d806d50d21 100644 --- a/qa/spec/support/shared_examples/scenario_shared_examples.rb +++ b/qa/spec/specs/scenario_shared_examples.rb @@ -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) diff --git a/qa/spec/specs/spec_helper.rb b/qa/spec/specs/spec_helper.rb new file mode 100644 index 00000000000..e4514c6c64f --- /dev/null +++ b/qa/spec/specs/spec_helper.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +require_relative '../../qa' + +require_relative 'scenario_shared_examples' diff --git a/qa/spec/support/formatters/test_stats_formatter_spec.rb b/qa/spec/support/formatters/test_stats_formatter_spec.rb index fb24743df3d..ba59588d186 100644 --- a/qa/spec/support/formatters/test_stats_formatter_spec.rb +++ b/qa/spec/support/formatters/test_stats_formatter_spec.rb @@ -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]) diff --git a/qa/spec/support/wait_for_requests_spec.rb b/qa/spec/support/wait_for_requests_spec.rb index 2492820b67f..221d61ea2b4 100644 --- a/qa/spec/support/wait_for_requests_spec.rb +++ b/qa/spec/support/wait_for_requests_spec.rb @@ -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 diff --git a/spec/controllers/profiles/two_factor_auths_controller_spec.rb b/spec/controllers/profiles/two_factor_auths_controller_spec.rb index 06f193a766e..33cba675777 100644 --- a/spec/controllers/profiles/two_factor_auths_controller_spec.rb +++ b/spec/controllers/profiles/two_factor_auths_controller_spec.rb @@ -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 diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb index fdfc21887a6..f4cad5790a3 100644 --- a/spec/controllers/projects/environments_controller_spec.rb +++ b/spec/controllers/projects/environments_controller_spec.rb @@ -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( diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb index 19e27144e66..56c12d73a3b 100644 --- a/spec/factories/ci/builds.rb +++ b/spec/factories/ci/builds.rb @@ -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' } diff --git a/spec/lib/gitlab/background_migration/backfill_incident_issue_escalation_statuses_spec.rb b/spec/lib/gitlab/background_migration/backfill_incident_issue_escalation_statuses_spec.rb deleted file mode 100644 index 242da383453..00000000000 --- a/spec/lib/gitlab/background_migration/backfill_incident_issue_escalation_statuses_spec.rb +++ /dev/null @@ -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 diff --git a/spec/migrations/20211214012507_backfill_incident_issue_escalation_statuses_spec.rb b/spec/migrations/20211214012507_backfill_incident_issue_escalation_statuses_spec.rb index a17fee6bab2..791c0595f0e 100644 --- a/spec/migrations/20211214012507_backfill_incident_issue_escalation_statuses_spec.rb +++ b/spec/migrations/20211214012507_backfill_incident_issue_escalation_statuses_spec.rb @@ -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 diff --git a/spec/migrations/20220321234317_remove_all_issuable_escalation_statuses_spec.rb b/spec/migrations/20220321234317_remove_all_issuable_escalation_statuses_spec.rb new file mode 100644 index 00000000000..44e20df1130 --- /dev/null +++ b/spec/migrations/20220321234317_remove_all_issuable_escalation_statuses_spec.rb @@ -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 diff --git a/spec/migrations/add_epics_relative_position_spec.rb b/spec/migrations/add_epics_relative_position_spec.rb new file mode 100644 index 00000000000..f3b7dd1727b --- /dev/null +++ b/spec/migrations/add_epics_relative_position_spec.rb @@ -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 diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb index b20c91c53c1..b42e73e6d93 100644 --- a/spec/models/environment_spec.rb +++ b/spec/models/environment_spec.rb @@ -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 } diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 05e38efea80..9f45487dbe4 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -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) diff --git a/spec/services/environments/stop_service_spec.rb b/spec/services/environments/stop_service_spec.rb index 31e6ebadc64..9e9ef127c67 100644 --- a/spec/services/environments/stop_service_spec.rb +++ b/spec/services/environments/stop_service_spec.rb @@ -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 diff --git a/spec/support/shared_examples/serializers/environment_serializer_shared_examples.rb b/spec/support/shared_examples/serializers/environment_serializer_shared_examples.rb index 8f63192e709..fcd52cdf7fa 100644 --- a/spec/support/shared_examples/serializers/environment_serializer_shared_examples.rb +++ b/spec/support/shared_examples/serializers/environment_serializer_shared_examples.rb @@ -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 diff --git a/spec/views/shared/_global_alert.html.haml_spec.rb b/spec/views/shared/_global_alert.html.haml_spec.rb deleted file mode 100644 index a400d5b39b0..00000000000 --- a/spec/views/shared/_global_alert.html.haml_spec.rb +++ /dev/null @@ -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(''.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