Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-10-31 09:09:32 +00:00
parent 853c0c530b
commit 5025412fc4
41 changed files with 729 additions and 472 deletions

View File

@ -15,7 +15,7 @@ $.fn.renderGFM = function renderGFM() {
syntaxHighlight(this.find('.js-syntax-highlight').get());
renderKroki(this.find('.js-render-kroki[hidden]').get());
renderMath(this.find('.js-render-math'));
renderSandboxedMermaid(this.find('.js-render-mermaid').get());
renderSandboxedMermaid(this.find('.js-render-mermaid'));
renderJSONTable(
Array.from(this.find('[lang="json"][data-lang-params="table"]').get()).map((e) => e.parentNode),
);

View File

@ -1,4 +1,5 @@
import { countBy } from 'lodash';
import $ from 'jquery';
import { once, countBy } from 'lodash';
import { __ } from '~/locale';
import {
getBaseURL,
@ -7,8 +8,7 @@ import {
joinPaths,
} from '~/lib/utils/url_utility';
import { darkModeEnabled } from '~/lib/utils/color_utils';
import { setAttributes, isElementVisible } from '~/lib/utils/dom_utils';
import { createAlert, VARIANT_WARNING } from '~/flash';
import { setAttributes } from '~/lib/utils/dom_utils';
import { unrestrictedPages } from './constants';
// Renders diagrams and flowcharts from text using Mermaid in any element with the
@ -27,30 +27,17 @@ import { unrestrictedPages } from './constants';
const SANDBOX_FRAME_PATH = '/-/sandbox/mermaid';
// This is an arbitrary number; Can be iterated upon when suitable.
export const MAX_CHAR_LIMIT = 2000;
const MAX_CHAR_LIMIT = 2000;
// Max # of mermaid blocks that can be rendered in a page.
export const MAX_MERMAID_BLOCK_LIMIT = 50;
const MAX_MERMAID_BLOCK_LIMIT = 50;
// Max # of `&` allowed in Chaining of links syntax
const MAX_CHAINING_OF_LINKS_LIMIT = 30;
export const BUFFER_IFRAME_HEIGHT = 10;
export const SANDBOX_ATTRIBUTES = 'allow-scripts allow-popups';
const ALERT_CONTAINER_CLASS = 'mermaid-alert-container';
export const LAZY_ALERT_SHOWN_CLASS = 'lazy-alert-shown';
// Keep a map of mermaid blocks we've already rendered.
const elsProcessingMap = new WeakMap();
let renderedMermaidBlocks = 0;
/**
* Determines whether a given Mermaid diagram is visible.
*
* @param {Element} el The Mermaid DOM node
* @returns
*/
const isVisibleMermaid = (el) => el.closest('details') === null && isElementVisible(el);
function shouldLazyLoadMermaidBlock(source) {
/**
* If source contains `&`, which means that it might
@ -117,8 +104,8 @@ function renderMermaidEl(el, source) {
);
}
function renderMermaids(els) {
if (!els.length) return;
function renderMermaids($els) {
if (!$els.length) return;
const pageName = document.querySelector('body').dataset.page;
@ -127,7 +114,7 @@ function renderMermaids(els) {
let renderedChars = 0;
els.forEach((el) => {
$els.each((i, el) => {
// Skipping all the elements which we've already queued in requestIdleCallback
if (elsProcessingMap.has(el)) {
return;
@ -146,29 +133,33 @@ function renderMermaids(els) {
renderedMermaidBlocks >= MAX_MERMAID_BLOCK_LIMIT ||
shouldLazyLoadMermaidBlock(source))
) {
const parent = el.parentNode;
const html = `
<div class="alert gl-alert gl-alert-warning alert-dismissible lazy-render-mermaid-container js-lazy-render-mermaid-container fade show" role="alert">
<div>
<div>
<div class="js-warning-text"></div>
<div class="gl-alert-actions">
<button type="button" class="js-lazy-render-mermaid btn gl-alert-action btn-confirm btn-md gl-button">Display</button>
</div>
</div>
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
</div>
`;
if (!parent.classList.contains(LAZY_ALERT_SHOWN_CLASS)) {
const alertContainer = document.createElement('div');
alertContainer.classList.add(ALERT_CONTAINER_CLASS);
alertContainer.classList.add('gl-mb-5');
parent.after(alertContainer);
createAlert({
message: __(
'Warning: Displaying this diagram might cause performance issues on this page.',
),
variant: VARIANT_WARNING,
parent: parent.parentNode,
containerSelector: `.${ALERT_CONTAINER_CLASS}`,
primaryButton: {
text: __('Display'),
clickHandler: () => {
alertContainer.remove();
renderMermaidEl(el, source);
},
},
});
parent.classList.add(LAZY_ALERT_SHOWN_CLASS);
const $parent = $(el).parent();
if (!$parent.hasClass('lazy-alert-shown')) {
$parent.after(html);
$parent
.siblings()
.find('.js-warning-text')
.text(
__('Warning: Displaying this diagram might cause performance issues on this page.'),
);
$parent.addClass('lazy-alert-shown');
}
return;
@ -185,33 +176,37 @@ function renderMermaids(els) {
});
}
export default function renderMermaid(els) {
if (!els.length) return;
const hookLazyRenderMermaidEvent = once(() => {
$(document.body).on('click', '.js-lazy-render-mermaid', function eventHandler() {
const parent = $(this).closest('.js-lazy-render-mermaid-container');
const pre = parent.prev();
const visibleMermaids = [];
const hiddenMermaids = [];
const el = pre.find('.js-render-mermaid');
for (const el of els) {
if (isVisibleMermaid(el)) {
visibleMermaids.push(el);
} else {
hiddenMermaids.push(el);
}
}
parent.remove();
// sandbox update
const element = el.get(0);
const { source } = fixElementSource(element);
renderMermaidEl(element, source);
});
});
export default function renderMermaid($els) {
if (!$els.length) return;
const visibleMermaids = $els.filter(function filter() {
return $(this).closest('details').length === 0 && $(this).is(':visible');
});
renderMermaids(visibleMermaids);
hiddenMermaids.forEach((el) => {
el.closest('details').addEventListener(
'toggle',
({ target: details }) => {
if (details.open) {
renderMermaids([...details.querySelectorAll('.js-render-mermaid')]);
}
},
{
once: true,
},
);
$els.closest('details').one('toggle', function toggle() {
if (this.open) {
renderMermaids($(this).find('.js-render-mermaid'));
}
});
hookLazyRenderMermaidEvent();
}

View File

@ -95,6 +95,16 @@ export default {
required: false,
default: true,
},
hasError: {
type: Boolean,
required: false,
default: false,
},
itemAddFailureMessage: {
type: String,
required: false,
default: '',
},
},
data() {
return {
@ -233,7 +243,7 @@ export default {
<div
v-if="isFormVisible"
class="js-add-related-issues-form-area card-body bordered-box bg-white"
:class="{ 'gl-mb-5': shouldShowTokenBody }"
:class="{ 'gl-mb-5': shouldShowTokenBody, 'gl-show-field-errors': hasError }"
>
<add-issuable-form
:show-categorized-issues="showCategorizedIssues"
@ -245,6 +255,8 @@ export default {
:auto-complete-epics="autoCompleteEpics"
:auto-complete-issues="autoCompleteIssues"
:path-id-separator="pathIdSeparator"
:has-error="hasError"
:item-add-failure-message="itemAddFailureMessage"
@pendingIssuableRemoveRequest="$emit('pendingIssuableRemoveRequest', $event)"
@addIssuableFormInput="$emit('addIssuableFormInput', $event)"
@addIssuableFormBlur="$emit('addIssuableFormBlur', $event)"

View File

@ -107,6 +107,8 @@ export default {
isSubmitting: false,
isFormVisible: false,
inputValue: '',
hasError: false,
errorMessage: null,
};
},
computed: {
@ -170,11 +172,11 @@ export default {
this.isFormVisible = false;
})
.catch(({ response }) => {
let errorMessage = addRelatedIssueErrorMap[this.issuableType];
this.hasError = true;
this.errorMessage = addRelatedIssueErrorMap[this.issuableType];
if (response && response.data && response.data.message) {
errorMessage = response.data.message;
this.errorMessage = response.data.message;
}
createAlert({ message: errorMessage });
})
.finally(() => {
this.isSubmitting = false;
@ -266,6 +268,8 @@ export default {
:issuable-type="issuableType"
:path-id-separator="pathIdSeparator"
:show-categorized-issues="showCategorizedIssues"
:has-error="hasError"
:item-add-failure-message="errorMessage"
@saveReorder="saveIssueOrder"
@toggleAddRelatedIssuesForm="onToggleAddRelatedIssuesForm"
@addIssuableFormInput="onInput"

View File

@ -1,5 +1,5 @@
<script>
import { isEmpty } from 'lodash';
import { cloneDeep, isEmpty } from 'lodash';
import { GlFormGroup, GlFormInput, GlFormRadio, GlFormRadioGroup, GlLink } from '@gitlab/ui';
import { __, s__ } from '~/locale';
@ -30,7 +30,7 @@ export default {
return {
maskEnabled: !isEmpty(this.initialUrlVariables),
url: this.initialUrl,
items: isEmpty(this.initialUrlVariables) ? [{}] : this.initialUrlVariables,
items: this.getInitialItems(),
};
},
computed: {
@ -54,6 +54,16 @@ export default {
},
},
methods: {
getInitialItems() {
return isEmpty(this.initialUrlVariables) ? [{}] : cloneDeep(this.initialUrlVariables);
},
isEditingItem(key) {
if (isEmpty(this.initialUrlVariables)) {
return false;
}
return this.initialUrlVariables.some((item) => item.key === key);
},
onItemInput({ index, key, value }) {
this.$set(this.items, index, { key, value });
},
@ -112,6 +122,7 @@ export default {
:index="index"
:item-key="key"
:item-value="value"
:is-editing="isEditingItem(key)"
@input="onItemInput"
@remove="removeItem"
/>

View File

@ -1,6 +1,7 @@
<script>
import { GlButton, GlFormGroup, GlFormInput } from '@gitlab/ui';
import { s__ } from '~/locale';
import { MASK_ITEM_VALUE_HIDDEN } from '../constants';
export default {
components: {
@ -24,6 +25,11 @@ export default {
required: false,
default: null,
},
isEditing: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
keyInputId() {
@ -32,6 +38,9 @@ export default {
valueInputId() {
return this.inputId('value');
},
displayValue() {
return this.isEditing ? MASK_ITEM_VALUE_HIDDEN : this.itemValue;
},
},
methods: {
inputId(type) {
@ -68,7 +77,8 @@ export default {
<gl-form-input
:id="valueInputId"
:name="inputName('value')"
:value="itemValue"
:value="displayValue"
:disabled="isEditing"
@input="onValueInput"
/>
</gl-form-group>
@ -82,9 +92,15 @@ export default {
:id="keyInputId"
:name="inputName('key')"
:value="itemKey"
:disabled="isEditing"
@input="onKeyInput"
/>
</gl-form-group>
<gl-button icon="remove" :aria-label="__('Remove')" @click="onRemoveClick" />
<gl-button
icon="remove"
:aria-label="__('Remove')"
:disabled="isEditing"
@click="onRemoveClick"
/>
</div>
</template>

View File

@ -15,3 +15,5 @@ export const descriptionText = {
),
[BRANCH_FILTER_REGEX]: s__('Webhooks|Regex such as %{REGEX_CODE} is supported.'),
};
export const MASK_ITEM_VALUE_HIDDEN = '************';

View File

@ -10,6 +10,11 @@ export default () => {
const { url: initialUrl, urlVariables } = el.dataset;
// Convert the array of 'key' strings to array of { key } objects
const initialUrlVariables = urlVariables
? JSON.parse(urlVariables)?.map((key) => ({ key }))
: undefined;
return new Vue({
el,
name: 'WebhookFormRoot',
@ -17,7 +22,7 @@ export default () => {
return createElement(FormUrlApp, {
props: {
initialUrl,
initialUrlVariables: urlVariables ? JSON.parse(urlVariables) : undefined,
initialUrlVariables,
},
});
},

View File

@ -4,7 +4,7 @@ module HooksHelper
def webhook_form_data(hook)
{
url: hook.url,
url_variables: nil
url_variables: Gitlab::Json.dump(hook.url_variables.keys)
}
end

View File

@ -1,32 +0,0 @@
# frozen_string_literal: true
module Clusters
module Applications
class PatchService < BaseHelmService
def execute
return unless app.scheduled?
app.make_updating!
patch
end
private
def patch
log_event(:begin_patch)
helm_api.update(update_command)
log_event(:schedule_wait_for_patch)
ClusterWaitForAppInstallationWorker.perform_in(
ClusterWaitForAppInstallationWorker::INTERVAL, app.name, app.id)
rescue Kubeclient::HttpError => e
log_error(e)
app.make_errored!(_('Kubernetes error: %{error_code}') % { error_code: e.error_code })
rescue StandardError => e
log_error(e)
app.make_errored!(_('Failed to update.'))
end
end
end
end

View File

@ -1,17 +0,0 @@
# frozen_string_literal: true
module Clusters
module Applications
class UpdateService < Clusters::Applications::BaseService
private
def worker_class(application)
ClusterPatchAppWorker
end
def builder
cluster.public_send(application_class.association_name) # rubocop:disable GitlabSecurity/PublicSend
end
end
end
end

View File

@ -1,5 +1,8 @@
# frozen_string_literal: true
# DEPRECATED
#
# To be removed by https://gitlab.com/gitlab-org/gitlab/-/issues/366573
class ClusterPatchAppWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
@ -12,9 +15,5 @@ class ClusterPatchAppWorker # rubocop:disable Scalability/IdempotentWorker
worker_has_external_dependencies!
loggable_arguments 0
def perform(app_name, app_id)
find_application(app_name, app_id) do |app|
Clusters::Applications::PatchService.new(app).execute
end
end
def perform(app_name, app_id); end
end

View File

@ -0,0 +1,22 @@
# frozen_string_literal: true
class AddTempIndexForUserDetailsFields < Gitlab::Database::Migration[2.0]
INDEX_NAME = 'tmp_idx_where_user_details_fields_filled'
disable_ddl_transaction!
def up
add_concurrent_index :users, :id, name: INDEX_NAME, where: <<~QUERY
(COALESCE(linkedin, '') IS DISTINCT FROM '')
OR (COALESCE(twitter, '') IS DISTINCT FROM '')
OR (COALESCE(skype, '') IS DISTINCT FROM '')
OR (COALESCE(website_url, '') IS DISTINCT FROM '')
OR (COALESCE(location, '') IS DISTINCT FROM '')
OR (COALESCE(organization, '') IS DISTINCT FROM '')
QUERY
end
def down
remove_concurrent_index_by_name :users, INDEX_NAME
end
end

View File

@ -0,0 +1,16 @@
# frozen_string_literal: true
class QueueBackfillUserDetailsFields < Gitlab::Database::Migration[2.0]
MIGRATION = 'BackfillUserDetailsFields'
INTERVAL = 2.minutes
restrict_gitlab_migration gitlab_schema: :gitlab_main
def up
queue_batched_background_migration(MIGRATION, :users, :id, job_interval: INTERVAL)
end
def down
delete_batched_background_migration(MIGRATION, :users, :id, [])
end
end

View File

@ -0,0 +1 @@
cdf3e65f07f700617f47435b79743b4b35307f47cf46a9696350e55af1774d42

View File

@ -0,0 +1 @@
6c3fe5bf01ac9e74f142ddb3e093867b62cf430f24ba885f8475ccf7f73899cb

View File

@ -31130,6 +31130,8 @@ CREATE INDEX tmp_idx_project_features_on_releases_al_and_repo_al_partial ON proj
CREATE INDEX tmp_idx_vulnerabilities_on_id_where_report_type_7_99 ON vulnerabilities USING btree (id) WHERE (report_type = ANY (ARRAY[7, 99]));
CREATE INDEX tmp_idx_where_user_details_fields_filled ON users USING btree (id) WHERE (((COALESCE(linkedin, ''::character varying))::text IS DISTINCT FROM ''::text) OR ((COALESCE(twitter, ''::character varying))::text IS DISTINCT FROM ''::text) OR ((COALESCE(skype, ''::character varying))::text IS DISTINCT FROM ''::text) OR ((COALESCE(website_url, ''::character varying))::text IS DISTINCT FROM ''::text) OR ((COALESCE(location, ''::character varying))::text IS DISTINCT FROM ''::text) OR ((COALESCE(organization, ''::character varying))::text IS DISTINCT FROM ''::text));
CREATE INDEX tmp_index_ci_job_artifacts_on_expire_at_where_locked_unknown ON ci_job_artifacts USING btree (expire_at, job_id) WHERE ((locked = 2) AND (expire_at IS NOT NULL));
CREATE INDEX tmp_index_ci_job_artifacts_on_id_expire_at_file_type_trace ON ci_job_artifacts USING btree (id) WHERE (((date_part('day'::text, timezone('UTC'::text, expire_at)) = ANY (ARRAY[(21)::double precision, (22)::double precision, (23)::double precision])) AND (date_part('minute'::text, timezone('UTC'::text, expire_at)) = ANY (ARRAY[(0)::double precision, (30)::double precision, (45)::double precision])) AND (date_part('second'::text, timezone('UTC'::text, expire_at)) = (0)::double precision)) OR (file_type = 3));

View File

@ -0,0 +1,14 @@
/**
* Look up the global node modules directory.
*
* Because we install markdownlint packages globally
* in the Docker image where this runs, we need to
* provide the path to the global install location
* when referencing global functions from our own node
* modules.
*
* Image:
* https://gitlab.com/gitlab-org/gitlab-docs/-/blob/main/dockerfiles/gitlab-docs-lint-markdown.Dockerfile
*/
const { execSync } = require('child_process');
module.exports.globalPath = execSync('yarn global dir').toString().trim() + '/node_modules/';

View File

@ -0,0 +1,26 @@
const { globalPath } = require('../require_helper');
const {
forEachLine,
getLineMetadata,
isBlankLine,
} = require(`${globalPath}/markdownlint-rule-helpers`);
module.exports = {
names: ['tabs-blank-lines'],
description: 'Tab elements must be surrounded by blank lines',
tags: ['gitlab-docs', 'tabs'],
function: (params, onError) => {
const tabElements = ['::Tabs', '::EndTabs', ':::TabTitle'];
forEachLine(getLineMetadata(params), (line, lineIndex) => {
const lineHasTab = tabElements.includes(line.split(' ')[0]);
const prevLine = params.lines[lineIndex - 1];
const nextLine = params.lines[lineIndex + 1];
if (lineHasTab && (!isBlankLine(prevLine) || !isBlankLine(nextLine))) {
onError({
lineNumber: lineIndex + 1,
});
}
});
},
};

View File

@ -0,0 +1,31 @@
const { globalPath } = require('../require_helper');
const { forEachLine, getLineMetadata } = require(`${globalPath}/markdownlint-rule-helpers`);
module.exports = {
names: ['tabs-title-markup'],
description: 'Incorrect number of colon characters for tag',
information: new URL('https://docs.gitlab.com/ee/development/documentation/styleguide/#tabs'),
tags: ['gitlab-docs', 'tabs'],
function: (params, onError) => {
// Note the correct number of colons in each tab tag type.
const wrapperColons = 2;
const titleColons = 3;
forEachLine(getLineMetadata(params), (line, lineIndex) => {
// Get the number of colons in this line.
const colonCount = [...line].filter((x) => x === ':').length;
// Throw an error in the case of a mismatch.
if (
((line.includes(':Tabs') || line.includes(':EndTabs')) && colonCount !== wrapperColons) ||
(line.includes(':TabTitle') && colonCount !== titleColons)
) {
const correctColonCount = line.includes(':TabTitle') ? wrapperColons : titleColons;
onError({
lineNumber: lineIndex + 1,
detail: `Actual: ${colonCount}; Expected: ${correctColonCount}`,
});
}
});
},
};

View File

@ -0,0 +1,23 @@
const { globalPath } = require('../require_helper');
const {
forEachLine,
getLineMetadata,
isBlankLine,
} = require(`${globalPath}/markdownlint-rule-helpers`);
module.exports = {
names: ['tabs-title-text'],
description: 'Tab without title text',
information: new URL('https://docs.gitlab.com/ee/development/documentation/styleguide/#tabs'),
tags: ['gitlab-docs', 'tabs'],
function: (params, onError) => {
forEachLine(getLineMetadata(params), (line, lineIndex) => {
if (!isBlankLine(line) && line.replace(':::TabTitle', '').trim() === '') {
onError({
lineNumber: lineIndex + 1,
detail: 'Expected: :::TabTitle <your title here>; Actual: :::TabTitle',
});
}
});
},
};

View File

@ -0,0 +1,21 @@
module.exports = {
names: ['tabs-wrapper-tags'],
description: 'Unequal number of tab start and end tags',
information: new URL('https://docs.gitlab.com/ee/development/documentation/styleguide/#tabs'),
tags: ['gitlab-docs', 'tabs'],
function: function rule(params, onError) {
const tabStarts = params.lines.filter((line) => line === '::Tabs');
const tabEnds = params.lines.filter((line) => line === '::EndTabs');
if (tabStarts.length !== tabEnds.length) {
const errorIndex =
params.lines.indexOf('::Tabs') > 0
? params.lines.indexOf('::Tabs')
: params.lines.indexOf('::EndTabs');
onError({
lineNumber: errorIndex + 1,
detail: `Opening tags: ${tabStarts.length}; Closing tags: ${tabEnds.length}`,
});
}
},
};

View File

@ -3,15 +3,17 @@
module API
module Entities
class PullMirror < Grape::Entity
expose :id
expose :status, as: :update_status
expose :url do |import_state|
expose :id, documentation: { type: 'integer', example: 101486 }
expose :status, as: :update_status, documentation: { type: 'string', example: 'finished' }
expose :url,
documentation: { type: 'string',
example: 'https://*****:*****@gitlab.com/gitlab-org/security/gitlab.git' } do |import_state|
import_state.project.safe_import_url
end
expose :last_error
expose :last_update_at
expose :last_update_started_at
expose :last_successful_update_at
expose :last_error, documentation: { type: 'string', example: nil }
expose :last_update_at, documentation: { type: 'dateTime', example: '2020-01-06T17:32:02.823Z' }
expose :last_update_started_at, documentation: { type: 'dateTime', example: '2020-01-06T17:32:02.823Z' }
expose :last_successful_update_at, documentation: { type: 'dateTime', example: '2020-01-06T17:32:02.823Z' }
end
end
end

View File

@ -0,0 +1,61 @@
# frozen_string_literal: true
module Gitlab
module BackgroundMigration
# Class that will backfill the following fields from user to user_details
# * linkedin
# * twitter
# * skype
# * website_url
# * location
# * organization
class BackfillUserDetailsFields < BatchedMigrationJob
operation_name :backfill_user_details_fields
def perform
query = <<~SQL
(COALESCE(linkedin, '') IS DISTINCT FROM '')
OR (COALESCE(twitter, '') IS DISTINCT FROM '')
OR (COALESCE(skype, '') IS DISTINCT FROM '')
OR (COALESCE(website_url, '') IS DISTINCT FROM '')
OR (COALESCE(location, '') IS DISTINCT FROM '')
OR (COALESCE(organization, '') IS DISTINCT FROM '')
SQL
field_limit = UserDetail::DEFAULT_FIELD_LENGTH
each_sub_batch(
batching_scope: ->(relation) {
relation.where(query).select(
'id AS user_id',
"substring(COALESCE(linkedin, '') from 1 for #{field_limit}) AS linkedin",
"substring(COALESCE(twitter, '') from 1 for #{field_limit}) AS twitter",
"substring(COALESCE(skype, '') from 1 for #{field_limit}) AS skype",
"substring(COALESCE(website_url, '') from 1 for #{field_limit}) AS website_url",
"substring(COALESCE(location, '') from 1 for #{field_limit}) AS location",
"substring(COALESCE(organization, '') from 1 for #{field_limit}) AS organization"
)
}
) do |sub_batch|
upsert_user_details_fields(sub_batch)
end
end
def upsert_user_details_fields(relation)
connection.execute(
<<~SQL
INSERT INTO user_details (user_id, linkedin, twitter, skype, website_url, location, organization)
#{relation.to_sql}
ON CONFLICT (user_id)
DO UPDATE SET
"linkedin" = EXCLUDED."linkedin",
"twitter" = EXCLUDED."twitter",
"skype" = EXCLUDED."skype",
"website_url" = EXCLUDED."website_url",
"location" = EXCLUDED."location",
"organization" = EXCLUDED."organization"
SQL
)
end
end
end
end

View File

@ -6,8 +6,8 @@ module Sidebars
class DeploymentsMenu < ::Sidebars::Menu
override :configure_menu_items
def configure_menu_items
add_item(feature_flags_menu_item)
add_item(environments_menu_item)
add_item(feature_flags_menu_item)
add_item(releases_menu_item)
true

View File

@ -16615,9 +16615,6 @@ msgstr ""
msgid "Failed to update the Canary Ingress."
msgstr ""
msgid "Failed to update."
msgstr ""
msgid "Failed to upgrade."
msgstr ""
@ -48761,6 +48758,9 @@ msgstr ""
msgid "must be unique by status and elapsed time within a policy"
msgstr ""
msgid "must belong to same project of the work item."
msgstr ""
msgid "must have a repository"
msgstr ""

View File

@ -54,7 +54,7 @@
"@gitlab/at.js": "1.5.7",
"@gitlab/favicon-overlay": "2.0.0",
"@gitlab/svgs": "3.5.0",
"@gitlab/ui": "49.2.0",
"@gitlab/ui": "49.2.1",
"@gitlab/visual-review-tools": "1.7.3",
"@gitlab/web-ide": "0.0.1-dev-20220815034418",
"@rails/actioncable": "6.1.4-7",

View File

@ -151,7 +151,7 @@ if [ -z "${MD_DOC_PATH}" ]
then
echo "Merged results pipeline detected, but no markdown files found. Skipping."
else
run_locally_or_in_docker 'markdownlint' "--config .markdownlint.yml ${MD_DOC_PATH}"
run_locally_or_in_docker 'markdownlint' "--config .markdownlint.yml ${MD_DOC_PATH} --rules doc/.markdownlint/rules"
fi
echo '=> Linting prose...'

View File

@ -6,6 +6,10 @@ FactoryBot.define do
enable_ssl_verification { false }
project
trait :url_variables do
url_variables { { 'abc' => 'supers3cret' } }
end
trait :token do
token { generate(:token) }
end

View File

@ -1,127 +1,34 @@
import { createWrapper } from '@vue/test-utils';
import { __ } from '~/locale';
import $ from 'jquery';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import renderMermaid, {
MAX_CHAR_LIMIT,
MAX_MERMAID_BLOCK_LIMIT,
LAZY_ALERT_SHOWN_CLASS,
} from '~/behaviors/markdown/render_sandboxed_mermaid';
import renderMermaid from '~/behaviors/markdown/render_sandboxed_mermaid';
describe('Mermaid diagrams renderer', () => {
// Finders
const findMermaidIframes = () => document.querySelectorAll('iframe[src="/-/sandbox/mermaid"]');
const findDangerousMermaidAlert = () =>
createWrapper(document.querySelector('[data-testid="alert-warning"]'));
// Helpers
const renderDiagrams = () => {
renderMermaid([...document.querySelectorAll('.js-render-mermaid')]);
jest.runAllTimers();
};
beforeEach(() => {
describe('Render mermaid diagrams for Gitlab Flavoured Markdown', () => {
it('Does something', () => {
document.body.dataset.page = '';
});
setHTMLFixture(`
<div class="gl-relative markdown-code-block js-markdown-code">
<pre data-sourcepos="1:1-7:3" class="code highlight js-syntax-highlight language-mermaid white" lang="mermaid" id="code-4">
<code class="js-render-mermaid">
<span id="LC1" class="line" lang="mermaid">graph TD;</span>
<span id="LC2" class="line" lang="mermaid">A--&gt;B</span>
<span id="LC3" class="line" lang="mermaid">A--&gt;C</span>
<span id="LC4" class="line" lang="mermaid">B--&gt;D</span>
<span id="LC5" class="line" lang="mermaid">C--&gt;D</span>
</code>
</pre>
<copy-code>
<button type="button" class="btn btn-default btn-md gl-button btn-icon has-tooltip" data-title="Copy to clipboard" data-clipboard-target="pre#code-4">
<svg><use xlink:href="/assets/icons-7f1680a3670112fe4c8ef57b9dfb93f0f61b43a2a479d7abd6c83bcb724b9201.svg#copy-to-clipboard"></use></svg>
</button>
</copy-code>
</div>`);
const els = $('pre.js-syntax-highlight').find('.js-render-mermaid');
renderMermaid(els);
jest.runAllTimers();
expect(document.querySelector('pre.js-syntax-highlight').classList).toContain('gl-sr-only');
afterEach(() => {
resetHTMLFixture();
});
it('renders a mermaid diagram', () => {
setHTMLFixture('<pre><code class="js-render-mermaid"></code></pre>');
expect(findMermaidIframes()).toHaveLength(0);
renderDiagrams();
expect(document.querySelector('pre').classList).toContain('gl-sr-only');
expect(findMermaidIframes()).toHaveLength(1);
});
describe('within a details element', () => {
beforeEach(() => {
setHTMLFixture('<details><pre><code class="js-render-mermaid"></code></pre></details>');
renderDiagrams();
});
it('does not render the diagram on load', () => {
expect(findMermaidIframes()).toHaveLength(0);
});
it('render the diagram when the details element is opened', () => {
document.querySelector('details').setAttribute('open', true);
document.querySelector('details').dispatchEvent(new Event('toggle'));
jest.runAllTimers();
expect(findMermaidIframes()).toHaveLength(1);
});
});
describe('dangerous diagrams', () => {
describe(`when the diagram's source exceeds ${MAX_CHAR_LIMIT} characters`, () => {
beforeEach(() => {
setHTMLFixture(
`<pre>
<code class="js-render-mermaid">${Array(MAX_CHAR_LIMIT + 1)
.fill('a')
.join('')}</code>
</pre>`,
);
renderDiagrams();
});
it('does not render the diagram on load', () => {
expect(findMermaidIframes()).toHaveLength(0);
});
it('shows a warning about performance impact when rendering the diagram', () => {
expect(document.querySelector('pre').classList).toContain(LAZY_ALERT_SHOWN_CLASS);
expect(findDangerousMermaidAlert().exists()).toBe(true);
expect(findDangerousMermaidAlert().text()).toContain(
__('Warning: Displaying this diagram might cause performance issues on this page.'),
);
});
it("renders the diagram when clicking on the alert's button", () => {
findDangerousMermaidAlert().find('button').trigger('click');
jest.runAllTimers();
expect(findMermaidIframes()).toHaveLength(1);
});
});
it(`stops rendering diagrams once the total rendered source exceeds ${MAX_CHAR_LIMIT} characters`, () => {
setHTMLFixture(
`<pre>
<code class="js-render-mermaid">${Array(MAX_CHAR_LIMIT - 1)
.fill('a')
.join('')}</code>
<code class="js-render-mermaid">2</code>
<code class="js-render-mermaid">3</code>
<code class="js-render-mermaid">4</code>
</pre>`,
);
renderDiagrams();
expect(findMermaidIframes()).toHaveLength(3);
});
// Note: The test case below is provided for convenience but should remain skipped as the DOM
// operations it requires are too expensive and would significantly slow down the test suite.
// eslint-disable-next-line jest/no-disabled-tests
it.skip(`stops rendering diagrams when the rendered diagrams count exceeds ${MAX_MERMAID_BLOCK_LIMIT}`, () => {
setHTMLFixture(
`<pre>
${Array(MAX_MERMAID_BLOCK_LIMIT + 1)
.fill('<code class="js-render-mermaid"></code>')
.join('')}
</pre>`,
);
renderDiagrams();
expect([...document.querySelectorAll('.js-render-mermaid')]).toHaveLength(
MAX_MERMAID_BLOCK_LIMIT + 1,
);
expect(findMermaidIframes()).toHaveLength(MAX_MERMAID_BLOCK_LIMIT);
});
});
});

View File

@ -201,18 +201,20 @@ describe('RelatedIssuesRoot', () => {
]);
});
it('displays a message from the backend upon error', async () => {
it('passes an error message from the backend upon error', async () => {
const input = '#123';
const message = 'error';
mock.onPost(defaultProps.endpoint).reply(409, { message });
wrapper.vm.store.setPendingReferences([issuable1.reference, issuable2.reference]);
expect(createAlert).not.toHaveBeenCalled();
expect(findRelatedIssuesBlock().props('hasError')).toBe(false);
expect(findRelatedIssuesBlock().props('itemAddFailureMessage')).toBe(null);
findRelatedIssuesBlock().vm.$emit('addIssuableFormSubmit', input);
await waitForPromises();
expect(createAlert).toHaveBeenCalledWith({ message });
expect(findRelatedIssuesBlock().props('hasError')).toBe(true);
expect(findRelatedIssuesBlock().props('itemAddFailureMessage')).toBe(message);
});
});

View File

@ -60,8 +60,10 @@ describe('FormUrlApp', () => {
expect(findAllUrlMaskItems()).toHaveLength(1);
const firstItem = findAllUrlMaskItems().at(0);
expect(firstItem.props('itemKey')).toBeNull();
expect(firstItem.props('itemValue')).toBeNull();
expect(firstItem.props()).toMatchObject({
itemKey: null,
itemValue: null,
});
});
});
@ -90,12 +92,18 @@ describe('FormUrlApp', () => {
expect(findAllUrlMaskItems()).toHaveLength(2);
const firstItem = findAllUrlMaskItems().at(0);
expect(firstItem.props('itemKey')).toBe(mockItem1.key);
expect(firstItem.props('itemValue')).toBe(mockItem1.value);
expect(firstItem.props()).toMatchObject({
itemKey: mockItem1.key,
itemValue: mockItem1.value,
isEditing: true,
});
const secondItem = findAllUrlMaskItems().at(1);
expect(secondItem.props('itemKey')).toBe(mockItem2.key);
expect(secondItem.props('itemValue')).toBe(mockItem2.value);
expect(secondItem.props()).toMatchObject({
itemKey: mockItem2.key,
itemValue: mockItem2.value,
isEditing: true,
});
});
describe('on mask item input', () => {
@ -106,8 +114,10 @@ describe('FormUrlApp', () => {
firstItem.vm.$emit('input', mockInput);
await nextTick();
expect(firstItem.props('itemKey')).toBe(mockInput.key);
expect(firstItem.props('itemValue')).toBe(mockInput.value);
expect(firstItem.props()).toMatchObject({
itemKey: mockInput.key,
itemValue: mockInput.value,
});
});
});
@ -119,8 +129,10 @@ describe('FormUrlApp', () => {
expect(findAllUrlMaskItems()).toHaveLength(3);
const lastItem = findAllUrlMaskItems().at(-1);
expect(lastItem.props('itemKey')).toBeNull();
expect(lastItem.props('itemValue')).toBeNull();
expect(lastItem.props()).toMatchObject({
itemKey: null,
itemValue: null,
});
});
});
@ -133,8 +145,10 @@ describe('FormUrlApp', () => {
expect(findAllUrlMaskItems()).toHaveLength(1);
const newFirstItem = findAllUrlMaskItems().at(0);
expect(newFirstItem.props('itemKey')).toBe(mockItem2.key);
expect(newFirstItem.props('itemValue')).toBe(mockItem2.value);
expect(newFirstItem.props()).toMatchObject({
itemKey: mockItem2.key,
itemValue: mockItem2.value,
});
});
});
});

