diff --git a/.rubocop_todo/database/multiple_databases.yml b/.rubocop_todo/database/multiple_databases.yml index e555c2f912b..f2768276060 100644 --- a/.rubocop_todo/database/multiple_databases.yml +++ b/.rubocop_todo/database/multiple_databases.yml @@ -1,14 +1,12 @@ --- Database/MultipleDatabases: Exclude: - - ee/lib/ee/gitlab/database.rb - ee/lib/gitlab/geo/database_tasks.rb - ee/lib/gitlab/geo/geo_tasks.rb - ee/lib/gitlab/geo/health_check.rb - ee/lib/gitlab/geo/log_cursor/daemon.rb - ee/lib/pseudonymizer/dumper.rb - ee/lib/pseudonymizer/pager.rb - - ee/lib/system_check/geo/geo_database_configured_check.rb - ee/spec/lib/pseudonymizer/dumper_spec.rb - ee/spec/services/ee/merge_requests/update_service_spec.rb - lib/backup/database.rb @@ -21,14 +19,12 @@ Database/MultipleDatabases: - lib/gitlab/database/migrations/observers/query_log.rb - lib/gitlab/database/partitioning_migration_helpers/backfill_partitioned_table.rb - lib/gitlab/database.rb - - lib/gitlab/database/schema_cache_with_renamed_table.rb - lib/gitlab/database/with_lock_retries.rb - lib/gitlab/gitlab_import/importer.rb - lib/gitlab/health_checks/db_check.rb - lib/gitlab/import_export/base/relation_factory.rb - lib/gitlab/import_export/group/relation_tree_restorer.rb - lib/gitlab/legacy_github_import/importer.rb - - lib/gitlab/metrics/samplers/database_sampler.rb - lib/gitlab/seeder.rb - lib/gitlab/sherlock/query.rb - lib/system_check/orphans/repository_check.rb @@ -39,15 +35,8 @@ Database/MultipleDatabases: - spec/lib/gitlab/database_spec.rb - spec/lib/gitlab/metrics/subscribers/active_record_spec.rb - spec/lib/gitlab/profiler_spec.rb - - spec/lib/gitlab/usage_data_metrics_spec.rb - - spec/lib/gitlab/usage_data_queries_spec.rb - spec/lib/gitlab/usage/metrics/names_suggestions/relation_parsers/constraints_spec.rb - spec/lib/gitlab/usage/metrics/names_suggestions/relation_parsers/joins_spec.rb - - spec/lib/gitlab/usage/metrics/instrumentations/database_metric_spec.rb - - spec/lib/gitlab/utils/usage_data_spec.rb - - spec/models/project_feature_usage_spec.rb - - spec/models/users_statistics_spec.rb - - spec/services/users/activity_service_spec.rb - spec/support/caching.rb - spec/support/gitlab/usage/metrics_instrumentation_shared_examples.rb - spec/support/helpers/database_connection_helpers.rb diff --git a/app/assets/javascripts/google_cloud/components/deployments_service_table.vue b/app/assets/javascripts/google_cloud/components/deployments_service_table.vue new file mode 100644 index 00000000000..7d27d7cf6b2 --- /dev/null +++ b/app/assets/javascripts/google_cloud/components/deployments_service_table.vue @@ -0,0 +1,61 @@ + + + + {{ $options.i18n.deployments }} + {{ $options.i18n.deploymentsDescription }} + + + {{ value.title }} + + + + diff --git a/app/assets/javascripts/google_cloud/components/home.vue b/app/assets/javascripts/google_cloud/components/home.vue index 05f39de66ee..8ef110dcf22 100644 --- a/app/assets/javascripts/google_cloud/components/home.vue +++ b/app/assets/javascripts/google_cloud/components/home.vue @@ -1,11 +1,13 @@ @@ -35,7 +45,12 @@ export default { :empty-illustration-url="emptyIllustrationUrl" /> - + + + diff --git a/app/assets/javascripts/group.js b/app/assets/javascripts/group.js index f255f8a084c..b6a6720e7a1 100644 --- a/app/assets/javascripts/group.js +++ b/app/assets/javascripts/group.js @@ -13,11 +13,8 @@ export default class Group { this.updateGroupPathSlugHandler = this.updateGroupPathSlug.bind(this); this.groupNames.forEach((groupName) => { - if (groupName.value === '') { - groupName.addEventListener('keyup', this.updateHandler); - - groupName.addEventListener('keyup', this.updateGroupPathSlugHandler); - } + groupName.addEventListener('keyup', this.updateHandler); + groupName.addEventListener('keyup', this.updateGroupPathSlugHandler); }); this.groupPaths.forEach((groupPath) => { diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/index.js b/app/assets/javascripts/packages_and_registries/container_registry/explorer/index.js index e6c197a30dd..ca5bd8d6964 100644 --- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/index.js +++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/index.js @@ -4,6 +4,7 @@ import { parseBoolean } from '~/lib/utils/common_utils'; import PerformancePlugin from '~/performance/vue_performance_plugin'; import Translate from '~/vue_shared/translate'; import RegistryBreadcrumb from '~/packages_and_registries/shared/components/registry_breadcrumb.vue'; +import { renderBreadcrumb } from '~/packages_and_registries/shared/utils'; import { apolloProvider } from './graphql/index'; import RegistryExplorer from './pages/index.vue'; import createRouter from './router'; @@ -84,38 +85,8 @@ export default () => { }, }); - const attachBreadcrumb = () => { - const breadCrumbEls = document.querySelectorAll('nav .js-breadcrumbs-list li'); - const breadCrumbEl = breadCrumbEls[breadCrumbEls.length - 1]; - const crumbs = [breadCrumbEl.querySelector('h2')]; - const nestedBreadcrumbEl = document.createElement('div'); - breadCrumbEl.replaceChild(nestedBreadcrumbEl, breadCrumbEl.querySelector('h2')); - return new Vue({ - el: nestedBreadcrumbEl, - router, - apolloProvider, - components: { - RegistryBreadcrumb, - }, - render(createElement) { - // FIXME(@tnir): this is a workaround until the MR gets merged: - // https://gitlab.com/gitlab-org/gitlab/-/merge_requests/48115 - const parentEl = breadCrumbEl.parentElement.parentElement; - if (parentEl) { - parentEl.classList.remove('breadcrumbs-container'); - parentEl.classList.add('gl-display-flex'); - parentEl.classList.add('w-100'); - } - // End of FIXME(@tnir) - return createElement('registry-breadcrumb', { - class: breadCrumbEl.className, - props: { - crumbs, - }, - }); - }, - }); + return { + attachBreadcrumb: renderBreadcrumb(router, apolloProvider, RegistryBreadcrumb), + attachMainComponent, }; - - return { attachBreadcrumb, attachMainComponent }; }; diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/npm_installation.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/npm_installation.vue index 95b09b25678..7479f748a56 100644 --- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/npm_installation.vue +++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/npm_installation.vue @@ -26,7 +26,7 @@ export default { GlSprintf, GlFormRadioGroup, }, - inject: ['npmPath'], + inject: ['npmInstanceUrl'], props: { packageEntity: { type: Object, @@ -66,7 +66,9 @@ export default { npmSetupCommand(type, endpointType) { const scope = this.packageEntity.name.substring(0, this.packageEntity.name.indexOf('/')); const npmPathForEndpoint = - endpointType === INSTANCE_PACKAGE_ENDPOINT_TYPE ? this.npmPath : this.packageEntity.npmUrl; + endpointType === INSTANCE_PACKAGE_ENDPOINT_TYPE + ? this.npmInstanceUrl + : this.packageEntity.npmUrl; if (type === NPM_PACKAGE_MANAGER) { return `echo ${scope}:registry=${npmPathForEndpoint}/ >> .npmrc`; diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_list_row.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_list_row.vue index 6fd96c0654f..6222c2e73d7 100644 --- a/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_list_row.vue +++ b/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_list_row.vue @@ -1,5 +1,5 @@ - + { return null; } - // TODO `activeRunnersCount` should be implemented using a GraphQL API - // https://gitlab.com/gitlab-org/gitlab/-/issues/333806 - const { runnerInstallHelpPage, registrationToken, activeRunnersCount } = el.dataset; + const { runnerInstallHelpPage, registrationToken } = el.dataset; const apolloProvider = new VueApollo({ defaultClient: createDefaultClient(), @@ -43,10 +41,6 @@ export const initAdminRunners = (selector = '#js-admin-runners') => { return h(AdminRunnersApp, { props: { registrationToken, - - // Runner counts are returned as formatted - // strings, we do not use `parseInt`. - activeRunnersCount, }, }); }, diff --git a/app/assets/javascripts/runner/components/registration/registration_token_reset_dropdown_item.vue b/app/assets/javascripts/runner/components/registration/registration_token_reset_dropdown_item.vue index edcbcb2bf69..0e259807f98 100644 --- a/app/assets/javascripts/runner/components/registration/registration_token_reset_dropdown_item.vue +++ b/app/assets/javascripts/runner/components/registration/registration_token_reset_dropdown_item.vue @@ -1,5 +1,5 @@ - + {{ __('Reset registration token') }} + + {{ $options.i18n.modalCopy }} + diff --git a/app/assets/javascripts/runner/components/stat/runner_online_stat.vue b/app/assets/javascripts/runner/components/stat/runner_online_stat.vue deleted file mode 100644 index b92b9badef0..00000000000 --- a/app/assets/javascripts/runner/components/stat/runner_online_stat.vue +++ /dev/null @@ -1,17 +0,0 @@ - - - - diff --git a/app/assets/javascripts/runner/components/stat/runner_stats.vue b/app/assets/javascripts/runner/components/stat/runner_stats.vue new file mode 100644 index 00000000000..d3693ee593e --- /dev/null +++ b/app/assets/javascripts/runner/components/stat/runner_stats.vue @@ -0,0 +1,49 @@ + + + + + + + + diff --git a/app/assets/javascripts/runner/components/stat/runner_status_stat.vue b/app/assets/javascripts/runner/components/stat/runner_status_stat.vue new file mode 100644 index 00000000000..b77bbe15541 --- /dev/null +++ b/app/assets/javascripts/runner/components/stat/runner_status_stat.vue @@ -0,0 +1,65 @@ + + + + diff --git a/app/assets/javascripts/runner/graphql/get_group_runners.query.graphql b/app/assets/javascripts/runner/graphql/get_group_runners.query.graphql index 6da9e276f74..f7bcd683718 100644 --- a/app/assets/javascripts/runner/graphql/get_group_runners.query.graphql +++ b/app/assets/javascripts/runner/graphql/get_group_runners.query.graphql @@ -13,7 +13,7 @@ query getGroupRunners( $sort: CiRunnerSort ) { group(fullPath: $groupFullPath) { - id + id # Apollo required runners( membership: DESCENDANTS before: $before diff --git a/app/assets/javascripts/runner/graphql/get_group_runners_count.query.graphql b/app/assets/javascripts/runner/graphql/get_group_runners_count.query.graphql new file mode 100644 index 00000000000..554eb09e372 --- /dev/null +++ b/app/assets/javascripts/runner/graphql/get_group_runners_count.query.graphql @@ -0,0 +1,20 @@ +query getGroupRunnersCount( + $groupFullPath: ID! + $status: CiRunnerStatus + $type: CiRunnerType + $tagList: [String!] + $search: String +) { + group(fullPath: $groupFullPath) { + id # Apollo required + runners( + membership: DESCENDANTS + status: $status + type: $type + tagList: $tagList + search: $search + ) { + count + } + } +} diff --git a/app/assets/javascripts/runner/group_runners/group_runners_app.vue b/app/assets/javascripts/runner/group_runners/group_runners_app.vue index f33f28c11e3..3a7b58e3dc9 100644 --- a/app/assets/javascripts/runner/group_runners/group_runners_app.vue +++ b/app/assets/javascripts/runner/group_runners/group_runners_app.vue @@ -9,7 +9,7 @@ import RegistrationDropdown from '../components/registration/registration_dropdo import RunnerFilteredSearchBar from '../components/runner_filtered_search_bar.vue'; import RunnerList from '../components/runner_list.vue'; import RunnerName from '../components/runner_name.vue'; -import RunnerOnlineStat from '../components/stat/runner_online_stat.vue'; +import RunnerStats from '../components/stat/runner_stats.vue'; import RunnerPagination from '../components/runner_pagination.vue'; import RunnerTypeTabs from '../components/runner_type_tabs.vue'; @@ -19,8 +19,12 @@ import { GROUP_FILTERED_SEARCH_NAMESPACE, GROUP_TYPE, GROUP_RUNNER_COUNT_LIMIT, + STATUS_ONLINE, + STATUS_OFFLINE, + STATUS_STALE, } from '../constants'; import getGroupRunnersQuery from '../graphql/get_group_runners.query.graphql'; +import getGroupRunnersCountQuery from '../graphql/get_group_runners_count.query.graphql'; import { fromUrlQueryToSearch, fromSearchToUrl, @@ -28,6 +32,17 @@ import { } from '../runner_search_utils'; import { captureException } from '../sentry_utils'; +const runnersCountSmartQuery = { + query: getGroupRunnersCountQuery, + fetchPolicy: fetchPolicies.CACHE_AND_NETWORK, + update(data) { + return data?.group?.runners?.count; + }, + error(error) { + this.reportToSentry(error); + }, +}; + export default { name: 'GroupRunnersApp', components: { @@ -36,7 +51,7 @@ export default { RunnerFilteredSearchBar, RunnerList, RunnerName, - RunnerOnlineStat, + RunnerStats, RunnerPagination, RunnerTypeTabs, }, @@ -89,6 +104,33 @@ export default { this.reportToSentry(error); }, }, + onlineRunnersTotal: { + ...runnersCountSmartQuery, + variables() { + return { + groupFullPath: this.groupFullPath, + status: STATUS_ONLINE, + }; + }, + }, + offlineRunnersTotal: { + ...runnersCountSmartQuery, + variables() { + return { + groupFullPath: this.groupFullPath, + status: STATUS_OFFLINE, + }; + }, + }, + staleRunnersTotal: { + ...runnersCountSmartQuery, + variables() { + return { + groupFullPath: this.groupFullPath, + status: STATUS_STALE, + }; + }, + }, }, computed: { variables() { @@ -147,7 +189,11 @@ export default { - + (environment) { where(environment: environment) } scope :for_assignee_username, -> (assignee_username) { joins(:assignees).merge(User.by_username(assignee_username)) } scope :search, -> (query) { fuzzy_search(query, [:title, :description, :monitoring_tool, :service]) } - scope :open, -> { with_status(open_statuses) } scope :not_resolved, -> { without_status(:resolved) } scope :with_prometheus_alert, -> { includes(:prometheus_alert) } scope :with_threat_monitoring_alerts, -> { where(domain: :threat_monitoring ) } @@ -143,18 +142,6 @@ module AlertManagement reference.to_i > 0 && reference.to_i <= Gitlab::Database::MAX_INT_VALUE end - def self.open_statuses - [:triggered, :acknowledged] - end - - def self.open_status?(status) - open_statuses.include?(status) - end - - def open? - self.class.open_status?(status_name) - end - def prometheus? monitoring_tool == Gitlab::AlertManagement::Payload::MONITORING_TOOLS[:prometheus] end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index b3e23adb7d6..dbfe184c048 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -763,9 +763,7 @@ module Ci def any_runners_available? cache_for_available_runners do - ::Gitlab::Database.allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/339937') do - project.active_runners.exists? - end + project.active_runners.exists? end end diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index 35616a542c1..1c469377231 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -141,16 +141,9 @@ module Ci project_groups = ::Group.joins(:projects).where(projects: { id: project_id }) - if Feature.enabled?(:ci_decompose_belonging_to_parent_group_of_project_query, default_enabled: :yaml) - belonging_to_group(project_groups.self_and_ancestors.pluck(:id)) - else - joins(:groups) - .where(namespaces: { id: project_groups.self_and_ancestors.as_ids }) - .allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/336433') - end + belonging_to_group(project_groups.self_and_ancestors.pluck(:id)) } - # deprecated scope :owned_or_instance_wide, -> (project_id) do from_union( [ @@ -159,7 +152,7 @@ module Ci instance_type ], remove_duplicates: false - ).allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/336433') + ) end scope :assignable_for, ->(project) do diff --git a/app/models/concerns/incident_management/escalatable.rb b/app/models/concerns/incident_management/escalatable.rb index 81eef50603a..a9e4a066e0e 100644 --- a/app/models/concerns/incident_management/escalatable.rb +++ b/app/models/concerns/incident_management/escalatable.rb @@ -27,6 +27,8 @@ module IncidentManagement ignored: 'No action will be taken' }.freeze + OPEN_STATUSES = [:triggered, :acknowledged].freeze + included do validates :status, presence: true @@ -34,6 +36,7 @@ module IncidentManagement # Descending sort order sorts statuses: Triggered > Acknowledged > Resolved > Ignored # https://gitlab.com/gitlab-org/gitlab/-/issues/221242#what-is-the-expected-correct-behavior scope :order_status, -> (sort_order) { order(status: sort_order == :asc ? :desc : :asc) } + scope :open, -> { with_status(OPEN_STATUSES) } state_machine :status, initial: :triggered do state :triggered, value: STATUSES[:triggered] @@ -89,6 +92,10 @@ module IncidentManagement @status_names ||= state_machine_statuses.keys end + def open_status?(status) + OPEN_STATUSES.include?(status) + end + private def state_machine_statuses @@ -99,6 +106,10 @@ module IncidentManagement def status_event_for(status) self.class.state_machines[:status].events.transitions_for(self, to: status.to_s.to_sym).first&.event end + + def open? + self.class.open_status?(status_name) + end end end end diff --git a/app/models/project.rb b/app/models/project.rb index 81ee1c1fe55..a553246ed79 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -1775,17 +1775,12 @@ class Project < ApplicationRecord def all_runners Ci::Runner.from_union([runners, group_runners, shared_runners]) - .allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/339937') end def all_available_runners Ci::Runner.from_union([runners, group_runners, available_shared_runners]) - .allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/339937') end - # Once issue 339937 is fixed, please search for all mentioned of - # https://gitlab.com/gitlab-org/gitlab/-/issues/339937, - # and remove the allow_cross_joins_across_databases. def active_runners strong_memoize(:active_runners) do all_available_runners.active @@ -1793,9 +1788,7 @@ class Project < ApplicationRecord end def any_online_runners?(&block) - ::Gitlab::Database.allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/339937') do - online_runners_with_tags.any?(&block) - end + online_runners_with_tags.any?(&block) end def valid_runners_token?(token) diff --git a/app/models/user.rb b/app/models/user.rb index fec37172284..3e08ac7612d 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -251,7 +251,7 @@ class User < ApplicationRecord validate :notification_email_verified, if: :notification_email_changed? validate :public_email_verified, if: :public_email_changed? validate :commit_email_verified, if: :commit_email_changed? - validate :signup_email_valid?, on: :create, if: ->(user) { !user.created_by_id } + validate :email_allowed_by_restrictions?, if: ->(user) { user.new_record? ? !user.created_by_id : user.email_changed? } validate :check_username_format, if: :username_changed? validates :theme_id, allow_nil: true, inclusion: { in: Gitlab::Themes.valid_ids, @@ -2145,14 +2145,14 @@ class User < ApplicationRecord end end - def signup_email_valid? + def email_allowed_by_restrictions? error = validate_admin_signup_restrictions(email) errors.add(:email, error) if error end def signup_email_invalid_message - _('is not allowed for sign-up.') + self.new_record? ? _('is not allowed for sign-up.') : _('is not allowed.') end def check_username_format diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb index 5338f047051..d53e136effb 100644 --- a/app/services/ci/create_pipeline_service.rb +++ b/app/services/ci/create_pipeline_service.rb @@ -95,13 +95,9 @@ module Ci .build! if pipeline.persisted? - if Feature.enabled?(:ci_publish_pipeline_events, pipeline.project, default_enabled: :yaml) - Gitlab::EventStore.publish( - Ci::PipelineCreatedEvent.new(data: { pipeline_id: pipeline.id }) - ) - else - schedule_head_pipeline_update - end + Gitlab::EventStore.publish( + Ci::PipelineCreatedEvent.new(data: { pipeline_id: pipeline.id }) + ) create_namespace_onboarding_action else @@ -141,12 +137,6 @@ module Ci commit.try(:id) end - def schedule_head_pipeline_update - pipeline.all_merge_requests.opened.each do |merge_request| - UpdateHeadPipelineForMergeRequestWorker.perform_async(merge_request.id) - end - end - def create_namespace_onboarding_action Namespaces::OnboardingPipelineCreatedWorker.perform_async(project.namespace_id) end diff --git a/app/services/ci/update_build_queue_service.rb b/app/services/ci/update_build_queue_service.rb index 146239bb7e5..2e38969c7a9 100644 --- a/app/services/ci/update_build_queue_service.rb +++ b/app/services/ci/update_build_queue_service.rb @@ -99,17 +99,15 @@ module Ci private def tick_for(build, runners) - ::Gitlab::Database.allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/339937') do - runners = runners.with_recent_runner_queue - runners = runners.with_tags if Feature.enabled?(:ci_preload_runner_tags, default_enabled: :yaml) + runners = runners.with_recent_runner_queue + runners = runners.with_tags if Feature.enabled?(:ci_preload_runner_tags, default_enabled: :yaml) - metrics.observe_active_runners(-> { runners.to_a.size }) + metrics.observe_active_runners(-> { runners.to_a.size }) - runners.each do |runner| - metrics.increment_runner_tick(runner) + runners.each do |runner| + metrics.increment_runner_tick(runner) - runner.pick_build!(build) - end + runner.pick_build!(build) end end diff --git a/app/services/incident_management/issuable_escalation_statuses/after_update_service.rb b/app/services/incident_management/issuable_escalation_statuses/after_update_service.rb new file mode 100644 index 00000000000..49d7198d7b0 --- /dev/null +++ b/app/services/incident_management/issuable_escalation_statuses/after_update_service.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +module IncidentManagement + module IssuableEscalationStatuses + class AfterUpdateService < ::BaseProjectService + def initialize(issuable, current_user) + @issuable = issuable + @escalation_status = issuable.escalation_status + @alert = issuable.alert_management_alert + + super(project: issuable.project, current_user: current_user) + end + + def execute + after_update + + ServiceResponse.success(payload: { escalation_status: escalation_status }) + end + + private + + attr_reader :issuable, :escalation_status, :alert + + def after_update + sync_to_alert + end + + def sync_to_alert + return unless alert + return unless escalation_status.status_previously_changed? + + ::AlertManagement::Alerts::UpdateService.new( + alert, + current_user, + status: escalation_status.status_name + ).execute + end + end + end +end + +::IncidentManagement::IssuableEscalationStatuses::AfterUpdateService.prepend_mod diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb index 083e422f164..66a1f0c8756 100644 --- a/app/services/issues/update_service.rb +++ b/app/services/issues/update_service.rb @@ -213,13 +213,8 @@ module Issues def handle_escalation_status_change(issue, old_escalation_status) return unless old_escalation_status.present? return if issue.escalation_status&.slice(:status, :policy_id) == old_escalation_status - return unless issue.alert_management_alert - ::AlertManagement::Alerts::UpdateService.new( - issue.alert_management_alert, - current_user, - status: issue.escalation_status.status_name - ).execute + ::IncidentManagement::IssuableEscalationStatuses::AfterUpdateService.new(issue, current_user).execute end # rubocop: disable CodeReuse/ActiveRecord diff --git a/app/views/admin/deploy_keys/index.html.haml b/app/views/admin/deploy_keys/index.html.haml index ba4abdc02e4..de2a737faa1 100644 --- a/app/views/admin/deploy_keys/index.html.haml +++ b/app/views/admin/deploy_keys/index.html.haml @@ -1,44 +1,3 @@ - page_title _('Deploy Keys') -- if Feature.enabled?(:admin_deploy_keys_vue, default_enabled: :yaml) - #js-admin-deploy-keys-table{ data: admin_deploy_keys_data } -- else - - if @deploy_keys.any? - %h3.page-title.deploy-keys-title - = _('Public deploy keys (%{deploy_keys_count})') % { deploy_keys_count: @deploy_keys.load.size } - = link_to _('New deploy key'), new_admin_deploy_key_path, class: 'float-right btn gl-button btn-confirm btn-md gl-button' - %table.table.b-table.gl-table.b-table-stacked-lg{ data: { testid: 'deploy-keys-list' } } - %thead - %tr - %th= _('Title') - %th= _('Fingerprint') - %th= _('Projects with write access') - %th= _('Created') - %th.gl-lg-w-1px.gl-white-space-nowrap - %span.gl-sr-only - = _('Actions') - %tbody - - @deploy_keys.each do |deploy_key| - %tr - %td{ data: { label: _('Title') } } - %div - = deploy_key.title - %td{ data: { label: _('Fingerprint') } } - %div - %code= deploy_key.fingerprint - %td{ data: { label: _('Projects with write access') } } - %div - - deploy_key.projects_with_write_access.each do |project| - = link_to project.full_name, admin_project_path(project), class: 'gl-display-block' - %td{ data: { label: _('Created') } } - %div - = time_ago_with_tooltip(deploy_key.created_at) - %td.gl-lg-w-1px.gl-white-space-nowrap{ data: { label: _('Actions') } } - %div - = link_to edit_admin_deploy_key_path(deploy_key), class: 'btn btn-default btn-md gl-button btn-icon gl-mr-3', aria: { label: _('Edit deploy key') } do - = sprite_icon('pencil', css_class: 'gl-button-icon') - = link_to admin_deploy_key_path(deploy_key), data: { confirm: _('Are you sure?') }, method: :delete, class: 'btn btn-danger btn-md gl-button btn-icon', aria: { label: _('Remove deploy key') } do - = sprite_icon('remove', css_class: 'gl-button-icon') - - - else - = render 'shared/empty_states/deploy_keys' +#js-admin-deploy-keys-table{ data: admin_deploy_keys_data } diff --git a/app/views/groups/packages/index.html.haml b/app/views/groups/packages/index.html.haml index d56a806f082..1c0627779ec 100644 --- a/app/views/groups/packages/index.html.haml +++ b/app/views/groups/packages/index.html.haml @@ -7,4 +7,7 @@ full_path: @group.full_path, endpoint: group_packages_path(@group), page_type: 'groups', - empty_list_illustration: image_path('illustrations/no-packages.svg'), } } + empty_list_illustration: image_path('illustrations/no-packages.svg'), + npm_instance_url: package_registry_instance_url(:npm), + project_list_url: '', + group_list_url: group_packages_path(@group) } } diff --git a/app/views/projects/packages/packages/index.html.haml b/app/views/projects/packages/packages/index.html.haml index c67b06218e2..4ab16f25dd2 100644 --- a/app/views/projects/packages/packages/index.html.haml +++ b/app/views/projects/packages/packages/index.html.haml @@ -7,4 +7,7 @@ full_path: @project.full_path, endpoint: project_packages_path(@project), page_type: 'projects', - empty_list_illustration: image_path('illustrations/no-packages.svg'), } } + empty_list_illustration: image_path('illustrations/no-packages.svg'), + npm_instance_url: package_registry_instance_url(:npm), + project_list_url: project_packages_path(@project), + group_list_url: '' } } diff --git a/app/views/projects/packages/packages/show.html.haml b/app/views/projects/packages/packages/show.html.haml deleted file mode 100644 index ebdc9e654f6..00000000000 --- a/app/views/projects/packages/packages/show.html.haml +++ /dev/null @@ -1,9 +0,0 @@ -- add_to_breadcrumbs _("Package Registry"), project_packages_path(@project) -- add_to_breadcrumbs @package.name, project_packages_path(@project) -- breadcrumb_title @package.version -- page_title _("Package Registry") -- @content_class = "limit-container-width" unless fluid_layout - -.row - .col-12 - #js-vue-packages-detail-new{ data: package_details_data(@project, @package) } diff --git a/app/views/shared/empty_states/_deploy_keys.html.haml b/app/views/shared/empty_states/_deploy_keys.html.haml deleted file mode 100644 index 6c615de9c56..00000000000 --- a/app/views/shared/empty_states/_deploy_keys.html.haml +++ /dev/null @@ -1,9 +0,0 @@ -.empty-state.gl-display-flex.gl-flex-direction-column.gl-flex-wrap.gl-text-center - .gl-flex-grow-0.gl-flex-shrink-0 - .svg-250.svg-content - = image_tag 'illustrations/empty-state/empty-deploy-keys-lg.svg' - .gl-flex-grow-0.gl-flex-shrink-0 - .text-content.gl-mx-auto.gl-my-0.gl-p-5 - %h4.h4= _('Deploy Keys') - %p= _('Deploy keys grant read/write access to all repositories in your instance') - = link_to _('New deploy key'), new_admin_deploy_key_path, class: 'gl-button btn btn-confirm btn-md' diff --git a/config/feature_flags/development/admin_deploy_keys_vue.yml b/config/feature_flags/development/admin_deploy_keys_vue.yml deleted file mode 100644 index 21e1b501d7a..00000000000 --- a/config/feature_flags/development/admin_deploy_keys_vue.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: admin_deploy_keys_vue -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/73580 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/344855 -milestone: '14.5' -type: development -group: group::access -default_enabled: true diff --git a/config/feature_flags/development/ci_decompose_belonging_to_parent_group_of_project_query.yml b/config/feature_flags/development/ci_decompose_belonging_to_parent_group_of_project_query.yml deleted file mode 100644 index 98950c0ccb1..00000000000 --- a/config/feature_flags/development/ci_decompose_belonging_to_parent_group_of_project_query.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: ci_decompose_belonging_to_parent_group_of_project_query -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/76454 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/348560 -milestone: '14.7' -type: development -group: group::pipeline execution -default_enabled: false diff --git a/config/feature_flags/development/ci_publish_pipeline_events.yml b/config/feature_flags/development/ci_publish_pipeline_events.yml deleted file mode 100644 index 2d47084f499..00000000000 --- a/config/feature_flags/development/ci_publish_pipeline_events.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: ci_publish_pipeline_events -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/34042 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/336752 -milestone: '14.3' -type: development -group: group::pipeline execution -default_enabled: false diff --git a/config/feature_flags/development/use_primary_and_secondary_stores_for_sessions.yml b/config/feature_flags/development/use_primary_and_secondary_stores_for_sessions.yml deleted file mode 100644 index 2204472c0a6..00000000000 --- a/config/feature_flags/development/use_primary_and_secondary_stores_for_sessions.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: use_primary_and_secondary_stores_for_sessions -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/73660 -rollout_issue_url: https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/1429 -milestone: '14.6' -type: development -group: group::memory -default_enabled: false diff --git a/config/feature_flags/development/use_primary_store_as_default_for_sessions.yml b/config/feature_flags/development/use_primary_store_as_default_for_sessions.yml deleted file mode 100644 index ac130ab7761..00000000000 --- a/config/feature_flags/development/use_primary_store_as_default_for_sessions.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: use_primary_store_as_default_for_sessions -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/75258 -rollout_issue_url: -milestone: '14.6' -type: development -group: group::memory -default_enabled: false diff --git a/config/metrics/schema.json b/config/metrics/schema.json index d416c7b6d6e..09376e32ef0 100644 --- a/config/metrics/schema.json +++ b/config/metrics/schema.json @@ -30,7 +30,7 @@ }, "status": { "type": ["string"], - "enum": ["active", "deprecated", "removed", "broken"] + "enum": ["active", "removed", "broken"] }, "milestone": { "type": ["string"], diff --git a/config/routes/group.rb b/config/routes/group.rb index da205163e6d..f7a8747d0cf 100644 --- a/config/routes/group.rb +++ b/config/routes/group.rb @@ -64,7 +64,7 @@ constraints(::Constraints::GroupUrlConstrainer.new) do post :toggle_subscription, on: :member end - resources :packages, only: [:index] + resources :packages, only: [:index, :show] resources :milestones, constraints: { id: %r{[^/]+} } do member do diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml index 710bde34fe7..80c49660bdb 100644 --- a/config/sidekiq_queues.yml +++ b/config/sidekiq_queues.yml @@ -211,6 +211,8 @@ - 1 - - incident_management_pending_escalations_issue_check - 1 +- - incident_management_pending_escalations_issue_create + - 1 - - integrations_create_external_cross_reference - 1 - - invalid_gpg_signature_update diff --git a/db/post_migrate/20211217174331_mark_recalculate_finding_signatures_as_completed.rb b/db/post_migrate/20211217174331_mark_recalculate_finding_signatures_as_completed.rb new file mode 100644 index 00000000000..316209ae1f4 --- /dev/null +++ b/db/post_migrate/20211217174331_mark_recalculate_finding_signatures_as_completed.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +class MarkRecalculateFindingSignaturesAsCompleted < Gitlab::Database::Migration[1.0] + MIGRATION = 'RecalculateVulnerabilitiesOccurrencesUuid' + + def up + # Only run migration for Gitlab.com + return unless ::Gitlab.com? + + # In previous migration marking jobs as successful was missed + Gitlab::Database::BackgroundMigrationJob + .for_migration_class(MIGRATION) + .pending + .update_all(status: :succeeded) + end + + def down + # no-op + end +end diff --git a/db/schema_migrations/20211217174331 b/db/schema_migrations/20211217174331 new file mode 100644 index 00000000000..32657e28f96 --- /dev/null +++ b/db/schema_migrations/20211217174331 @@ -0,0 +1 @@ +649360f4069aac4784f4d039015f8dda3f4bae28e8132f841e25b48f034a392e \ No newline at end of file diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 645748d061e..80c9e94415a 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -2962,6 +2962,27 @@ Input type: `IssueSetEscalationPolicyInput` | `errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | | `issue` | [`Issue`](#issue) | Issue after mutation. | +### `Mutation.issueSetEscalationStatus` + +Input type: `IssueSetEscalationStatusInput` + +#### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| `iid` | [`String!`](#string) | IID of the issue to mutate. | +| `projectPath` | [`ID!`](#id) | Project the issue to mutate is in. | +| `status` | [`IssueEscalationStatus!`](#issueescalationstatus) | Set the escalation status. | + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| `errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | +| `issue` | [`Issue`](#issue) | Issue after mutation. | + ### `Mutation.issueSetIteration` Input type: `IssueSetIterationInput` @@ -10317,6 +10338,7 @@ Relationship between an epic and an issue. | `epic` | [`Epic`](#epic) | Epic to which this issue belongs. | | `epicIssueId` | [`ID!`](#id) | ID of the epic-issue relation. | | `escalationPolicy` | [`EscalationPolicyType`](#escalationpolicytype) | Escalation policy associated with the issue. Available for issues which support escalation. | +| `escalationStatus` | [`IssueEscalationStatus`](#issueescalationstatus) | Escalation status of the issue. | | `healthStatus` | [`HealthStatus`](#healthstatus) | Current health status. | | `hidden` | [`Boolean`](#boolean) | Indicates the issue is hidden because the author has been banned. Will always return `null` if `ban_user_feature_flag` feature flag is disabled. | | `humanTimeEstimate` | [`String`](#string) | Human-readable time estimate of the issue. | @@ -11500,6 +11522,7 @@ Returns [`VulnerabilitySeveritiesCount`](#vulnerabilityseveritiescount). | `emailsDisabled` | [`Boolean!`](#boolean) | Indicates if a project has email notifications disabled: `true` if email notifications are disabled. | | `epic` | [`Epic`](#epic) | Epic to which this issue belongs. | | `escalationPolicy` | [`EscalationPolicyType`](#escalationpolicytype) | Escalation policy associated with the issue. Available for issues which support escalation. | +| `escalationStatus` | [`IssueEscalationStatus`](#issueescalationstatus) | Escalation status of the issue. | | `healthStatus` | [`HealthStatus`](#healthstatus) | Current health status. | | `hidden` | [`Boolean`](#boolean) | Indicates the issue is hidden because the author has been banned. Will always return `null` if `ban_user_feature_flag` feature flag is disabled. | | `humanTimeEstimate` | [`String`](#string) | Human-readable time estimate of the issue. | @@ -16821,6 +16844,17 @@ Iteration ID wildcard values for issue creation. | ----- | ----------- | | `CURRENT` | Current iteration. | +### `IssueEscalationStatus` + +Issue escalation status values. + +| Value | Description | +| ----- | ----------- | +| `ACKNOWLEDGED` | Someone is actively investigating the problem. | +| `IGNORED` | No action will be taken. | +| `RESOLVED` | The problem has been addressed. | +| `TRIGGERED` | Investigation has not started. | + ### `IssueSort` Values for sorting issues. diff --git a/doc/user/admin_area/license.md b/doc/user/admin_area/license.md index d5087c20e6f..c3f0c94db21 100644 --- a/doc/user/admin_area/license.md +++ b/doc/user/admin_area/license.md @@ -94,7 +94,7 @@ a license, upload the license in the **Admin Area** in the web user interface. ## What happens when your license expires -One month before the license expires, a message with the upcoming expiration +Fifteen days before the license expires, a notification banner with the upcoming expiration date displays to GitLab administrators. When your license expires, GitLab locks features, like Git pushes diff --git a/lib/gitlab/redis/multi_store.rb b/lib/gitlab/redis/multi_store.rb deleted file mode 100644 index ff7758f3e53..00000000000 --- a/lib/gitlab/redis/multi_store.rb +++ /dev/null @@ -1,232 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module Redis - class MultiStore - include Gitlab::Utils::StrongMemoize - - class ReadFromPrimaryError < StandardError - def message - 'Value not found on the redis primary store. Read from the redis secondary store successful.' - end - end - class MethodMissingError < StandardError - def message - 'Method missing. Falling back to execute method on the redis secondary store.' - end - end - - attr_reader :primary_store, :secondary_store, :instance_name - - FAILED_TO_READ_ERROR_MESSAGE = 'Failed to read from the redis primary_store.' - FAILED_TO_WRITE_ERROR_MESSAGE = 'Failed to write to the redis primary_store.' - - SKIP_LOG_METHOD_MISSING_FOR_COMMANDS = %i(info).freeze - - READ_COMMANDS = %i( - get - mget - smembers - scard - ).freeze - - WRITE_COMMANDS = %i( - set - setnx - setex - sadd - srem - del - pipelined - flushdb - ).freeze - - def initialize(primary_store, secondary_store, instance_name) - @primary_store = primary_store - @secondary_store = secondary_store - @instance_name = instance_name - - validate_stores! - end - # rubocop:disable GitlabSecurity/PublicSend - READ_COMMANDS.each do |name| - define_method(name) do |*args, &block| - if use_primary_and_secondary_stores? - read_command(name, *args, &block) - else - default_store.send(name, *args, &block) - end - end - end - - WRITE_COMMANDS.each do |name| - define_method(name) do |*args, &block| - if use_primary_and_secondary_stores? - write_command(name, *args, &block) - else - default_store.send(name, *args, &block) - end - end - end - - def method_missing(...) - return @instance.send(...) if @instance - - log_method_missing(...) - - default_store.send(...) - end - # rubocop:enable GitlabSecurity/PublicSend - - def respond_to_missing?(command_name, include_private = false) - true - end - - # This is needed because of Redis::Rack::Connection is requiring Redis::Store - # https://github.com/redis-store/redis-rack/blob/a833086ba494083b6a384a1a4e58b36573a9165d/lib/redis/rack/connection.rb#L15 - # Done similarly in https://github.com/lsegal/yard/blob/main/lib/yard/templates/template.rb#L122 - def is_a?(klass) - return true if klass == default_store.class - - super(klass) - end - alias_method :kind_of?, :is_a? - - def to_s - use_primary_and_secondary_stores? ? primary_store.to_s : default_store.to_s - end - - def use_primary_and_secondary_stores? - feature_table_exists? && Feature.enabled?("use_primary_and_secondary_stores_for_#{instance_name.underscore}", default_enabled: :yaml) && !same_redis_store? - end - - def use_primary_store_as_default? - feature_table_exists? && Feature.enabled?("use_primary_store_as_default_for_#{instance_name.underscore}", default_enabled: :yaml) && !same_redis_store? - end - - private - - # @return [Boolean] - def feature_table_exists? - Feature::FlipperFeature.table_exists? - rescue StandardError - false - end - - def default_store - use_primary_store_as_default? ? primary_store : secondary_store - end - - def log_method_missing(command_name, *_args) - return if SKIP_LOG_METHOD_MISSING_FOR_COMMANDS.include?(command_name) - - log_error(MethodMissingError.new, command_name) - increment_method_missing_count(command_name) - end - - def read_command(command_name, *args, &block) - if @instance - send_command(@instance, command_name, *args, &block) - else - read_one_with_fallback(command_name, *args, &block) - end - end - - def write_command(command_name, *args, &block) - if @instance - send_command(@instance, command_name, *args, &block) - else - write_both(command_name, *args, &block) - end - end - - def read_one_with_fallback(command_name, *args, &block) - begin - value = send_command(primary_store, command_name, *args, &block) - rescue StandardError => e - log_error(e, command_name, - multi_store_error_message: FAILED_TO_READ_ERROR_MESSAGE) - end - - value ||= fallback_read(command_name, *args, &block) - - value - end - - def fallback_read(command_name, *args, &block) - value = send_command(secondary_store, command_name, *args, &block) - - if value - log_error(ReadFromPrimaryError.new, command_name) - increment_read_fallback_count(command_name) - end - - value - end - - def write_both(command_name, *args, &block) - begin - send_command(primary_store, command_name, *args, &block) - rescue StandardError => e - log_error(e, command_name, - multi_store_error_message: FAILED_TO_WRITE_ERROR_MESSAGE) - end - - send_command(secondary_store, command_name, *args, &block) - end - - def same_redis_store? - strong_memoize(:same_redis_store) do - # " - primary_store.inspect == secondary_store.inspect - end - end - - # rubocop:disable GitlabSecurity/PublicSend - def send_command(redis_instance, command_name, *args, &block) - if block_given? - # Make sure that block is wrapped and executed only on the redis instance that is executing the block - redis_instance.send(command_name, *args) do |*params| - with_instance(redis_instance, *params, &block) - end - else - redis_instance.send(command_name, *args) - end - end - # rubocop:enable GitlabSecurity/PublicSend - - def with_instance(instance, *params) - @instance = instance - - yield(*params) - ensure - @instance = nil - end - - def increment_read_fallback_count(command_name) - @read_fallback_counter ||= Gitlab::Metrics.counter(:gitlab_redis_multi_store_read_fallback_total, 'Client side Redis MultiStore reading fallback') - @read_fallback_counter.increment(command: command_name, instance_name: instance_name) - end - - def increment_method_missing_count(command_name) - @method_missing_counter ||= Gitlab::Metrics.counter(:gitlab_redis_multi_store_method_missing_total, 'Client side Redis MultiStore method missing') - @method_missing_counter.increment(command: command_name, instance_name: instance_name) - end - - def validate_stores! - raise ArgumentError, 'primary_store is required' unless primary_store - raise ArgumentError, 'secondary_store is required' unless secondary_store - raise ArgumentError, 'instance_name is required' unless instance_name - raise ArgumentError, 'invalid primary_store' unless primary_store.is_a?(::Redis) - raise ArgumentError, 'invalid secondary_store' unless secondary_store.is_a?(::Redis) - end - - def log_error(exception, command_name, extra = {}) - Gitlab::ErrorTracking.log_exception( - exception, - command_name: command_name, - extra: extra.merge(instance_name: instance_name)) - end - end - end -end diff --git a/lib/gitlab/redis/sessions.rb b/lib/gitlab/redis/sessions.rb index c547828d907..ddcfdf6e798 100644 --- a/lib/gitlab/redis/sessions.rb +++ b/lib/gitlab/redis/sessions.rb @@ -9,39 +9,9 @@ module Gitlab IP_SESSIONS_LOOKUP_NAMESPACE = 'session:lookup:ip:gitlab2' OTP_SESSIONS_NAMESPACE = 'session:otp' - class << self - # The data we store on Sessions used to be stored on SharedState. - def config_fallback - SharedState - end - - private - - def redis - # Don't use multistore if redis.sessions configuration is not provided - return super if config_fallback? - - primary_store = ::Redis.new(params) - secondary_store = ::Redis.new(config_fallback.params) - - MultiStore.new(primary_store, secondary_store, store_name) - end - end - - def store(extras = {}) - # Don't use multistore if redis.sessions configuration is not provided - return super if self.class.config_fallback? - - primary_store = create_redis_store(redis_store_options, extras) - secondary_store = create_redis_store(self.class.config_fallback.params, extras) - - MultiStore.new(primary_store, secondary_store, self.class.store_name) - end - - private - - def create_redis_store(options, extras) - ::Redis::Store.new(options.merge(extras)) + # The data we store on Sessions used to be stored on SharedState. + def self.config_fallback + SharedState end end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index eef2ca6eaaf..adc6002547b 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -7485,6 +7485,12 @@ msgstr "" msgid "Closes this %{quick_action_target}." msgstr "" +msgid "Cloud Run" +msgstr "" + +msgid "Cloud Storage" +msgstr "" + msgid "Cluster" msgstr "" @@ -9034,6 +9040,9 @@ msgstr "" msgid "Configure existing installation" msgstr "" +msgid "Configure pipelines to deploy web apps, backend services, APIs and static resources to Google Cloud" +msgstr "" + msgid "Configure repository mirroring." msgstr "" @@ -9061,6 +9070,9 @@ msgstr "" msgid "Configure the way a user creates a new account." msgstr "" +msgid "Configure via Merge Request" +msgstr "" + msgid "Configure which lists are shown for anyone who visits this board" msgstr "" @@ -11737,6 +11749,9 @@ msgstr[1] "" msgid "Deploy Keys" msgstr "" +msgid "Deploy container based web apps on Google managed clusters" +msgstr "" + msgid "Deploy freezes" msgstr "" @@ -11752,6 +11767,9 @@ msgstr "" msgid "Deploy progress not found. To see pods, ensure your environment matches %{linkStart}deploy board criteria%{linkEnd}." msgstr "" +msgid "Deploy static assets and resources to Google managed CDN" +msgstr "" + msgid "Deploy to..." msgstr "" @@ -28930,9 +28948,6 @@ msgstr "" msgid "Public deploy keys" msgstr "" -msgid "Public deploy keys (%{deploy_keys_count})" -msgstr "" - msgid "Public pipelines" msgstr "" @@ -30726,10 +30741,13 @@ msgstr "" msgid "Runners|Offline" msgstr "" +msgid "Runners|Offline runners" +msgstr "" + msgid "Runners|Online" msgstr "" -msgid "Runners|Online Runners" +msgid "Runners|Online runners" msgstr "" msgid "Runners|Paused" @@ -30825,6 +30843,9 @@ msgstr "" msgid "Runners|Stale" msgstr "" +msgid "Runners|Stale runners" +msgstr "" + msgid "Runners|Status" msgstr "" @@ -42268,6 +42289,9 @@ msgstr "" msgid "is not allowed since the group is not top-level group." msgstr "" +msgid "is not allowed." +msgstr "" + msgid "is not allowed. We do not currently support project-level iterations" msgstr "" diff --git a/qa/qa/page/project/packages/show.rb b/qa/qa/page/project/packages/show.rb index 4872c0bc705..5ba9ad7df40 100644 --- a/qa/qa/page/project/packages/show.rb +++ b/qa/qa/page/project/packages/show.rb @@ -5,7 +5,7 @@ module QA module Project module Packages class Show < QA::Page::Base - view 'app/assets/javascripts/packages_and_registries/package_registry/components/details/app.vue' do + view 'app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue' do element :delete_button element :delete_modal_button element :package_information_content diff --git a/spec/controllers/groups/packages_controller_spec.rb b/spec/controllers/groups/packages_controller_spec.rb new file mode 100644 index 00000000000..fc9b79da47c --- /dev/null +++ b/spec/controllers/groups/packages_controller_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Groups::PackagesController do + let_it_be(:group) { create(:group) } + + let(:page) { :index } + let(:additional_parameters) { {} } + + subject do + get page, params: additional_parameters.merge({ + group_id: group + }) + end + + context 'GET #index' do + it_behaves_like 'returning response status', :ok + end + + context 'GET #show' do + let(:page) { :show } + let(:additional_parameters) { { id: 1 } } + + it_behaves_like 'returning response status', :ok + end +end diff --git a/spec/controllers/projects/packages/packages_controller_spec.rb b/spec/controllers/projects/packages/packages_controller_spec.rb new file mode 100644 index 00000000000..da9cae47c62 --- /dev/null +++ b/spec/controllers/projects/packages/packages_controller_spec.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Projects::Packages::PackagesController do + let_it_be(:project) { create(:project, :public) } + + let(:page) { :index } + let(:additional_parameters) { {} } + + subject do + get page, params: additional_parameters.merge({ + project_id: project, + namespace_id: project.namespace + }) + end + + context 'GET #index' do + it_behaves_like 'returning response status', :ok + end + + context 'GET #show' do + let(:page) { :show } + let(:additional_parameters) { { id: 1 } } + + it_behaves_like 'returning response status', :ok + end +end diff --git a/spec/features/admin/admin_deploy_keys_spec.rb b/spec/features/admin/admin_deploy_keys_spec.rb index 9b74aa2ac5a..88b8fcd8d5e 100644 --- a/spec/features/admin/admin_deploy_keys_spec.rb +++ b/spec/features/admin/admin_deploy_keys_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe 'admin deploy keys' do +RSpec.describe 'admin deploy keys', :js do include Spec::Support::Helpers::ModalHelpers let_it_be(:admin) { create(:admin) } @@ -15,112 +15,81 @@ RSpec.describe 'admin deploy keys' do gitlab_enable_admin_mode_sign_in(admin) end - shared_examples 'renders deploy keys correctly' do - it 'show all public deploy keys' do - visit admin_deploy_keys_path + it 'show all public deploy keys' do + visit admin_deploy_keys_path - page.within(find('[data-testid="deploy-keys-list"]', match: :first)) do - expect(page).to have_content(deploy_key.title) - expect(page).to have_content(another_deploy_key.title) - end - end - - it 'shows all the projects the deploy key has write access' do - write_key = create(:deploy_keys_project, :write_access, deploy_key: deploy_key) - - visit admin_deploy_keys_path - - page.within(find('[data-testid="deploy-keys-list"]', match: :first)) do - expect(page).to have_content(write_key.project.full_name) - end - end - - describe 'create a new deploy key' do - let(:new_ssh_key) { attributes_for(:key)[:key] } - - before do - visit admin_deploy_keys_path - click_link 'New deploy key' - end - - it 'creates a new deploy key' do - fill_in 'deploy_key_title', with: 'laptop' - fill_in 'deploy_key_key', with: new_ssh_key - click_button 'Create' - - expect(current_path).to eq admin_deploy_keys_path - - page.within(find('[data-testid="deploy-keys-list"]', match: :first)) do - expect(page).to have_content('laptop') - end - end - end - - describe 'update an existing deploy key' do - before do - visit admin_deploy_keys_path - page.within('tr', text: deploy_key.title) do - click_link(_('Edit deploy key')) - end - end - - it 'updates an existing deploy key' do - fill_in 'deploy_key_title', with: 'new-title' - click_button 'Save changes' - - expect(current_path).to eq admin_deploy_keys_path - - page.within(find('[data-testid="deploy-keys-list"]', match: :first)) do - expect(page).to have_content('new-title') - end - end + page.within(find('[data-testid="deploy-keys-list"]', match: :first)) do + expect(page).to have_content(deploy_key.title) + expect(page).to have_content(another_deploy_key.title) end end - context 'when `admin_deploy_keys_vue` feature flag is enabled', :js do - it_behaves_like 'renders deploy keys correctly' + it 'shows all the projects the deploy key has write access' do + write_key = create(:deploy_keys_project, :write_access, deploy_key: deploy_key) - describe 'remove an existing deploy key' do - before do - visit admin_deploy_keys_path - end + visit admin_deploy_keys_path - it 'removes an existing deploy key' do - accept_gl_confirm('Are you sure you want to delete this deploy key?', button_text: 'Delete') do - page.within('tr', text: deploy_key.title) do - click_button _('Delete deploy key') - end - end - - expect(current_path).to eq admin_deploy_keys_path - page.within(find('[data-testid="deploy-keys-list"]', match: :first)) do - expect(page).not_to have_content(deploy_key.title) - end - end + page.within(find('[data-testid="deploy-keys-list"]', match: :first)) do + expect(page).to have_content(write_key.project.full_name) end end - context 'when `admin_deploy_keys_vue` feature flag is disabled' do + describe 'create a new deploy key' do + let(:new_ssh_key) { attributes_for(:key)[:key] } + before do - stub_feature_flags(admin_deploy_keys_vue: false) + visit admin_deploy_keys_path + click_link 'New deploy key' end - it_behaves_like 'renders deploy keys correctly' + it 'creates a new deploy key' do + fill_in 'deploy_key_title', with: 'laptop' + fill_in 'deploy_key_key', with: new_ssh_key + click_button 'Create' - describe 'remove an existing deploy key' do - before do - visit admin_deploy_keys_path + expect(current_path).to eq admin_deploy_keys_path + + page.within(find('[data-testid="deploy-keys-list"]', match: :first)) do + expect(page).to have_content('laptop') + end + end + end + + describe 'update an existing deploy key' do + before do + visit admin_deploy_keys_path + page.within('tr', text: deploy_key.title) do + click_link(_('Edit deploy key')) + end + end + + it 'updates an existing deploy key' do + fill_in 'deploy_key_title', with: 'new-title' + click_button 'Save changes' + + expect(current_path).to eq admin_deploy_keys_path + + page.within(find('[data-testid="deploy-keys-list"]', match: :first)) do + expect(page).to have_content('new-title') + end + end + end + + describe 'remove an existing deploy key' do + before do + visit admin_deploy_keys_path + end + + it 'removes an existing deploy key' do + accept_gl_confirm('Are you sure you want to delete this deploy key?', button_text: 'Delete') do + page.within('tr', text: deploy_key.title) do + click_button _('Delete deploy key') + end end - it 'removes an existing deploy key' do - page.within('tr', text: deploy_key.title) do - click_link _('Remove deploy key') - end - - expect(current_path).to eq admin_deploy_keys_path - page.within(find('[data-testid="deploy-keys-list"]', match: :first)) do - expect(page).not_to have_content(deploy_key.title) - end + expect(current_path).to eq admin_deploy_keys_path + page.within(find('[data-testid="deploy-keys-list"]', match: :first)) do + expect(page).not_to have_content(deploy_key.title) end end end diff --git a/spec/features/admin/admin_runners_spec.rb b/spec/features/admin/admin_runners_spec.rb index c53948ee6f3..ceb91b86876 100644 --- a/spec/features/admin/admin_runners_spec.rb +++ b/spec/features/admin/admin_runners_spec.rb @@ -21,12 +21,16 @@ RSpec.describe "Admin Runners" do context "when there are runners" do it 'has all necessary texts' do - create(:ci_runner, :instance, contacted_at: Time.now) + create(:ci_runner, :instance, created_at: 1.year.ago, contacted_at: Time.now) + create(:ci_runner, :instance, created_at: 1.year.ago, contacted_at: 1.week.ago) + create(:ci_runner, :instance, created_at: 1.year.ago, contacted_at: 1.year.ago) visit admin_runners_path expect(page).to have_text "Register an instance runner" - expect(page).to have_text "Online Runners 1" + expect(page).to have_text "Online runners 1" + expect(page).to have_text "Offline runners 2" + expect(page).to have_text "Stale runners 1" end it 'with an instance runner shows an instance badge' do @@ -387,7 +391,11 @@ RSpec.describe "Admin Runners" do it 'has all necessary texts including no runner message' do expect(page).to have_text "Register an instance runner" - expect(page).to have_text "Online Runners 0" + + expect(page).to have_text "Online runners 0" + expect(page).to have_text "Offline runners 0" + expect(page).to have_text "Stale runners 0" + expect(page).to have_text 'No runners found' end @@ -451,7 +459,9 @@ RSpec.describe "Admin Runners" do before do click_on 'Reset registration token' - page.accept_alert + within_modal do + click_button('OK', match: :first) + end wait_for_requests end diff --git a/spec/features/groups/packages_spec.rb b/spec/features/groups/packages_spec.rb index 3c2ade6b274..26338b03349 100644 --- a/spec/features/groups/packages_spec.rb +++ b/spec/features/groups/packages_spec.rb @@ -42,6 +42,9 @@ RSpec.describe 'Group Packages' do let_it_be(:maven_package) { create(:maven_package, project: second_project, name: 'aaa', created_at: 2.days.ago, version: '2.0.0') } let_it_be(:packages) { [npm_package, maven_package] } + let(:package) { packages.first } + let(:package_details_path) { group_package_path(group, package) } + it_behaves_like 'packages list', check_project_name: true it_behaves_like 'package details link' diff --git a/spec/features/projects/packages_spec.rb b/spec/features/projects/packages_spec.rb index 7fcc8200b1c..8180f6b9aff 100644 --- a/spec/features/projects/packages_spec.rb +++ b/spec/features/projects/packages_spec.rb @@ -35,6 +35,9 @@ RSpec.describe 'Packages' do let_it_be(:maven_package) { create(:maven_package, project: project, name: 'aaa', created_at: 2.days.ago, version: '2.0.0') } let_it_be(:packages) { [npm_package, maven_package] } + let(:package) { packages.first } + let(:package_details_path) { project_package_path(project, package) } + it_behaves_like 'packages list' it_behaves_like 'package details link' diff --git a/spec/frontend/fixtures/runner.rb b/spec/frontend/fixtures/runner.rb index b5dd3576e8b..36e6cf72750 100644 --- a/spec/frontend/fixtures/runner.rb +++ b/spec/frontend/fixtures/runner.rb @@ -24,99 +24,109 @@ RSpec.describe 'Runner (JavaScript fixtures)' do remove_repository(project) end - describe GraphQL::Query, type: :request do - get_runners_query_name = 'get_runners.query.graphql' - + describe do before do sign_in(admin) enable_admin_mode!(admin) end - let_it_be(:query) do - get_graphql_query_as_string("#{query_path}#{get_runners_query_name}") + describe GraphQL::Query, type: :request do + get_runners_query_name = 'get_runners.query.graphql' + + let_it_be(:query) do + get_graphql_query_as_string("#{query_path}#{get_runners_query_name}") + end + + it "#{fixtures_path}#{get_runners_query_name}.json" do + post_graphql(query, current_user: admin, variables: {}) + + expect_graphql_errors_to_be_empty + end + + it "#{fixtures_path}#{get_runners_query_name}.paginated.json" do + post_graphql(query, current_user: admin, variables: { first: 2 }) + + expect_graphql_errors_to_be_empty + end end - it "#{fixtures_path}#{get_runners_query_name}.json" do - post_graphql(query, current_user: admin, variables: {}) + describe GraphQL::Query, type: :request do + get_runners_count_query_name = 'get_runners_count.query.graphql' - expect_graphql_errors_to_be_empty + let_it_be(:query) do + get_graphql_query_as_string("#{query_path}#{get_runners_count_query_name}") + end + + it "#{fixtures_path}#{get_runners_count_query_name}.json" do + post_graphql(query, current_user: admin, variables: {}) + + expect_graphql_errors_to_be_empty + end end - it "#{fixtures_path}#{get_runners_query_name}.paginated.json" do - post_graphql(query, current_user: admin, variables: { first: 2 }) + describe GraphQL::Query, type: :request do + get_runner_query_name = 'get_runner.query.graphql' - expect_graphql_errors_to_be_empty + let_it_be(:query) do + get_graphql_query_as_string("#{query_path}#{get_runner_query_name}") + end + + it "#{fixtures_path}#{get_runner_query_name}.json" do + post_graphql(query, current_user: admin, variables: { + id: instance_runner.to_global_id.to_s + }) + + expect_graphql_errors_to_be_empty + end end end - describe GraphQL::Query, type: :request do - get_runners_count_query_name = 'get_runners_count.query.graphql' - - before do - sign_in(admin) - enable_admin_mode!(admin) - end - - let_it_be(:query) do - get_graphql_query_as_string("#{query_path}#{get_runners_count_query_name}") - end - - it "#{fixtures_path}#{get_runners_count_query_name}.json" do - post_graphql(query, current_user: admin, variables: {}) - - expect_graphql_errors_to_be_empty - end - end - - describe GraphQL::Query, type: :request do - get_runner_query_name = 'get_runner.query.graphql' - - before do - sign_in(admin) - enable_admin_mode!(admin) - end - - let_it_be(:query) do - get_graphql_query_as_string("#{query_path}#{get_runner_query_name}") - end - - it "#{fixtures_path}#{get_runner_query_name}.json" do - post_graphql(query, current_user: admin, variables: { - id: instance_runner.to_global_id.to_s - }) - - expect_graphql_errors_to_be_empty - end - end - - describe GraphQL::Query, type: :request do - get_group_runners_query_name = 'get_group_runners.query.graphql' - + describe do let_it_be(:group_owner) { create(:user) } before do group.add_owner(group_owner) end - let_it_be(:query) do - get_graphql_query_as_string("#{query_path}#{get_group_runners_query_name}") + describe GraphQL::Query, type: :request do + get_group_runners_query_name = 'get_group_runners.query.graphql' + + let_it_be(:query) do + get_graphql_query_as_string("#{query_path}#{get_group_runners_query_name}") + end + + it "#{fixtures_path}#{get_group_runners_query_name}.json" do + post_graphql(query, current_user: group_owner, variables: { + groupFullPath: group.full_path + }) + + expect_graphql_errors_to_be_empty + end + + it "#{fixtures_path}#{get_group_runners_query_name}.paginated.json" do + post_graphql(query, current_user: group_owner, variables: { + groupFullPath: group.full_path, + first: 1 + }) + + expect_graphql_errors_to_be_empty + end end - it "#{fixtures_path}#{get_group_runners_query_name}.json" do - post_graphql(query, current_user: group_owner, variables: { - groupFullPath: group.full_path - }) + describe GraphQL::Query, type: :request do + get_group_runners_count_query_name = 'get_group_runners_count.query.graphql' - expect_graphql_errors_to_be_empty - end + let_it_be(:query) do + get_graphql_query_as_string("#{query_path}#{get_group_runners_count_query_name}") + end - it "#{fixtures_path}#{get_group_runners_query_name}.paginated.json" do - post_graphql(query, current_user: group_owner, variables: { - groupFullPath: group.full_path, - first: 1 - }) + it "#{fixtures_path}#{get_group_runners_count_query_name}.json" do + post_graphql(query, current_user: group_owner, variables: { + groupFullPath: group.full_path + }) - expect_graphql_errors_to_be_empty + expect_graphql_errors_to_be_empty + end end end end diff --git a/spec/frontend/google_cloud/components/app_spec.js b/spec/frontend/google_cloud/components/app_spec.js index 570ac1e6ed1..92bc7596f7d 100644 --- a/spec/frontend/google_cloud/components/app_spec.js +++ b/spec/frontend/google_cloud/components/app_spec.js @@ -24,6 +24,8 @@ const HOME_PROPS = { serviceAccounts: [{}, {}], createServiceAccountUrl: '#url-create-service-account', emptyIllustrationUrl: '#url-empty-illustration', + deploymentsCloudRunUrl: '#url-deployments-cloud-run', + deploymentsCloudStorageUrl: '#deploymentsCloudStorageUrl', }; describe('google_cloud App component', () => { diff --git a/spec/frontend/google_cloud/components/deployments_service_table_spec.js b/spec/frontend/google_cloud/components/deployments_service_table_spec.js new file mode 100644 index 00000000000..76c3bfd00a8 --- /dev/null +++ b/spec/frontend/google_cloud/components/deployments_service_table_spec.js @@ -0,0 +1,40 @@ +import { mount } from '@vue/test-utils'; +import { GlButton, GlTable } from '@gitlab/ui'; +import DeploymentsServiceTable from '~/google_cloud/components/deployments_service_table.vue'; + +describe('google_cloud DeploymentsServiceTable component', () => { + let wrapper; + + const findTable = () => wrapper.findComponent(GlTable); + const findButtons = () => findTable().findAllComponents(GlButton); + const findCloudRunButton = () => findButtons().at(0); + const findCloudStorageButton = () => findButtons().at(1); + + beforeEach(() => { + const propsData = { + cloudRunUrl: '#url-deployments-cloud-run', + cloudStorageUrl: '#url-deployments-cloud-storage', + }; + wrapper = mount(DeploymentsServiceTable, { propsData }); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('should contain a table', () => { + expect(findTable().exists()).toBe(true); + }); + + it('should contain configure cloud run button', () => { + const cloudRunButton = findCloudRunButton(); + expect(cloudRunButton.exists()).toBe(true); + expect(cloudRunButton.props().disabled).toBe(true); + }); + + it('should contain configure cloud storage button', () => { + const cloudStorageButton = findCloudStorageButton(); + expect(cloudStorageButton.exists()).toBe(true); + expect(cloudStorageButton.props().disabled).toBe(true); + }); +}); diff --git a/spec/frontend/google_cloud/components/home_spec.js b/spec/frontend/google_cloud/components/home_spec.js index 9b4c3a79f11..3a009fc88ce 100644 --- a/spec/frontend/google_cloud/components/home_spec.js +++ b/spec/frontend/google_cloud/components/home_spec.js @@ -20,6 +20,8 @@ describe('google_cloud Home component', () => { serviceAccounts: [{}, {}], createServiceAccountUrl: '#url-create-service-account', emptyIllustrationUrl: '#url-empty-illustration', + deploymentsCloudRunUrl: '#url-deployments-cloud-run', + deploymentsCloudStorageUrl: '#deploymentsCloudStorageUrl', }; beforeEach(() => { @@ -42,7 +44,7 @@ describe('google_cloud Home component', () => { it('should contain three tab items', () => { expect(findTabItemsModel()).toEqual([ { title: 'Configuration', disabled: undefined }, - { title: 'Deployments', disabled: '' }, + { title: 'Deployments', disabled: undefined }, { title: 'Services', disabled: '' }, ]); }); diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/npm_installation_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/npm_installation_spec.js.snap index 16b573bb4a0..4520ae9c328 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/npm_installation_spec.js.snap +++ b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/npm_installation_spec.js.snap @@ -32,7 +32,7 @@ exports[`NpmInstallation renders all the messages 1`] = ` { function createComponent({ data = {} } = {}) { wrapper = shallowMountExtended(NpmInstallation, { provide: { - npmPath: 'npmPath', + npmInstanceUrl: 'npmInstanceUrl', }, propsData: { packageEntity, @@ -117,7 +117,7 @@ describe('NpmInstallation', () => { it('renders the correct setup command', () => { expect(findCodeInstructions().at(1).props()).toMatchObject({ - instruction: 'echo @gitlab-org:registry=npmPath/ >> .npmrc', + instruction: 'echo @gitlab-org:registry=npmInstanceUrl/ >> .npmrc', multiline: false, trackingAction: TRACKING_ACTION_COPY_NPM_SETUP_COMMAND, }); @@ -139,7 +139,7 @@ describe('NpmInstallation', () => { await nextTick(); expect(findCodeInstructions().at(1).props()).toMatchObject({ - instruction: `echo @gitlab-org:registry=npmPath/ >> .npmrc`, + instruction: `echo @gitlab-org:registry=npmInstanceUrl/ >> .npmrc`, multiline: false, trackingAction: TRACKING_ACTION_COPY_NPM_SETUP_COMMAND, }); @@ -161,7 +161,7 @@ describe('NpmInstallation', () => { it('renders the correct registry command', () => { expect(findCodeInstructions().at(1).props()).toMatchObject({ - instruction: 'echo \\"@gitlab-org:registry\\" \\"npmPath/\\" >> .yarnrc', + instruction: 'echo \\"@gitlab-org:registry\\" \\"npmInstanceUrl/\\" >> .yarnrc', multiline: false, trackingAction: TRACKING_ACTION_COPY_YARN_SETUP_COMMAND, }); @@ -183,7 +183,7 @@ describe('NpmInstallation', () => { await nextTick(); expect(findCodeInstructions().at(1).props()).toMatchObject({ - instruction: 'echo \\"@gitlab-org:registry\\" \\"npmPath/\\" >> .yarnrc', + instruction: 'echo \\"@gitlab-org:registry\\" \\"npmInstanceUrl/\\" >> .yarnrc', multiline: false, trackingAction: TRACKING_ACTION_COPY_YARN_SETUP_COMMAND, }); diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/package_list_row_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/package_list_row_spec.js.snap index 165ee962417..18a99f70756 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/package_list_row_spec.js.snap +++ b/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/package_list_row_spec.js.snap @@ -22,16 +22,20 @@ exports[`packages_list_row renders 1`] = ` - - + diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js index 292667ec47c..9467a613b2a 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js @@ -1,7 +1,11 @@ -import { GlLink, GlSprintf } from '@gitlab/ui'; +import { GlSprintf } from '@gitlab/ui'; +import { createLocalVue } from '@vue/test-utils'; +import VueRouter from 'vue-router'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; + import PackagesListRow from '~/packages_and_registries/package_registry/components/list/package_list_row.vue'; import PackagePath from '~/packages_and_registries/shared/components/package_path.vue'; import PackageTags from '~/packages_and_registries/shared/components/package_tags.vue'; @@ -13,6 +17,9 @@ import { PACKAGE_ERROR_STATUS } from '~/packages_and_registries/package_registry import ListItem from '~/vue_shared/components/registry/list_item.vue'; import { packageData, packagePipelines, packageProject, packageTags } from '../../mock_data'; +const localVue = createLocalVue(); +localVue.use(VueRouter); + describe('packages_list_row', () => { let wrapper; @@ -28,7 +35,7 @@ describe('packages_list_row', () => { const findDeleteButton = () => wrapper.findByTestId('action-delete'); const findPackageIconAndName = () => wrapper.find(PackageIconAndName); const findListItem = () => wrapper.findComponent(ListItem); - const findPackageLink = () => wrapper.findComponent(GlLink); + const findPackageLink = () => wrapper.findByTestId('details-link'); const findWarningIcon = () => wrapper.findByTestId('warning-icon'); const findLeftSecondaryInfos = () => wrapper.findByTestId('left-secondary-infos'); const findPublishMethod = () => wrapper.findComponent(PublishMethod); @@ -40,6 +47,7 @@ describe('packages_list_row', () => { provide = defaultProvide, } = {}) => { wrapper = shallowMountExtended(PackagesListRow, { + localVue, provide, stubs: { ListItem, @@ -63,6 +71,15 @@ describe('packages_list_row', () => { expect(wrapper.element).toMatchSnapshot(); }); + it('has a link to navigate to the details page', () => { + mountComponent(); + + expect(findPackageLink().props()).toMatchObject({ + event: 'click', + to: { name: 'details', params: { id: getIdFromGraphQLId(packageWithoutTags.id) } }, + }); + }); + describe('tags', () => { it('renders package tags when a package has tags', () => { mountComponent({ packageEntity: packageWithTags }); @@ -120,7 +137,7 @@ describe('packages_list_row', () => { }); it('details link is disabled', () => { - expect(findPackageLink().attributes('disabled')).toBe('true'); + expect(findPackageLink().props('event')).toBe(''); }); it('has a warning icon', () => { diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/app_spec.js b/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js similarity index 95% rename from spec/frontend/packages_and_registries/package_registry/components/details/app_spec.js rename to spec/frontend/packages_and_registries/package_registry/pages/details_spec.js index 8b7aa943ace..637e2edf3be 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/details/app_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js @@ -9,7 +9,7 @@ import waitForPromises from 'helpers/wait_for_promises'; import createFlash from '~/flash'; import AdditionalMetadata from '~/packages_and_registries/package_registry/components/details/additional_metadata.vue'; -import PackagesApp from '~/packages_and_registries/package_registry/components/details/app.vue'; +import PackagesApp from '~/packages_and_registries/package_registry/pages/details.vue'; import DependencyRow from '~/packages_and_registries/package_registry/components/details/dependency_row.vue'; import InstallationCommands from '~/packages_and_registries/package_registry/components/details/installation_commands.vue'; import PackageFiles from '~/packages_and_registries/package_registry/components/details/package_files.vue'; @@ -36,7 +36,7 @@ import { packageFiles, packageDestroyFileMutation, packageDestroyFileMutationError, -} from '../../mock_data'; +} from '../mock_data'; jest.mock('~/flash'); useMockLocationHelper(); @@ -47,18 +47,22 @@ describe('PackagesApp', () => { let wrapper; let apolloProvider; + const breadCrumbState = { + updateName: jest.fn(), + }; + const provide = { packageId: '111', - svgPath: 'svgPath', - npmPath: 'npmPath', - npmHelpPath: 'npmHelpPath', + emptyListIllustration: 'svgPath', projectListUrl: 'projectListUrl', groupListUrl: 'groupListUrl', + breadCrumbState, }; function createComponent({ resolver = jest.fn().mockResolvedValue(packageDetailsQuery()), fileDeleteMutationResolver = jest.fn().mockResolvedValue(packageDestroyFileMutation()), + routeId = '1', } = {}) { localVue.use(VueApollo); @@ -84,6 +88,13 @@ describe('PackagesApp', () => { GlTabs, GlTab, }, + mocks: { + $route: { + params: { + id: routeId, + }, + }, + }, }); } @@ -172,6 +183,15 @@ describe('PackagesApp', () => { }); }); + it('calls the appropriate function to set the breadcrumbState', async () => { + const { name, version } = packageData(); + createComponent(); + + await waitForPromises(); + + expect(breadCrumbState.updateName).toHaveBeenCalledWith(`${name} v ${version}`); + }); + describe('delete package', () => { const originalReferrer = document.referrer; const setReferrer = (value = packageDetailsQuery().data.package.project.name) => { diff --git a/spec/frontend/packages_and_registries/shared/components/__snapshots__/registry_breadcrumb_spec.js.snap b/spec/frontend/packages_and_registries/shared/components/__snapshots__/registry_breadcrumb_spec.js.snap index 7044c1285d8..fa02d60e440 100644 --- a/spec/frontend/packages_and_registries/shared/components/__snapshots__/registry_breadcrumb_spec.js.snap +++ b/spec/frontend/packages_and_registries/shared/components/__snapshots__/registry_breadcrumb_spec.js.snap @@ -24,16 +24,20 @@ exports[`Registry Breadcrumb when is not rootRoute renders 1`] = ` class="gl-breadcrumb-separator" data-testid="separator" > - - - + + + + diff --git a/spec/frontend/pipeline_editor/components/header/validation_segment_spec.js b/spec/frontend/pipeline_editor/components/header/validation_segment_spec.js index d97851a1d55..570323826d1 100644 --- a/spec/frontend/pipeline_editor/components/header/validation_segment_spec.js +++ b/spec/frontend/pipeline_editor/components/header/validation_segment_spec.js @@ -1,11 +1,14 @@ +import VueApollo from 'vue-apollo'; import { GlIcon } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; +import { shallowMount, createLocalVue } from '@vue/test-utils'; import { escape } from 'lodash'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; +import createMockApollo from 'helpers/mock_apollo_helper'; import { sprintf } from '~/locale'; import ValidationSegment, { i18n, } from '~/pipeline_editor/components/header/validation_segment.vue'; +import getAppStatus from '~/pipeline_editor/graphql/queries/client/app_status.query.graphql'; import { CI_CONFIG_STATUS_INVALID, EDITOR_APP_STATUS_EMPTY, @@ -21,12 +24,29 @@ import { mockYmlHelpPagePath, } from '../../mock_data'; +const localVue = createLocalVue(); +localVue.use(VueApollo); + describe('Validation segment component', () => { let wrapper; - const createComponent = ({ props = {}, appStatus }) => { + const mockApollo = createMockApollo(); + + const createComponent = ({ props = {}, appStatus = EDITOR_APP_STATUS_INVALID }) => { + mockApollo.clients.defaultClient.cache.writeQuery({ + query: getAppStatus, + data: { + app: { + __typename: 'PipelineEditorApp', + status: appStatus, + }, + }, + }); + wrapper = extendedWrapper( shallowMount(ValidationSegment, { + localVue, + apolloProvider: mockApollo, provide: { ymlHelpPagePath: mockYmlHelpPagePath, lintUnavailableHelpPagePath: mockLintUnavailableHelpPagePath, @@ -36,12 +56,6 @@ describe('Validation segment component', () => { ciFileContent: mockCiYml, ...props, }, - // Simulate graphQL client query result - data() { - return { - appStatus, - }; - }, }), ); }; @@ -99,6 +113,7 @@ describe('Validation segment component', () => { appStatus: EDITOR_APP_STATUS_INVALID, }); }); + it('has warning icon', () => { expect(findIcon().props('name')).toBe('warning-solid'); }); diff --git a/spec/frontend/runner/admin_runners/admin_runners_app_spec.js b/spec/frontend/runner/admin_runners/admin_runners_app_spec.js index d7f5bb43e41..42be691ba4c 100644 --- a/spec/frontend/runner/admin_runners/admin_runners_app_spec.js +++ b/spec/frontend/runner/admin_runners/admin_runners_app_spec.js @@ -13,6 +13,7 @@ import AdminRunnersApp from '~/runner/admin_runners/admin_runners_app.vue'; import RunnerTypeTabs from '~/runner/components/runner_type_tabs.vue'; import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_bar.vue'; import RunnerList from '~/runner/components/runner_list.vue'; +import RunnerStats from '~/runner/components/stat/runner_stats.vue'; import RegistrationDropdown from '~/runner/components/registration/registration_dropdown.vue'; import RunnerPagination from '~/runner/components/runner_pagination.vue'; @@ -37,7 +38,6 @@ import FilteredSearch from '~/vue_shared/components/filtered_search_bar/filtered import { runnersData, runnersCountData, runnersDataPaginated } from '../mock_data'; const mockRegistrationToken = 'MOCK_REGISTRATION_TOKEN'; -const mockActiveRunnersCount = '2'; jest.mock('~/flash'); jest.mock('~/runner/sentry_utils'); @@ -54,6 +54,7 @@ describe('AdminRunnersApp', () => { let mockRunnersQuery; let mockRunnersCountQuery; + const findRunnerStats = () => wrapper.findComponent(RunnerStats); const findRegistrationDropdown = () => wrapper.findComponent(RegistrationDropdown); const findRunnerTypeTabs = () => wrapper.findComponent(RunnerTypeTabs); const findRunnerList = () => wrapper.findComponent(RunnerList); @@ -70,15 +71,16 @@ describe('AdminRunnersApp', () => { [getRunnersCountQuery, mockRunnersCountQuery], ]; - wrapper = mountFn(AdminRunnersApp, { - localVue, - apolloProvider: createMockApollo(handlers), - propsData: { - registrationToken: mockRegistrationToken, - activeRunnersCount: mockActiveRunnersCount, - ...props, - }, - }); + wrapper = extendedWrapper( + mountFn(AdminRunnersApp, { + localVue, + apolloProvider: createMockApollo(handlers), + propsData: { + registrationToken: mockRegistrationToken, + ...props, + }, + }), + ); }; beforeEach(async () => { @@ -95,6 +97,18 @@ describe('AdminRunnersApp', () => { wrapper.destroy(); }); + it('shows total runner counts', async () => { + createComponent({ mountFn: mount }); + + await waitForPromises(); + + const stats = findRunnerStats().text(); + + expect(stats).toMatch('Online runners 4'); + expect(stats).toMatch('Offline runners 4'); + expect(stats).toMatch('Stale runners 4'); + }); + it('shows the runner tabs with a runner count for each type', async () => { mockRunnersCountQuery.mockImplementation(({ type }) => { let count; @@ -198,12 +212,6 @@ describe('AdminRunnersApp', () => { ]); }); - it('shows the active runner count', () => { - createComponent({ mountFn: mount }); - - expect(wrapper.text()).toMatch(new RegExp(`Online Runners ${mockActiveRunnersCount}`)); - }); - describe('when a filter is preselected', () => { beforeEach(async () => { setWindowLocation(`?status[]=${STATUS_ACTIVE}&runner_type[]=${INSTANCE_TYPE}&tag[]=tag1`); diff --git a/spec/frontend/runner/components/registration/registration_token_reset_dropdown_item_spec.js b/spec/frontend/runner/components/registration/registration_token_reset_dropdown_item_spec.js index 4e93d6bcaa3..e75decddf70 100644 --- a/spec/frontend/runner/components/registration/registration_token_reset_dropdown_item_spec.js +++ b/spec/frontend/runner/components/registration/registration_token_reset_dropdown_item_spec.js @@ -1,4 +1,4 @@ -import { GlDropdownItem, GlLoadingIcon, GlToast } from '@gitlab/ui'; +import { GlDropdownItem, GlLoadingIcon, GlToast, GlModal } from '@gitlab/ui'; import { createLocalVue, shallowMount } from '@vue/test-utils'; import { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; @@ -9,6 +9,7 @@ import RegistrationTokenResetDropdownItem from '~/runner/components/registration import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '~/runner/constants'; import runnersRegistrationTokenResetMutation from '~/runner/graphql/runners_registration_token_reset.mutation.graphql'; import { captureException } from '~/runner/sentry_utils'; +import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; jest.mock('~/flash'); jest.mock('~/runner/sentry_utils'); @@ -18,14 +19,18 @@ localVue.use(VueApollo); localVue.use(GlToast); const mockNewToken = 'NEW_TOKEN'; +const modalID = 'token-reset-modal'; describe('RegistrationTokenResetDropdownItem', () => { let wrapper; let runnersRegistrationTokenResetMutationHandler; let showToast; + const mockEvent = { preventDefault: jest.fn() }; const findDropdownItem = () => wrapper.findComponent(GlDropdownItem); const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); + const findModal = () => wrapper.findComponent(GlModal); + const clickSubmit = () => findModal().vm.$emit('primary', mockEvent); const createComponent = ({ props, provide = {} } = {}) => { wrapper = shallowMount(RegistrationTokenResetDropdownItem, { @@ -38,6 +43,9 @@ describe('RegistrationTokenResetDropdownItem', () => { apolloProvider: createMockApollo([ [runnersRegistrationTokenResetMutation, runnersRegistrationTokenResetMutationHandler], ]), + directives: { + GlModal: createMockDirective(), + }, }); showToast = wrapper.vm.$toast ? jest.spyOn(wrapper.vm.$toast, 'show') : null; @@ -54,8 +62,6 @@ describe('RegistrationTokenResetDropdownItem', () => { }); createComponent(); - - jest.spyOn(window, 'confirm'); }); afterEach(() => { @@ -66,6 +72,18 @@ describe('RegistrationTokenResetDropdownItem', () => { expect(findDropdownItem().exists()).toBe(true); }); + describe('modal directive integration', () => { + it('has the correct ID on the dropdown', () => { + const binding = getBinding(findDropdownItem().element, 'gl-modal'); + + expect(binding.value).toBe(modalID); + }); + + it('has the correct ID on the modal', () => { + expect(findModal().props('modalId')).toBe(modalID); + }); + }); + describe('On click and confirmation', () => { const mockGroupId = '11'; const mockProjectId = '22'; @@ -82,9 +100,8 @@ describe('RegistrationTokenResetDropdownItem', () => { props: { type }, }); - window.confirm.mockReturnValueOnce(true); - findDropdownItem().trigger('click'); + clickSubmit(); await waitForPromises(); }); @@ -114,7 +131,6 @@ describe('RegistrationTokenResetDropdownItem', () => { describe('On click without confirmation', () => { beforeEach(async () => { - window.confirm.mockReturnValueOnce(false); findDropdownItem().vm.$emit('click'); await waitForPromises(); }); @@ -142,8 +158,8 @@ describe('RegistrationTokenResetDropdownItem', () => { runnersRegistrationTokenResetMutationHandler.mockRejectedValueOnce(new Error(mockErrorMsg)); - window.confirm.mockReturnValueOnce(true); findDropdownItem().trigger('click'); + clickSubmit(); await waitForPromises(); expect(createAlert).toHaveBeenLastCalledWith({ @@ -168,8 +184,8 @@ describe('RegistrationTokenResetDropdownItem', () => { }, }); - window.confirm.mockReturnValueOnce(true); findDropdownItem().trigger('click'); + clickSubmit(); await waitForPromises(); expect(createAlert).toHaveBeenLastCalledWith({ @@ -184,8 +200,8 @@ describe('RegistrationTokenResetDropdownItem', () => { describe('Immediately after click', () => { it('shows loading state', async () => { - window.confirm.mockReturnValue(true); findDropdownItem().trigger('click'); + clickSubmit(); await nextTick(); expect(findLoadingIcon().exists()).toBe(true); diff --git a/spec/frontend/runner/components/stat/runner_online_stat_spec.js b/spec/frontend/runner/components/stat/runner_online_stat_spec.js deleted file mode 100644 index 18f865aa22c..00000000000 --- a/spec/frontend/runner/components/stat/runner_online_stat_spec.js +++ /dev/null @@ -1,34 +0,0 @@ -import { GlSingleStat } from '@gitlab/ui/dist/charts'; -import { shallowMount, mount } from '@vue/test-utils'; -import RunnerOnlineBadge from '~/runner/components/stat/runner_online_stat.vue'; - -describe('RunnerOnlineBadge', () => { - let wrapper; - - const findSingleStat = () => wrapper.findComponent(GlSingleStat); - - const createComponent = ({ props = {} } = {}, mountFn = shallowMount) => { - wrapper = mountFn(RunnerOnlineBadge, { - propsData: { - value: '99', - ...props, - }, - }); - }; - - afterEach(() => { - wrapper.destroy(); - }); - - it('Uses a success appearance', () => { - createComponent({}, shallowMount); - - expect(findSingleStat().props('variant')).toBe('success'); - }); - - it('Renders a value', () => { - createComponent({}, mount); - - expect(wrapper.text()).toMatch(new RegExp(`Online Runners 99\\s+online`)); - }); -}); diff --git a/spec/frontend/runner/components/stat/runner_stats_spec.js b/spec/frontend/runner/components/stat/runner_stats_spec.js new file mode 100644 index 00000000000..68db8621ef0 --- /dev/null +++ b/spec/frontend/runner/components/stat/runner_stats_spec.js @@ -0,0 +1,46 @@ +import { shallowMount, mount } from '@vue/test-utils'; +import RunnerStats from '~/runner/components/stat/runner_stats.vue'; +import RunnerStatusStat from '~/runner/components/stat/runner_status_stat.vue'; +import { STATUS_ONLINE, STATUS_OFFLINE, STATUS_STALE } from '~/runner/constants'; + +describe('RunnerStats', () => { + let wrapper; + + const findRunnerStatusStatAt = (i) => wrapper.findAllComponents(RunnerStatusStat).at(i); + + const createComponent = ({ props = {}, mountFn = shallowMount } = {}) => { + wrapper = mountFn(RunnerStats, { + propsData: { + onlineRunnersCount: 3, + offlineRunnersCount: 2, + staleRunnersCount: 1, + ...props, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + it('Displays all the stats', () => { + createComponent({ mountFn: mount }); + + const stats = wrapper.text(); + + expect(stats).toMatch('Online runners 3'); + expect(stats).toMatch('Offline runners 2'); + expect(stats).toMatch('Stale runners 1'); + }); + + it.each` + i | status + ${0} | ${STATUS_ONLINE} + ${1} | ${STATUS_OFFLINE} + ${2} | ${STATUS_STALE} + `('Displays status types at index $i', ({ i, status }) => { + createComponent(); + + expect(findRunnerStatusStatAt(i).props('status')).toBe(status); + }); +}); diff --git a/spec/frontend/runner/components/stat/runner_status_stat_spec.js b/spec/frontend/runner/components/stat/runner_status_stat_spec.js new file mode 100644 index 00000000000..3218272eac7 --- /dev/null +++ b/spec/frontend/runner/components/stat/runner_status_stat_spec.js @@ -0,0 +1,67 @@ +import { GlSingleStat } from '@gitlab/ui/dist/charts'; +import { shallowMount, mount } from '@vue/test-utils'; +import RunnerStatusStat from '~/runner/components/stat/runner_status_stat.vue'; +import { STATUS_ONLINE, STATUS_OFFLINE, STATUS_STALE } from '~/runner/constants'; + +describe('RunnerStatusStat', () => { + let wrapper; + + const findSingleStat = () => wrapper.findComponent(GlSingleStat); + + const createComponent = ({ props = {} } = {}, mountFn = shallowMount) => { + wrapper = mountFn(RunnerStatusStat, { + propsData: { + status: STATUS_ONLINE, + value: 99, + ...props, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + describe.each` + status | variant | title | badge + ${STATUS_ONLINE} | ${'success'} | ${'Online runners'} | ${'online'} + ${STATUS_OFFLINE} | ${'muted'} | ${'Offline runners'} | ${'offline'} + ${STATUS_STALE} | ${'warning'} | ${'Stale runners'} | ${'stale'} + `('Renders a stat for status "$status"', ({ status, variant, title, badge }) => { + beforeEach(() => { + createComponent({ props: { status } }, mount); + }); + + it('Renders text', () => { + expect(wrapper.text()).toMatch(new RegExp(`${title} 99\\s+${badge}`)); + }); + + it(`Uses variant ${variant}`, () => { + expect(findSingleStat().props('variant')).toBe(variant); + }); + }); + + it('Formats stat number', () => { + createComponent({ props: { value: 1000 } }, mount); + + expect(wrapper.text()).toMatch('Online runners 1,000'); + }); + + it('Shows a null result', () => { + createComponent({ props: { value: null } }, mount); + + expect(wrapper.text()).toMatch('Online runners -'); + }); + + it('Shows an undefined result', () => { + createComponent({ props: { value: undefined } }, mount); + + expect(wrapper.text()).toMatch('Online runners -'); + }); + + it('Shows result for an unknown status', () => { + createComponent({ props: { status: 'UNKNOWN' } }, mount); + + expect(wrapper.text()).toMatch('Runners 99'); + }); +}); diff --git a/spec/frontend/runner/group_runners/group_runners_app_spec.js b/spec/frontend/runner/group_runners/group_runners_app_spec.js index 0ce6feceb5b..034b7848f35 100644 --- a/spec/frontend/runner/group_runners/group_runners_app_spec.js +++ b/spec/frontend/runner/group_runners/group_runners_app_spec.js @@ -12,6 +12,7 @@ import { updateHistory } from '~/lib/utils/url_utility'; import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_bar.vue'; import RunnerList from '~/runner/components/runner_list.vue'; +import RunnerStats from '~/runner/components/stat/runner_stats.vue'; import RegistrationDropdown from '~/runner/components/registration/registration_dropdown.vue'; import RunnerPagination from '~/runner/components/runner_pagination.vue'; @@ -26,10 +27,11 @@ import { RUNNER_PAGE_SIZE, } from '~/runner/constants'; import getGroupRunnersQuery from '~/runner/graphql/get_group_runners.query.graphql'; +import getGroupRunnersCountQuery from '~/runner/graphql/get_group_runners_count.query.graphql'; import GroupRunnersApp from '~/runner/group_runners/group_runners_app.vue'; import { captureException } from '~/runner/sentry_utils'; import FilteredSearch from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue'; -import { groupRunnersData, groupRunnersDataPaginated } from '../mock_data'; +import { groupRunnersData, groupRunnersDataPaginated, groupRunnersCountData } from '../mock_data'; const localVue = createLocalVue(); localVue.use(VueApollo); @@ -48,7 +50,9 @@ jest.mock('~/lib/utils/url_utility', () => ({ describe('GroupRunnersApp', () => { let wrapper; let mockGroupRunnersQuery; + let mockGroupRunnersCountQuery; + const findRunnerStats = () => wrapper.findComponent(RunnerStats); const findRegistrationDropdown = () => wrapper.findComponent(RegistrationDropdown); const findRunnerList = () => wrapper.findComponent(RunnerList); const findRunnerPagination = () => extendedWrapper(wrapper.findComponent(RunnerPagination)); @@ -59,7 +63,10 @@ describe('GroupRunnersApp', () => { const findFilteredSearch = () => wrapper.findComponent(FilteredSearch); const createComponent = ({ props = {}, mountFn = shallowMount } = {}) => { - const handlers = [[getGroupRunnersQuery, mockGroupRunnersQuery]]; + const handlers = [ + [getGroupRunnersQuery, mockGroupRunnersQuery], + [getGroupRunnersCountQuery, mockGroupRunnersCountQuery], + ]; wrapper = mountFn(GroupRunnersApp, { localVue, @@ -77,11 +84,24 @@ describe('GroupRunnersApp', () => { setWindowLocation(`/groups/${mockGroupFullPath}/-/runners`); mockGroupRunnersQuery = jest.fn().mockResolvedValue(groupRunnersData); + mockGroupRunnersCountQuery = jest.fn().mockResolvedValue(groupRunnersCountData); createComponent(); await waitForPromises(); }); + it('shows total runner counts', async () => { + createComponent({ mountFn: mount }); + + await waitForPromises(); + + const stats = findRunnerStats().text(); + + expect(stats).toMatch('Online runners 2'); + expect(stats).toMatch('Offline runners 2'); + expect(stats).toMatch('Stale runners 2'); + }); + it('shows the runner setup instructions', () => { expect(findRegistrationDropdown().props('registrationToken')).toBe(mockRegistrationToken); expect(findRegistrationDropdown().props('type')).toBe(GROUP_TYPE); @@ -129,28 +149,6 @@ describe('GroupRunnersApp', () => { ); }); - describe('shows the active runner count', () => { - const expectedOnlineCount = (count) => new RegExp(`Online Runners ${count}`); - - it('with a regular value', () => { - createComponent({ mountFn: mount }); - - expect(wrapper.text()).toMatch(expectedOnlineCount(mockGroupRunnersLimitedCount)); - }); - - it('at the limit', () => { - createComponent({ props: { groupRunnersLimitedCount: 1000 }, mountFn: mount }); - - expect(wrapper.text()).toMatch(expectedOnlineCount('1,000')); - }); - - it('over the limit', () => { - createComponent({ props: { groupRunnersLimitedCount: 1001 }, mountFn: mount }); - - expect(wrapper.text()).toMatch(expectedOnlineCount('1,000\\+')); - }); - }); - describe('when a filter is preselected', () => { beforeEach(async () => { setWindowLocation(`?status[]=${STATUS_ACTIVE}&runner_type[]=${INSTANCE_TYPE}`); diff --git a/spec/frontend/runner/mock_data.js b/spec/frontend/runner/mock_data.js index d4f265b2d7b..9c430e205ea 100644 --- a/spec/frontend/runner/mock_data.js +++ b/spec/frontend/runner/mock_data.js @@ -8,6 +8,7 @@ import runnerData from 'test_fixtures/graphql/runner/get_runner.query.graphql.js // Group queries import groupRunnersData from 'test_fixtures/graphql/runner/get_group_runners.query.graphql.json'; +import groupRunnersCountData from 'test_fixtures/graphql/runner/get_group_runners_count.query.graphql.json'; import groupRunnersDataPaginated from 'test_fixtures/graphql/runner/get_group_runners.query.graphql.paginated.json'; export { @@ -16,5 +17,6 @@ export { runnersDataPaginated, runnersData, groupRunnersData, + groupRunnersCountData, groupRunnersDataPaginated, }; diff --git a/spec/frontend/security_configuration/mock_data.js b/spec/frontend/security_configuration/mock_data.js index 32b1a0bff06..37ecce3886d 100644 --- a/spec/frontend/security_configuration/mock_data.js +++ b/spec/frontend/security_configuration/mock_data.js @@ -5,16 +5,16 @@ export const textProviderIds = [101, 102]; export const securityTrainingProviders = [ { id: textProviderIds[0], - name: 'Kontra', - description: 'Interactive developer security education.', - url: 'https://application.security/', + name: 'Vendor Name 1', + description: 'Interactive developer security education', + url: 'https://www.example.org/security/training', isEnabled: false, }, { id: textProviderIds[1], - name: 'SecureCodeWarrior', + name: 'Vendor Name 2', description: 'Security training with guide and learning pathways.', - url: 'https://www.securecodewarrior.com/', + url: 'https://www.vendornametwo.com/', isEnabled: true, }, ]; diff --git a/spec/graphql/mutations/issues/set_escalation_status_spec.rb b/spec/graphql/mutations/issues/set_escalation_status_spec.rb new file mode 100644 index 00000000000..d41118b1812 --- /dev/null +++ b/spec/graphql/mutations/issues/set_escalation_status_spec.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Mutations::Issues::SetEscalationStatus do + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project) } + let_it_be(:issue, reload: true) { create(:incident, project: project) } + let_it_be(:escalation_status, reload: true) { create(:incident_management_issuable_escalation_status, issue: issue) } + + let(:status) { :acknowledged } + let(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) } + + describe '#resolve' do + let(:args) { { status: status } } + let(:mutated_issue) { result[:issue] } + + subject(:result) { mutation.resolve(project_path: issue.project.full_path, iid: issue.iid, **args) } + + it_behaves_like 'permission level for issue mutation is correctly verified', true + + context 'when the user can update the issue' do + before_all do + project.add_reporter(user) + end + + it_behaves_like 'permission level for issue mutation is correctly verified', true + + context 'when the user can update the escalation status' do + before_all do + project.add_developer(user) + end + + it 'returns the issue with the escalation policy' do + expect(mutated_issue).to eq(issue) + expect(mutated_issue.escalation_status.status_name).to eq(status) + expect(result[:errors]).to be_empty + end + + it 'returns errors when issue update fails' do + issue.update_column(:author_id, nil) + + expect(result[:errors]).not_to be_empty + end + + context 'with non-incident issue is provided' do + let_it_be(:issue) { create(:issue, project: project) } + + it 'raises an error' do + expect { result }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable, 'Feature unavailable for provided issue') + end + end + + context 'with feature disabled' do + before do + stub_feature_flags(incident_escalations: false) + end + + it 'raises an error' do + expect { result }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable, 'Feature unavailable for provided issue') + end + end + end + end + end +end diff --git a/spec/graphql/types/incident_management/escalation_status_enum_spec.rb b/spec/graphql/types/incident_management/escalation_status_enum_spec.rb new file mode 100644 index 00000000000..b39d4d9324e --- /dev/null +++ b/spec/graphql/types/incident_management/escalation_status_enum_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe GitlabSchema.types['IssueEscalationStatus'] do + specify { expect(described_class.graphql_name).to eq('IssueEscalationStatus') } + + describe 'statuses' do + using RSpec::Parameterized::TableSyntax + + where(:status_name, :status_value) do + 'TRIGGERED' | :triggered + 'ACKNOWLEDGED' | :acknowledged + 'RESOLVED' | :resolved + 'IGNORED' | :ignored + 'INVALID' | nil + end + + with_them do + it 'exposes a status with the correct value' do + expect(described_class.values[status_name]&.value).to eq(status_value) + end + end + end +end diff --git a/spec/graphql/types/issue_type_spec.rb b/spec/graphql/types/issue_type_spec.rb index 1b8bf007a73..1d4590cbb4e 100644 --- a/spec/graphql/types/issue_type_spec.rb +++ b/spec/graphql/types/issue_type_spec.rb @@ -18,7 +18,7 @@ RSpec.describe GitlabSchema.types['Issue'] do confidential hidden discussion_locked upvotes downvotes merge_requests_count user_notes_count user_discussions_count web_path web_url relative_position emails_disabled subscribed time_estimate total_time_spent human_time_estimate human_total_time_spent closed_at created_at updated_at task_completion_status design_collection alert_management_alert severity current_user_todos moved moved_to - create_note_email timelogs project_id customer_relations_contacts] + create_note_email timelogs project_id customer_relations_contacts escalation_status] fields.each do |field_name| expect(described_class).to have_graphql_field(field_name) @@ -257,4 +257,49 @@ RSpec.describe GitlabSchema.types['Issue'] do end end end + + describe 'escalation_status' do + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project, :public) } + let_it_be(:issue, reload: true) { create(:issue, project: project) } + + let(:execute) { GitlabSchema.execute(query, context: { current_user: user }).as_json } + let(:query) do + %( + query { + project(fullPath: "#{project.full_path}") { + issue(iid: "#{issue.iid}") { + escalationStatus + } + } + } + ) + end + + subject(:status) { execute.dig('data', 'project', 'issue', 'escalationStatus') } + + it { is_expected.to be_nil } + + context 'for an incident' do + before do + issue.update!(issue_type: Issue.issue_types[:incident]) + end + + it { is_expected.to be_nil } + + context 'with an escalation status record' do + let!(:escalation_status) { create(:incident_management_issuable_escalation_status, issue: issue) } + + it { is_expected.to eq(escalation_status.status_name.to_s.upcase) } + + context 'with feature disabled' do + before do + stub_feature_flags(incident_escalations: false) + end + + it { is_expected.to be_nil } + end + end + end + end end diff --git a/spec/helpers/ci/runners_helper_spec.rb b/spec/helpers/ci/runners_helper_spec.rb index 9694c7cb4b7..832b4da0e20 100644 --- a/spec/helpers/ci/runners_helper_spec.rb +++ b/spec/helpers/ci/runners_helper_spec.rb @@ -79,8 +79,7 @@ RSpec.describe Ci::RunnersHelper do it 'returns the data in format' do expect(helper.admin_runners_data_attributes).to eq({ runner_install_help_page: 'https://docs.gitlab.com/runner/install/', - registration_token: Gitlab::CurrentSettings.runners_registration_token, - active_runners_count: '0' + registration_token: Gitlab::CurrentSettings.runners_registration_token }) end end diff --git a/spec/lib/gitlab/redis/multi_store_spec.rb b/spec/lib/gitlab/redis/multi_store_spec.rb deleted file mode 100644 index 663040e0ca7..00000000000 --- a/spec/lib/gitlab/redis/multi_store_spec.rb +++ /dev/null @@ -1,716 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::Redis::MultiStore do - using RSpec::Parameterized::TableSyntax - - let_it_be(:redis_store_class) do - Class.new(Gitlab::Redis::Wrapper) do - def config_file_name - config_file_name = "spec/fixtures/config/redis_new_format_host.yml" - Rails.root.join(config_file_name).to_s - end - - def self.name - 'Sessions' - end - end - end - - let_it_be(:primary_db) { 1 } - let_it_be(:secondary_db) { 2 } - let_it_be(:primary_store) { create_redis_store(redis_store_class.params, db: primary_db, serializer: nil) } - let_it_be(:secondary_store) { create_redis_store(redis_store_class.params, db: secondary_db, serializer: nil) } - let_it_be(:instance_name) { 'TestStore' } - let_it_be(:multi_store) { described_class.new(primary_store, secondary_store, instance_name)} - - subject { multi_store.send(name, *args) } - - before do - skip_feature_flags_yaml_validation - skip_default_enabled_yaml_check - end - - after(:all) do - primary_store.flushdb - secondary_store.flushdb - end - - context 'when primary_store is nil' do - let(:multi_store) { described_class.new(nil, secondary_store, instance_name)} - - it 'fails with exception' do - expect { multi_store }.to raise_error(ArgumentError, /primary_store is required/) - end - end - - context 'when secondary_store is nil' do - let(:multi_store) { described_class.new(primary_store, nil, instance_name)} - - it 'fails with exception' do - expect { multi_store }.to raise_error(ArgumentError, /secondary_store is required/) - end - end - - context 'when instance_name is nil' do - let(:instance_name) { nil } - let(:multi_store) { described_class.new(primary_store, secondary_store, instance_name)} - - it 'fails with exception' do - expect { multi_store }.to raise_error(ArgumentError, /instance_name is required/) - end - end - - context 'when primary_store is not a ::Redis instance' do - before do - allow(primary_store).to receive(:is_a?).with(::Redis).and_return(false) - end - - it 'fails with exception' do - expect { described_class.new(primary_store, secondary_store, instance_name) }.to raise_error(ArgumentError, /invalid primary_store/) - end - end - - context 'when secondary_store is not a ::Redis instance' do - before do - allow(secondary_store).to receive(:is_a?).with(::Redis).and_return(false) - end - - it 'fails with exception' do - expect { described_class.new(primary_store, secondary_store, instance_name) }.to raise_error(ArgumentError, /invalid secondary_store/) - end - end - - context 'with READ redis commands' do - let_it_be(:key1) { "redis:{1}:key_a" } - let_it_be(:key2) { "redis:{1}:key_b" } - let_it_be(:value1) { "redis_value1"} - let_it_be(:value2) { "redis_value2"} - let_it_be(:skey) { "redis:set:key" } - let_it_be(:keys) { [key1, key2] } - let_it_be(:values) { [value1, value2] } - let_it_be(:svalues) { [value2, value1] } - - where(:case_name, :name, :args, :value, :block) do - 'execute :get command' | :get | ref(:key1) | ref(:value1) | nil - 'execute :mget command' | :mget | ref(:keys) | ref(:values) | nil - 'execute :mget with block' | :mget | ref(:keys) | ref(:values) | ->(value) { value } - 'execute :smembers command' | :smembers | ref(:skey) | ref(:svalues) | nil - 'execute :scard command' | :scard | ref(:skey) | 2 | nil - end - - before(:all) do - primary_store.multi do |multi| - multi.set(key1, value1) - multi.set(key2, value2) - multi.sadd(skey, value1) - multi.sadd(skey, value2) - end - - secondary_store.multi do |multi| - multi.set(key1, value1) - multi.set(key2, value2) - multi.sadd(skey, value1) - multi.sadd(skey, value2) - end - end - - RSpec.shared_examples_for 'reads correct value' do - it 'returns the correct value' do - if value.is_a?(Array) - # :smembers does not guarantee the order it will return the values (unsorted set) - is_expected.to match_array(value) - else - is_expected.to eq(value) - end - end - end - - RSpec.shared_examples_for 'fallback read from the secondary store' do - let(:counter) { Gitlab::Metrics::NullMetric.instance } - - before do - allow(Gitlab::Metrics).to receive(:counter).and_return(counter) - end - - it 'fallback and execute on secondary instance' do - expect(secondary_store).to receive(name).with(*args).and_call_original - - subject - end - - it 'logs the ReadFromPrimaryError' do - expect(Gitlab::ErrorTracking).to receive(:log_exception).with(an_instance_of(Gitlab::Redis::MultiStore::ReadFromPrimaryError), - hash_including(command_name: name, extra: hash_including(instance_name: instance_name))) - - subject - end - - it 'increment read fallback count metrics' do - expect(counter).to receive(:increment).with(command: name, instance_name: instance_name) - - subject - end - - include_examples 'reads correct value' - - context 'when fallback read from the secondary instance raises an exception' do - before do - allow(secondary_store).to receive(name).with(*args).and_raise(StandardError) - end - - it 'fails with exception' do - expect { subject }.to raise_error(StandardError) - end - end - end - - RSpec.shared_examples_for 'secondary store' do - it 'execute on the secondary instance' do - expect(secondary_store).to receive(name).with(*args).and_call_original - - subject - end - - include_examples 'reads correct value' - - it 'does not execute on the primary store' do - expect(primary_store).not_to receive(name) - - subject - end - end - - with_them do - describe "#{name}" do - before do - allow(primary_store).to receive(name).and_call_original - allow(secondary_store).to receive(name).and_call_original - end - - context 'with feature flag :use_primary_and_secondary_stores_for_test_store' do - before do - stub_feature_flags(use_primary_and_secondary_stores_for_test_store: true) - end - - context 'when reading from the primary is successful' do - it 'returns the correct value' do - expect(primary_store).to receive(name).with(*args).and_call_original - - subject - end - - it 'does not execute on the secondary store' do - expect(secondary_store).not_to receive(name) - - subject - end - - include_examples 'reads correct value' - end - - context 'when reading from primary instance is raising an exception' do - before do - allow(primary_store).to receive(name).with(*args).and_raise(StandardError) - allow(Gitlab::ErrorTracking).to receive(:log_exception) - end - - it 'logs the exception' do - expect(Gitlab::ErrorTracking).to receive(:log_exception).with(an_instance_of(StandardError), - hash_including(extra: hash_including(:multi_store_error_message, instance_name: instance_name), - command_name: name)) - - subject - end - - include_examples 'fallback read from the secondary store' - end - - context 'when reading from primary instance return no value' do - before do - allow(primary_store).to receive(name).and_return(nil) - end - - include_examples 'fallback read from the secondary store' - end - - context 'when the command is executed within pipelined block' do - subject do - multi_store.pipelined do - multi_store.send(name, *args) - end - end - - it 'is executed only 1 time on primary instance' do - expect(primary_store).to receive(name).with(*args).once - - subject - end - end - - if params[:block] - subject do - multi_store.send(name, *args, &block) - end - - context 'when block is provided' do - it 'yields to the block' do - expect(primary_store).to receive(name).and_yield(value) - - subject - end - - include_examples 'reads correct value' - end - end - end - - context 'with feature flag :use_primary_and_secondary_stores_for_test_store' do - before do - stub_feature_flags(use_primary_and_secondary_stores_for_test_store: false) - end - - context 'with feature flag :use_primary_store_as_default_for_test_store is disabled' do - before do - stub_feature_flags(use_primary_store_as_default_for_test_store: false) - end - - it_behaves_like 'secondary store' - end - - context 'with feature flag :use_primary_store_as_default_for_test_store is enabled' do - before do - stub_feature_flags(use_primary_store_as_default_for_test_store: true) - end - - it 'execute on the primary instance' do - expect(primary_store).to receive(name).with(*args).and_call_original - - subject - end - - include_examples 'reads correct value' - - it 'does not execute on the secondary store' do - expect(secondary_store).not_to receive(name) - - subject - end - end - end - - context 'with both primary and secondary store using same redis instance' do - let(:primary_store) { create_redis_store(redis_store_class.params, db: primary_db, serializer: nil) } - let(:secondary_store) { create_redis_store(redis_store_class.params, db: primary_db, serializer: nil) } - let(:multi_store) { described_class.new(primary_store, secondary_store, instance_name)} - - it_behaves_like 'secondary store' - end - end - end - end - - context 'with WRITE redis commands' do - let_it_be(:key1) { "redis:{1}:key_a" } - let_it_be(:key2) { "redis:{1}:key_b" } - let_it_be(:value1) { "redis_value1"} - let_it_be(:value2) { "redis_value2"} - let_it_be(:key1_value1) { [key1, value1] } - let_it_be(:key1_value2) { [key1, value2] } - let_it_be(:ttl) { 10 } - let_it_be(:key1_ttl_value1) { [key1, ttl, value1] } - let_it_be(:skey) { "redis:set:key" } - let_it_be(:svalues1) { [value2, value1] } - let_it_be(:svalues2) { [value1] } - let_it_be(:skey_value1) { [skey, value1] } - let_it_be(:skey_value2) { [skey, value2] } - - where(:case_name, :name, :args, :expected_value, :verification_name, :verification_args) do - 'execute :set command' | :set | ref(:key1_value1) | ref(:value1) | :get | ref(:key1) - 'execute :setnx command' | :setnx | ref(:key1_value2) | ref(:value1) | :get | ref(:key2) - 'execute :setex command' | :setex | ref(:key1_ttl_value1) | ref(:ttl) | :ttl | ref(:key1) - 'execute :sadd command' | :sadd | ref(:skey_value2) | ref(:svalues1) | :smembers | ref(:skey) - 'execute :srem command' | :srem | ref(:skey_value1) | [] | :smembers | ref(:skey) - 'execute :del command' | :del | ref(:key2) | nil | :get | ref(:key2) - 'execute :flushdb command' | :flushdb | nil | 0 | :dbsize | nil - end - - before do - primary_store.flushdb - secondary_store.flushdb - - primary_store.multi do |multi| - multi.set(key2, value1) - multi.sadd(skey, value1) - end - - secondary_store.multi do |multi| - multi.set(key2, value1) - multi.sadd(skey, value1) - end - end - - RSpec.shared_examples_for 'verify that store contains values' do |store| - it "#{store} redis store contains correct values", :aggregate_errors do - subject - - redis_store = multi_store.send(store) - - if expected_value.is_a?(Array) - # :smembers does not guarantee the order it will return the values - expect(redis_store.send(verification_name, *verification_args)).to match_array(expected_value) - else - expect(redis_store.send(verification_name, *verification_args)).to eq(expected_value) - end - end - end - - with_them do - describe "#{name}" do - let(:expected_args) {args || no_args } - - before do - allow(primary_store).to receive(name).and_call_original - allow(secondary_store).to receive(name).and_call_original - end - - context 'with feature flag :use_primary_and_secondary_stores_for_test_store' do - before do - stub_feature_flags(use_primary_and_secondary_stores_for_test_store: true) - end - - context 'when executing on primary instance is successful' do - it 'executes on both primary and secondary redis store', :aggregate_errors do - expect(primary_store).to receive(name).with(*expected_args).and_call_original - expect(secondary_store).to receive(name).with(*expected_args).and_call_original - - subject - end - - include_examples 'verify that store contains values', :primary_store - include_examples 'verify that store contains values', :secondary_store - end - - context 'when executing on the primary instance is raising an exception' do - before do - allow(primary_store).to receive(name).with(*expected_args).and_raise(StandardError) - allow(Gitlab::ErrorTracking).to receive(:log_exception) - end - - it 'logs the exception and execute on secondary instance', :aggregate_errors do - expect(Gitlab::ErrorTracking).to receive(:log_exception).with(an_instance_of(StandardError), - hash_including(extra: hash_including(:multi_store_error_message), command_name: name)) - expect(secondary_store).to receive(name).with(*expected_args).and_call_original - - subject - end - - include_examples 'verify that store contains values', :secondary_store - end - - context 'when the command is executed within pipelined block' do - subject do - multi_store.pipelined do - multi_store.send(name, *args) - end - end - - it 'is executed only 1 time on each instance', :aggregate_errors do - expect(primary_store).to receive(name).with(*expected_args).once - expect(secondary_store).to receive(name).with(*expected_args).once - - subject - end - - include_examples 'verify that store contains values', :primary_store - include_examples 'verify that store contains values', :secondary_store - end - end - - context 'with feature flag :use_primary_and_secondary_stores_for_test_store is disabled' do - before do - stub_feature_flags(use_primary_and_secondary_stores_for_test_store: false) - end - - context 'with feature flag :use_primary_store_as_default_for_test_store is disabled' do - before do - stub_feature_flags(use_primary_store_as_default_for_test_store: false) - end - - it 'executes only on the secondary redis store', :aggregate_errors do - expect(secondary_store).to receive(name).with(*expected_args) - expect(primary_store).not_to receive(name).with(*expected_args) - - subject - end - - include_examples 'verify that store contains values', :secondary_store - end - - context 'with feature flag :use_primary_store_as_default_for_test_store is enabled' do - before do - stub_feature_flags(use_primary_store_as_default_for_test_store: true) - end - - it 'executes only on the primary_redis redis store', :aggregate_errors do - expect(primary_store).to receive(name).with(*expected_args) - expect(secondary_store).not_to receive(name).with(*expected_args) - - subject - end - - include_examples 'verify that store contains values', :primary_store - end - end - end - end - end - - context 'with unsupported command' do - let(:counter) { Gitlab::Metrics::NullMetric.instance } - - before do - primary_store.flushdb - secondary_store.flushdb - allow(Gitlab::Metrics).to receive(:counter).and_return(counter) - end - - let_it_be(:key) { "redis:counter" } - - subject { multi_store.incr(key) } - - it 'executes method missing' do - expect(multi_store).to receive(:method_missing) - - subject - end - - context 'when command is not in SKIP_LOG_METHOD_MISSING_FOR_COMMANDS' do - it 'logs MethodMissingError' do - expect(Gitlab::ErrorTracking).to receive(:log_exception).with(an_instance_of(Gitlab::Redis::MultiStore::MethodMissingError), - hash_including(command_name: :incr, extra: hash_including(instance_name: instance_name))) - - subject - end - - it 'increments method missing counter' do - expect(counter).to receive(:increment).with(command: :incr, instance_name: instance_name) - - subject - end - end - - context 'when command is in SKIP_LOG_METHOD_MISSING_FOR_COMMANDS' do - subject { multi_store.info } - - it 'does not log MethodMissingError' do - expect(Gitlab::ErrorTracking).not_to receive(:log_exception) - - subject - end - - it 'does not increment method missing counter' do - expect(counter).not_to receive(:increment) - - subject - end - end - - context 'with feature flag :use_primary_store_as_default_for_test_store is enabled' do - before do - stub_feature_flags(use_primary_store_as_default_for_test_store: true) - end - - it 'fallback and executes only on the secondary store', :aggregate_errors do - expect(primary_store).to receive(:incr).with(key).and_call_original - expect(secondary_store).not_to receive(:incr) - - subject - end - - it 'correct value is stored on the secondary store', :aggregate_errors do - subject - - expect(secondary_store.get(key)).to be_nil - expect(primary_store.get(key)).to eq('1') - end - end - - context 'with feature flag :use_primary_store_as_default_for_test_store is disabled' do - before do - stub_feature_flags(use_primary_store_as_default_for_test_store: false) - end - - it 'fallback and executes only on the secondary store', :aggregate_errors do - expect(secondary_store).to receive(:incr).with(key).and_call_original - expect(primary_store).not_to receive(:incr) - - subject - end - - it 'correct value is stored on the secondary store', :aggregate_errors do - subject - - expect(primary_store.get(key)).to be_nil - expect(secondary_store.get(key)).to eq('1') - end - end - - context 'when the command is executed within pipelined block' do - subject do - multi_store.pipelined do - multi_store.incr(key) - end - end - - it 'is executed only 1 time on each instance', :aggregate_errors do - expect(primary_store).to receive(:incr).with(key).once - expect(secondary_store).to receive(:incr).with(key).once - - subject - end - - it "both redis stores are containing correct values", :aggregate_errors do - subject - - expect(primary_store.get(key)).to eq('1') - expect(secondary_store.get(key)).to eq('1') - end - end - end - - describe '#to_s' do - subject { multi_store.to_s } - - context 'with feature flag :use_primary_and_secondary_stores_for_test_store is enabled' do - before do - stub_feature_flags(use_primary_and_secondary_stores_for_test_store: true) - end - - it 'returns same value as primary_store' do - is_expected.to eq(primary_store.to_s) - end - end - - context 'with feature flag :use_primary_and_secondary_stores_for_test_store is disabled' do - before do - stub_feature_flags(use_primary_and_secondary_stores_for_test_store: false) - end - - context 'with feature flag :use_primary_store_as_default_for_test_store is enabled' do - before do - stub_feature_flags(use_primary_store_as_default_for_test_store: true) - end - - it 'returns same value as primary_store' do - is_expected.to eq(primary_store.to_s) - end - end - - context 'with feature flag :use_primary_store_as_default_for_test_store is disabled' do - before do - stub_feature_flags(use_primary_store_as_default_for_test_store: false) - end - - it 'returns same value as primary_store' do - is_expected.to eq(secondary_store.to_s) - end - end - end - end - - describe '#is_a?' do - it 'returns true for ::Redis::Store' do - expect(multi_store.is_a?(::Redis::Store)).to be true - end - end - - describe '#use_primary_and_secondary_stores?' do - context 'with feature flag :use_primary_and_secondary_stores_for_test_store is enabled' do - before do - stub_feature_flags(use_primary_and_secondary_stores_for_test_store: true) - end - - it 'multi store is disabled' do - expect(multi_store.use_primary_and_secondary_stores?).to be true - end - end - - context 'with feature flag :use_primary_and_secondary_stores_for_test_store is disabled' do - before do - stub_feature_flags(use_primary_and_secondary_stores_for_test_store: false) - end - - it 'multi store is disabled' do - expect(multi_store.use_primary_and_secondary_stores?).to be false - end - end - - context 'with empty DB' do - before do - allow(Feature::FlipperFeature).to receive(:table_exists?).and_return(false) - end - - it 'multi store is disabled' do - expect(multi_store.use_primary_and_secondary_stores?).to be false - end - end - - context 'when FF table guard raises' do - before do - allow(Feature::FlipperFeature).to receive(:table_exists?).and_raise - end - - it 'multi store is disabled' do - expect(multi_store.use_primary_and_secondary_stores?).to be false - end - end - end - - describe '#use_primary_store_as_default?' do - context 'with feature flag :use_primary_store_as_default_for_test_store is enabled' do - before do - stub_feature_flags(use_primary_store_as_default_for_test_store: true) - end - - it 'multi store is disabled' do - expect(multi_store.use_primary_store_as_default?).to be true - end - end - - context 'with feature flag :use_primary_store_as_default_for_test_store is disabled' do - before do - stub_feature_flags(use_primary_store_as_default_for_test_store: false) - end - - it 'multi store is disabled' do - expect(multi_store.use_primary_store_as_default?).to be false - end - end - - context 'with empty DB' do - before do - allow(Feature::FlipperFeature).to receive(:table_exists?).and_return(false) - end - - it 'multi store is disabled' do - expect(multi_store.use_primary_and_secondary_stores?).to be false - end - end - - context 'when FF table guard raises' do - before do - allow(Feature::FlipperFeature).to receive(:table_exists?).and_raise - end - - it 'multi store is disabled' do - expect(multi_store.use_primary_and_secondary_stores?).to be false - end - end - end - - def create_redis_store(options, extras = {}) - ::Redis::Store.new(options.merge(extras)) - end -end diff --git a/spec/lib/gitlab/redis/sessions_spec.rb b/spec/lib/gitlab/redis/sessions_spec.rb index 6ecbbf3294d..b02864cb73d 100644 --- a/spec/lib/gitlab/redis/sessions_spec.rb +++ b/spec/lib/gitlab/redis/sessions_spec.rb @@ -6,31 +6,16 @@ RSpec.describe Gitlab::Redis::Sessions do it_behaves_like "redis_new_instance_shared_examples", 'sessions', Gitlab::Redis::SharedState describe 'redis instance used in connection pool' do - before do + around do |example| + clear_pool + example.run + ensure clear_pool end - after do - clear_pool - end - - context 'when redis.sessions configuration is not provided' do - it 'uses ::Redis instance' do - expect(described_class).to receive(:config_fallback?).and_return(true) - - described_class.pool.with do |redis_instance| - expect(redis_instance).to be_instance_of(::Redis) - end - end - end - - context 'when redis.sessions configuration is provided' do - it 'instantiates an instance of MultiStore' do - expect(described_class).to receive(:config_fallback?).and_return(false) - - described_class.pool.with do |redis_instance| - expect(redis_instance).to be_instance_of(::Gitlab::Redis::MultiStore) - end + it 'uses ::Redis instance' do + described_class.pool.with do |redis_instance| + expect(redis_instance).to be_instance_of(::Redis) end end @@ -44,49 +29,9 @@ RSpec.describe Gitlab::Redis::Sessions do describe '#store' do subject(:store) { described_class.store(namespace: described_class::SESSION_NAMESPACE) } - context 'when redis.sessions configuration is NOT provided' do - it 'instantiates ::Redis instance' do - expect(described_class).to receive(:config_fallback?).and_return(true) - expect(store).to be_instance_of(::Redis::Store) - end - end - - context 'when redis.sessions configuration is provided' do - let(:config_new_format_host) { "spec/fixtures/config/redis_new_format_host.yml" } - let(:config_new_format_socket) { "spec/fixtures/config/redis_new_format_socket.yml" } - - before do - redis_clear_raw_config!(Gitlab::Redis::Sessions) - redis_clear_raw_config!(Gitlab::Redis::SharedState) - allow(described_class).to receive(:config_fallback?).and_return(false) - end - - after do - redis_clear_raw_config!(Gitlab::Redis::Sessions) - redis_clear_raw_config!(Gitlab::Redis::SharedState) - end - - # Check that Gitlab::Redis::Sessions is configured as MultiStore with proper attrs. - it 'instantiates an instance of MultiStore', :aggregate_failures do - expect(described_class).to receive(:config_file_name).and_return(config_new_format_host) - expect(::Gitlab::Redis::SharedState).to receive(:config_file_name).and_return(config_new_format_socket) - - expect(store).to be_instance_of(::Gitlab::Redis::MultiStore) - - expect(store.primary_store.to_s).to eq("Redis Client connected to test-host:6379 against DB 99 with namespace session:gitlab") - expect(store.secondary_store.to_s).to eq("Redis Client connected to /path/to/redis.sock against DB 0 with namespace session:gitlab") - - expect(store.instance_name).to eq('Sessions') - end - - context 'when MultiStore correctly configured' do - before do - allow(described_class).to receive(:config_file_name).and_return(config_new_format_host) - allow(::Gitlab::Redis::SharedState).to receive(:config_file_name).and_return(config_new_format_socket) - end - - it_behaves_like 'multi store feature flags', :use_primary_and_secondary_stores_for_sessions, :use_primary_store_as_default_for_sessions - end + # Check that Gitlab::Redis::Sessions is configured as RedisStore. + it 'instantiates an instance of Redis::Store' do + expect(store).to be_instance_of(::Redis::Store) end end end diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/database_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/database_metric_spec.rb index 3fca65cc5a4..4d84423cde4 100644 --- a/spec/lib/gitlab/usage/metrics/instrumentations/database_metric_spec.rb +++ b/spec/lib/gitlab/usage/metrics/instrumentations/database_metric_spec.rb @@ -19,7 +19,7 @@ RSpec.describe Gitlab::Usage::Metrics::Instrumentations::DatabaseMetric do let_it_be(:issues) { Issue.all } before do - allow(ActiveRecord::Base.connection).to receive(:transaction_open?).and_return(false) + allow(Issue.connection).to receive(:transaction_open?).and_return(false) end it 'calculates a correct result' do diff --git a/spec/lib/gitlab/usage_data_queries_spec.rb b/spec/lib/gitlab/usage_data_queries_spec.rb index 64eff76a9f2..a8cf87d9364 100644 --- a/spec/lib/gitlab/usage_data_queries_spec.rb +++ b/spec/lib/gitlab/usage_data_queries_spec.rb @@ -3,10 +3,6 @@ require 'spec_helper' RSpec.describe Gitlab::UsageDataQueries do - before do - allow(ActiveRecord::Base.connection).to receive(:transaction_open?).and_return(false) - end - describe '#add_metric' do let(:metric) { 'CountBoardsMetric' } diff --git a/spec/migrations/20211217174331_mark_recalculate_finding_signatures_as_completed_spec.rb b/spec/migrations/20211217174331_mark_recalculate_finding_signatures_as_completed_spec.rb new file mode 100644 index 00000000000..c5058f30d82 --- /dev/null +++ b/spec/migrations/20211217174331_mark_recalculate_finding_signatures_as_completed_spec.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true +require 'spec_helper' +require_migration! + +def create_background_migration_jobs(ids, status, created_at) + proper_status = case status + when :pending + Gitlab::Database::BackgroundMigrationJob.statuses['pending'] + when :succeeded + Gitlab::Database::BackgroundMigrationJob.statuses['succeeded'] + else + raise ArgumentError + end + + background_migration_jobs.create!( + class_name: 'RecalculateVulnerabilitiesOccurrencesUuid', + arguments: Array(ids), + status: proper_status, + created_at: created_at + ) +end + +RSpec.describe MarkRecalculateFindingSignaturesAsCompleted, :migration do + let_it_be(:background_migration_jobs) { table(:background_migration_jobs) } + + context 'when RecalculateVulnerabilitiesOccurrencesUuid jobs are present' do + before do + create_background_migration_jobs([1, 2, 3], :succeeded, DateTime.new(2021, 5, 5, 0, 2)) + create_background_migration_jobs([4, 5, 6], :pending, DateTime.new(2021, 5, 5, 0, 4)) + + create_background_migration_jobs([1, 2, 3], :succeeded, DateTime.new(2021, 8, 18, 0, 0)) + create_background_migration_jobs([4, 5, 6], :pending, DateTime.new(2021, 8, 18, 0, 2)) + create_background_migration_jobs([7, 8, 9], :pending, DateTime.new(2021, 8, 18, 0, 4)) + end + + describe 'gitlab.com' do + before do + allow(::Gitlab).to receive(:com?).and_return(true) + end + + it 'marks all jobs as succeeded' do + expect(background_migration_jobs.where(status: 1).count).to eq(2) + + migrate! + + expect(background_migration_jobs.where(status: 1).count).to eq(5) + end + end + + describe 'self managed' do + before do + allow(::Gitlab).to receive(:com?).and_return(false) + end + + it 'does not change job status' do + expect(background_migration_jobs.where(status: 1).count).to eq(2) + + migrate! + + expect(background_migration_jobs.where(status: 1).count).to eq(2) + end + end + end +end diff --git a/spec/models/alert_management/alert_spec.rb b/spec/models/alert_management/alert_spec.rb index 35398e29062..40bdfd4bc92 100644 --- a/spec/models/alert_management/alert_spec.rb +++ b/spec/models/alert_management/alert_spec.rb @@ -211,12 +211,6 @@ RSpec.describe AlertManagement::Alert do end end - describe '.open' do - subject { described_class.open } - - it { is_expected.to contain_exactly(acknowledged_alert, triggered_alert) } - end - describe '.not_resolved' do subject { described_class.not_resolved } @@ -324,33 +318,6 @@ RSpec.describe AlertManagement::Alert do end end - describe '.open_status?' do - using RSpec::Parameterized::TableSyntax - - where(:status, :is_open_status) do - :triggered | true - :acknowledged | true - :resolved | false - :ignored | false - nil | false - end - - with_them do - it 'returns true when the status is open status' do - expect(described_class.open_status?(status)).to eq(is_open_status) - end - end - end - - describe '#open?' do - it 'returns true when the status is open status' do - expect(triggered_alert.open?).to be true - expect(acknowledged_alert.open?).to be true - expect(resolved_alert.open?).to be false - expect(ignored_alert.open?).to be false - end - end - describe '#to_reference' do it { expect(triggered_alert.to_reference).to eq("^alert##{triggered_alert.iid}") } end diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb index e3816c31f1c..8f66978c311 100644 --- a/spec/models/ci/runner_spec.rb +++ b/spec/models/ci/runner_spec.rb @@ -239,44 +239,20 @@ RSpec.describe Ci::Runner do end end - context 'when ci_decompose_belonging_to_parent_group_of_project_query is enabled' do - context 'when use_traversal_ids* are enabled' do - it_behaves_like '.belonging_to_parent_group_of_project' - end - - context 'when use_traversal_ids* are disabled' do - before do - stub_feature_flags( - use_traversal_ids: false, - use_traversal_ids_for_ancestors: false, - use_traversal_ids_for_ancestor_scopes: false - ) - end - - it_behaves_like '.belonging_to_parent_group_of_project' - end + context 'when use_traversal_ids* are enabled' do + it_behaves_like '.belonging_to_parent_group_of_project' end - context 'when ci_decompose_belonging_to_parent_group_of_project_query is disabled' do + context 'when use_traversal_ids* are disabled' do before do - stub_feature_flags(ci_decompose_belonging_to_parent_group_of_project_query: false) + stub_feature_flags( + use_traversal_ids: false, + use_traversal_ids_for_ancestors: false, + use_traversal_ids_for_ancestor_scopes: false + ) end - context 'when use_traversal_ids* are enabled' do - it_behaves_like '.belonging_to_parent_group_of_project' - end - - context 'when use_traversal_ids* are disabled' do - before do - stub_feature_flags( - use_traversal_ids: false, - use_traversal_ids_for_ancestors: false, - use_traversal_ids_for_ancestor_scopes: false - ) - end - - it_behaves_like '.belonging_to_parent_group_of_project' - end + it_behaves_like '.belonging_to_parent_group_of_project' end describe '.owned_or_instance_wide' do diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index b523f60d045..d4c105619cd 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -542,6 +542,13 @@ RSpec.describe User do expect(user).to be_invalid expect(user.errors.messages[:email].first).to eq(expected_error) end + + it 'does not allow user to update email to a non-allowlisted domain' do + user = create(:user, email: "info@test.example.com") + + expect { user.update!(email: "test@notexample.com") } + .to raise_error(StandardError, 'Validation failed: Email is not allowed. Check with your administrator.') + end end context 'when a signup domain is allowed and subdomains are not allowed' do @@ -608,6 +615,13 @@ RSpec.describe User do user = build(:user, email: 'info@example.com', created_by_id: 1) expect(user).to be_valid end + + it 'does not allow user to update email to a denied domain' do + user = create(:user, email: 'info@test.com') + + expect { user.update!(email: 'info@example.com') } + .to raise_error(StandardError, 'Validation failed: Email is not allowed. Check with your administrator.') + end end context 'when a signup domain is denied but a wildcard subdomain is allowed' do @@ -679,6 +693,13 @@ RSpec.describe User do expect(user.errors.messages[:email].first).to eq(expected_error) end + it 'does not allow user to update email to a restricted domain' do + user = create(:user, email: 'info@test.com') + + expect { user.update!(email: 'info@gitlab.com') } + .to raise_error(StandardError, 'Validation failed: Email is not allowed. Check with your administrator.') + end + it 'does accept a valid email address' do user = build(:user, email: 'info@test.com') diff --git a/spec/models/users_statistics_spec.rb b/spec/models/users_statistics_spec.rb index 8553d0bfdb0..add9bd18755 100644 --- a/spec/models/users_statistics_spec.rb +++ b/spec/models/users_statistics_spec.rb @@ -43,7 +43,7 @@ RSpec.describe UsersStatistics do create_list(:user, 2, :bot) create_list(:user, 1, :blocked) - allow(ActiveRecord::Base.connection).to receive(:transaction_open?).and_return(false) + allow(described_class.connection).to receive(:transaction_open?).and_return(false) end context 'when successful' do diff --git a/spec/requests/api/graphql/mutations/issues/set_escalation_status_spec.rb b/spec/requests/api/graphql/mutations/issues/set_escalation_status_spec.rb new file mode 100644 index 00000000000..0166871502b --- /dev/null +++ b/spec/requests/api/graphql/mutations/issues/set_escalation_status_spec.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Setting the escalation status of an incident' do + include GraphqlHelpers + + let_it_be(:project) { create(:project) } + let_it_be(:issue) { create(:incident, project: project) } + let_it_be(:escalation_status) { create(:incident_management_issuable_escalation_status, issue: issue) } + let_it_be(:user) { create(:user) } + + let(:status) { 'ACKNOWLEDGED' } + let(:input) { { project_path: project.full_path, iid: issue.iid.to_s, status: status } } + + let(:current_user) { user } + let(:mutation) do + graphql_mutation(:issue_set_escalation_status, input) do + <<~QL + clientMutationId + errors + issue { + iid + escalationStatus + } + QL + end + end + + let(:mutation_response) { graphql_mutation_response(:issue_set_escalation_status) } + + before_all do + project.add_developer(user) + end + + context 'when user does not have permission to edit the escalation status' do + let(:current_user) { create(:user) } + + before_all do + project.add_reporter(user) + end + + it_behaves_like 'a mutation that returns a top-level access error' + end + + context 'with non-incident issue is provided' do + let_it_be(:issue) { create(:issue, project: project) } + + it_behaves_like 'a mutation that returns top-level errors', errors: ['Feature unavailable for provided issue'] + end + + context 'with feature disabled' do + before do + stub_feature_flags(incident_escalations: false) + end + + it_behaves_like 'a mutation that returns top-level errors', errors: ['Feature unavailable for provided issue'] + end + + it 'sets given escalation_policy to the escalation status for the issue' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response['errors']).to be_empty + expect(mutation_response['issue']['escalationStatus']).to eq(status) + expect(escalation_status.reload.status_name).to eq(:acknowledged) + end + + context 'when status argument is not given' do + let(:input) { {} } + + it_behaves_like 'a mutation that returns top-level errors' do + let(:match_errors) { contain_exactly(include('status (Expected value to not be null)')) } + end + end + + context 'when status argument is invalid' do + let(:status) { 'INVALID' } + + it_behaves_like 'an invalid argument to the mutation', argument_name: :status + end +end diff --git a/spec/requests/api/graphql/project/issues_spec.rb b/spec/requests/api/graphql/project/issues_spec.rb index b3e91afb5b3..f358ec3e53f 100644 --- a/spec/requests/api/graphql/project/issues_spec.rb +++ b/spec/requests/api/graphql/project/issues_spec.rb @@ -539,6 +539,43 @@ RSpec.describe 'getting an issue list for a project' do end end + context 'when fetching escalation status' do + let_it_be(:escalation_status) { create(:incident_management_issuable_escalation_status, issue: issue_a) } + + let(:statuses) { issue_data.to_h { |issue| [issue['iid'], issue['escalationStatus']] } } + let(:fields) do + <<~QUERY + edges { + node { + id + escalationStatus + } + } + QUERY + end + + before do + issue_a.update!(issue_type: Issue.issue_types[:incident]) + end + + it 'returns the escalation status values' do + post_graphql(query, current_user: current_user) + + statuses = issues_data.map { |issue| issue.dig('node', 'escalationStatus') } + + expect(statuses).to contain_exactly(escalation_status.status_name.upcase.to_s, nil) + end + + it 'avoids N+1 queries', :aggregate_failures do + base_count = ActiveRecord::QueryRecorder.new { run_with_clean_state(query, context: { current_user: current_user }) } + + new_incident = create(:incident, project: project) + create(:incident_management_issuable_escalation_status, issue: new_incident) + + expect { run_with_clean_state(query, context: { current_user: current_user }) }.not_to exceed_query_limit(base_count) + end + end + describe 'N+1 query checks' do let(:extra_iid_for_second_query) { issue_b.iid.to_s } let(:search_params) { { iids: [issue_a.iid.to_s] } } diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb index 98ec02d59c6..9cc5a245333 100644 --- a/spec/services/ci/create_pipeline_service_spec.rb +++ b/spec/services/ci/create_pipeline_service_spec.rb @@ -161,30 +161,6 @@ RSpec.describe Ci::CreatePipelineService do expect(merge_request_1.reload.head_pipeline).to eq(head_pipeline) expect(merge_request_2.reload.head_pipeline).to eq(head_pipeline) end - - # TODO: remove after ci_publish_pipeline_events FF is removed - # https://gitlab.com/gitlab-org/gitlab/-/issues/336752 - it 'does not schedule sync update for the head pipeline of the merge request' do - expect(UpdateHeadPipelineForMergeRequestWorker) - .not_to receive(:perform_async) - - execute_service(ref: 'feature', after: nil) - end - end - - context 'when feature flag ci_publish_pipeline_events is disabled' do - before do - stub_feature_flags(ci_publish_pipeline_events: false) - end - - it 'schedules update for the head pipeline of the merge request' do - expect(UpdateHeadPipelineForMergeRequestWorker) - .to receive(:perform_async).with(merge_request_1.id) - expect(UpdateHeadPipelineForMergeRequestWorker) - .to receive(:perform_async).with(merge_request_2.id) - - execute_service(ref: 'feature', after: nil) - end end end diff --git a/spec/services/incident_management/issuable_escalation_statuses/after_update_service_spec.rb b/spec/services/incident_management/issuable_escalation_statuses/after_update_service_spec.rb new file mode 100644 index 00000000000..78c93fd4591 --- /dev/null +++ b/spec/services/incident_management/issuable_escalation_statuses/after_update_service_spec.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe IncidentManagement::IssuableEscalationStatuses::AfterUpdateService do + let_it_be(:current_user) { create(:user) } + let_it_be(:escalation_status, reload: true) { create(:incident_management_issuable_escalation_status, :triggered) } + let_it_be(:issue, reload: true) { escalation_status.issue } + let_it_be(:project) { issue.project } + let_it_be(:alert) { create(:alert_management_alert, issue: issue, project: project) } + + let(:status_event) { :acknowledge } + let(:update_params) { { incident_management_issuable_escalation_status_attributes: { status_event: status_event } } } + let(:service) { IncidentManagement::IssuableEscalationStatuses::AfterUpdateService.new(issue, current_user) } + + subject(:result) do + issue.update!(update_params) + service.execute + end + + before do + issue.project.add_developer(current_user) + end + + shared_examples 'does not attempt to update the alert' do + specify do + expect(::AlertManagement::Alerts::UpdateService).not_to receive(:new) + + expect(result).to be_success + end + end + + context 'with status attributes' do + it 'updates an the associated alert with status changes' do + expect(::AlertManagement::Alerts::UpdateService) + .to receive(:new) + .with(alert, current_user, { status: :acknowledged }) + .and_call_original + + expect(result).to be_success + expect(alert.reload.status).to eq(escalation_status.reload.status) + end + + context 'when incident is not associated with an alert' do + before do + alert.destroy! + end + + it_behaves_like 'does not attempt to update the alert' + end + + context 'when status was not changed' do + let(:status_event) { :trigger } + + it_behaves_like 'does not attempt to update the alert' + end + end +end diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb index 98d2ab1341e..969d07ae4bd 100644 --- a/spec/services/issues/update_service_spec.rb +++ b/spec/services/issues/update_service_spec.rb @@ -1166,9 +1166,15 @@ RSpec.describe Issues::UpdateService, :mailer do context 'updating escalation status' do let(:opts) { { escalation_status: { status: 'acknowledged' } } } + let(:escalation_update_class) { ::IncidentManagement::IssuableEscalationStatuses::AfterUpdateService } shared_examples 'updates the escalation status record' do |expected_status| + let(:service_double) { instance_double(escalation_update_class) } + it 'has correct value' do + expect(escalation_update_class).to receive(:new).with(issue, user).and_return(service_double) + expect(service_double).to receive(:execute) + update_issue(opts) expect(issue.escalation_status.status_name).to eq(expected_status) @@ -1185,7 +1191,7 @@ RSpec.describe Issues::UpdateService, :mailer do end it 'does not trigger side-effects' do - expect(::AlertManagement::Alerts::UpdateService).not_to receive(:new) + expect(escalation_update_class).not_to receive(:new) update_issue(opts) end @@ -1207,6 +1213,7 @@ RSpec.describe Issues::UpdateService, :mailer do it 'syncs the update back to the alert' do update_issue(opts) + expect(issue.escalation_status.status_name).to eq(:acknowledged) expect(alert.reload.status_name).to eq(:acknowledged) end end diff --git a/spec/support/shared_examples/features/packages_shared_examples.rb b/spec/support/shared_examples/features/packages_shared_examples.rb index d14b4638ca5..ded30f32314 100644 --- a/spec/support/shared_examples/features/packages_shared_examples.rb +++ b/spec/support/shared_examples/features/packages_shared_examples.rb @@ -19,14 +19,12 @@ RSpec.shared_examples 'packages list' do |check_project_name: false| end RSpec.shared_examples 'package details link' do |property| - let(:package) { packages.first } - it 'navigates to the correct url' do page.within(packages_table_selector) do click_link package.name end - expect(page).to have_current_path(project_package_path(package.project, package)) + expect(page).to have_current_path(package_details_path) expect(page).to have_css('.packages-app h2[data-testid="title"]', text: package.name) diff --git a/spec/support/shared_examples/lib/gitlab/redis/multi_store_feature_flags_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/redis/multi_store_feature_flags_shared_examples.rb deleted file mode 100644 index 046c70bf779..00000000000 --- a/spec/support/shared_examples/lib/gitlab/redis/multi_store_feature_flags_shared_examples.rb +++ /dev/null @@ -1,43 +0,0 @@ -# frozen_string_literal: true - -RSpec.shared_examples 'multi store feature flags' do |use_primary_and_secondary_stores, use_primary_store_as_default| - context "with feature flag :#{use_primary_and_secondary_stores} is enabled" do - before do - stub_feature_flags(use_primary_and_secondary_stores => true) - end - - it 'multi store is enabled' do - expect(subject.use_primary_and_secondary_stores?).to be true - end - end - - context "with feature flag :#{use_primary_and_secondary_stores} is disabled" do - before do - stub_feature_flags(use_primary_and_secondary_stores => false) - end - - it 'multi store is disabled' do - expect(subject.use_primary_and_secondary_stores?).to be false - end - end - - context "with feature flag :#{use_primary_store_as_default} is enabled" do - before do - stub_feature_flags(use_primary_store_as_default => true) - end - - it 'primary store is enabled' do - expect(subject.use_primary_store_as_default?).to be true - end - end - - context "with feature flag :#{use_primary_store_as_default} is disabled" do - before do - stub_feature_flags(use_primary_store_as_default => false) - end - - it 'primary store is disabled' do - expect(subject.use_primary_store_as_default?).to be false - end - end -end diff --git a/spec/support/shared_examples/models/concerns/incident_management/escalatable_shared_examples.rb b/spec/support/shared_examples/models/concerns/incident_management/escalatable_shared_examples.rb index 7b33a95bfa1..8ee76efc896 100644 --- a/spec/support/shared_examples/models/concerns/incident_management/escalatable_shared_examples.rb +++ b/spec/support/shared_examples/models/concerns/incident_management/escalatable_shared_examples.rb @@ -95,6 +95,12 @@ RSpec.shared_examples 'a model including Escalatable' do it { is_expected.to eq([ignored_escalatable, resolved_escalatable, acknowledged_escalatable, triggered_escalatable]) } end end + + describe '.open' do + subject { all_escalatables.open } + + it { is_expected.to contain_exactly(acknowledged_escalatable, triggered_escalatable) } + end end describe '.status_value' do @@ -133,6 +139,24 @@ RSpec.shared_examples 'a model including Escalatable' do end end + describe '.open_status?' do + using RSpec::Parameterized::TableSyntax + + where(:status, :is_open_status) do + :triggered | true + :acknowledged | true + :resolved | false + :ignored | false + nil | false + end + + with_them do + it 'returns true when the status is open status' do + expect(described_class.open_status?(status)).to eq(is_open_status) + end + end + end + describe '#trigger' do subject { escalatable.trigger } @@ -237,6 +261,15 @@ RSpec.shared_examples 'a model including Escalatable' do end end + describe '#open?' do + it 'returns true when the status is open status' do + expect(triggered_escalatable.open?).to be true + expect(acknowledged_escalatable.open?).to be true + expect(resolved_escalatable.open?).to be false + expect(ignored_escalatable.open?).to be false + end + end + private def factory_from_class(klass)
{{ $options.i18n.deploymentsDescription }}
{{ $options.i18n.modalCopy }}