Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
		
							parent
							
								
									f33d28f789
								
							
						
					
					
						commit
						5849e597a0
					
				|  | @ -1,4 +1,10 @@ | |||
| import { initEmojiMap, getEmojiInfo, emojiFallbackImageSrc, emojiImageTag } from '../emoji'; | ||||
| import { | ||||
|   initEmojiMap, | ||||
|   getEmojiInfo, | ||||
|   emojiFallbackImageSrc, | ||||
|   emojiImageTag, | ||||
|   findCustomEmoji, | ||||
| } from '../emoji'; | ||||
| import isEmojiUnicodeSupported from '../emoji/support'; | ||||
| 
 | ||||
| class GlEmoji extends HTMLElement { | ||||
|  | @ -33,6 +39,7 @@ class GlEmoji extends HTMLElement { | |||
|         this.childNodes && | ||||
|         Array.prototype.every.call(this.childNodes, (childNode) => childNode.nodeType === 3); | ||||
| 
 | ||||
|       const customEmoji = findCustomEmoji(name); | ||||
|       const hasImageFallback = fallbackSrc?.length > 0; | ||||
|       const hasCssSpriteFallback = fallbackSpriteClass?.length > 0; | ||||
| 
 | ||||
|  | @ -51,7 +58,7 @@ class GlEmoji extends HTMLElement { | |||
|         this.classList.add(fallbackSpriteClass); | ||||
|       } else if (hasImageFallback) { | ||||
|         this.innerHTML = ''; | ||||
|         this.appendChild(emojiImageTag(name, fallbackSrc)); | ||||
|         this.appendChild(emojiImageTag(name, customEmoji?.src || fallbackSrc)); | ||||
|       } else { | ||||
|         const src = emojiFallbackImageSrc(name); | ||||
|         this.innerHTML = ''; | ||||
|  |  | |||
|  | @ -292,7 +292,9 @@ export default { | |||
|     <design-note-pin :is-resolved="discussion.resolved" :label="discussion.index" /> | ||||
|     <ul | ||||
|       class="design-discussion bordered-box gl-relative gl-p-0 gl-list-style-none" | ||||
|       :class="{ 'gl-bg-blue-50': isDiscussionActive }" | ||||
|       data-qa-selector="design_discussion_content" | ||||
|       data-testid="design-discussion-content" | ||||
|     > | ||||
|       <design-note | ||||
|         :note="firstNote" | ||||
|  | @ -300,7 +302,6 @@ export default { | |||
|         :is-resolving="isResolving" | ||||
|         :is-discussion="true" | ||||
|         :noteable-id="noteableId" | ||||
|         :class="{ 'gl-bg-blue-50': isDiscussionActive }" | ||||
|         @delete-note="showDeleteNoteConfirmationModal($event)" | ||||
|       > | ||||
|         <template v-if="isLoggedIn && discussion.resolvable" #resolve-discussion> | ||||
|  | @ -343,7 +344,6 @@ export default { | |||
|         :is-resolving="isResolving" | ||||
|         :noteable-id="noteableId" | ||||
|         :is-discussion="false" | ||||
|         :class="{ 'gl-bg-blue-50': isDiscussionActive }" | ||||
|         @delete-note="showDeleteNoteConfirmationModal($event)" | ||||
|       /> | ||||
|       <li | ||||
|  |  | |||
|  | @ -131,7 +131,7 @@ export default { | |||
| 
 | ||||
| <template> | ||||
|   <timeline-entry-item :id="`note_${noteAnchorId}`" class="design-note note-form"> | ||||
|     <gl-avatar-link :href="author.webUrl" class="gl-float-left gl-mr-3"> | ||||
|     <gl-avatar-link :href="author.webUrl" class="gl-float-left gl-mr-3 link-inherit-color"> | ||||
|       <gl-avatar :size="32" :src="author.avatarUrl" :entity-name="author.username" /> | ||||
|     </gl-avatar-link> | ||||
| 
 | ||||
|  | @ -140,7 +140,7 @@ export default { | |||
|         <gl-link | ||||
|           v-once | ||||
|           :href="author.webUrl" | ||||
|           class="js-user-link" | ||||
|           class="js-user-link link-inherit-color" | ||||
|           data-testid="user-link" | ||||
|           :data-user-id="authorId" | ||||
|           :data-username="author.username" | ||||
|  | @ -152,7 +152,7 @@ export default { | |||
|         <span class="note-headline-light note-headline-meta"> | ||||
|           <span class="system-note-message"> <slot></slot> </span> | ||||
|           <gl-link | ||||
|             class="note-timestamp system-note-separator gl-display-block gl-mb-2 gl-font-sm" | ||||
|             class="note-timestamp system-note-separator gl-display-block gl-mb-2 gl-font-sm link-inherit-color" | ||||
|             :href="`#note_${noteAnchorId}`" | ||||
|           > | ||||
|             <time-ago-tooltip :time="note.createdAt" tooltip-placement="bottom" /> | ||||
|  | @ -175,7 +175,6 @@ export default { | |||
|         <gl-disclosure-dropdown | ||||
|           v-if="isEditingAndHasPermissions" | ||||
|           v-gl-tooltip.hover | ||||
|           toggle-class="btn-sm" | ||||
|           icon="ellipsis_v" | ||||
|           category="tertiary" | ||||
|           data-qa-selector="design_discussion_actions_ellipsis_dropdown" | ||||
|  |  | |||
|  | @ -33,6 +33,9 @@ export default { | |||
|       this.renderGroup = true; | ||||
|       this.$emit('appear', this.category); | ||||
|     }, | ||||
|     onClick(emoji) { | ||||
|       this.$emit('click', { category: this.category, emoji }); | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
|  | @ -48,7 +51,7 @@ export default { | |||
|         :key="index" | ||||
|         :emojis="emojiGroup" | ||||
|         :render-group="renderGroup" | ||||
|         :click-emoji="(emoji) => $emit('click', emoji)" | ||||
|         :click-emoji="(emoji) => onClick(emoji)" | ||||
|       /> | ||||
|     </template> | ||||
|     <p v-else> | ||||
|  |  | |||
|  | @ -2,7 +2,7 @@ | |||
| import { GlIcon, GlDropdown, GlSearchBoxByType } from '@gitlab/ui'; | ||||
| import { findLastIndex } from 'lodash'; | ||||
| import VirtualList from 'vue-virtual-scroll-list'; | ||||
| import { CATEGORY_NAMES } from '~/emoji'; | ||||
| import { CATEGORY_NAMES, getEmojiCategoryMap } from '~/emoji'; | ||||
| import { CATEGORY_ICON_MAP, FREQUENTLY_USED_KEY } from '../constants'; | ||||
| import Category from './category.vue'; | ||||
| import EmojiList from './emoji_list.vue'; | ||||
|  | @ -49,6 +49,7 @@ export default { | |||
|     categoryNames() { | ||||
|       return CATEGORY_NAMES.filter((c) => { | ||||
|         if (c === FREQUENTLY_USED_KEY) return hasFrequentlyUsedEmojis(); | ||||
|         if (c === 'custom') return getEmojiCategoryMap()?.custom.length > 0; | ||||
|         return true; | ||||
|       }).map((category) => ({ | ||||
|         name: category, | ||||
|  | @ -66,10 +67,13 @@ export default { | |||
| 
 | ||||
|       this.$refs.virtualScoller.setScrollTop(top); | ||||
|     }, | ||||
|     selectEmoji(name) { | ||||
|       this.$emit('click', name); | ||||
|     selectEmoji({ category, emoji }) { | ||||
|       this.$emit('click', emoji); | ||||
|       this.$refs.dropdown.hide(); | ||||
|       addToFrequentlyUsed(name); | ||||
| 
 | ||||
|       if (category !== 'custom') { | ||||
|         addToFrequentlyUsed(emoji); | ||||
|       } | ||||
|     }, | ||||
|     getBoundaryElement() { | ||||
|       return this.boundary || document.querySelector('.content-wrapper') || 'scrollParent'; | ||||
|  |  | |||
|  | @ -52,7 +52,7 @@ export const getEmojiCategories = memoize(async () => { | |||
| 
 | ||||
|   return Object.freeze( | ||||
|     Object.keys(categories) | ||||
|       .filter((c) => c !== FREQUENTLY_USED_KEY) | ||||
|       .filter((c) => c !== FREQUENTLY_USED_KEY && categories[c].length) | ||||
|       .reduce((acc, category) => { | ||||
|         const emojis = chunk(categories[category], EMOJIS_PER_ROW); | ||||
|         const height = generateCategoryHeight(emojis.length); | ||||
|  |  | |||
|  | @ -3,6 +3,7 @@ export const FREQUENTLY_USED_COOKIE_KEY = 'frequently_used_emojis'; | |||
| 
 | ||||
| export const CATEGORY_ICON_MAP = { | ||||
|   [FREQUENTLY_USED_KEY]: 'history', | ||||
|   custom: 'tanuki', | ||||
|   activity: 'dumbbell', | ||||
|   people: 'smiley', | ||||
|   nature: 'nature', | ||||
|  |  | |||
|  | @ -1,14 +1,17 @@ | |||
| import { escape, minBy } from 'lodash'; | ||||
| import emojiRegexFactory from 'emoji-regex'; | ||||
| import emojiAliases from 'emojis/aliases.json'; | ||||
| import createApolloClient from '~/lib/graphql'; | ||||
| import { setAttributes } from '~/lib/utils/dom_utils'; | ||||
| import { getEmojiScoreWithIntent } from '~/emoji/utils'; | ||||
| import AccessorUtilities from '../lib/utils/accessor'; | ||||
| import axios from '../lib/utils/axios_utils'; | ||||
| import customEmojiQuery from './queries/custom_emoji.query.graphql'; | ||||
| import { CACHE_KEY, CACHE_VERSION_KEY, CATEGORY_ICON_MAP, FREQUENTLY_USED_KEY } from './constants'; | ||||
| 
 | ||||
| let emojiMap = null; | ||||
| let validEmojiNames = null; | ||||
| 
 | ||||
| export const FALLBACK_EMOJI_KEY = 'grey_question'; | ||||
| 
 | ||||
| // Keep the version in sync with `lib/gitlab/emoji.rb`
 | ||||
|  | @ -53,9 +56,42 @@ async function loadEmojiWithNames() { | |||
|   }, {}); | ||||
| } | ||||
| 
 | ||||
| export async function loadCustomEmojiWithNames() { | ||||
|   if (document.body?.dataset?.group && window.gon?.features?.customEmoji) { | ||||
|     const client = createApolloClient(); | ||||
|     const { data } = await client.query({ | ||||
|       query: customEmojiQuery, | ||||
|       variables: { | ||||
|         groupPath: document.body.dataset.group, | ||||
|       }, | ||||
|     }); | ||||
| 
 | ||||
|     return data?.group?.customEmoji?.nodes?.reduce((acc, e) => { | ||||
|       // Map the custom emoji into the format of the normal emojis
 | ||||
|       acc[e.name] = { | ||||
|         c: 'custom', | ||||
|         d: e.name, | ||||
|         e: undefined, | ||||
|         name: e.name, | ||||
|         src: e.url, | ||||
|         u: 'custom', | ||||
|       }; | ||||
| 
 | ||||
|       return acc; | ||||
|     }, {}); | ||||
|   } | ||||
| 
 | ||||
|   return {}; | ||||
| } | ||||
| 
 | ||||
| async function prepareEmojiMap() { | ||||
|   emojiMap = await loadEmojiWithNames(); | ||||
|   return Promise.all([loadEmojiWithNames(), loadCustomEmojiWithNames()]).then((values) => { | ||||
|     emojiMap = { | ||||
|       ...values[0], | ||||
|       ...values[1], | ||||
|     }; | ||||
|     validEmojiNames = [...Object.keys(emojiMap), ...Object.keys(emojiAliases)]; | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| export function initEmojiMap() { | ||||
|  | @ -84,6 +120,10 @@ export function getAllEmoji() { | |||
|   return emojiMap; | ||||
| } | ||||
| 
 | ||||
| export function findCustomEmoji(name) { | ||||
|   return emojiMap[name]; | ||||
| } | ||||
| 
 | ||||
| function getAliasesMatchingQuery(query) { | ||||
|   return Object.keys(emojiAliases) | ||||
|     .filter((alias) => alias.includes(query)) | ||||
|  | @ -176,7 +216,7 @@ export const CATEGORY_NAMES = Object.keys(CATEGORY_ICON_MAP); | |||
| 
 | ||||
| let emojiCategoryMap; | ||||
| export function getEmojiCategoryMap() { | ||||
|   if (!emojiCategoryMap) { | ||||
|   if (!emojiCategoryMap && emojiMap) { | ||||
|     emojiCategoryMap = CATEGORY_NAMES.reduce((acc, category) => { | ||||
|       if (category === FREQUENTLY_USED_KEY) { | ||||
|         return acc; | ||||
|  | @ -218,10 +258,11 @@ export function getEmojiInfo(query, fallback = true) { | |||
| } | ||||
| 
 | ||||
| export function emojiFallbackImageSrc(inputName) { | ||||
|   const { name } = getEmojiInfo(inputName); | ||||
|   return `${gon.asset_host || ''}${ | ||||
|     gon.relative_url_root || '' | ||||
|   }/-/emojis/${EMOJI_VERSION}/${name}.png`;
 | ||||
|   const { name, src } = getEmojiInfo(inputName); | ||||
|   return ( | ||||
|     src || | ||||
|     `${gon.asset_host || ''}${gon.relative_url_root || ''}/-/emojis/${EMOJI_VERSION}/${name}.png` | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| export function emojiImageTag(name, src) { | ||||
|  |  | |||
|  | @ -0,0 +1,12 @@ | |||
| query getCustomEmoji($groupPath: ID!) { | ||||
|   group(fullPath: $groupPath) { | ||||
|     id | ||||
|     customEmoji { | ||||
|       nodes { | ||||
|         id | ||||
|         name | ||||
|         url | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | @ -1,5 +1,6 @@ | |||
| <script> | ||||
| import { GlModal, GlSprintf } from '@gitlab/ui'; | ||||
| import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; | ||||
| import csrf from '~/lib/utils/csrf'; | ||||
| import { __, s__ } from '~/locale'; | ||||
| 
 | ||||
|  | @ -8,6 +9,7 @@ export default { | |||
|     GlModal, | ||||
|     GlSprintf, | ||||
|   }, | ||||
|   mixins: [glFeatureFlagMixin()], | ||||
|   props: { | ||||
|     actionUrl: { | ||||
|       type: String, | ||||
|  | @ -67,6 +69,9 @@ export default { | |||
|     }, | ||||
|   }, | ||||
|   i18n: { | ||||
|     textdelay: s__(`Profiles| | ||||
| You are about to permanently delete %{yourAccount}, and all of the issues, merge requests, and groups linked to your account. | ||||
| Once you confirm %{deleteAccount}, it cannot be undone or recovered. You might have to wait seven days before creating a new account with the same username or email.`), | ||||
|     text: s__(`Profiles| | ||||
| You are about to permanently delete %{yourAccount}, and all of the issues, merge requests, and groups linked to your account. | ||||
| Once you confirm %{deleteAccount}, it cannot be undone or recovered.`), | ||||
|  | @ -85,7 +90,16 @@ Once you confirm %{deleteAccount}, it cannot be undone or recovered.`), | |||
|     @primary="onSubmit" | ||||
|   > | ||||
|     <p> | ||||
|       <gl-sprintf :message="$options.i18n.text"> | ||||
|       <gl-sprintf v-if="glFeatures.delayDeleteOwnUser" :message="$options.i18n.textdelay"> | ||||
|         <template #yourAccount> | ||||
|           <strong>{{ s__('Profiles|your account') }}</strong> | ||||
|         </template> | ||||
| 
 | ||||
|         <template #deleteAccount> | ||||
|           <strong>{{ s__('Profiles|Delete account') }}</strong> | ||||
|         </template> | ||||
|       </gl-sprintf> | ||||
|       <gl-sprintf v-else :message="$options.i18n.text"> | ||||
|         <template #yourAccount> | ||||
|           <strong>{{ s__('Profiles|your account') }}</strong> | ||||
|         </template> | ||||
|  |  | |||
|  | @ -115,11 +115,11 @@ $t-gray-a-16-design-pin: rgba($black, 0.16); | |||
|   flex-basis: 28%; | ||||
| 
 | ||||
|   .link-inherit-color { | ||||
|     &, | ||||
|     &:hover, | ||||
|     &:active, | ||||
|     &:focus { | ||||
|       color: inherit; | ||||
|       text-decoration: none; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|  | @ -159,19 +159,10 @@ $t-gray-a-16-design-pin: rgba($black, 0.16); | |||
|       transition: background $gl-transition-duration-medium $general-hover-transition-curve; | ||||
|       border-top-left-radius: $border-radius-default; // same border radius used by .bordered-box | ||||
|       border-top-right-radius: $border-radius-default; | ||||
| 
 | ||||
|       a { | ||||
|         color: inherit; | ||||
|       } | ||||
| 
 | ||||
|       .note-text a { | ||||
|         color: var(--blue-600, $blue-600); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     .reply-wrapper { | ||||
|       padding: $gl-padding-8; | ||||
|       background: $gray-10; | ||||
|       border-radius: 0 0 $border-radius-default $border-radius-default; | ||||
|     } | ||||
|   } | ||||
|  |  | |||
|  | @ -7,6 +7,7 @@ class Profiles::AccountsController < Profiles::ApplicationController | |||
|   urgency :low, [:show] | ||||
| 
 | ||||
|   def show | ||||
|     push_frontend_feature_flag(:delay_delete_own_user) | ||||
|     render(locals: show_view_variables) | ||||
|   end | ||||
| 
 | ||||
|  |  | |||
|  | @ -33,7 +33,7 @@ class AwardEmojisFinder | |||
|   def validate_params | ||||
|     return unless params.present? | ||||
| 
 | ||||
|     validate_name_param | ||||
|     validate_name_param unless Feature.enabled?(:custom_emoji) | ||||
|     validate_awarded_by_param | ||||
|   end | ||||
| 
 | ||||
|  |  | |||
|  | @ -824,16 +824,6 @@ class Group < Namespace | |||
|     ).call | ||||
|   end | ||||
| 
 | ||||
|   def update_shared_runners_setting!(state) | ||||
|     raise ArgumentError unless SHARED_RUNNERS_SETTINGS.include?(state) | ||||
| 
 | ||||
|     case state | ||||
|     when SR_DISABLED_AND_UNOVERRIDABLE then disable_shared_runners! # also disallows override | ||||
|     when SR_DISABLED_WITH_OVERRIDE, SR_DISABLED_AND_OVERRIDABLE then disable_shared_runners_and_allow_override! | ||||
|     when SR_ENABLED then enable_shared_runners! # set both to true | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   def first_owner | ||||
|     owners.first || parent&.first_owner || owner | ||||
|   end | ||||
|  | @ -1068,45 +1058,6 @@ class Group < Namespace | |||
|       Arel::Nodes::SqlLiteral.new(column_alias)) | ||||
|   end | ||||
| 
 | ||||
|   def disable_shared_runners! | ||||
|     update!( | ||||
|       shared_runners_enabled: false, | ||||
|       allow_descendants_override_disabled_shared_runners: false) | ||||
| 
 | ||||
|     group_ids = descendants | ||||
|     unless group_ids.empty? | ||||
|       Group.by_id(group_ids).update_all( | ||||
|         shared_runners_enabled: false, | ||||
|         allow_descendants_override_disabled_shared_runners: false) | ||||
|     end | ||||
| 
 | ||||
|     all_projects.update_all(shared_runners_enabled: false) | ||||
|   end | ||||
| 
 | ||||
|   def disable_shared_runners_and_allow_override! | ||||
|     # enabled -> disabled_and_overridable | ||||
|     if shared_runners_enabled? | ||||
|       update!( | ||||
|         shared_runners_enabled: false, | ||||
|         allow_descendants_override_disabled_shared_runners: true) | ||||
| 
 | ||||
|       group_ids = descendants | ||||
|       unless group_ids.empty? | ||||
|         Group.by_id(group_ids).update_all(shared_runners_enabled: false) | ||||
|       end | ||||
| 
 | ||||
|       all_projects.update_all(shared_runners_enabled: false) | ||||
| 
 | ||||
|     # disabled_and_unoverridable -> disabled_and_overridable | ||||
|     else | ||||
|       update!(allow_descendants_override_disabled_shared_runners: true) | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   def enable_shared_runners! | ||||
|     update!(shared_runners_enabled: true) | ||||
|   end | ||||
| 
 | ||||
|   def runners_token_prefix | ||||
|     RunnersTokenPrefixable::RUNNERS_TOKEN_PREFIX | ||||
|   end | ||||
|  |  | |||
|  | @ -2311,7 +2311,7 @@ class User < ApplicationRecord | |||
|     return super if ::Gitlab::CurrentSettings.email_confirmation_setting_soft? | ||||
| 
 | ||||
|     # Following devise logic for method, we want to return `true` | ||||
|     # See: https://github.com/heartcombo/devise/blob/main/lib/devise/models/confirmable.rb#L191-L218 | ||||
|     # See: https://github.com/heartcombo/devise/blob/ec0674523e7909579a5a008f16fb9fe0c3a71712/lib/devise/models/confirmable.rb#L191-L218 | ||||
|     true | ||||
|   end | ||||
|   alias_method :in_confirmation_period?, :confirmation_period_valid? | ||||
|  |  | |||
|  | @ -25,7 +25,14 @@ module Groups | |||
|     end | ||||
| 
 | ||||
|     def update_shared_runners | ||||
|       group.update_shared_runners_setting!(params[:shared_runners_setting]) | ||||
|       case params[:shared_runners_setting] | ||||
|       when Namespace::SR_DISABLED_AND_UNOVERRIDABLE | ||||
|         disable_shared_runners! # also disallows override | ||||
|       when Namespace::SR_DISABLED_WITH_OVERRIDE, Namespace::SR_DISABLED_AND_OVERRIDABLE | ||||
|         disable_shared_runners_and_allow_override! | ||||
|       when Namespace::SR_ENABLED | ||||
|         enable_shared_runners! # set both to true | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     def update_pending_builds? | ||||
|  | @ -41,5 +48,42 @@ module Groups | |||
|         ::Ci::UpdatePendingBuildService.new(group, pending_builds_params).execute | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     def disable_shared_runners! | ||||
|       group.update!( | ||||
|         shared_runners_enabled: false, | ||||
|         allow_descendants_override_disabled_shared_runners: false) | ||||
| 
 | ||||
|       group_ids = group.descendants | ||||
|       unless group_ids.empty? | ||||
|         Group.by_id(group_ids).update_all( | ||||
|           shared_runners_enabled: false, | ||||
|           allow_descendants_override_disabled_shared_runners: false) | ||||
|       end | ||||
| 
 | ||||
|       group.all_projects.update_all(shared_runners_enabled: false) | ||||
|     end | ||||
| 
 | ||||
|     def disable_shared_runners_and_allow_override! | ||||
|       # enabled -> disabled_and_overridable | ||||
|       if group.shared_runners_enabled? | ||||
|         group.update!( | ||||
|           shared_runners_enabled: false, | ||||
|           allow_descendants_override_disabled_shared_runners: true) | ||||
| 
 | ||||
|         group_ids = group.descendants | ||||
|         Group.by_id(group_ids).update_all(shared_runners_enabled: false) unless group_ids.empty? | ||||
| 
 | ||||
|         group.all_projects.update_all(shared_runners_enabled: false) | ||||
| 
 | ||||
|       # disabled_and_unoverridable -> disabled_and_overridable | ||||
|       else | ||||
|         group.update!(allow_descendants_override_disabled_shared_runners: true) | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     def enable_shared_runners! | ||||
|       group.update!(shared_runners_enabled: true) | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  |  | |||
|  | @ -33,10 +33,15 @@ class RunPipelineScheduleWorker # rubocop:disable Scalability/IdempotentWorker | |||
|   def run_pipeline_schedule(schedule, user) | ||||
|     response = Ci::CreatePipelineService | ||||
|       .new(schedule.project, user, ref: schedule.ref) | ||||
|       .execute(:schedule, ignore_skip_ci: true, save_on_errors: false, schedule: schedule) | ||||
|       .execute( | ||||
|         :schedule, | ||||
|         save_on_errors: Feature.enabled?(:persist_failed_pipelines_from_schedules, schedule.project), | ||||
|         ignore_skip_ci: true, schedule: schedule | ||||
|       ) | ||||
| 
 | ||||
|     return response if response.payload.persisted? | ||||
| 
 | ||||
|     # Remove with FF persist_failed_pipelines_from_schedules enabled, as corrupted yml is not longer logged | ||||
|     # This is a user operation error such as corrupted .gitlab-ci.yml. Log the error for debugging purpose. | ||||
|     log_extra_metadata_on_done(:pipeline_creation_error, response.message) | ||||
|   rescue StandardError => e | ||||
|  |  | |||
|  | @ -0,0 +1,8 @@ | |||
| --- | ||||
| name: persist_failed_pipelines_from_schedules | ||||
| introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/124371 | ||||
| rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/416297 | ||||
| milestone: '16.2' | ||||
| type: development | ||||
| group: group::pipeline execution | ||||
| default_enabled: false | ||||
|  | @ -0,0 +1,24 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| class ChangeUnconfirmedCreatedAtIndexOnUsers < Gitlab::Database::Migration[2.1] | ||||
|   disable_ddl_transaction! | ||||
| 
 | ||||
|   OLD_INDEX_NAME = 'index_users_on_unconfirmed_and_created_at_for_active_humans' | ||||
|   NEW_INDEX_NAME = 'index_users_on_unconfirmed_created_at_active_type_sign_in_count' | ||||
| 
 | ||||
|   def up | ||||
|     add_concurrent_index :users, [:created_at, :id], | ||||
|       name: NEW_INDEX_NAME, | ||||
|       where: "confirmed_at IS NULL AND state = 'active' AND user_type IN (0) AND sign_in_count = 0" | ||||
| 
 | ||||
|     remove_concurrent_index_by_name :users, OLD_INDEX_NAME | ||||
|   end | ||||
| 
 | ||||
|   def down | ||||
|     add_concurrent_index :users, [:created_at, :id], | ||||
|       name: OLD_INDEX_NAME, | ||||
|       where: "confirmed_at IS NULL AND state = 'active' AND user_type IN (0)" | ||||
| 
 | ||||
|     remove_concurrent_index_by_name :users, NEW_INDEX_NAME | ||||
|   end | ||||
| end | ||||
|  | @ -0,0 +1 @@ | |||
| e728befa42eb6749929e758ece0f29ec57cd7614a378b8c5e4dc24f134f39185 | ||||
|  | @ -33171,7 +33171,7 @@ CREATE INDEX index_users_on_state_and_user_type ON users USING btree (state, use | |||
| 
 | ||||
| CREATE UNIQUE INDEX index_users_on_static_object_token ON users USING btree (static_object_token); | ||||
| 
 | ||||
| CREATE INDEX index_users_on_unconfirmed_and_created_at_for_active_humans ON users USING btree (created_at, id) WHERE ((confirmed_at IS NULL) AND ((state)::text = 'active'::text) AND (user_type = 0)); | ||||
| CREATE INDEX index_users_on_unconfirmed_created_at_active_type_sign_in_count ON users USING btree (created_at, id) WHERE ((confirmed_at IS NULL) AND ((state)::text = 'active'::text) AND (user_type = 0) AND (sign_in_count = 0)); | ||||
| 
 | ||||
| CREATE INDEX index_users_on_unconfirmed_email ON users USING btree (unconfirmed_email) WHERE (unconfirmed_email IS NOT NULL); | ||||
| 
 | ||||
|  |  | |||
|  | @ -46,9 +46,9 @@ For an overview, see | |||
| 
 | ||||
| After you add a group, the following data is synced to Jira for all projects in that group: | ||||
| 
 | ||||
| - New merge requests, branches, and commits | ||||
| - Existing merge requests (GitLab 13.8 and later) | ||||
| - Existing branches and commits (GitLab 15.11 and later) | ||||
| - New merge requests, branches, and commits. | ||||
| - Existing merge requests (GitLab 13.8 and later). | ||||
| - Existing branches and commits (GitLab 15.11 and later). You must delete and add any namespaces that were added to the GitLab for Jira Cloud app in GitLab 15.10 and earlier. | ||||
| 
 | ||||
| ## Update the GitLab for Jira Cloud app | ||||
| 
 | ||||
|  |  | |||
|  | @ -63,7 +63,7 @@ module Backup | |||
|         progress.flush | ||||
|       end | ||||
|     ensure | ||||
|       ::Gitlab::Database::EachDatabase.each_database_connection( | ||||
|       ::Gitlab::Database::EachDatabase.each_connection( | ||||
|         only: base_models_for_backup.keys, include_shared: false | ||||
|       ) do |connection, _| | ||||
|         Gitlab::Database::TransactionTimeoutSettings.new(connection).restore_timeouts | ||||
|  | @ -259,7 +259,7 @@ module Backup | |||
|       @database_to_snapshot_id = {} | ||||
| 
 | ||||
|       if @database_to_snapshot_id.empty? | ||||
|         ::Gitlab::Database::EachDatabase.each_database_connection( | ||||
|         ::Gitlab::Database::EachDatabase.each_connection( | ||||
|           only: base_models_for_backup.keys, include_shared: false | ||||
|         ) do |connection, database_name| | ||||
|           @database_to_snapshot_id[database_name] = nil | ||||
|  |  | |||
|  | @ -9,6 +9,8 @@ module Gitlab | |||
|       class ProjectPipelineStatus | ||||
|         include Gitlab::Utils::StrongMemoize | ||||
| 
 | ||||
|         STATUS_KEY_TTL = 8.hours | ||||
| 
 | ||||
|         attr_accessor :sha, :status, :ref, :project, :loaded | ||||
| 
 | ||||
|         def self.load_for_project(project) | ||||
|  | @ -89,12 +91,17 @@ module Gitlab | |||
|             self.sha, self.status, self.ref = redis.hmget(cache_key, :sha, :status, :ref) | ||||
| 
 | ||||
|             self.status = nil if self.status.empty? | ||||
| 
 | ||||
|             redis.expire(cache_key, STATUS_KEY_TTL) | ||||
|           end | ||||
|         end | ||||
| 
 | ||||
|         def store_in_cache | ||||
|           with_redis do |redis| | ||||
|             redis.mapped_hmset(cache_key, { sha: sha, status: status, ref: ref }) | ||||
|             redis.pipelined do |p| | ||||
|               p.mapped_hmset(cache_key, { sha: sha, status: status, ref: ref }) | ||||
|               p.expire(cache_key, STATUS_KEY_TTL) | ||||
|             end | ||||
|           end | ||||
|         end | ||||
| 
 | ||||
|  |  | |||
|  | @ -4,7 +4,7 @@ module Gitlab | |||
|   module Database | ||||
|     module EachDatabase | ||||
|       class << self | ||||
|         def each_database_connection(only: nil, include_shared: true) | ||||
|         def each_connection(only: nil, include_shared: true) | ||||
|           selected_names = Array.wrap(only) | ||||
|           base_models = select_base_models(selected_names) | ||||
| 
 | ||||
|  | @ -18,7 +18,6 @@ module Gitlab | |||
|             end | ||||
|           end | ||||
|         end | ||||
|         alias_method :each_db_connection, :each_database_connection | ||||
| 
 | ||||
|         def each_model_connection(models, only_on: nil, &blk) | ||||
|           selected_databases = Array.wrap(only_on).map(&:to_sym) | ||||
|  |  | |||
|  | @ -32,7 +32,7 @@ module Gitlab | |||
|             result_dir = background_migrations_dir(for_database, legacy_mode) | ||||
| 
 | ||||
|             # Only one loop iteration since we pass `only:` here | ||||
|             Gitlab::Database::EachDatabase.each_database_connection(only: for_database) do |connection| | ||||
|             Gitlab::Database::EachDatabase.each_connection(only: for_database) do |connection| | ||||
|               from_id = batched_migrations_last_id(for_database).read | ||||
| 
 | ||||
|               runner = Gitlab::Database::Migrations::TestBatchedBackgroundRunner | ||||
|  | @ -68,7 +68,7 @@ module Gitlab | |||
|             runner = nil | ||||
|             base_dir = background_migrations_dir(for_database, false) | ||||
| 
 | ||||
|             Gitlab::Database::EachDatabase.each_database_connection(only: for_database) do |connection| | ||||
|             Gitlab::Database::EachDatabase.each_connection(only: for_database) do |connection| | ||||
|               runner = Gitlab::Database::Migrations::BatchedMigrationLastId | ||||
|                          .new(connection, base_dir) | ||||
|             end | ||||
|  |  | |||
|  | @ -40,7 +40,7 @@ module Gitlab | |||
|               next if model < ::Gitlab::Database::SharedModel && !(model < TableWithoutModel) | ||||
| 
 | ||||
|               model_connection_name = model.connection_db_config.name | ||||
|               Gitlab::Database::EachDatabase.each_db_connection(include_shared: false) do |connection, connection_name| | ||||
|               Gitlab::Database::EachDatabase.each_connection(include_shared: false) do |connection, connection_name| | ||||
|                 if connection_name != model_connection_name | ||||
|                   PartitionManager.new(model, connection: connection).sync_partitions | ||||
|                 end | ||||
|  | @ -64,7 +64,7 @@ module Gitlab | |||
| 
 | ||||
|           Gitlab::AppLogger.info(message: 'Dropping detached postgres partitions') | ||||
| 
 | ||||
|           Gitlab::Database::EachDatabase.each_database_connection do | ||||
|           Gitlab::Database::EachDatabase.each_connection do | ||||
|             DetachedPartitionDropper.new.perform | ||||
|           end | ||||
| 
 | ||||
|  |  | |||
|  | @ -20,7 +20,7 @@ module Gitlab | |||
|       end | ||||
| 
 | ||||
|       def self.invoke(database = nil) | ||||
|         Gitlab::Database::EachDatabase.each_database_connection do |connection, connection_name| | ||||
|         Gitlab::Database::EachDatabase.each_connection do |connection, connection_name| | ||||
|           next if database && database.to_s != connection_name.to_s | ||||
| 
 | ||||
|           Gitlab::Database::SharedModel.logger = Logger.new($stdout) if Gitlab::Utils.to_boolean(ENV['LOG_QUERIES_TO_CONSOLE'], default: false) | ||||
|  |  | |||
|  | @ -13,7 +13,7 @@ module Gitlab | |||
|       end | ||||
| 
 | ||||
|       def unlock_writes | ||||
|         Gitlab::Database::EachDatabase.each_database_connection do |connection, database_name| | ||||
|         Gitlab::Database::EachDatabase.each_connection do |connection, database_name| | ||||
|           tables_to_lock(connection) do |table_name, schema_name| | ||||
|             # TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/366834 | ||||
|             next if schema_name.in? GITLAB_SCHEMAS_TO_IGNORE | ||||
|  | @ -28,7 +28,7 @@ module Gitlab | |||
|       # It locks the tables on the database where they don't belong. Also it unlocks the tables | ||||
|       # on the database where they belong | ||||
|       def lock_writes | ||||
|         Gitlab::Database::EachDatabase.each_database_connection(include_shared: false) do |connection, database_name| | ||||
|         Gitlab::Database::EachDatabase.each_connection(include_shared: false) do |connection, database_name| | ||||
|           schemas_for_connection = Gitlab::Database.gitlab_schemas_for_connection(connection) | ||||
| 
 | ||||
|           tables_to_lock(connection) do |table_name, schema_name| | ||||
|  |  | |||
|  | @ -75,6 +75,7 @@ module Gitlab | |||
|       # To be removed with https://gitlab.com/gitlab-org/gitlab/-/issues/399248 | ||||
|       push_frontend_feature_flag(:remove_monitor_metrics) | ||||
|       push_frontend_feature_flag(:gitlab_duo, current_user) | ||||
|       push_frontend_feature_flag(:custom_emoji) | ||||
|     end | ||||
| 
 | ||||
|     # Exposes the state of a feature flag to the frontend code. | ||||
|  |  | |||
|  | @ -126,12 +126,12 @@ module Gitlab | |||
|     end | ||||
| 
 | ||||
|     def self.without_statement_timeout | ||||
|       Gitlab::Database::EachDatabase.each_database_connection do |connection| | ||||
|       Gitlab::Database::EachDatabase.each_connection do |connection| | ||||
|         connection.execute('SET statement_timeout=0') | ||||
|       end | ||||
|       yield | ||||
|     ensure | ||||
|       Gitlab::Database::EachDatabase.each_database_connection do |connection| | ||||
|       Gitlab::Database::EachDatabase.each_connection do |connection| | ||||
|         connection.execute('RESET statement_timeout') | ||||
|       end | ||||
|     end | ||||
|  |  | |||
|  | @ -8,7 +8,7 @@ namespace :dev do | |||
|     ENV['force'] = 'yes' | ||||
|     Rake::Task["gitlab:setup"].invoke | ||||
| 
 | ||||
|     Gitlab::Database::EachDatabase.each_database_connection do |connection| | ||||
|     Gitlab::Database::EachDatabase.each_connection do |connection| | ||||
|       # Make sure DB statistics are up to date. | ||||
|       # gitlab:setup task can insert quite a bit of data, especially with MASS_INSERT=1 | ||||
|       # so ANALYZE can take more than default 15s statement timeout. This being a dev task, | ||||
|  | @ -61,7 +61,7 @@ namespace :dev do | |||
|         AND pid <> pg_backend_pid(); | ||||
|       SQL | ||||
| 
 | ||||
|       Gitlab::Database::EachDatabase.each_database_connection(include_shared: false) do |connection| | ||||
|       Gitlab::Database::EachDatabase.each_connection(include_shared: false) do |connection| | ||||
|         connection.execute(cmd) | ||||
|       rescue ActiveRecord::NoDatabaseError | ||||
|       end | ||||
|  |  | |||
|  | @ -33,7 +33,7 @@ namespace :gitlab do | |||
|         exit 1 | ||||
|       end | ||||
| 
 | ||||
|       Gitlab::Database::EachDatabase.each_database_connection(only: only_on) do |connection, name| | ||||
|       Gitlab::Database::EachDatabase.each_connection(only: only_on) do |connection, name| | ||||
|         connection.execute("INSERT INTO schema_migrations (version) VALUES (#{connection.quote(version)})") | ||||
| 
 | ||||
|         puts "Successfully marked '#{version}' as complete on database #{name}".color(:green) | ||||
|  | @ -57,7 +57,7 @@ namespace :gitlab do | |||
|     end | ||||
| 
 | ||||
|     def drop_tables(only_on: nil) | ||||
|       Gitlab::Database::EachDatabase.each_database_connection(only: only_on) do |connection, name| | ||||
|       Gitlab::Database::EachDatabase.each_connection(only: only_on) do |connection, name| | ||||
|         # In PostgreSQLAdapter, data_sources returns both views and tables, so use tables instead | ||||
|         tables = connection.tables | ||||
| 
 | ||||
|  | @ -292,7 +292,7 @@ namespace :gitlab do | |||
|             exit | ||||
|           end | ||||
| 
 | ||||
|           Gitlab::Database::EachDatabase.each_database_connection(only: database_name) do | ||||
|           Gitlab::Database::EachDatabase.each_connection(only: database_name) do | ||||
|             Gitlab::Database::AsyncIndexes.execute_pending_actions!(how_many: args[:pick].to_i) | ||||
|           end | ||||
|         end | ||||
|  | @ -322,7 +322,7 @@ namespace :gitlab do | |||
|             exit | ||||
|           end | ||||
| 
 | ||||
|           Gitlab::Database::EachDatabase.each_database_connection(only: database_name) do | ||||
|           Gitlab::Database::EachDatabase.each_connection(only: database_name) do | ||||
|             Gitlab::Database::AsyncConstraints.validate_pending_entries!(how_many: args[:pick].to_i) | ||||
|           end | ||||
|         end | ||||
|  | @ -413,7 +413,7 @@ namespace :gitlab do | |||
| 
 | ||||
|     desc 'Run all pending batched migrations' | ||||
|     task execute_batched_migrations: :environment do | ||||
|       Gitlab::Database::EachDatabase.each_database_connection do |connection, name| | ||||
|       Gitlab::Database::EachDatabase.each_connection do |connection, name| | ||||
|         Gitlab::Database::BackgroundMigration::BatchedMigration.with_status(:active).queue_order.each do |migration| | ||||
|           Gitlab::AppLogger.info("Executing batched migration #{migration.id} on database #{name} inline") | ||||
|           Gitlab::Database::BackgroundMigration::BatchedMigrationRunner.new(connection: connection).run_entire_migration(migration) | ||||
|  |  | |||
|  | @ -5,7 +5,7 @@ task migration_fix_15_11: [:environment] do | |||
|   next if Gitlab.com? | ||||
| 
 | ||||
|   only_on = %i[main ci].select { |db| Gitlab::Database.has_database?(db) } | ||||
|   Gitlab::Database::EachDatabase.each_database_connection(only: only_on) do |conn, database| | ||||
|   Gitlab::Database::EachDatabase.each_connection(only: only_on) do |conn, database| | ||||
|     begin | ||||
|       first_migration = conn.execute('SELECT * FROM schema_migrations ORDER BY version ASC LIMIT 1') | ||||
|     rescue ActiveRecord::StatementInvalid | ||||
|  |  | |||
|  | @ -1959,6 +1959,9 @@ msgstr "" | |||
| msgid "AI|I don't see how I can help. Please give better instructions!" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "AI|May provide inappropriate responses not representative of GitLab's views. Do not input personal data." | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "AI|Populate issue description" | ||||
| msgstr "" | ||||
| 
 | ||||
|  | @ -35122,6 +35125,9 @@ msgstr "" | |||
| msgid "Profiles| You are about to permanently delete %{yourAccount}, and all of the issues, merge requests, and groups linked to your account. Once you confirm %{deleteAccount}, it cannot be undone or recovered." | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Profiles| You are about to permanently delete %{yourAccount}, and all of the issues, merge requests, and groups linked to your account. Once you confirm %{deleteAccount}, it cannot be undone or recovered. You might have to wait seven days before creating a new account with the same username or email." | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Profiles| You are going to change the username %{currentUsernameBold} to %{newUsernameBold}. Profile and projects will be redirected to the %{newUsername} namespace but this redirect will expire once the %{currentUsername} namespace is registered by another user or group. Please update your Git repository remotes as soon as possible." | ||||
| msgstr "" | ||||
| 
 | ||||
|  | @ -55056,6 +55062,9 @@ msgstr "" | |||
| msgid "must be before %{expiry_date}" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "must be false when email confirmation setting is off" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "must be greater than start date" | ||||
| msgstr "" | ||||
| 
 | ||||
|  |  | |||
|  | @ -124,7 +124,7 @@ RSpec.describe 'Database schema', feature_category: :database do | |||
|   }.with_indifferent_access.freeze | ||||
| 
 | ||||
|   context 'for table' do | ||||
|     Gitlab::Database::EachDatabase.each_database_connection do |connection, _| | ||||
|     Gitlab::Database::EachDatabase.each_connection do |connection, _| | ||||
|       schemas_for_connection = Gitlab::Database.gitlab_schemas_for_connection(connection) | ||||
|       (connection.tables - TABLE_PARTITIONS).sort.each do |table| | ||||
|         table_schema = Gitlab::Database::GitlabSchema.table_schema(table) | ||||
|  | @ -300,7 +300,7 @@ RSpec.describe 'Database schema', feature_category: :database do | |||
| 
 | ||||
|   context 'primary keys' do | ||||
|     it 'expects every table to have a primary key defined' do | ||||
|       Gitlab::Database::EachDatabase.each_database_connection do |connection, _| | ||||
|       Gitlab::Database::EachDatabase.each_connection do |connection, _| | ||||
|         schemas_for_connection = Gitlab::Database.gitlab_schemas_for_connection(connection) | ||||
| 
 | ||||
|         problematic_tables = connection.tables.select do |table| | ||||
|  |  | |||
|  | @ -12,6 +12,10 @@ RSpec.describe AwardEmojisFinder do | |||
|   let_it_be(:issue_2_thumbsup) { create(:award_emoji, name: 'thumbsup', awardable: issue_2) } | ||||
|   let_it_be(:issue_2_thumbsdown) { create(:award_emoji, name: 'thumbsdown', awardable: issue_2) } | ||||
| 
 | ||||
|   before do | ||||
|     stub_feature_flags(custom_emoji: false) | ||||
|   end | ||||
| 
 | ||||
|   describe 'param validation' do | ||||
|     it 'raises an error if `name` is invalid' do | ||||
|       expect { described_class.new(issue_1, { name: 'invalid' }).execute }.to raise_error( | ||||
|  |  | |||
|  | @ -1,11 +1,18 @@ | |||
| import { initEmojiMock, clearEmojiMock } from 'helpers/emoji'; | ||||
| import waitForPromises from 'helpers/wait_for_promises'; | ||||
| import { createMockClient } from 'helpers/mock_apollo_helper'; | ||||
| import installGlEmojiElement from '~/behaviors/gl_emoji'; | ||||
| import { EMOJI_VERSION } from '~/emoji'; | ||||
| import customEmojiQuery from '~/emoji/queries/custom_emoji.query.graphql'; | ||||
| 
 | ||||
| import * as EmojiUnicodeSupport from '~/emoji/support'; | ||||
| 
 | ||||
| let mockClient; | ||||
| 
 | ||||
| jest.mock('~/emoji/support'); | ||||
| jest.mock('~/lib/graphql', () => { | ||||
|   return () => mockClient; | ||||
| }); | ||||
| 
 | ||||
| describe('gl_emoji', () => { | ||||
|   const emojiData = { | ||||
|  | @ -36,16 +43,17 @@ describe('gl_emoji', () => { | |||
|     return div.firstElementChild; | ||||
|   } | ||||
| 
 | ||||
|   beforeEach(async () => { | ||||
|     await initEmojiMock(emojiData); | ||||
|   }); | ||||
| 
 | ||||
|   afterEach(() => { | ||||
|     clearEmojiMock(); | ||||
| 
 | ||||
|     document.body.innerHTML = ''; | ||||
|   }); | ||||
| 
 | ||||
|   describe('standard emoji', () => { | ||||
|     beforeEach(async () => { | ||||
|       await initEmojiMock(emojiData); | ||||
|     }); | ||||
| 
 | ||||
|     describe.each([ | ||||
|       [ | ||||
|         'bomb emoji just with name attribute', | ||||
|  | @ -134,3 +142,45 @@ describe('gl_emoji', () => { | |||
|       expect(window.gon.emoji_sprites_css_added).toBe(true); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   describe('custom emoji', () => { | ||||
|     beforeEach(async () => { | ||||
|       mockClient = createMockClient([ | ||||
|         [ | ||||
|           customEmojiQuery, | ||||
|           jest.fn().mockResolvedValue({ | ||||
|             data: { | ||||
|               group: { | ||||
|                 id: 1, | ||||
|                 customEmoji: { | ||||
|                   nodes: [{ id: 1, name: 'parrot', url: 'parrot.gif' }], | ||||
|                 }, | ||||
|               }, | ||||
|             }, | ||||
|           }), | ||||
|         ], | ||||
|       ]); | ||||
| 
 | ||||
|       window.gon = { features: { customEmoji: true } }; | ||||
|       document.body.dataset.group = 'test-group'; | ||||
| 
 | ||||
|       await initEmojiMock(emojiData); | ||||
|     }); | ||||
| 
 | ||||
|     afterEach(() => { | ||||
|       window.gon = {}; | ||||
|       delete document.body.dataset.group; | ||||
|     }); | ||||
| 
 | ||||
|     it('renders custom emoji', async () => { | ||||
|       const glEmojiElement = markupToDomElement('<gl-emoji data-name="parrot"></gl-emoji>'); | ||||
| 
 | ||||
|       await waitForPromises(); | ||||
| 
 | ||||
|       const img = glEmojiElement.querySelector('img'); | ||||
| 
 | ||||
|       expect(glEmojiElement.dataset.unicodeVersion).toBe('custom'); | ||||
|       expect(img.getAttribute('src')).toBe('parrot.gif'); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
|  |  | |||
|  | @ -6,7 +6,7 @@ exports[`Design note component should match the snapshot 1`] = ` | |||
|   id="note_123" | ||||
| > | ||||
|   <glavatarlink-stub | ||||
|     class="gl-float-left gl-mr-3" | ||||
|     class="gl-float-left gl-mr-3 link-inherit-color" | ||||
|     href="https://gitlab.com/user" | ||||
|   > | ||||
|     <glavatar-stub | ||||
|  | @ -24,7 +24,7 @@ exports[`Design note component should match the snapshot 1`] = ` | |||
|   > | ||||
|     <div> | ||||
|       <gllink-stub | ||||
|         class="js-user-link" | ||||
|         class="js-user-link link-inherit-color" | ||||
|         data-testid="user-link" | ||||
|         data-user-id="1" | ||||
|         data-username="foo-bar" | ||||
|  | @ -53,7 +53,7 @@ exports[`Design note component should match the snapshot 1`] = ` | |||
|         /> | ||||
|           | ||||
|         <gllink-stub | ||||
|           class="note-timestamp system-note-separator gl-display-block gl-mb-2 gl-font-sm" | ||||
|           class="note-timestamp system-note-separator gl-display-block gl-mb-2 gl-font-sm link-inherit-color" | ||||
|           href="#note_123" | ||||
|         > | ||||
|           <timeagotooltip-stub | ||||
|  |  | |||
|  | @ -29,6 +29,7 @@ const DEFAULT_TODO_COUNT = 2; | |||
| describe('Design discussions component', () => { | ||||
|   let wrapper; | ||||
| 
 | ||||
|   const findDesignNotesList = () => wrapper.find('[data-testid="design-discussion-content"]'); | ||||
|   const findDesignNotes = () => wrapper.findAllComponents(DesignNote); | ||||
|   const findReplyPlaceholder = () => wrapper.findComponent(ReplyPlaceholder); | ||||
|   const findReplyForm = () => wrapper.findComponent(DesignReplyForm); | ||||
|  | @ -287,7 +288,7 @@ describe('Design discussions component', () => { | |||
| 
 | ||||
|   describe('when any note from a discussion is active', () => { | ||||
|     it.each([notes[0], notes[0].discussion.notes.nodes[1]])( | ||||
|       'applies correct class to all notes in the active discussion', | ||||
|       'applies correct class to the active discussion', | ||||
|       (note) => { | ||||
|         createComponent({ | ||||
|           props: { discussion: mockDiscussion }, | ||||
|  | @ -299,11 +300,7 @@ describe('Design discussions component', () => { | |||
|           }, | ||||
|         }); | ||||
| 
 | ||||
|         expect( | ||||
|           wrapper | ||||
|             .findAllComponents(DesignNote) | ||||
|             .wrappers.every((designNote) => designNote.classes('gl-bg-blue-50')), | ||||
|         ).toBe(true); | ||||
|         expect(findDesignNotesList().classes('gl-bg-blue-50')).toBe(true); | ||||
|       }, | ||||
|     ); | ||||
|   }); | ||||
|  |  | |||
|  | @ -7,6 +7,7 @@ import { | |||
|   clearEmojiMock, | ||||
| } from 'helpers/emoji'; | ||||
| import { trimText } from 'helpers/text_helper'; | ||||
| import { createMockClient } from 'helpers/mock_apollo_helper'; | ||||
| import { | ||||
|   glEmojiTag, | ||||
|   searchEmoji, | ||||
|  | @ -14,6 +15,8 @@ import { | |||
|   sortEmoji, | ||||
|   initEmojiMap, | ||||
|   getAllEmoji, | ||||
|   emojiFallbackImageSrc, | ||||
|   loadCustomEmojiWithNames, | ||||
| } from '~/emoji'; | ||||
| 
 | ||||
| import isEmojiUnicodeSupported, { | ||||
|  | @ -25,6 +28,12 @@ import isEmojiUnicodeSupported, { | |||
|   isPersonZwjEmoji, | ||||
| } from '~/emoji/support/is_emoji_unicode_supported'; | ||||
| import { NEUTRAL_INTENT_MULTIPLIER } from '~/emoji/constants'; | ||||
| import customEmojiQuery from '~/emoji/queries/custom_emoji.query.graphql'; | ||||
| 
 | ||||
| let mockClient; | ||||
| jest.mock('~/lib/graphql', () => { | ||||
|   return () => mockClient; | ||||
| }); | ||||
| 
 | ||||
| const emptySupportMap = { | ||||
|   personZwj: false, | ||||
|  | @ -45,12 +54,35 @@ const emptySupportMap = { | |||
|   1.1: false, | ||||
| }; | ||||
| 
 | ||||
| function createMockEmojiClient() { | ||||
|   mockClient = createMockClient([ | ||||
|     [ | ||||
|       customEmojiQuery, | ||||
|       jest.fn().mockResolvedValue({ | ||||
|         data: { | ||||
|           group: { | ||||
|             id: 1, | ||||
|             customEmoji: { | ||||
|               nodes: [{ id: 1, name: 'parrot', url: 'parrot.gif' }], | ||||
|             }, | ||||
|           }, | ||||
|         }, | ||||
|       }), | ||||
|     ], | ||||
|   ]); | ||||
| 
 | ||||
|   window.gon = { features: { customEmoji: true } }; | ||||
|   document.body.dataset.group = 'test-group'; | ||||
| } | ||||
| 
 | ||||
| describe('emoji', () => { | ||||
|   beforeEach(async () => { | ||||
|     await initEmojiMock(); | ||||
|   }); | ||||
| 
 | ||||
|   afterEach(() => { | ||||
|     window.gon = {}; | ||||
|     delete document.body.dataset.group; | ||||
|     clearEmojiMock(); | ||||
|   }); | ||||
| 
 | ||||
|  | @ -690,4 +722,67 @@ describe('emoji', () => { | |||
|       expect(scoredItems.sort(sortEmoji)).toEqual(expected); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   describe('emojiFallbackImageSrc', () => { | ||||
|     beforeEach(async () => { | ||||
|       createMockEmojiClient(); | ||||
| 
 | ||||
|       await initEmojiMock(); | ||||
|     }); | ||||
| 
 | ||||
|     it.each` | ||||
|       emoji         | src | ||||
|       ${'thumbsup'} | ${'/-/emojis/2/thumbsup.png'} | ||||
|       ${'parrot'}   | ${'parrot.gif'} | ||||
|     `('returns $src for emoji with name $emoji', ({ emoji, src }) => {
 | ||||
|       expect(emojiFallbackImageSrc(emoji)).toBe(src); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   describe('loadCustomEmojiWithNames', () => { | ||||
|     beforeEach(() => { | ||||
|       createMockEmojiClient(); | ||||
|     }); | ||||
| 
 | ||||
|     describe('flag disabled', () => { | ||||
|       beforeEach(() => { | ||||
|         window.gon = {}; | ||||
|       }); | ||||
| 
 | ||||
|       it('returns empty object', async () => { | ||||
|         const result = await loadCustomEmojiWithNames(); | ||||
| 
 | ||||
|         expect(result).toEqual({}); | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     describe('when not in a group', () => { | ||||
|       beforeEach(() => { | ||||
|         delete document.body.dataset.group; | ||||
|       }); | ||||
| 
 | ||||
|       it('returns empty object', async () => { | ||||
|         const result = await loadCustomEmojiWithNames(); | ||||
| 
 | ||||
|         expect(result).toEqual({}); | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     describe('when in a group with flag enabled', () => { | ||||
|       it('returns empty object', async () => { | ||||
|         const result = await loadCustomEmojiWithNames(); | ||||
| 
 | ||||
|         expect(result).toEqual({ | ||||
|           parrot: { | ||||
|             c: 'custom', | ||||
|             d: 'parrot', | ||||
|             e: undefined, | ||||
|             name: 'parrot', | ||||
|             src: 'parrot.gif', | ||||
|             u: 'custom', | ||||
|           }, | ||||
|         }); | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
|  |  | |||
|  | @ -1,6 +1,7 @@ | |||
| import { GlModal, GlLink, GlSprintf } from '@gitlab/ui'; | ||||
| import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; | ||||
| import { mockTracking, unmockTracking } from 'helpers/tracking_helper'; | ||||
| import { stubComponent, RENDER_ALL_SLOTS_TEMPLATE } from 'helpers/stub_component'; | ||||
| import { sprintf } from '~/locale'; | ||||
| import SecurityPatchUpgradeAlertModal from '~/gitlab_version_check/components/security_patch_upgrade_alert_modal.vue'; | ||||
| import * as utils from '~/gitlab_version_check/utils'; | ||||
|  | @ -14,6 +15,8 @@ import { | |||
| describe('SecurityPatchUpgradeAlertModal', () => { | ||||
|   let wrapper; | ||||
|   let trackingSpy; | ||||
|   const hideMock = jest.fn(); | ||||
|   const { i18n } = SecurityPatchUpgradeAlertModal; | ||||
| 
 | ||||
|   const defaultProps = { | ||||
|     currentVersion: '11.1.1', | ||||
|  | @ -28,14 +31,20 @@ describe('SecurityPatchUpgradeAlertModal', () => { | |||
|         ...props, | ||||
|       }, | ||||
|       stubs: { | ||||
|         GlModal, | ||||
|         GlSprintf, | ||||
|         GlModal: stubComponent(GlModal, { | ||||
|           methods: { | ||||
|             hide: hideMock, | ||||
|           }, | ||||
|           template: RENDER_ALL_SLOTS_TEMPLATE, | ||||
|         }), | ||||
|       }, | ||||
|     }); | ||||
|   }; | ||||
| 
 | ||||
|   afterEach(() => { | ||||
|     unmockTracking(); | ||||
|     hideMock.mockClear(); | ||||
|   }); | ||||
| 
 | ||||
|   const expectDispatchedTracking = (action, label) => { | ||||
|  | @ -63,12 +72,12 @@ describe('SecurityPatchUpgradeAlertModal', () => { | |||
|     }); | ||||
| 
 | ||||
|     it('renders the modal title correctly', () => { | ||||
|       expect(findGlModalTitle().text()).toBe(wrapper.vm.$options.i18n.modalTitle); | ||||
|       expect(findGlModalTitle().text()).toBe(i18n.modalTitle); | ||||
|     }); | ||||
| 
 | ||||
|     it('renders modal body without suggested versions', () => { | ||||
|       expect(findGlModalBody().text()).toBe( | ||||
|         sprintf(wrapper.vm.$options.i18n.modalBodyNoStableVersions, { | ||||
|         sprintf(i18n.modalBodyNoStableVersions, { | ||||
|           currentVersion: defaultProps.currentVersion, | ||||
|         }), | ||||
|       ); | ||||
|  | @ -90,7 +99,7 @@ describe('SecurityPatchUpgradeAlertModal', () => { | |||
| 
 | ||||
|     describe('Learn more link', () => { | ||||
|       it('renders with correct text and link', () => { | ||||
|         expect(findGlLink().text()).toBe(wrapper.vm.$options.i18n.learnMore); | ||||
|         expect(findGlLink().text()).toBe(i18n.learnMore); | ||||
|         expect(findGlLink().attributes('href')).toBe(ABOUT_RELEASES_PAGE); | ||||
|       }); | ||||
| 
 | ||||
|  | @ -102,12 +111,8 @@ describe('SecurityPatchUpgradeAlertModal', () => { | |||
|     }); | ||||
| 
 | ||||
|     describe('Remind me button', () => { | ||||
|       beforeEach(() => { | ||||
|         wrapper.vm.$refs.alertModal.hide = jest.fn(); | ||||
|       }); | ||||
| 
 | ||||
|       it('renders with correct text', () => { | ||||
|         expect(findGlRemindButton().text()).toBe(wrapper.vm.$options.i18n.secondaryButtonText); | ||||
|         expect(findGlRemindButton().text()).toBe(i18n.secondaryButtonText); | ||||
|       }); | ||||
| 
 | ||||
|       it(`tracks click ${TRACKING_LABELS.REMIND_ME_BTN} when clicked`, async () => { | ||||
|  | @ -126,13 +131,13 @@ describe('SecurityPatchUpgradeAlertModal', () => { | |||
|       it('hides the modal', async () => { | ||||
|         await findGlRemindButton().vm.$emit('click'); | ||||
| 
 | ||||
|         expect(wrapper.vm.$refs.alertModal.hide).toHaveBeenCalled(); | ||||
|         expect(hideMock).toHaveBeenCalled(); | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     describe('Upgrade button', () => { | ||||
|       it('renders with correct text and link', () => { | ||||
|         expect(findGlUpgradeButton().text()).toBe(wrapper.vm.$options.i18n.primaryButtonText); | ||||
|         expect(findGlUpgradeButton().text()).toBe(i18n.primaryButtonText); | ||||
|         expect(findGlUpgradeButton().attributes('href')).toBe(UPGRADE_DOCS_URL); | ||||
|       }); | ||||
| 
 | ||||
|  | @ -160,7 +165,7 @@ describe('SecurityPatchUpgradeAlertModal', () => { | |||
| 
 | ||||
|     it('renders modal body with suggested versions', () => { | ||||
|       expect(findGlModalBody().text()).toBe( | ||||
|         sprintf(wrapper.vm.$options.i18n.modalBodyStableVersions, { | ||||
|         sprintf(i18n.modalBodyStableVersions, { | ||||
|           currentVersion: defaultProps.currentVersion, | ||||
|           latestStableVersions: latestStableVersions.join(', '), | ||||
|         }), | ||||
|  | @ -176,9 +181,7 @@ describe('SecurityPatchUpgradeAlertModal', () => { | |||
|     }); | ||||
| 
 | ||||
|     it('renders modal details', () => { | ||||
|       expect(findGlModalDetails().text()).toBe( | ||||
|         sprintf(wrapper.vm.$options.i18n.modalDetails, { details }), | ||||
|       ); | ||||
|       expect(findGlModalDetails().text()).toBe(sprintf(i18n.modalDetails, { details })); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|  |  | |||
|  | @ -17,6 +17,7 @@ import { | |||
| import { DRAWIO_ORIGIN } from 'spec/test_constants'; | ||||
| 
 | ||||
| jest.mock('~/emoji'); | ||||
| jest.mock('~/lib/graphql'); | ||||
| 
 | ||||
| describe('WikiForm', () => { | ||||
|   let wrapper; | ||||
|  |  | |||
|  | @ -21,6 +21,7 @@ import waitForPromises from 'helpers/wait_for_promises'; | |||
| 
 | ||||
| jest.mock('~/emoji'); | ||||
| jest.mock('autosize'); | ||||
| jest.mock('~/lib/graphql'); | ||||
| 
 | ||||
| describe('vue_shared/component/markdown/markdown_editor', () => { | ||||
|   useLocalStorageSpy(); | ||||
|  |  | |||
|  | @ -188,9 +188,11 @@ RSpec.describe Gitlab::Cache::Ci::ProjectPipelineStatus, :clean_gitlab_redis_cac | |||
| 
 | ||||
|       pipeline_status.store_in_cache | ||||
|       read_sha, read_status = Gitlab::Redis::Cache.with { |redis| redis.hmget(cache_key, :sha, :status) } | ||||
|       ttl = Gitlab::Redis::Cache.with { |redis| redis.ttl(cache_key) } | ||||
| 
 | ||||
|       expect(read_sha).to eq('123456') | ||||
|       expect(read_status).to eq('failed') | ||||
|       expect(ttl).to be > 0 | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|  | @ -254,14 +256,24 @@ RSpec.describe Gitlab::Cache::Ci::ProjectPipelineStatus, :clean_gitlab_redis_cac | |||
|     end | ||||
| 
 | ||||
|     describe '#load_from_cache' do | ||||
|       subject { pipeline_status.load_from_cache } | ||||
| 
 | ||||
|       it 'reads the status from redis_cache' do | ||||
|         pipeline_status.load_from_cache | ||||
|         subject | ||||
| 
 | ||||
|         expect(pipeline_status.sha).to eq(sha) | ||||
|         expect(pipeline_status.status).to eq(status) | ||||
|         expect(pipeline_status.ref).to eq(ref) | ||||
|       end | ||||
| 
 | ||||
|       it 'refreshes ttl' do | ||||
|         subject | ||||
| 
 | ||||
|         ttl = Gitlab::Redis::Cache.with { |redis| redis.ttl(cache_key) } | ||||
| 
 | ||||
|         expect(ttl).to be > 0 | ||||
|       end | ||||
| 
 | ||||
|       context 'when status is empty string' do | ||||
|         before do | ||||
|           Gitlab::Redis::Cache.with do |redis| | ||||
|  | @ -271,7 +283,7 @@ RSpec.describe Gitlab::Cache::Ci::ProjectPipelineStatus, :clean_gitlab_redis_cac | |||
|         end | ||||
| 
 | ||||
|         it 'reads the status as nil' do | ||||
|           pipeline_status.load_from_cache | ||||
|           subject | ||||
| 
 | ||||
|           expect(pipeline_status.status).to eq(nil) | ||||
|         end | ||||
|  |  | |||
|  | @ -3,7 +3,7 @@ | |||
| require 'spec_helper' | ||||
| 
 | ||||
| RSpec.describe Gitlab::Database::EachDatabase do | ||||
|   describe '.each_database_connection', :add_ci_connection do | ||||
|   describe '.each_connection', :add_ci_connection do | ||||
|     let(:database_base_models) { { main: ActiveRecord::Base, ci: Ci::ApplicationRecord }.with_indifferent_access } | ||||
| 
 | ||||
|     before do | ||||
|  | @ -17,7 +17,7 @@ RSpec.describe Gitlab::Database::EachDatabase do | |||
|       expect(Gitlab::Database::SharedModel).to receive(:using_connection) | ||||
|         .with(Ci::ApplicationRecord.connection).ordered.and_yield | ||||
| 
 | ||||
|       expect { |b| described_class.each_database_connection(&b) } | ||||
|       expect { |b| described_class.each_connection(&b) } | ||||
|         .to yield_successive_args( | ||||
|           [ActiveRecord::Base.connection, 'main'], | ||||
|           [Ci::ApplicationRecord.connection, 'ci'] | ||||
|  | @ -29,7 +29,7 @@ RSpec.describe Gitlab::Database::EachDatabase do | |||
|         expect(Gitlab::Database::SharedModel).to receive(:using_connection) | ||||
|           .with(Ci::ApplicationRecord.connection).ordered.and_yield | ||||
| 
 | ||||
|         expect { |b| described_class.each_database_connection(only: 'ci', &b) } | ||||
|         expect { |b| described_class.each_connection(only: 'ci', &b) } | ||||
|           .to yield_successive_args([Ci::ApplicationRecord.connection, 'ci']) | ||||
|       end | ||||
| 
 | ||||
|  | @ -38,7 +38,7 @@ RSpec.describe Gitlab::Database::EachDatabase do | |||
|           expect(Gitlab::Database::SharedModel).to receive(:using_connection) | ||||
|             .with(Ci::ApplicationRecord.connection).ordered.and_yield | ||||
| 
 | ||||
|           expect { |b| described_class.each_database_connection(only: :ci, &b) } | ||||
|           expect { |b| described_class.each_connection(only: :ci, &b) } | ||||
|             .to yield_successive_args([Ci::ApplicationRecord.connection, 'ci']) | ||||
|         end | ||||
|       end | ||||
|  | @ -46,7 +46,7 @@ RSpec.describe Gitlab::Database::EachDatabase do | |||
|       context 'when the selected names are invalid' do | ||||
|         it 'does not yield any connections' do | ||||
|           expect do |b| | ||||
|             described_class.each_database_connection(only: :notvalid, &b) | ||||
|             described_class.each_connection(only: :notvalid, &b) | ||||
|           rescue ArgumentError => e | ||||
|             expect(e.message).to match(/notvalid is not a valid database name/) | ||||
|           end.not_to yield_control | ||||
|  | @ -54,7 +54,7 @@ RSpec.describe Gitlab::Database::EachDatabase do | |||
| 
 | ||||
|         it 'raises an error' do | ||||
|           expect do | ||||
|             described_class.each_database_connection(only: :notvalid) {} | ||||
|             described_class.each_connection(only: :notvalid) {} | ||||
|           end.to raise_error(ArgumentError, /notvalid is not a valid database name/) | ||||
|         end | ||||
|       end | ||||
|  | @ -78,7 +78,7 @@ RSpec.describe Gitlab::Database::EachDatabase do | |||
|           db_config.name != 'main' ? 'main' : nil | ||||
|         end | ||||
| 
 | ||||
|         expect { |b| described_class.each_database_connection(include_shared: false, &b) } | ||||
|         expect { |b| described_class.each_connection(include_shared: false, &b) } | ||||
|           .to yield_successive_args([ActiveRecord::Base.connection, 'main']) | ||||
|       end | ||||
|     end | ||||
|  |  | |||
|  | @ -257,7 +257,7 @@ RSpec.describe Gitlab::Database::Partitioning, feature_category: :database do | |||
|     end | ||||
| 
 | ||||
|     it 'drops detached partitions for each database' do | ||||
|       expect(Gitlab::Database::EachDatabase).to receive(:each_database_connection).and_yield | ||||
|       expect(Gitlab::Database::EachDatabase).to receive(:each_connection).and_yield | ||||
| 
 | ||||
|       expect { described_class.drop_detached_partitions } | ||||
|         .to change { Postgresql::DetachedPartition.count }.from(2).to(0) | ||||
|  |  | |||
|  | @ -2657,232 +2657,6 @@ RSpec.describe Group, feature_category: :groups_and_projects do | |||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe '#update_shared_runners_setting!' do | ||||
|     context 'enabled' do | ||||
|       subject { group.update_shared_runners_setting!('enabled') } | ||||
| 
 | ||||
|       context 'group that its ancestors have shared runners disabled' do | ||||
|         let_it_be(:parent, reload: true) { create(:group, :shared_runners_disabled) } | ||||
|         let_it_be(:group, reload: true) { create(:group, :shared_runners_disabled, parent: parent) } | ||||
|         let_it_be(:project, reload: true) { create(:project, shared_runners_enabled: false, group: group) } | ||||
| 
 | ||||
|         it 'raises exception' do | ||||
|           expect { subject } | ||||
|             .to raise_error(ActiveRecord::RecordInvalid, 'Validation failed: Shared runners enabled cannot be enabled because parent group has shared Runners disabled') | ||||
|         end | ||||
| 
 | ||||
|         it 'does not enable shared runners' do | ||||
|           expect do | ||||
|             begin | ||||
|               subject | ||||
|             rescue StandardError | ||||
|               nil | ||||
|             end | ||||
| 
 | ||||
|             parent.reload | ||||
|             group.reload | ||||
|             project.reload | ||||
|           end.to not_change { parent.shared_runners_enabled } | ||||
|             .and not_change { group.shared_runners_enabled } | ||||
|             .and not_change { project.shared_runners_enabled } | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       context 'root group with shared runners disabled' do | ||||
|         let_it_be(:group) { create(:group, :shared_runners_disabled) } | ||||
|         let_it_be(:sub_group) { create(:group, :shared_runners_disabled, parent: group) } | ||||
|         let_it_be(:project) { create(:project, shared_runners_enabled: false, group: sub_group) } | ||||
| 
 | ||||
|         it 'enables shared Runners only for itself' do | ||||
|           expect { subject_and_reload(group, sub_group, project) } | ||||
|             .to change { group.shared_runners_enabled }.from(false).to(true) | ||||
|             .and not_change { sub_group.shared_runners_enabled } | ||||
|             .and not_change { project.shared_runners_enabled } | ||||
|         end | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'disabled_and_unoverridable' do | ||||
|       let_it_be(:group) { create(:group) } | ||||
|       let_it_be(:sub_group) { create(:group, :shared_runners_disabled, :allow_descendants_override_disabled_shared_runners, parent: group) } | ||||
|       let_it_be(:sub_group_2) { create(:group, parent: group) } | ||||
|       let_it_be(:project) { create(:project, group: group, shared_runners_enabled: true) } | ||||
|       let_it_be(:project_2) { create(:project, group: sub_group_2, shared_runners_enabled: true) } | ||||
| 
 | ||||
|       subject { group.update_shared_runners_setting!(Namespace::SR_DISABLED_AND_UNOVERRIDABLE) } | ||||
| 
 | ||||
|       it 'disables shared Runners for all descendant groups and projects' do | ||||
|         expect { subject_and_reload(group, sub_group, sub_group_2, project, project_2) } | ||||
|           .to change { group.shared_runners_enabled }.from(true).to(false) | ||||
|           .and not_change { group.allow_descendants_override_disabled_shared_runners } | ||||
|           .and not_change { sub_group.shared_runners_enabled } | ||||
|           .and change { sub_group.allow_descendants_override_disabled_shared_runners }.from(true).to(false) | ||||
|           .and change { sub_group_2.shared_runners_enabled }.from(true).to(false) | ||||
|           .and not_change { sub_group_2.allow_descendants_override_disabled_shared_runners } | ||||
|           .and change { project.shared_runners_enabled }.from(true).to(false) | ||||
|           .and change { project_2.shared_runners_enabled }.from(true).to(false) | ||||
|       end | ||||
| 
 | ||||
|       context 'with override on self' do | ||||
|         let_it_be(:group) { create(:group, :shared_runners_disabled, :allow_descendants_override_disabled_shared_runners) } | ||||
| 
 | ||||
|         it 'disables it' do | ||||
|           expect { subject_and_reload(group) } | ||||
|             .to not_change { group.shared_runners_enabled } | ||||
|             .and change { group.allow_descendants_override_disabled_shared_runners }.from(true).to(false) | ||||
|         end | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'disabled_and_overridable' do | ||||
|       subject { group.update_shared_runners_setting!(Namespace::SR_DISABLED_AND_OVERRIDABLE) } | ||||
| 
 | ||||
|       context 'top level group' do | ||||
|         let_it_be(:group) { create(:group, :shared_runners_disabled) } | ||||
|         let_it_be(:sub_group) { create(:group, :shared_runners_disabled, parent: group) } | ||||
|         let_it_be(:project) { create(:project, shared_runners_enabled: false, group: sub_group) } | ||||
| 
 | ||||
|         it 'enables allow descendants to override only for itself' do | ||||
|           expect { subject_and_reload(group, sub_group, project) } | ||||
|             .to change { group.allow_descendants_override_disabled_shared_runners }.from(false).to(true) | ||||
|             .and not_change { group.shared_runners_enabled } | ||||
|             .and not_change { sub_group.allow_descendants_override_disabled_shared_runners } | ||||
|             .and not_change { sub_group.shared_runners_enabled } | ||||
|             .and not_change { project.shared_runners_enabled } | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       context 'group that its ancestors have shared Runners disabled but allows to override' do | ||||
|         let_it_be(:parent) { create(:group, :shared_runners_disabled, :allow_descendants_override_disabled_shared_runners) } | ||||
|         let_it_be(:group) { create(:group, :shared_runners_disabled, parent: parent) } | ||||
|         let_it_be(:project) { create(:project, shared_runners_enabled: false, group: group) } | ||||
| 
 | ||||
|         it 'enables allow descendants to override' do | ||||
|           expect { subject_and_reload(parent, group, project) } | ||||
|             .to not_change { parent.allow_descendants_override_disabled_shared_runners } | ||||
|             .and not_change { parent.shared_runners_enabled } | ||||
|             .and change { group.allow_descendants_override_disabled_shared_runners }.from(false).to(true) | ||||
|             .and not_change { group.shared_runners_enabled } | ||||
|             .and not_change { project.shared_runners_enabled } | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       context 'when parent does not allow' do | ||||
|         let_it_be(:parent, reload: true) { create(:group, :shared_runners_disabled, allow_descendants_override_disabled_shared_runners: false) } | ||||
|         let_it_be(:group, reload: true) { create(:group, :shared_runners_disabled, allow_descendants_override_disabled_shared_runners: false, parent: parent) } | ||||
| 
 | ||||
|         it 'raises exception' do | ||||
|           expect { subject } | ||||
|             .to raise_error(ActiveRecord::RecordInvalid, 'Validation failed: Allow descendants override disabled shared runners cannot be enabled because parent group does not allow it') | ||||
|         end | ||||
| 
 | ||||
|         it 'does not allow descendants to override' do | ||||
|           expect do | ||||
|             begin | ||||
|               subject | ||||
|             rescue StandardError | ||||
|               nil | ||||
|             end | ||||
| 
 | ||||
|             parent.reload | ||||
|             group.reload | ||||
|           end.to not_change { parent.allow_descendants_override_disabled_shared_runners } | ||||
|             .and not_change { parent.shared_runners_enabled } | ||||
|             .and not_change { group.allow_descendants_override_disabled_shared_runners } | ||||
|             .and not_change { group.shared_runners_enabled } | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       context 'top level group that has shared Runners enabled' do | ||||
|         let_it_be(:group) { create(:group, shared_runners_enabled: true) } | ||||
|         let_it_be(:sub_group) { create(:group, shared_runners_enabled: true, parent: group) } | ||||
|         let_it_be(:project) { create(:project, shared_runners_enabled: true, group: sub_group) } | ||||
| 
 | ||||
|         it 'enables allow descendants to override & disables shared runners everywhere' do | ||||
|           expect { subject_and_reload(group, sub_group, project) } | ||||
|             .to change { group.shared_runners_enabled }.from(true).to(false) | ||||
|             .and change { group.allow_descendants_override_disabled_shared_runners }.from(false).to(true) | ||||
|             .and change { sub_group.shared_runners_enabled }.from(true).to(false) | ||||
|             .and change { project.shared_runners_enabled }.from(true).to(false) | ||||
|         end | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'disabled_with_override (deprecated)' do | ||||
|       subject { group.update_shared_runners_setting!(Namespace::SR_DISABLED_WITH_OVERRIDE) } | ||||
| 
 | ||||
|       context 'top level group' do | ||||
|         let_it_be(:group) { create(:group, :shared_runners_disabled) } | ||||
|         let_it_be(:sub_group) { create(:group, :shared_runners_disabled, parent: group) } | ||||
|         let_it_be(:project) { create(:project, shared_runners_enabled: false, group: sub_group) } | ||||
| 
 | ||||
|         it 'enables allow descendants to override only for itself' do | ||||
|           expect { subject_and_reload(group, sub_group, project) } | ||||
|             .to change { group.allow_descendants_override_disabled_shared_runners }.from(false).to(true) | ||||
|             .and not_change { group.shared_runners_enabled } | ||||
|             .and not_change { sub_group.allow_descendants_override_disabled_shared_runners } | ||||
|             .and not_change { sub_group.shared_runners_enabled } | ||||
|             .and not_change { project.shared_runners_enabled } | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       context 'group that its ancestors have shared Runners disabled but allows to override' do | ||||
|         let_it_be(:parent) { create(:group, :shared_runners_disabled, :allow_descendants_override_disabled_shared_runners) } | ||||
|         let_it_be(:group) { create(:group, :shared_runners_disabled, parent: parent) } | ||||
|         let_it_be(:project) { create(:project, shared_runners_enabled: false, group: group) } | ||||
| 
 | ||||
|         it 'enables allow descendants to override' do | ||||
|           expect { subject_and_reload(parent, group, project) } | ||||
|             .to not_change { parent.allow_descendants_override_disabled_shared_runners } | ||||
|             .and not_change { parent.shared_runners_enabled } | ||||
|             .and change { group.allow_descendants_override_disabled_shared_runners }.from(false).to(true) | ||||
|             .and not_change { group.shared_runners_enabled } | ||||
|             .and not_change { project.shared_runners_enabled } | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       context 'when parent does not allow' do | ||||
|         let_it_be(:parent, reload: true) { create(:group, :shared_runners_disabled, allow_descendants_override_disabled_shared_runners: false) } | ||||
|         let_it_be(:group, reload: true) { create(:group, :shared_runners_disabled, allow_descendants_override_disabled_shared_runners: false, parent: parent) } | ||||
| 
 | ||||
|         it 'raises exception' do | ||||
|           expect { subject } | ||||
|             .to raise_error(ActiveRecord::RecordInvalid, 'Validation failed: Allow descendants override disabled shared runners cannot be enabled because parent group does not allow it') | ||||
|         end | ||||
| 
 | ||||
|         it 'does not allow descendants to override' do | ||||
|           expect do | ||||
|             begin | ||||
|               subject | ||||
|             rescue StandardError | ||||
|               nil | ||||
|             end | ||||
| 
 | ||||
|             parent.reload | ||||
|             group.reload | ||||
|           end.to not_change { parent.allow_descendants_override_disabled_shared_runners } | ||||
|             .and not_change { parent.shared_runners_enabled } | ||||
|             .and not_change { group.allow_descendants_override_disabled_shared_runners } | ||||
|             .and not_change { group.shared_runners_enabled } | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       context 'top level group that has shared Runners enabled' do | ||||
|         let_it_be(:group) { create(:group, shared_runners_enabled: true) } | ||||
|         let_it_be(:sub_group) { create(:group, shared_runners_enabled: true, parent: group) } | ||||
|         let_it_be(:project) { create(:project, shared_runners_enabled: true, group: sub_group) } | ||||
| 
 | ||||
|         it 'enables allow descendants to override & disables shared runners everywhere' do | ||||
|           expect { subject_and_reload(group, sub_group, project) } | ||||
|             .to change { group.shared_runners_enabled }.from(true).to(false) | ||||
|             .and change { group.allow_descendants_override_disabled_shared_runners }.from(false).to(true) | ||||
|             .and change { sub_group.shared_runners_enabled }.from(true).to(false) | ||||
|             .and change { project.shared_runners_enabled }.from(true).to(false) | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe "#default_branch_name" do | ||||
|     context "when group.namespace_settings does not have a default branch name" do | ||||
|       it "returns nil" do | ||||
|  |  | |||
|  | @ -3,15 +3,17 @@ | |||
| require 'spec_helper' | ||||
| 
 | ||||
| RSpec.describe Groups::UpdateSharedRunnersService, feature_category: :groups_and_projects do | ||||
|   include ReloadHelpers | ||||
| 
 | ||||
|   let(:user) { create(:user) } | ||||
|   let(:group) { create(:group) } | ||||
|   let(:params) { {} } | ||||
|   let(:service) { described_class.new(group, user, params) } | ||||
| 
 | ||||
|   describe '#execute' do | ||||
|     subject { described_class.new(group, user, params).execute } | ||||
|     subject { service.execute } | ||||
| 
 | ||||
|     context 'when current_user is not the group owner' do | ||||
|       let_it_be(:group) { create(:group) } | ||||
|       let(:group) { create(:group) } | ||||
| 
 | ||||
|       let(:params) { { shared_runners_setting: 'enabled' } } | ||||
| 
 | ||||
|  | @ -19,9 +21,7 @@ RSpec.describe Groups::UpdateSharedRunnersService, feature_category: :groups_and | |||
|         group.add_maintainer(user) | ||||
|       end | ||||
| 
 | ||||
|       it 'results error and does not call any method' do | ||||
|         expect(group).not_to receive(:update_shared_runners_setting!) | ||||
| 
 | ||||
|       it 'returns error' do | ||||
|         expect(subject[:status]).to eq(:error) | ||||
|         expect(subject[:message]).to eq('Operation not allowed') | ||||
|         expect(subject[:http_status]).to eq(403) | ||||
|  | @ -36,23 +36,36 @@ RSpec.describe Groups::UpdateSharedRunnersService, feature_category: :groups_and | |||
|       context 'enable shared Runners' do | ||||
|         let(:params) { { shared_runners_setting: 'enabled' } } | ||||
| 
 | ||||
|         context 'group that its ancestors have shared runners disabled' do | ||||
|           let_it_be(:parent) { create(:group, :shared_runners_disabled) } | ||||
|           let_it_be(:group) { create(:group, :shared_runners_disabled, parent: parent) } | ||||
|         context 'when ancestor disable shared runners' do | ||||
|           let(:parent) { create(:group, :shared_runners_disabled) } | ||||
|           let(:group) { create(:group, :shared_runners_disabled, parent: parent) } | ||||
|           let!(:project) { create(:project, shared_runners_enabled: false, group: group) } | ||||
| 
 | ||||
|           it 'results error' do | ||||
|           it 'returns an error and does not enable shared runners' do | ||||
|             expect do | ||||
|               expect(subject[:status]).to eq(:error) | ||||
|               expect(subject[:message]).to eq('Validation failed: Shared runners enabled cannot be enabled because parent group has shared Runners disabled') | ||||
| 
 | ||||
|               reload_models(parent, group, project) | ||||
|             end.to not_change { parent.shared_runners_enabled } | ||||
|               .and not_change { group.shared_runners_enabled } | ||||
|               .and not_change { project.shared_runners_enabled } | ||||
|           end | ||||
|         end | ||||
| 
 | ||||
|         context 'root group with shared runners disabled' do | ||||
|           let_it_be(:group) { create(:group, :shared_runners_disabled) } | ||||
| 
 | ||||
|           it 'receives correct method and succeeds' do | ||||
|             expect(group).to receive(:update_shared_runners_setting!).with('enabled') | ||||
|         context 'when updating root group' do | ||||
|           let(:group) { create(:group, :shared_runners_disabled) } | ||||
|           let(:sub_group) { create(:group, :shared_runners_disabled, parent: group) } | ||||
|           let!(:project) { create(:project, shared_runners_enabled: false, group: sub_group) } | ||||
| 
 | ||||
|           it 'enables shared Runners only for itself' do | ||||
|             expect do | ||||
|               expect(subject[:status]).to eq(:success) | ||||
| 
 | ||||
|               reload_models(group, sub_group, project) | ||||
|             end.to change { group.shared_runners_enabled }.from(false).to(true) | ||||
|               .and not_change { sub_group.shared_runners_enabled }.from(false) | ||||
|               .and not_change { project.shared_runners_enabled }.from(false) | ||||
|           end | ||||
|         end | ||||
| 
 | ||||
|  | @ -75,7 +88,7 @@ RSpec.describe Groups::UpdateSharedRunnersService, feature_category: :groups_and | |||
|             let(:params) { { shared_runners_setting: 'invalid_enabled' } } | ||||
| 
 | ||||
|             it 'does not update pending builds for the group' do | ||||
|               expect(::Ci::UpdatePendingBuildService).not_to receive(:new).and_call_original | ||||
|               expect(::Ci::UpdatePendingBuildService).not_to receive(:new) | ||||
| 
 | ||||
|               subject | ||||
| 
 | ||||
|  | @ -87,20 +100,46 @@ RSpec.describe Groups::UpdateSharedRunnersService, feature_category: :groups_and | |||
|       end | ||||
| 
 | ||||
|       context 'disable shared Runners' do | ||||
|         let_it_be(:group) { create(:group) } | ||||
|         let!(:group) { create(:group) } | ||||
|         let!(:sub_group) { create(:group, :shared_runners_disabled, :allow_descendants_override_disabled_shared_runners, parent: group) } | ||||
|         let!(:sub_group2) { create(:group, parent: group) } | ||||
|         let!(:project) { create(:project, group: group, shared_runners_enabled: true) } | ||||
|         let!(:project2) { create(:project, group: sub_group2, shared_runners_enabled: true) } | ||||
| 
 | ||||
|         let(:params) { { shared_runners_setting: Namespace::SR_DISABLED_AND_UNOVERRIDABLE } } | ||||
| 
 | ||||
|         it 'receives correct method and succeeds' do | ||||
|           expect(group).to receive(:update_shared_runners_setting!).with(Namespace::SR_DISABLED_AND_UNOVERRIDABLE) | ||||
| 
 | ||||
|         it 'disables shared Runners for all descendant groups and projects' do | ||||
|           expect do | ||||
|             expect(subject[:status]).to eq(:success) | ||||
| 
 | ||||
|             reload_models(group, sub_group, sub_group2, project, project2) | ||||
|           end.to change { group.shared_runners_enabled }.from(true).to(false) | ||||
|             .and not_change { group.allow_descendants_override_disabled_shared_runners } | ||||
|             .and not_change { sub_group.shared_runners_enabled } | ||||
|             .and change { sub_group.allow_descendants_override_disabled_shared_runners }.from(true).to(false) | ||||
|             .and change { sub_group2.shared_runners_enabled }.from(true).to(false) | ||||
|             .and not_change { sub_group2.allow_descendants_override_disabled_shared_runners } | ||||
|             .and change { project.shared_runners_enabled }.from(true).to(false) | ||||
|             .and change { project2.shared_runners_enabled }.from(true).to(false) | ||||
|         end | ||||
| 
 | ||||
|         context 'with override on self' do | ||||
|           let(:group) { create(:group, :shared_runners_disabled, :allow_descendants_override_disabled_shared_runners) } | ||||
| 
 | ||||
|           it 'disables it' do | ||||
|             expect do | ||||
|               expect(subject[:status]).to eq(:success) | ||||
| 
 | ||||
|               group.reload | ||||
|             end | ||||
|               .to not_change { group.shared_runners_enabled } | ||||
|               .and change { group.allow_descendants_override_disabled_shared_runners }.from(true).to(false) | ||||
|           end | ||||
|         end | ||||
| 
 | ||||
|         context 'when group has pending builds' do | ||||
|           let_it_be(:project) { create(:project, namespace: group) } | ||||
|           let_it_be(:pending_build_1) { create(:ci_pending_build, project: project, instance_runners_enabled: true) } | ||||
|           let_it_be(:pending_build_2) { create(:ci_pending_build, project: project, instance_runners_enabled: true) } | ||||
|           let!(:pending_build_1) { create(:ci_pending_build, project: project, instance_runners_enabled: true) } | ||||
|           let!(:pending_build_2) { create(:ci_pending_build, project: project, instance_runners_enabled: true) } | ||||
| 
 | ||||
|           it 'updates pending builds for the group' do | ||||
|             expect(::Ci::UpdatePendingBuildService).to receive(:new).and_call_original | ||||
|  | @ -113,52 +152,90 @@ RSpec.describe Groups::UpdateSharedRunnersService, feature_category: :groups_and | |||
|         end | ||||
|       end | ||||
| 
 | ||||
|       context 'allow descendants to override' do | ||||
|       shared_examples 'allow descendants to override' do | ||||
|         context 'top level group' do | ||||
|           let!(:group) { create(:group, :shared_runners_disabled) } | ||||
|           let!(:sub_group) { create(:group, :shared_runners_disabled, parent: group) } | ||||
|           let!(:project) { create(:project, shared_runners_enabled: false, group: sub_group) } | ||||
| 
 | ||||
|           it 'enables allow descendants to override only for itself' do | ||||
|             expect do | ||||
|               expect(subject[:status]).to eq(:success) | ||||
| 
 | ||||
|               reload_models(group, sub_group, project) | ||||
|             end.to change { group.allow_descendants_override_disabled_shared_runners }.from(false).to(true) | ||||
|               .and not_change { group.shared_runners_enabled } | ||||
|               .and not_change { sub_group.allow_descendants_override_disabled_shared_runners } | ||||
|               .and not_change { sub_group.shared_runners_enabled } | ||||
|               .and not_change { project.shared_runners_enabled } | ||||
|           end | ||||
|         end | ||||
| 
 | ||||
|         context 'when ancestor disables shared Runners but allows to override' do | ||||
|           let!(:parent) { create(:group, :shared_runners_disabled, :allow_descendants_override_disabled_shared_runners) } | ||||
|           let!(:group) { create(:group, :shared_runners_disabled, parent: parent) } | ||||
|           let!(:project) { create(:project, shared_runners_enabled: false, group: group) } | ||||
| 
 | ||||
|           it 'enables allow descendants to override' do | ||||
|             expect do | ||||
|               expect(subject[:status]).to eq(:success) | ||||
| 
 | ||||
|               reload_models(parent, group, project) | ||||
|             end | ||||
|               .to not_change { parent.allow_descendants_override_disabled_shared_runners } | ||||
|               .and not_change { parent.shared_runners_enabled } | ||||
|               .and change { group.allow_descendants_override_disabled_shared_runners }.from(false).to(true) | ||||
|               .and not_change { group.shared_runners_enabled } | ||||
|               .and not_change { project.shared_runners_enabled } | ||||
|           end | ||||
|         end | ||||
| 
 | ||||
|         context 'when ancestor disables shared runners' do | ||||
|           let(:parent) { create(:group, :shared_runners_disabled) } | ||||
|           let(:group) { create(:group, :shared_runners_disabled, parent: parent) } | ||||
|           let!(:project) { create(:project, shared_runners_enabled: false, group: group) } | ||||
| 
 | ||||
|           it 'returns an error and does not enable shared runners' do | ||||
|             expect do | ||||
|               expect(subject[:status]).to eq(:error) | ||||
|               expect(subject[:message]).to eq('Validation failed: Allow descendants override disabled shared runners cannot be enabled because parent group does not allow it') | ||||
| 
 | ||||
|               reload_models(parent, group, project) | ||||
|             end.to not_change { parent.shared_runners_enabled } | ||||
|               .and not_change { group.shared_runners_enabled } | ||||
|               .and not_change { project.shared_runners_enabled } | ||||
|           end | ||||
|         end | ||||
| 
 | ||||
|         context 'top level group that has shared Runners enabled' do | ||||
|           let!(:group) { create(:group, shared_runners_enabled: true) } | ||||
|           let!(:sub_group) { create(:group, shared_runners_enabled: true, parent: group) } | ||||
|           let!(:project) { create(:project, shared_runners_enabled: true, group: sub_group) } | ||||
| 
 | ||||
|           it 'enables allow descendants to override & disables shared runners everywhere' do | ||||
|             expect do | ||||
|               expect(subject[:status]).to eq(:success) | ||||
| 
 | ||||
|               reload_models(group, sub_group, project) | ||||
|             end | ||||
|               .to change { group.shared_runners_enabled }.from(true).to(false) | ||||
|               .and change { group.allow_descendants_override_disabled_shared_runners }.from(false).to(true) | ||||
|               .and change { sub_group.shared_runners_enabled }.from(true).to(false) | ||||
|               .and change { project.shared_runners_enabled }.from(true).to(false) | ||||
|           end | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       context "when using SR_DISABLED_AND_OVERRIDABLE" do | ||||
|         let(:params) { { shared_runners_setting: Namespace::SR_DISABLED_AND_OVERRIDABLE } } | ||||
| 
 | ||||
|         context 'top level group' do | ||||
|           let_it_be(:group) { create(:group, :shared_runners_disabled) } | ||||
| 
 | ||||
|           it 'receives correct method and succeeds' do | ||||
|             expect(group).to receive(:update_shared_runners_setting!).with(Namespace::SR_DISABLED_AND_OVERRIDABLE) | ||||
| 
 | ||||
|             expect(subject[:status]).to eq(:success) | ||||
|           end | ||||
|         include_examples 'allow descendants to override' | ||||
|       end | ||||
| 
 | ||||
|         context 'when parent does not allow' do | ||||
|           let_it_be(:parent) { create(:group, :shared_runners_disabled, allow_descendants_override_disabled_shared_runners: false) } | ||||
|           let_it_be(:group) { create(:group, :shared_runners_disabled, allow_descendants_override_disabled_shared_runners: false, parent: parent) } | ||||
| 
 | ||||
|           it 'results error' do | ||||
|             expect(subject[:status]).to eq(:error) | ||||
|             expect(subject[:message]).to eq('Validation failed: Allow descendants override disabled shared runners cannot be enabled because parent group does not allow it') | ||||
|           end | ||||
|         end | ||||
| 
 | ||||
|         context 'when using DISABLED_WITH_OVERRIDE (deprecated)' do | ||||
|       context "when using SR_DISABLED_WITH_OVERRIDE" do | ||||
|         let(:params) { { shared_runners_setting: Namespace::SR_DISABLED_WITH_OVERRIDE } } | ||||
| 
 | ||||
|           context 'top level group' do | ||||
|             let_it_be(:group) { create(:group, :shared_runners_disabled) } | ||||
| 
 | ||||
|             it 'receives correct method and succeeds' do | ||||
|               expect(group).to receive(:update_shared_runners_setting!).with(Namespace::SR_DISABLED_WITH_OVERRIDE) | ||||
| 
 | ||||
|               expect(subject[:status]).to eq(:success) | ||||
|             end | ||||
|           end | ||||
| 
 | ||||
|           context 'when parent does not allow' do | ||||
|             let_it_be(:parent) { create(:group, :shared_runners_disabled, allow_descendants_override_disabled_shared_runners: false) } | ||||
|             let_it_be(:group) { create(:group, :shared_runners_disabled, allow_descendants_override_disabled_shared_runners: false, parent: parent) } | ||||
| 
 | ||||
|             it 'results error' do | ||||
|               expect(subject[:status]).to eq(:error) | ||||
|               expect(subject[:message]).to eq('Validation failed: Allow descendants override disabled shared runners cannot be enabled because parent group does not allow it') | ||||
|             end | ||||
|           end | ||||
|         end | ||||
|         include_examples 'allow descendants to override' | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|  |  | |||
|  | @ -1030,17 +1030,17 @@ RSpec.describe Projects::CreateService, '#execute', feature_category: :groups_an | |||
|       end | ||||
| 
 | ||||
|       before do | ||||
|         group.update_shared_runners_setting!(shared_runners_setting) | ||||
|         group.update!(shared_runners_enabled: shared_runners_enabled, | ||||
|           allow_descendants_override_disabled_shared_runners: allow_to_override) | ||||
| 
 | ||||
|         user.refresh_authorized_projects # Ensure cache is warm | ||||
|       end | ||||
| 
 | ||||
|       context 'default value based on parent group setting' do | ||||
|         where(:shared_runners_setting, :desired_config_for_new_project, :expected_result_for_project) do | ||||
|           Namespace::SR_ENABLED                    | nil | true | ||||
|           Namespace::SR_DISABLED_WITH_OVERRIDE     | nil | false | ||||
|           Namespace::SR_DISABLED_AND_OVERRIDABLE   | nil | false | ||||
|           Namespace::SR_DISABLED_AND_UNOVERRIDABLE | nil | false | ||||
|         where(:shared_runners_enabled, :allow_to_override, :desired_config_for_new_project, :expected_result_for_project) do | ||||
|           true  | false | nil | true | ||||
|           false | true  | nil | false | ||||
|           false | false | nil | false | ||||
|         end | ||||
| 
 | ||||
|         with_them do | ||||
|  | @ -1057,14 +1057,12 @@ RSpec.describe Projects::CreateService, '#execute', feature_category: :groups_an | |||
|       end | ||||
| 
 | ||||
|       context 'parent group is present and allows desired config' do | ||||
|         where(:shared_runners_setting, :desired_config_for_new_project, :expected_result_for_project) do | ||||
|           Namespace::SR_ENABLED                    | true  | true | ||||
|           Namespace::SR_ENABLED                    | false | false | ||||
|           Namespace::SR_DISABLED_WITH_OVERRIDE     | false | false | ||||
|           Namespace::SR_DISABLED_WITH_OVERRIDE     | true  | true | ||||
|           Namespace::SR_DISABLED_AND_OVERRIDABLE   | false | false | ||||
|           Namespace::SR_DISABLED_AND_OVERRIDABLE   | true  | true | ||||
|           Namespace::SR_DISABLED_AND_UNOVERRIDABLE | false | false | ||||
|         where(:shared_runners_enabled, :allow_to_override, :desired_config_for_new_project, :expected_result_for_project) do | ||||
|           true  | false | true  | true | ||||
|           true  | false | false | false | ||||
|           false | true  | false | false | ||||
|           false | true  | true  | true | ||||
|           false | false | false | false | ||||
|         end | ||||
| 
 | ||||
|         with_them do | ||||
|  | @ -1080,8 +1078,8 @@ RSpec.describe Projects::CreateService, '#execute', feature_category: :groups_an | |||
|       end | ||||
| 
 | ||||
|       context 'parent group is present and disallows desired config' do | ||||
|         where(:shared_runners_setting, :desired_config_for_new_project) do | ||||
|           Namespace::SR_DISABLED_AND_UNOVERRIDABLE | true | ||||
|         where(:shared_runners_enabled, :allow_to_override, :desired_config_for_new_project) do | ||||
|           false | false | true | ||||
|         end | ||||
| 
 | ||||
|         with_them do | ||||
|  |  | |||
|  | @ -99,7 +99,7 @@ module DbCleaner | |||
|         AND pid <> pg_backend_pid(); | ||||
|     SQL | ||||
| 
 | ||||
|     Gitlab::Database::EachDatabase.each_database_connection(include_shared: false) do |connection| | ||||
|     Gitlab::Database::EachDatabase.each_connection(include_shared: false) do |connection| | ||||
|       connection.execute(cmd) | ||||
|     end | ||||
| 
 | ||||
|  |  | |||
|  | @ -19,7 +19,7 @@ module Database | |||
|     def execute_on_each_database(query, databases: %I[main ci]) | ||||
|       databases = databases.select { |database_name| database_exists?(database_name) } | ||||
| 
 | ||||
|       Gitlab::Database::EachDatabase.each_database_connection(only: databases, include_shared: false) do |connection, _| | ||||
|       Gitlab::Database::EachDatabase.each_connection(only: databases, include_shared: false) do |connection, _| | ||||
|         next unless Gitlab::Database.gitlab_schemas_for_connection(connection).include?(:gitlab_shared) | ||||
| 
 | ||||
|         connection.execute(query) | ||||
|  |  | |||
|  | @ -4,9 +4,4 @@ module ReloadHelpers | |||
|   def reload_models(*models) | ||||
|     models.compact.map(&:reload) | ||||
|   end | ||||
| 
 | ||||
|   def subject_and_reload(...) | ||||
|     subject | ||||
|     reload_models(...) | ||||
|   end | ||||
| end | ||||
|  |  | |||
|  | @ -86,7 +86,7 @@ RSpec.describe 'dev rake tasks' do | |||
|     end | ||||
| 
 | ||||
|     def expect_connections_to_be_terminated | ||||
|       expect(Gitlab::Database::EachDatabase).to receive(:each_database_connection) | ||||
|       expect(Gitlab::Database::EachDatabase).to receive(:each_connection) | ||||
|         .with(include_shared: false) | ||||
|         .and_call_original | ||||
| 
 | ||||
|  |  | |||
|  | @ -1109,7 +1109,7 @@ RSpec.describe 'gitlab:db namespace rake task', :silence_stdout, feature_categor | |||
|     before do | ||||
|       each_database = class_double('Gitlab::Database::EachDatabase').as_stubbed_const | ||||
| 
 | ||||
|       allow(each_database).to receive(:each_database_connection) | ||||
|       allow(each_database).to receive(:each_connection) | ||||
|         .and_yield(connections[:main], 'main') | ||||
|         .and_yield(connections[:ci], 'ci') | ||||
| 
 | ||||
|  |  | |||
|  | @ -77,11 +77,21 @@ RSpec.describe PipelineScheduleWorker, :sidekiq_inline, feature_category: :conti | |||
|         stub_ci_pipeline_yaml_file(YAML.dump(rspec: { variables: 'rspec' } )) | ||||
|       end | ||||
| 
 | ||||
|       it 'does not creates a new pipeline' do | ||||
|       it 'creates a new pipeline' do | ||||
|         expect { subject }.to change { project.ci_pipelines.count }.by(1) | ||||
|       end | ||||
| 
 | ||||
|       context 'with feature flag persist_failed_pipelines_from_schedules disabled' do | ||||
|         before do | ||||
|           stub_feature_flags(persist_failed_pipelines_from_schedules: false) | ||||
|         end | ||||
| 
 | ||||
|         it 'does not create a new pipeline' do | ||||
|           expect { subject }.not_to change { project.ci_pipelines.count } | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   context 'when the schedule is not runnable by the user' do | ||||
|     it 'does not deactivate the schedule' do | ||||
|  |  | |||
|  | @ -61,7 +61,7 @@ RSpec.describe RunPipelineScheduleWorker, feature_category: :continuous_integrat | |||
|         before do | ||||
|           expect(Ci::CreatePipelineService).to receive(:new).with(project, user, ref: pipeline_schedule.ref).and_return(create_pipeline_service) | ||||
| 
 | ||||
|           expect(create_pipeline_service).to receive(:execute).with(:schedule, ignore_skip_ci: true, save_on_errors: false, schedule: pipeline_schedule).and_return(service_response) | ||||
|           expect(create_pipeline_service).to receive(:execute).with(:schedule, ignore_skip_ci: true, save_on_errors: true, schedule: pipeline_schedule).and_return(service_response) | ||||
|         end | ||||
| 
 | ||||
|         context "when pipeline is persisted" do | ||||
|  | @ -124,7 +124,26 @@ RSpec.describe RunPipelineScheduleWorker, feature_category: :continuous_integrat | |||
| 
 | ||||
|         it 'creates a pipeline' do | ||||
|           expect(Ci::CreatePipelineService).to receive(:new).with(project, user, ref: pipeline_schedule.ref).and_return(create_pipeline_service) | ||||
|           expect(create_pipeline_service).to receive(:execute).with(:schedule, ignore_skip_ci: true, save_on_errors: false, schedule: pipeline_schedule).and_return(service_response) | ||||
|           expect(create_pipeline_service).to receive(:execute).with(:schedule, ignore_skip_ci: true, save_on_errors: true, schedule: pipeline_schedule).and_return(service_response) | ||||
| 
 | ||||
|           worker.perform(pipeline_schedule.id, user.id) | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       context 'with feature flag persist_failed_pipelines_from_schedules disabled' do | ||||
|         before do | ||||
|           stub_feature_flags(persist_failed_pipelines_from_schedules: false) | ||||
|         end | ||||
| 
 | ||||
|         it 'does not save_on_errors' do | ||||
|           expect(Ci::CreatePipelineService).to receive(:new).with(project, user, ref: pipeline_schedule.ref).and_return(create_pipeline_service) | ||||
| 
 | ||||
|           expect(create_pipeline_service).to receive(:execute).with( | ||||
|             :schedule, | ||||
|             ignore_skip_ci: true, | ||||
|             save_on_errors: false, | ||||
|             schedule: pipeline_schedule | ||||
|           ) | ||||
| 
 | ||||
|           worker.perform(pipeline_schedule.id, user.id) | ||||
|         end | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue