Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
8007620dc7
commit
86fa823611
31 changed files with 325 additions and 48 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -96,4 +96,4 @@ apollo.config.js
|
|||
/tmp/matching_foss_tests.txt
|
||||
/tmp/matching_tests.txt
|
||||
ee/changelogs/unreleased-ee
|
||||
|
||||
/sitespeed-result
|
||||
|
|
|
@ -572,7 +572,7 @@ export class AwardsHandler {
|
|||
}
|
||||
|
||||
findMatchingEmojiElements(query) {
|
||||
const emojiMatches = this.emoji.filterEmojiNamesByAlias(query);
|
||||
const emojiMatches = this.emoji.queryEmojiNames(query);
|
||||
const $emojiElements = $('.emoji-menu-list:not(.frequent-emojis) [data-name]');
|
||||
const $matchingElements = $emojiElements.filter(
|
||||
(i, elm) => emojiMatches.indexOf(elm.dataset.name) >= 0,
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
<script>
|
||||
/* eslint-disable vue/no-v-html */
|
||||
import { GlIcon } from '@gitlab/ui';
|
||||
import { GlIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
|
||||
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
|
||||
import iconBranch from '../svg/icon_branch.svg';
|
||||
import limitWarning from './limit_warning_component.vue';
|
||||
|
@ -13,6 +12,9 @@ export default {
|
|||
limitWarning,
|
||||
GlIcon,
|
||||
},
|
||||
directives: {
|
||||
SafeHtml,
|
||||
},
|
||||
props: {
|
||||
items: {
|
||||
type: Array,
|
||||
|
@ -47,7 +49,7 @@ export default {
|
|||
<a :href="build.url" class="pipeline-id"> #{{ build.id }} </a>
|
||||
<gl-icon :size="16" name="fork" />
|
||||
<a :href="build.branch.url" class="ref-name"> {{ build.branch.name }} </a>
|
||||
<span class="icon-branch" v-html="iconBranch"> </span>
|
||||
<span v-safe-html="iconBranch" class="icon-branch"> </span>
|
||||
<a :href="build.commitUrl" class="commit-sha"> {{ build.shortSha }} </a>
|
||||
</h5>
|
||||
<span>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { uniq } from 'lodash';
|
||||
import fuzzaldrinPlus from 'fuzzaldrin-plus';
|
||||
import emojiAliases from 'emojis/aliases.json';
|
||||
import axios from '../lib/utils/axios_utils';
|
||||
|
||||
|
@ -62,13 +63,18 @@ export function isEmojiNameValid(name) {
|
|||
return validEmojiNames.indexOf(name) >= 0;
|
||||
}
|
||||
|
||||
export function filterEmojiNames(filter) {
|
||||
const match = filter.toLowerCase();
|
||||
return validEmojiNames.filter(name => name.indexOf(match) >= 0);
|
||||
}
|
||||
|
||||
export function filterEmojiNamesByAlias(filter) {
|
||||
return uniq(filterEmojiNames(filter).map(name => normalizeEmojiName(name)));
|
||||
/**
|
||||
* Search emoji by name or alias. Returns a normalized, deduplicated list of
|
||||
* names.
|
||||
*
|
||||
* Calling with an empty filter returns an empty array.
|
||||
*
|
||||
* @param {String}
|
||||
* @returns {Array}
|
||||
*/
|
||||
export function queryEmojiNames(filter) {
|
||||
const matches = fuzzaldrinPlus.filter(validEmojiNames, filter);
|
||||
return uniq(matches.map(name => normalizeEmojiName(name)));
|
||||
}
|
||||
|
||||
let emojiCategoryMap;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { take } from 'lodash';
|
||||
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
|
||||
import { sanitize } from 'dompurify';
|
||||
import { sanitize } from '~/lib/dompurify';
|
||||
import { FREQUENT_ITEMS, HOUR_IN_MS } from './constants';
|
||||
|
||||
export const isMobile = () => ['md', 'sm', 'xs'].includes(bp.getBreakpointSize());
|
||||
|
|
|
@ -1,10 +1,14 @@
|
|||
<script>
|
||||
import { GlLink } from '@gitlab/ui';
|
||||
import { GlLink, GlTooltipDirective, GlSprintf } from '@gitlab/ui';
|
||||
import { formatDate } from '~/lib/utils/datetime_utility';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlLink,
|
||||
GlSprintf,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
props: {
|
||||
alert: {
|
||||
|
@ -24,17 +28,23 @@ export default {
|
|||
<div
|
||||
class="gl-border-solid gl-border-1 gl-border-gray-100 gl-p-5 gl-mb-3 gl-rounded-base gl-display-flex gl-justify-content-space-between"
|
||||
>
|
||||
<div class="text-truncate gl-pr-3">
|
||||
<div class="gl-pr-3">
|
||||
<span class="gl-font-weight-bold">{{ s__('HighlightBar|Original alert:') }}</span>
|
||||
<gl-link :href="alert.detailsUrl">{{ alert.title }}</gl-link>
|
||||
<gl-link v-gl-tooltip :title="alert.title" :href="alert.detailsUrl">
|
||||
<gl-sprintf :message="__('Alert #%{alertId}')">
|
||||
<template #alertId>
|
||||
<span>{{ alert.iid }}</span>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</gl-link>
|
||||
</div>
|
||||
|
||||
<div class="gl-pr-3 gl-white-space-nowrap">
|
||||
<div class="gl-pr-3">
|
||||
<span class="gl-font-weight-bold">{{ s__('HighlightBar|Alert start time:') }}</span>
|
||||
{{ startTime }}
|
||||
</div>
|
||||
|
||||
<div class="gl-white-space-nowrap">
|
||||
<div>
|
||||
<span class="gl-font-weight-bold">{{ s__('HighlightBar|Alert events:') }}</span>
|
||||
<span>{{ alert.eventCount }}</span>
|
||||
</div>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { sanitize } from 'dompurify';
|
||||
import { sanitize } from '~/lib/dompurify';
|
||||
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
|
||||
import updateDescription from '../utils/update_description';
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { sanitize } from 'dompurify';
|
||||
import { sanitize } from '~/lib/dompurify';
|
||||
|
||||
// We currently load + parse the data from the issue app and related merge request
|
||||
let cachedParsedData;
|
||||
|
|
53
app/assets/javascripts/lib/dompurify.js
Normal file
53
app/assets/javascripts/lib/dompurify.js
Normal file
|
@ -0,0 +1,53 @@
|
|||
import { sanitize as dompurifySanitize, addHook } from 'dompurify';
|
||||
import { getBaseURL, relativePathToAbsolute } from '~/lib/utils/url_utility';
|
||||
|
||||
// Safely allow SVG <use> tags
|
||||
|
||||
const defaultConfig = {
|
||||
ADD_TAGS: ['use'],
|
||||
};
|
||||
|
||||
// Only icons urls from `gon` are allowed
|
||||
const getAllowedIconUrls = (gon = window.gon) =>
|
||||
[gon.sprite_file_icons, gon.sprite_icons].filter(Boolean);
|
||||
|
||||
const isUrlAllowed = url => getAllowedIconUrls().some(allowedUrl => url.startsWith(allowedUrl));
|
||||
|
||||
const isHrefSafe = url =>
|
||||
isUrlAllowed(url) || isUrlAllowed(relativePathToAbsolute(url, getBaseURL()));
|
||||
|
||||
const removeUnsafeHref = (node, attr) => {
|
||||
if (!node.hasAttribute(attr)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isHrefSafe(node.getAttribute(attr))) {
|
||||
node.removeAttribute(attr);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Sanitize icons' <use> tag attributes, to safely include
|
||||
* svgs such as in:
|
||||
*
|
||||
* <svg viewBox="0 0 100 100">
|
||||
* <use href="/assets/icons-xxx.svg#icon_name"></use>
|
||||
* </svg>
|
||||
*
|
||||
* @param {Object} node - Node to sanitize
|
||||
*/
|
||||
const sanitizeSvgIcon = node => {
|
||||
removeUnsafeHref(node, 'href');
|
||||
|
||||
// Note: `xlink:href` is deprecated, but still in use
|
||||
// https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/xlink:href
|
||||
removeUnsafeHref(node, 'xlink:href');
|
||||
};
|
||||
|
||||
addHook('afterSanitizeAttributes', node => {
|
||||
if (node.tagName.toLowerCase() === 'use') {
|
||||
sanitizeSvgIcon(node);
|
||||
}
|
||||
});
|
||||
|
||||
export const sanitize = (val, config = defaultConfig) => dompurifySanitize(val, config);
|
|
@ -1,5 +1,5 @@
|
|||
import fuzzaldrinPlus from 'fuzzaldrin-plus';
|
||||
import { sanitize } from 'dompurify';
|
||||
import { sanitize } from '~/lib/dompurify';
|
||||
|
||||
/**
|
||||
* Wraps substring matches with HTML `<span>` elements.
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<script>
|
||||
/* eslint-disable vue/no-v-html */
|
||||
import marked from 'marked';
|
||||
import { sanitize } from 'dompurify';
|
||||
import katex from 'katex';
|
||||
import { sanitize } from '~/lib/dompurify';
|
||||
import Prompt from './prompt.vue';
|
||||
|
||||
const renderer = new marked.Renderer();
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
/* eslint-disable vue/no-v-html */
|
||||
import { sanitize } from 'dompurify';
|
||||
import { sanitize } from '~/lib/dompurify';
|
||||
import Prompt from '../prompt.vue';
|
||||
|
||||
export default {
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import $ from 'jquery';
|
||||
import fuzzaldrinPlus from 'fuzzaldrin-plus';
|
||||
import { sanitize } from 'dompurify';
|
||||
import { sanitize } from '~/lib/dompurify';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import { joinPaths, escapeFileUrl } from '~/lib/utils/url_utility';
|
||||
import { deprecatedCreateFlash as flash } from '~/flash';
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { GlTooltipDirective, GlLink, GlBadge, GlButton, GlIcon } from '@gitlab/ui';
|
||||
import { GlTooltipDirective, GlLink, GlBadge, GlButton } from '@gitlab/ui';
|
||||
import { BACK_URL_PARAM } from '~/releases/constants';
|
||||
import { setUrlParams } from '~/lib/utils/url_utility';
|
||||
|
||||
|
@ -8,7 +8,6 @@ export default {
|
|||
components: {
|
||||
GlLink,
|
||||
GlBadge,
|
||||
GlIcon,
|
||||
GlButton,
|
||||
},
|
||||
directives: {
|
||||
|
@ -55,11 +54,10 @@ export default {
|
|||
v-gl-tooltip
|
||||
category="primary"
|
||||
variant="default"
|
||||
icon="pencil"
|
||||
class="gl-mr-3 js-edit-button ml-2 pb-2"
|
||||
:title="__('Edit this release')"
|
||||
:href="editLink"
|
||||
>
|
||||
<gl-icon name="pencil" />
|
||||
</gl-button>
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import Vue from 'vue';
|
||||
|
||||
import { sanitize } from 'dompurify';
|
||||
import { sanitize } from '~/lib/dompurify';
|
||||
|
||||
import UsersCache from './lib/utils/users_cache';
|
||||
import UserPopover from './vue_shared/components/user_popover/user_popover.vue';
|
||||
|
|
|
@ -211,6 +211,14 @@
|
|||
:weight: 1
|
||||
:idempotent:
|
||||
:tags: []
|
||||
- :name: cronjob:member_invitation_reminder_emails
|
||||
:feature_category: :subgroups
|
||||
:has_external_dependencies:
|
||||
:urgency: :low
|
||||
:resource_boundary: :unknown
|
||||
:weight: 1
|
||||
:idempotent:
|
||||
:tags: []
|
||||
- :name: cronjob:metrics_dashboard_schedule_annotations_prune
|
||||
:feature_category: :metrics
|
||||
:has_external_dependencies:
|
||||
|
|
15
app/workers/member_invitation_reminder_emails_worker.rb
Normal file
15
app/workers/member_invitation_reminder_emails_worker.rb
Normal file
|
@ -0,0 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class MemberInvitationReminderEmailsWorker # rubocop:disable Scalability/IdempotentWorker
|
||||
include ApplicationWorker
|
||||
include CronjobQueue # rubocop:disable Scalability/CronWorkerContext
|
||||
|
||||
feature_category :subgroups
|
||||
urgency :low
|
||||
|
||||
def perform
|
||||
return unless Gitlab::Experimentation.enabled?(:invitation_reminders)
|
||||
|
||||
# To keep this MR small, implementation will be done in another MR: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/42981/diffs?commit_id=8063606e0f83957b2dd38d660ee986f24dee6138
|
||||
end
|
||||
end
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Surface Alert number GFM reference in highlight bar
|
||||
merge_request: 42832
|
||||
author:
|
||||
type: changed
|
5
changelogs/unreleased/issuable-award-fuzzy-search.yml
Normal file
5
changelogs/unreleased/issuable-award-fuzzy-search.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Use fuzzy matching for issuable awards
|
||||
merge_request: 42674
|
||||
author: Ethan Reesor (@firelizzard)
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix size of edit button on releases page
|
||||
merge_request: 42779
|
||||
author:
|
||||
type: fixed
|
|
@ -517,6 +517,9 @@ Settings.cron_jobs['ci_platform_metrics_update_cron_worker']['job_class'] = 'CiP
|
|||
Settings.cron_jobs['analytics_instance_statistics_count_job_trigger_worker'] ||= Settingslogic.new({})
|
||||
Settings.cron_jobs['analytics_instance_statistics_count_job_trigger_worker']['cron'] ||= '50 23 */1 * *'
|
||||
Settings.cron_jobs['analytics_instance_statistics_count_job_trigger_worker']['job_class'] ||= 'Analytics::InstanceStatistics::CountJobTriggerWorker'
|
||||
Settings.cron_jobs['member_invitation_reminder_emails_worker'] ||= Settingslogic.new({})
|
||||
Settings.cron_jobs['member_invitation_reminder_emails_worker']['cron'] ||= '0 0 * * *'
|
||||
Settings.cron_jobs['member_invitation_reminder_emails_worker']['job_class'] = 'MemberInvitationReminderEmailsWorker'
|
||||
|
||||
Gitlab.ee do
|
||||
Settings.cron_jobs['adjourned_group_deletion_worker'] ||= Settingslogic.new({})
|
||||
|
|
|
@ -432,7 +432,7 @@ To avoid this error, use the applicable HTML entity code (`<` or `>`) inst
|
|||
- In JavaScript:
|
||||
|
||||
```javascript
|
||||
import { sanitize } from 'dompurify';
|
||||
import { sanitize } from '~/lib/dompurify';
|
||||
|
||||
const i18n = { LESS_THAN_ONE_HOUR: sanitize(__('In < 1 hour'), { ALLOWED_TAGS: [] }) };
|
||||
|
||||
|
|
|
@ -176,7 +176,7 @@ module Gitlab
|
|||
name: name.presence || concurrent_foreign_key_name(source, column)
|
||||
}
|
||||
|
||||
if foreign_key_exists?(source, target, options)
|
||||
if foreign_key_exists?(source, target, **options)
|
||||
warning_message = "Foreign key not created because it exists already " \
|
||||
"(this may be due to an aborted migration or similar): " \
|
||||
"source: #{source}, target: #{target}, column: #{options[:column]}, "\
|
||||
|
@ -330,13 +330,13 @@ module Gitlab
|
|||
# * +timing_configuration+ - [[ActiveSupport::Duration, ActiveSupport::Duration], ...] lock timeout for the block, sleep time before the next iteration, defaults to `Gitlab::Database::WithLockRetries::DEFAULT_TIMING_CONFIGURATION`
|
||||
# * +logger+ - [Gitlab::JsonLogger]
|
||||
# * +env+ - [Hash] custom environment hash, see the example with `DISABLE_LOCK_RETRIES`
|
||||
def with_lock_retries(**args, &block)
|
||||
def with_lock_retries(*args, **kwargs, &block)
|
||||
merged_args = {
|
||||
klass: self.class,
|
||||
logger: Gitlab::BackgroundMigration::Logger
|
||||
}.merge(args)
|
||||
}.merge(kwargs)
|
||||
|
||||
Gitlab::Database::WithLockRetries.new(merged_args).run(&block)
|
||||
Gitlab::Database::WithLockRetries.new(**merged_args).run(&block)
|
||||
end
|
||||
|
||||
def true_value
|
||||
|
|
|
@ -2182,6 +2182,9 @@ msgid_plural "Alerts"
|
|||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "Alert #%{alertId}"
|
||||
msgstr ""
|
||||
|
||||
msgid "AlertManagement|Acknowledged"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -309,6 +309,16 @@ describe('AwardsHandler', () => {
|
|||
expect($('[data-name=alien]').is(':visible')).toBe(true);
|
||||
expect($('.js-emoji-menu-search').val()).toBe('');
|
||||
});
|
||||
|
||||
it('should fuzzy filter the emoji', async () => {
|
||||
await openAndWaitForEmojiMenu();
|
||||
|
||||
awardsHandler.searchEmojis('sgls');
|
||||
|
||||
expect($('[data-name=angel]').is(':visible')).toBe(false);
|
||||
expect($('[data-name=anger]').is(':visible')).toBe(false);
|
||||
expect($('[data-name=sunglasses]').is(':visible')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('emoji menu', () => {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { trimText } from 'helpers/text_helper';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import { initEmojiMap, glEmojiTag, EMOJI_VERSION } from '~/emoji';
|
||||
import { initEmojiMap, glEmojiTag, queryEmojiNames, EMOJI_VERSION } from '~/emoji';
|
||||
import isEmojiUnicodeSupported, {
|
||||
isFlagEmoji,
|
||||
isRainbowFlagEmoji,
|
||||
|
@ -57,8 +57,15 @@ describe('gl_emoji', () => {
|
|||
let mock;
|
||||
|
||||
beforeEach(() => {
|
||||
const emojiData = Object.fromEntries(
|
||||
Object.values(emojiFixtureMap).map(m => {
|
||||
const { name: n, moji: e, unicodeVersion: u, category: c, description: d } = m;
|
||||
return [n, { c, e, d, u }];
|
||||
}),
|
||||
);
|
||||
|
||||
mock = new MockAdapter(axios);
|
||||
mock.onGet(`/-/emojis/${EMOJI_VERSION}/emojis.json`).reply(200);
|
||||
mock.onGet(`/-/emojis/${EMOJI_VERSION}/emojis.json`).reply(200, JSON.stringify(emojiData));
|
||||
|
||||
return initEmojiMap().catch(() => {});
|
||||
});
|
||||
|
@ -378,4 +385,15 @@ describe('gl_emoji', () => {
|
|||
expect(isSupported).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('queryEmojiNames', () => {
|
||||
const contains = (e, term) => {
|
||||
const names = queryEmojiNames(term);
|
||||
expect(names.indexOf(e.name) >= 0).toBe(true);
|
||||
};
|
||||
|
||||
it('should match by name', () => contains(emojiFixtureMap.grey_question, 'grey_question'));
|
||||
it('should match by partial name', () => contains(emojiFixtureMap.grey_question, 'question'));
|
||||
it('should fuzzy match by name', () => contains(emojiFixtureMap.grey_question, 'grqtn'));
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import { GlLink } from '@gitlab/ui';
|
||||
import { GlLink, GlSprintf } from '@gitlab/ui';
|
||||
import HighlightBar from '~/issue_show/components/incidents/highlight_bar.vue';
|
||||
import { formatDate } from '~/lib/utils/datetime_utility';
|
||||
|
||||
|
@ -9,6 +9,7 @@ describe('Highlight Bar', () => {
|
|||
let wrapper;
|
||||
|
||||
const alert = {
|
||||
iid: 1,
|
||||
startedAt: '2020-05-29T10:39:22Z',
|
||||
detailsUrl: 'http://127.0.0.1:3000/root/unique-alerts/-/alert_management/1/details',
|
||||
eventCount: 1,
|
||||
|
@ -20,6 +21,9 @@ describe('Highlight Bar', () => {
|
|||
propsData: {
|
||||
alert,
|
||||
},
|
||||
stubs: {
|
||||
GlSprintf,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -39,7 +43,8 @@ describe('Highlight Bar', () => {
|
|||
it('renders a link to the alert page', () => {
|
||||
expect(findLink().exists()).toBe(true);
|
||||
expect(findLink().attributes('href')).toBe(alert.detailsUrl);
|
||||
expect(findLink().text()).toContain(alert.title);
|
||||
expect(findLink().attributes('title')).toBe(alert.title);
|
||||
expect(findLink().text()).toBe(`Alert #${alert.iid}`);
|
||||
});
|
||||
|
||||
it('renders formatted start time of the alert', () => {
|
||||
|
|
98
spec/frontend/lib/dompurify_spec.js
Normal file
98
spec/frontend/lib/dompurify_spec.js
Normal file
|
@ -0,0 +1,98 @@
|
|||
import { sanitize } from '~/lib/dompurify';
|
||||
|
||||
// GDK
|
||||
const rootGon = {
|
||||
sprite_file_icons: '/assets/icons-123a.svg',
|
||||
sprite_icons: '/assets/icons-456b.svg',
|
||||
};
|
||||
|
||||
// Production
|
||||
const absoluteGon = {
|
||||
sprite_file_icons: `${window.location.protocol}//${window.location.hostname}/assets/icons-123a.svg`,
|
||||
sprite_icons: `${window.location.protocol}//${window.location.hostname}/assets/icons-456b.svg`,
|
||||
};
|
||||
|
||||
const expectedSanitized = '<svg><use></use></svg>';
|
||||
|
||||
const safeUrls = {
|
||||
root: Object.values(rootGon).map(url => `${url}#ellipsis_h`),
|
||||
absolute: Object.values(absoluteGon).map(url => `${url}#ellipsis_h`),
|
||||
};
|
||||
|
||||
const unsafeUrls = [
|
||||
'/an/evil/url',
|
||||
'../../../evil/url',
|
||||
'https://evil.url/assets/icons-123a.svg',
|
||||
'https://evil.url/assets/icons-456b.svg',
|
||||
`https://evil.url/${rootGon.sprite_icons}`,
|
||||
`https://evil.url/${rootGon.sprite_file_icons}`,
|
||||
`https://evil.url/${absoluteGon.sprite_icons}`,
|
||||
`https://evil.url/${absoluteGon.sprite_file_icons}`,
|
||||
];
|
||||
|
||||
describe('~/lib/dompurify', () => {
|
||||
let originalGon;
|
||||
|
||||
it('uses local configuration when given', () => {
|
||||
// As dompurify uses a "Persistent Configuration", it might
|
||||
// ignore config, this check verifies we respect
|
||||
// https://github.com/cure53/DOMPurify#persistent-configuration
|
||||
expect(sanitize('<br>', { ALLOWED_TAGS: [] })).toBe('');
|
||||
expect(sanitize('<strong></strong>', { ALLOWED_TAGS: [] })).toBe('');
|
||||
});
|
||||
|
||||
describe.each`
|
||||
type | gon
|
||||
${'root'} | ${rootGon}
|
||||
${'absolute'} | ${absoluteGon}
|
||||
`('when gon contains $type icon urls', ({ type, gon }) => {
|
||||
beforeAll(() => {
|
||||
originalGon = window.gon;
|
||||
window.gon = gon;
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
window.gon = originalGon;
|
||||
});
|
||||
|
||||
it('allows no href attrs', () => {
|
||||
const htmlHref = `<svg><use></use></svg>`;
|
||||
expect(sanitize(htmlHref)).toBe(htmlHref);
|
||||
});
|
||||
|
||||
it.each(safeUrls[type])('allows safe URL %s', url => {
|
||||
const htmlHref = `<svg><use href="${url}"></use></svg>`;
|
||||
expect(sanitize(htmlHref)).toBe(htmlHref);
|
||||
|
||||
const htmlXlink = `<svg><use xlink:href="${url}"></use></svg>`;
|
||||
expect(sanitize(htmlXlink)).toBe(htmlXlink);
|
||||
});
|
||||
|
||||
it.each(unsafeUrls)('sanitizes unsafe URL %s', url => {
|
||||
const htmlHref = `<svg><use href="${url}"></use></svg>`;
|
||||
const htmlXlink = `<svg><use xlink:href="${url}"></use></svg>`;
|
||||
|
||||
expect(sanitize(htmlHref)).toBe(expectedSanitized);
|
||||
expect(sanitize(htmlXlink)).toBe(expectedSanitized);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when gon does not contain icon urls', () => {
|
||||
beforeAll(() => {
|
||||
originalGon = window.gon;
|
||||
window.gon = {};
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
window.gon = originalGon;
|
||||
});
|
||||
|
||||
it.each([...safeUrls.root, ...safeUrls.absolute, ...unsafeUrls])('sanitizes URL %s', url => {
|
||||
const htmlHref = `<svg><use href="${url}"></use></svg>`;
|
||||
const htmlXlink = `<svg><use xlink:href="${url}"></use></svg>`;
|
||||
|
||||
expect(sanitize(htmlHref)).toBe(expectedSanitized);
|
||||
expect(sanitize(htmlXlink)).toBe(expectedSanitized);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,11 +1,12 @@
|
|||
import MockAdapter from 'axios-mock-adapter';
|
||||
import $ from 'jquery';
|
||||
import { TEST_HOST } from 'helpers/test_constants';
|
||||
import { sanitize } from 'dompurify';
|
||||
import { sanitize } from '~/lib/dompurify';
|
||||
import ProjectFindFile from '~/project_find_file';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
|
||||
jest.mock('dompurify', () => ({
|
||||
jest.mock('~/lib/dompurify', () => ({
|
||||
addHook: jest.fn(),
|
||||
sanitize: jest.fn(val => val),
|
||||
}));
|
||||
|
||||
|
|
|
@ -5,17 +5,20 @@ require 'spec_helper'
|
|||
RSpec.describe Ci::PipelinePresenter do
|
||||
include Gitlab::Routing
|
||||
|
||||
let(:user) { create(:user) }
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be_with_reload(:project) { create(:project, :test_repo) }
|
||||
let_it_be_with_reload(:pipeline) { create(:ci_pipeline, project: project) }
|
||||
let(:current_user) { user }
|
||||
let(:project) { create(:project, :test_repo) }
|
||||
let(:pipeline) { create(:ci_pipeline, project: project) }
|
||||
|
||||
subject(:presenter) do
|
||||
described_class.new(pipeline)
|
||||
end
|
||||
|
||||
before do
|
||||
before_all do
|
||||
project.add_developer(user)
|
||||
end
|
||||
|
||||
before do
|
||||
allow(presenter).to receive(:current_user) { current_user }
|
||||
end
|
||||
|
||||
|
@ -184,8 +187,8 @@ RSpec.describe Ci::PipelinePresenter do
|
|||
describe '#all_related_merge_request_text' do
|
||||
subject { presenter.all_related_merge_request_text }
|
||||
|
||||
let(:mr_1) { create(:merge_request) }
|
||||
let(:mr_2) { create(:merge_request) }
|
||||
let_it_be(:mr_1) { create(:merge_request) }
|
||||
let_it_be(:mr_2) { create(:merge_request) }
|
||||
|
||||
context 'with zero related merge requests (branch pipeline)' do
|
||||
it { is_expected.to eq('No related merge requests found.') }
|
||||
|
@ -242,7 +245,7 @@ RSpec.describe Ci::PipelinePresenter do
|
|||
end
|
||||
|
||||
context 'permissions' do
|
||||
let(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline, source_project: project) }
|
||||
let_it_be_with_refind(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline, source_project: project) }
|
||||
let(:pipeline) { merge_request.all_pipelines.take }
|
||||
|
||||
shared_examples 'private merge requests' do
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe MemberInvitationReminderEmailsWorker do
|
||||
describe '#perform' do
|
||||
subject { described_class.new.perform }
|
||||
|
||||
context 'feature flag disabled' do
|
||||
before do
|
||||
stub_experiment(invitation_reminders: false)
|
||||
end
|
||||
|
||||
it 'does not raise an error' do
|
||||
expect { subject }.not_to raise_error
|
||||
end
|
||||
end
|
||||
|
||||
context 'feature flag enabled' do
|
||||
before do
|
||||
stub_experiment(invitation_reminders: true)
|
||||
end
|
||||
|
||||
it 'does not raise an error' do
|
||||
expect { subject }.not_to raise_error
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue