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();
|
||||
validEmojiNames = [...Object.keys(emojiMap), ...Object.keys(emojiAliases)];
|
||||
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,101 +43,144 @@ describe('gl_emoji', () => {
|
|||
return div.firstElementChild;
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
await initEmojiMock(emojiData);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
clearEmojiMock();
|
||||
|
||||
document.body.innerHTML = '';
|
||||
});
|
||||
|
||||
describe.each([
|
||||
[
|
||||
'bomb emoji just with name attribute',
|
||||
'<gl-emoji data-name="bomb"></gl-emoji>',
|
||||
'<gl-emoji data-name="bomb" data-unicode-version="6.0" title="bomb">💣</gl-emoji>',
|
||||
`<gl-emoji data-name="bomb" data-unicode-version="6.0" title="bomb"><img class="emoji" title=":bomb:" alt=":bomb:" src="/-/emojis/${EMOJI_VERSION}/bomb.png" width="16" height="16" align="absmiddle"></gl-emoji>`,
|
||||
],
|
||||
[
|
||||
'bomb emoji with name attribute and unicode version',
|
||||
'<gl-emoji data-name="bomb" data-unicode-version="6.0">💣</gl-emoji>',
|
||||
'<gl-emoji data-name="bomb" data-unicode-version="6.0">💣</gl-emoji>',
|
||||
`<gl-emoji data-name="bomb" data-unicode-version="6.0"><img class="emoji" title=":bomb:" alt=":bomb:" src="/-/emojis/${EMOJI_VERSION}/bomb.png" width="16" height="16" align="absmiddle"></gl-emoji>`,
|
||||
],
|
||||
[
|
||||
'bomb emoji with sprite fallback',
|
||||
'<gl-emoji data-fallback-sprite-class="emoji-bomb" data-name="bomb"></gl-emoji>',
|
||||
'<gl-emoji data-fallback-sprite-class="emoji-bomb" data-name="bomb" data-unicode-version="6.0" title="bomb">💣</gl-emoji>',
|
||||
'<gl-emoji data-fallback-sprite-class="emoji-bomb" data-name="bomb" data-unicode-version="6.0" title="bomb" class="emoji-icon emoji-bomb">💣</gl-emoji>',
|
||||
],
|
||||
[
|
||||
'bomb emoji with image fallback',
|
||||
'<gl-emoji data-fallback-src="/bomb.png" data-name="bomb"></gl-emoji>',
|
||||
'<gl-emoji data-fallback-src="/bomb.png" data-name="bomb" data-unicode-version="6.0" title="bomb">💣</gl-emoji>',
|
||||
'<gl-emoji data-fallback-src="/bomb.png" data-name="bomb" data-unicode-version="6.0" title="bomb"><img class="emoji" title=":bomb:" alt=":bomb:" src="/bomb.png" width="16" height="16" align="absmiddle"></gl-emoji>',
|
||||
],
|
||||
[
|
||||
'invalid emoji',
|
||||
'<gl-emoji data-name="invalid_emoji"></gl-emoji>',
|
||||
'<gl-emoji data-name="grey_question" data-unicode-version="6.0" title="white question mark ornament">❔</gl-emoji>',
|
||||
`<gl-emoji data-name="grey_question" data-unicode-version="6.0" title="white question mark ornament"><img class="emoji" title=":grey_question:" alt=":grey_question:" src="/-/emojis/${EMOJI_VERSION}/grey_question.png" width="16" height="16" align="absmiddle"></gl-emoji>`,
|
||||
],
|
||||
[
|
||||
'custom emoji with image fallback',
|
||||
'<gl-emoji data-fallback-src="https://cultofthepartyparrot.com/parrots/hd/parrot.gif" data-name="party-parrot" data-unicode-version="custom"></gl-emoji>',
|
||||
'<gl-emoji data-fallback-src="https://cultofthepartyparrot.com/parrots/hd/parrot.gif" data-name="party-parrot" data-unicode-version="custom"><img class="emoji" title=":party-parrot:" alt=":party-parrot:" src="https://cultofthepartyparrot.com/parrots/hd/parrot.gif" width="16" height="16" align="absmiddle"></gl-emoji>',
|
||||
'<gl-emoji data-fallback-src="https://cultofthepartyparrot.com/parrots/hd/parrot.gif" data-name="party-parrot" data-unicode-version="custom"><img class="emoji" title=":party-parrot:" alt=":party-parrot:" src="https://cultofthepartyparrot.com/parrots/hd/parrot.gif" width="16" height="16" align="absmiddle"></gl-emoji>',
|
||||
],
|
||||
])('%s', (name, markup, withEmojiSupport, withoutEmojiSupport) => {
|
||||
it(`renders correctly with emoji support`, async () => {
|
||||
jest.spyOn(EmojiUnicodeSupport, 'default').mockReturnValue(true);
|
||||
const glEmojiElement = markupToDomElement(markup);
|
||||
describe('standard emoji', () => {
|
||||
beforeEach(async () => {
|
||||
await initEmojiMock(emojiData);
|
||||
});
|
||||
|
||||
describe.each([
|
||||
[
|
||||
'bomb emoji just with name attribute',
|
||||
'<gl-emoji data-name="bomb"></gl-emoji>',
|
||||
'<gl-emoji data-name="bomb" data-unicode-version="6.0" title="bomb">💣</gl-emoji>',
|
||||
`<gl-emoji data-name="bomb" data-unicode-version="6.0" title="bomb"><img class="emoji" title=":bomb:" alt=":bomb:" src="/-/emojis/${EMOJI_VERSION}/bomb.png" width="16" height="16" align="absmiddle"></gl-emoji>`,
|
||||
],
|
||||
[
|
||||
'bomb emoji with name attribute and unicode version',
|
||||
'<gl-emoji data-name="bomb" data-unicode-version="6.0">💣</gl-emoji>',
|
||||
'<gl-emoji data-name="bomb" data-unicode-version="6.0">💣</gl-emoji>',
|
||||
`<gl-emoji data-name="bomb" data-unicode-version="6.0"><img class="emoji" title=":bomb:" alt=":bomb:" src="/-/emojis/${EMOJI_VERSION}/bomb.png" width="16" height="16" align="absmiddle"></gl-emoji>`,
|
||||
],
|
||||
[
|
||||
'bomb emoji with sprite fallback',
|
||||
'<gl-emoji data-fallback-sprite-class="emoji-bomb" data-name="bomb"></gl-emoji>',
|
||||
'<gl-emoji data-fallback-sprite-class="emoji-bomb" data-name="bomb" data-unicode-version="6.0" title="bomb">💣</gl-emoji>',
|
||||
'<gl-emoji data-fallback-sprite-class="emoji-bomb" data-name="bomb" data-unicode-version="6.0" title="bomb" class="emoji-icon emoji-bomb">💣</gl-emoji>',
|
||||
],
|
||||
[
|
||||
'bomb emoji with image fallback',
|
||||
'<gl-emoji data-fallback-src="/bomb.png" data-name="bomb"></gl-emoji>',
|
||||
'<gl-emoji data-fallback-src="/bomb.png" data-name="bomb" data-unicode-version="6.0" title="bomb">💣</gl-emoji>',
|
||||
'<gl-emoji data-fallback-src="/bomb.png" data-name="bomb" data-unicode-version="6.0" title="bomb"><img class="emoji" title=":bomb:" alt=":bomb:" src="/bomb.png" width="16" height="16" align="absmiddle"></gl-emoji>',
|
||||
],
|
||||
[
|
||||
'invalid emoji',
|
||||
'<gl-emoji data-name="invalid_emoji"></gl-emoji>',
|
||||
'<gl-emoji data-name="grey_question" data-unicode-version="6.0" title="white question mark ornament">❔</gl-emoji>',
|
||||
`<gl-emoji data-name="grey_question" data-unicode-version="6.0" title="white question mark ornament"><img class="emoji" title=":grey_question:" alt=":grey_question:" src="/-/emojis/${EMOJI_VERSION}/grey_question.png" width="16" height="16" align="absmiddle"></gl-emoji>`,
|
||||
],
|
||||
[
|
||||
'custom emoji with image fallback',
|
||||
'<gl-emoji data-fallback-src="https://cultofthepartyparrot.com/parrots/hd/parrot.gif" data-name="party-parrot" data-unicode-version="custom"></gl-emoji>',
|
||||
'<gl-emoji data-fallback-src="https://cultofthepartyparrot.com/parrots/hd/parrot.gif" data-name="party-parrot" data-unicode-version="custom"><img class="emoji" title=":party-parrot:" alt=":party-parrot:" src="https://cultofthepartyparrot.com/parrots/hd/parrot.gif" width="16" height="16" align="absmiddle"></gl-emoji>',
|
||||
'<gl-emoji data-fallback-src="https://cultofthepartyparrot.com/parrots/hd/parrot.gif" data-name="party-parrot" data-unicode-version="custom"><img class="emoji" title=":party-parrot:" alt=":party-parrot:" src="https://cultofthepartyparrot.com/parrots/hd/parrot.gif" width="16" height="16" align="absmiddle"></gl-emoji>',
|
||||
],
|
||||
])('%s', (name, markup, withEmojiSupport, withoutEmojiSupport) => {
|
||||
it(`renders correctly with emoji support`, async () => {
|
||||
jest.spyOn(EmojiUnicodeSupport, 'default').mockReturnValue(true);
|
||||
const glEmojiElement = markupToDomElement(markup);
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(glEmojiElement.outerHTML).toBe(withEmojiSupport);
|
||||
});
|
||||
|
||||
it(`renders correctly without emoji support`, async () => {
|
||||
jest.spyOn(EmojiUnicodeSupport, 'default').mockReturnValue(false);
|
||||
const glEmojiElement = markupToDomElement(markup);
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(glEmojiElement.outerHTML).toBe(withoutEmojiSupport);
|
||||
});
|
||||
});
|
||||
|
||||
it('escapes gl-emoji name', async () => {
|
||||
const glEmojiElement = markupToDomElement(
|
||||
"<gl-emoji data-name='"x="y" onload="alert(document.location.href)"' data-unicode-version='x'>abc</gl-emoji>",
|
||||
);
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(glEmojiElement.outerHTML).toBe(withEmojiSupport);
|
||||
expect(glEmojiElement.outerHTML).toBe(
|
||||
'<gl-emoji data-name=""x="y" onload="alert(document.location.href)"" data-unicode-version="x"><img class="emoji" title=":"x="y" onload="alert(document.location.href)":" alt=":"x="y" onload="alert(document.location.href)":" src="/-/emojis/2/grey_question.png" width="16" height="16" align="absmiddle"></gl-emoji>',
|
||||
);
|
||||
});
|
||||
|
||||
it(`renders correctly without emoji support`, async () => {
|
||||
it('Adds sprite CSS if emojis are not supported', async () => {
|
||||
const testPath = '/test-path.css';
|
||||
jest.spyOn(EmojiUnicodeSupport, 'default').mockReturnValue(false);
|
||||
const glEmojiElement = markupToDomElement(markup);
|
||||
window.gon.emoji_sprites_css_path = testPath;
|
||||
|
||||
expect(document.head.querySelector(`link[href="${testPath}"]`)).toBe(null);
|
||||
expect(window.gon.emoji_sprites_css_added).toBe(undefined);
|
||||
|
||||
markupToDomElement(
|
||||
'<gl-emoji data-fallback-sprite-class="emoji-bomb" data-name="bomb"></gl-emoji>',
|
||||
);
|
||||
await waitForPromises();
|
||||
|
||||
expect(glEmojiElement.outerHTML).toBe(withoutEmojiSupport);
|
||||
expect(document.head.querySelector(`link[href="${testPath}"]`).outerHTML).toBe(
|
||||
'<link rel="stylesheet" href="/test-path.css">',
|
||||
);
|
||||
expect(window.gon.emoji_sprites_css_added).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('escapes gl-emoji name', async () => {
|
||||
const glEmojiElement = markupToDomElement(
|
||||
"<gl-emoji data-name='"x="y" onload="alert(document.location.href)"' data-unicode-version='x'>abc</gl-emoji>",
|
||||
);
|
||||
describe('custom emoji', () => {
|
||||
beforeEach(async () => {
|
||||
mockClient = createMockClient([
|
||||
[
|
||||
customEmojiQuery,
|
||||
jest.fn().mockResolvedValue({
|
||||
data: {
|
||||
group: {
|
||||
id: 1,
|
||||
customEmoji: {
|
||||
nodes: [{ id: 1, name: 'parrot', url: 'parrot.gif' }],
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
]);
|
||||
|
||||
await waitForPromises();
|
||||
window.gon = { features: { customEmoji: true } };
|
||||
document.body.dataset.group = 'test-group';
|
||||
|
||||
expect(glEmojiElement.outerHTML).toBe(
|
||||
'<gl-emoji data-name=""x="y" onload="alert(document.location.href)"" data-unicode-version="x"><img class="emoji" title=":"x="y" onload="alert(document.location.href)":" alt=":"x="y" onload="alert(document.location.href)":" src="/-/emojis/2/grey_question.png" width="16" height="16" align="absmiddle"></gl-emoji>',
|
||||
);
|
||||
});
|
||||
await initEmojiMock(emojiData);
|
||||
});
|
||||
|
||||
it('Adds sprite CSS if emojis are not supported', async () => {
|
||||
const testPath = '/test-path.css';
|
||||
jest.spyOn(EmojiUnicodeSupport, 'default').mockReturnValue(false);
|
||||
window.gon.emoji_sprites_css_path = testPath;
|
||||
afterEach(() => {
|
||||
window.gon = {};
|
||||
delete document.body.dataset.group;
|
||||
});
|
||||
|
||||
expect(document.head.querySelector(`link[href="${testPath}"]`)).toBe(null);
|
||||
expect(window.gon.emoji_sprites_css_added).toBe(undefined);
|
||||
it('renders custom emoji', async () => {
|
||||
const glEmojiElement = markupToDomElement('<gl-emoji data-name="parrot"></gl-emoji>');
|
||||
|
||||
markupToDomElement(
|
||||
'<gl-emoji data-fallback-sprite-class="emoji-bomb" data-name="bomb"></gl-emoji>',
|
||||
);
|
||||
await waitForPromises();
|
||||
await waitForPromises();
|
||||
|
||||
expect(document.head.querySelector(`link[href="${testPath}"]`).outerHTML).toBe(
|
||||
'<link rel="stylesheet" href="/test-path.css">',
|
||||
);
|
||||
expect(window.gon.emoji_sprites_css_added).toBe(true);
|
||||
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
|
||||
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')
|
||||
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) }
|
||||
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 'receives correct method and succeeds' do
|
||||
expect(group).to receive(:update_shared_runners_setting!).with('enabled')
|
||||
it 'enables shared Runners only for itself' do
|
||||
expect do
|
||||
expect(subject[:status]).to eq(:success)
|
||||
|
||||
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)
|
||||
|
||||
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
|
||||
let(:params) { { shared_runners_setting: Namespace::SR_DISABLED_AND_OVERRIDABLE } }
|
||||
|
||||
shared_examples 'allow descendants to override' do
|
||||
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
|
||||
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
|
||||
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)
|
||||
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)
|
||||
end
|
||||
|
||||
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 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) }
|
||||
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 'results error' do
|
||||
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')
|
||||
end
|
||||
|
||||
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 } }
|
||||
|
||||
include_examples 'allow descendants to override'
|
||||
end
|
||||
|
||||
context "when using SR_DISABLED_WITH_OVERRIDE" do
|
||||
let(:params) { { shared_runners_setting: Namespace::SR_DISABLED_WITH_OVERRIDE } }
|
||||
|
||||
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,8 +77,18 @@ 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
|
||||
expect { subject }.not_to change { project.ci_pipelines.count }
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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