Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-06-27 06:08:04 +00:00
parent f33d28f789
commit 5849e597a0
57 changed files with 693 additions and 535 deletions

View File

@ -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 = '';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,12 @@
query getCustomEmoji($groupPath: ID!) {
group(fullPath: $groupPath) {
id
customEmoji {
nodes {
id
name
url
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1 @@
e728befa42eb6749929e758ece0f29ec57cd7614a378b8c5e4dc24f134f39185

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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='&#34;x=&#34y&#34 onload=&#34;alert(document.location.href)&#34;' data-unicode-version='x'>abc</gl-emoji>",
);
await waitForPromises();
expect(glEmojiElement.outerHTML).toBe(withEmojiSupport);
expect(glEmojiElement.outerHTML).toBe(
'<gl-emoji data-name="&quot;x=&quot;y&quot; onload=&quot;alert(document.location.href)&quot;" data-unicode-version="x"><img class="emoji" title=":&quot;x=&quot;y&quot; onload=&quot;alert(document.location.href)&quot;:" alt=":&quot;x=&quot;y&quot; onload=&quot;alert(document.location.href)&quot;:" 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='&#34;x=&#34y&#34 onload=&#34;alert(document.location.href)&#34;' 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="&quot;x=&quot;y&quot; onload=&quot;alert(document.location.href)&quot;" data-unicode-version="x"><img class="emoji" title=":&quot;x=&quot;y&quot; onload=&quot;alert(document.location.href)&quot;:" alt=":&quot;x=&quot;y&quot; onload=&quot;alert(document.location.href)&quot;:" 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');
});
});
});

View File

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

View File

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

View File

@ -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',
},
});
});
});
});
});

View File

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

View File

@ -17,6 +17,7 @@ import {
import { DRAWIO_ORIGIN } from 'spec/test_constants';
jest.mock('~/emoji');
jest.mock('~/lib/graphql');
describe('WikiForm', () => {
let wrapper;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,9 +4,4 @@ module ReloadHelpers
def reload_models(*models)
models.compact.map(&:reload)
end
def subject_and_reload(...)
subject
reload_models(...)
end
end

View File

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

View File

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

View File

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

View File

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