View File

@ -31,19 +31,42 @@ describe('FormUrlMaskItem', () => {
describe('template', () => {
it('renders input for key and value', () => {
createComponent();
createComponent({ props: { itemKey: mockKey, itemValue: mockValue } });
const keyInput = findMaskItemKey();
expect(keyInput.attributes('label')).toBe(FormUrlMaskItem.i18n.keyLabel);
expect(keyInput.findComponent(GlFormInput).attributes('name')).toBe(
'hook[url_variables][][key]',
);
expect(keyInput.findComponent(GlFormInput).attributes()).toMatchObject({
name: 'hook[url_variables][][key]',
value: mockKey,
});
const valueInput = findMaskItemValue();
expect(valueInput.attributes('label')).toBe(FormUrlMaskItem.i18n.valueLabel);
expect(valueInput.findComponent(GlFormInput).attributes('name')).toBe(
'hook[url_variables][][value]',
);
expect(valueInput.findComponent(GlFormInput).attributes()).toMatchObject({
name: 'hook[url_variables][][value]',
value: mockValue,
});
});
describe('when isEditing is true', () => {
beforeEach(() => {
createComponent({ props: { isEditing: true } });
});
it('renders disabled key and value', () => {
expect(findMaskItemKey().findComponent(GlFormInput).attributes('disabled')).toBe('true');
expect(findMaskItemValue().findComponent(GlFormInput).attributes('disabled')).toBe('true');
});
it('renders disabled remove button', () => {
expect(findRemoveButton().attributes('disabled')).toBe('true');
});
it('displays ************ as input value', () => {
expect(findMaskItemValue().findComponent(GlFormInput).attributes('value')).toBe(
'************',
);
});
});
describe('on key input', () => {

View File

@ -3,16 +3,33 @@
require 'spec_helper'
RSpec.describe HooksHelper do
let(:project) { create(:project) }
let(:project_hook) { create(:project_hook, project: project) }
let(:service_hook) { create(:service_hook, integration: create(:drone_ci_integration)) }
let(:system_hook) { create(:system_hook) }
let(:project) { build_stubbed(:project) }
let(:project_hook) { build_stubbed(:project_hook, project: project) }
let(:service_hook) { build_stubbed(:service_hook, integration: build_stubbed(:drone_ci_integration)) }
let(:system_hook) { build_stubbed(:system_hook) }
describe '#webhook_form_data' do
subject { helper.webhook_form_data(project_hook) }
it { expect(subject[:url]).to eq(project_hook.url) }
it { expect(subject[:url_variables]).to be_nil }
context 'when there are no URL variables' do
it 'returns proper data' do
expect(subject).to match(
url: project_hook.url,
url_variables: Gitlab::Json.dump([])
)
end
end
context 'when there are URL variables' do
let(:project_hook) { build_stubbed(:project_hook, :url_variables, project: project) }
it 'returns proper data' do
expect(subject).to match(
url: project_hook.url,
url_variables: Gitlab::Json.dump(['abc'])
)
end
end
end
describe '#link_to_test_hook' do
@ -31,7 +48,7 @@ RSpec.describe HooksHelper do
describe '#hook_log_path' do
context 'with a project hook' do
let(:web_hook_log) { create(:web_hook_log, web_hook: project_hook) }
let(:web_hook_log) { build_stubbed(:web_hook_log, web_hook: project_hook) }
it 'returns project-namespaced link' do
expect(helper.hook_log_path(project_hook, web_hook_log))
@ -40,7 +57,7 @@ RSpec.describe HooksHelper do
end
context 'with a service hook' do
let(:web_hook_log) { create(:web_hook_log, web_hook: service_hook) }
let(:web_hook_log) { build_stubbed(:web_hook_log, web_hook: service_hook) }
it 'returns project-namespaced link' do
expect(helper.hook_log_path(project_hook, web_hook_log))
@ -49,7 +66,7 @@ RSpec.describe HooksHelper do
end
context 'with a system hook' do
let(:web_hook_log) { create(:web_hook_log, web_hook: system_hook) }
let(:web_hook_log) { build_stubbed(:web_hook_log, web_hook: system_hook) }
it 'returns admin-namespaced link' do
expect(helper.hook_log_path(system_hook, web_hook_log))

View File

@ -0,0 +1,222 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration::BackfillUserDetailsFields, :migration, schema: 20221018232820 do
let(:users) { table(:users) }
let(:user_details) { table(:user_details) }
let!(:user_all_fields_backfill) do
users.create!(
name: generate(:name),
email: generate(:email),
projects_limit: 1,
linkedin: 'linked-in',
twitter: '@twitter',
skype: 'skype',
website_url: 'https://example.com',
location: 'Antarctica',
organization: 'Gitlab'
)
end
let!(:user_long_details_fields) do
length = UserDetail::DEFAULT_FIELD_LENGTH + 1
users.create!(
name: generate(:name),
email: generate(:email),
projects_limit: 1,
linkedin: 'l' * length,
twitter: 't' * length,
skype: 's' * length,
website_url: "https://#{'a' * (length - 12)}.com",
location: 'l' * length,
organization: 'o' * length
)
end
let!(:user_nil_details_fields) do
users.create!(
name: generate(:name),
email: generate(:email),
projects_limit: 1
)
end
let!(:user_empty_details_fields) do
users.create!(
name: generate(:name),
email: generate(:email),
projects_limit: 1,
linkedin: '',
twitter: '',
skype: '',
website_url: '',
location: '',
organization: ''
)
end
let!(:user_with_bio) do
users.create!(
name: generate(:name),
email: generate(:email),
projects_limit: 1,
linkedin: 'linked-in',
twitter: '@twitter',
skype: 'skype',
website_url: 'https://example.com',
location: 'Antarctica',
organization: 'Gitlab'
)
end
let!(:bio_user_details) do
user_details
.find_or_create_by!(user_id: user_with_bio.id)
.update!(bio: 'bio')
end
let!(:user_with_details) do
users.create!(
name: generate(:name),
email: generate(:email),
projects_limit: 1,
linkedin: 'linked-in',
twitter: '@twitter',
skype: 'skype',
website_url: 'https://example.com',
location: 'Antarctica',
organization: 'Gitlab'
)
end
let!(:existing_user_details) do
user_details
.find_or_create_by!(user_id: user_with_details.id)
.update!(
linkedin: 'linked-in',
twitter: '@twitter',
skype: 'skype',
website_url: 'https://example.com',
location: 'Antarctica',
organization: 'Gitlab'
)
end
let!(:user_different_details) do
users.create!(
name: generate(:name),
email: generate(:email),
projects_limit: 1,
linkedin: 'linked-in',
twitter: '@twitter',
skype: 'skype',
website_url: 'https://example.com',
location: 'Antarctica',
organization: 'Gitlab'
)
end
let!(:differing_details) do
user_details
.find_or_create_by!(user_id: user_different_details.id)
.update!(
linkedin: 'details-in',
twitter: '@details',
skype: 'details_skype',
website_url: 'https://details.site',
location: 'Details Location',
organization: 'Details Organization'
)
end
let(:user_ids) do
[
user_all_fields_backfill,
user_long_details_fields,
user_nil_details_fields,
user_empty_details_fields,
user_with_bio,
user_with_details,
user_different_details
].map(&:id)
end
subject do
described_class.new(
start_id: user_ids.min,
end_id: user_ids.max,
batch_table: 'users',
batch_column: 'id',
sub_batch_size: 1_000,
pause_ms: 0,
connection: ApplicationRecord.connection
)
end
it 'processes all relevant records' do
expect { subject.perform }.to change { user_details.all.size }.to(5)
end
it 'backfills new user_details fields' do
subject.perform
user_detail = user_details.find_by!(user_id: user_all_fields_backfill.id)
expect(user_detail.linkedin).to eq('linked-in')
expect(user_detail.twitter).to eq('@twitter')
expect(user_detail.skype).to eq('skype')
expect(user_detail.website_url).to eq('https://example.com')
expect(user_detail.location).to eq('Antarctica')
expect(user_detail.organization).to eq('Gitlab')
end
it 'does not migrate nil fields' do
subject.perform
expect(user_details.find_by(user_id: user_nil_details_fields)).to be_nil
end
it 'does not migrate empty fields' do
subject.perform
expect(user_details.find_by(user_id: user_empty_details_fields)).to be_nil
end
it 'backfills new fields without overwriting existing `bio` field' do
subject.perform
user_detail = user_details.find_by!(user_id: user_with_bio.id)
expect(user_detail.bio).to eq('bio')
expect(user_detail.linkedin).to eq('linked-in')
expect(user_detail.twitter).to eq('@twitter')
expect(user_detail.skype).to eq('skype')
expect(user_detail.website_url).to eq('https://example.com')
expect(user_detail.location).to eq('Antarctica')
expect(user_detail.organization).to eq('Gitlab')
end
context 'when user details are unchanged' do
it 'does not change existing details' do
expect { subject.perform }.not_to change {
user_details.find_by!(user_id: user_with_details.id).attributes
}
end
end
context 'when user details are changed' do
it 'updates existing user details' do
expect { subject.perform }.to change {
user_details.find_by!(user_id: user_different_details.id).attributes
}
user_detail = user_details.find_by!(user_id: user_different_details.id)
expect(user_detail.linkedin).to eq('linked-in')
expect(user_detail.twitter).to eq('@twitter')
expect(user_detail.skype).to eq('skype')
expect(user_detail.website_url).to eq('https://example.com')
expect(user_detail.location).to eq('Antarctica')
expect(user_detail.organization).to eq('Gitlab')
end
end
end

View File

@ -0,0 +1,24 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!
RSpec.describe QueueBackfillUserDetailsFields do
let_it_be(:batched_migration) { described_class::MIGRATION }
it 'schedules a new batched migration' do
reversible_migration do |migration|
migration.before -> {
expect(batched_migration).not_to have_scheduled_batched_migration
}
migration.after -> {
expect(batched_migration).to have_scheduled_batched_migration(
table_name: :users,
column_name: :id,
interval: described_class::INTERVAL
)
}
end
end
end

View File

@ -1,80 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Clusters::Applications::PatchService do
describe '#execute' do
let(:application) { create(:clusters_applications_knative, :scheduled) }
let!(:update_command) { application.update_command }
let(:service) { described_class.new(application) }
let(:helm_client) { instance_double(Gitlab::Kubernetes::Helm::API) }
before do
allow(service).to receive(:update_command).and_return(update_command)
allow(service).to receive(:helm_api).and_return(helm_client)
end
context 'when there are no errors' do
before do
expect(helm_client).to receive(:update).with(update_command)
allow(ClusterWaitForAppInstallationWorker).to receive(:perform_in).and_return(nil)
end
it 'make the application updating' do
expect(application.cluster).not_to be_nil
service.execute
expect(application).to be_updating
end
it 'schedule async installation status check' do
expect(ClusterWaitForAppInstallationWorker).to receive(:perform_in).once
service.execute
end
end
context 'when kubernetes cluster communication fails' do
let(:error) { Kubeclient::HttpError.new(500, 'system failure', nil) }
before do
expect(helm_client).to receive(:update).with(update_command).and_raise(error)
end
include_examples 'logs kubernetes errors' do
let(:error_name) { 'Kubeclient::HttpError' }
let(:error_message) { 'system failure' }
let(:error_code) { 500 }
end
it 'make the application errored' do
service.execute
expect(application).to be_update_errored
expect(application.status_reason).to eq(_('Kubernetes error: %{error_code}') % { error_code: 500 })
end
end
context 'a non kubernetes error happens' do
let(:application) { create(:clusters_applications_knative, :scheduled) }
let(:error) { StandardError.new('something bad happened') }
include_examples 'logs kubernetes errors' do
let(:error_name) { 'StandardError' }
let(:error_message) { 'something bad happened' }
let(:error_code) { nil }
end
before do
expect(helm_client).to receive(:update).with(update_command).and_raise(error)
end
it 'make the application errored' do
service.execute
expect(application).to be_update_errored
expect(application.status_reason).to eq(_('Failed to update.'))
end
end
end
end

View File

@ -1,91 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Clusters::Applications::UpdateService do
include TestRequestHelpers
let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
let(:user) { create(:user) }
let(:params) { { application: 'knative', hostname: 'update.example.com', pages_domain_id: domain.id } }
let(:service) { described_class.new(cluster, user, params) }
let(:domain) { create(:pages_domain, :instance_serverless) }
subject { service.execute(test_request) }
describe '#execute' do
before do
allow(ClusterPatchAppWorker).to receive(:perform_async)
end
context 'application is not installed' do
it 'raises Clusters::Applications::BaseService::InvalidApplicationError' do
expect(ClusterPatchAppWorker).not_to receive(:perform_async)
expect { subject }
.to raise_exception { Clusters::Applications::BaseService::InvalidApplicationError }
.and not_change { Clusters::Applications::Knative.count }
.and not_change { Clusters::Applications::Knative.with_status(:scheduled).count }
end
end
context 'application is installed' do
context 'application is schedulable' do
let!(:application) do
create(:clusters_applications_knative, status: 3, cluster: cluster)
end
it 'updates the application data' do
expect do
subject
end.to change { application.reload.hostname }.to(params[:hostname])
end
it 'makes application scheduled!' do
subject
expect(application.reload).to be_scheduled
end
it 'schedules ClusterPatchAppWorker' do
expect(ClusterPatchAppWorker).to receive(:perform_async)
subject
end
context 'knative application' do
let(:associate_domain_service) { double('AssociateDomainService') }
it 'executes AssociateDomainService' do
expect(Serverless::AssociateDomainService).to receive(:new) do |knative, args|
expect(knative.id).to eq(application.id)
expect(args[:pages_domain_id]).to eq(params[:pages_domain_id])
expect(args[:creator]).to eq(user)
associate_domain_service
end
expect(associate_domain_service).to receive(:execute)
subject
end
end
end
context 'application is not schedulable' do
let!(:application) do
create(:clusters_applications_knative, status: 4, cluster: cluster)
end
it 'raises StateMachines::InvalidTransition' do
expect(ClusterPatchAppWorker).not_to receive(:perform_async)
expect { subject }
.to raise_exception { StateMachines::InvalidTransition }
.and not_change { application.reload.hostname }
.and not_change { Clusters::Applications::Knative.with_status(:scheduled).count }
end
end
end
end
end

View File

@ -67,8 +67,8 @@ RSpec.shared_context 'project navbar structure' do
{
nav_item: _('Deployments'),
nav_sub_items: [
_('Feature Flags'),
_('Environments'),
_('Feature Flags'),
_('Releases')
]
},

View File

@ -38,7 +38,7 @@ RSpec.shared_examples Integrations::HasWebHook do
end
describe '#url_variables' do
it 'returns a string' do
it 'returns a hash' do
expect(integration.url_variables).to be_a(Hash)
end
end

View File

@ -1113,10 +1113,10 @@
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-3.5.0.tgz#226240b7aa93db986f4c6f7738ca2a1846b5234d"
integrity sha512-/djPsJzUY7i/FaydRVt3ZyXiFf5HGNo1rg2mfLn1EpXvT4zc2ag5ECwnYcPb97KgqFCJX6Tk+Ndu8Wh3GoOW1g==
"@gitlab/ui@49.2.0":
version "49.2.0"
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-49.2.0.tgz#45eedbe943bccbb6d986d66bf7c6294c82e89366"
integrity sha512-S7jfYtmh2Z36bum48aqb+NFLl/WAqow5gOXfWjdl1lGXjpKZ27neJPTWfpYi2PRyhmPs8ptVg7zKaxXJMZ7cgA==
"@gitlab/ui@49.2.1":
version "49.2.1"
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-49.2.1.tgz#362dda68799d6ecfd32c8e0a4eb1409f20ddec4d"
integrity sha512-dutmZTGQDDn7nPzGFtI6YEnqF7yhnD6tY6ymGQ1U0bkdDcjR8GOMvDn3Gc09505go6ESt0A4dXwleboDgoFP0w==
dependencies:
"@popperjs/core" "^2.11.2"
bootstrap-vue "2.20.1"