Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-08-22 09:11:23 +00:00
parent dc90a96501
commit 535401c636
72 changed files with 487 additions and 325 deletions

View File

@ -3,6 +3,7 @@ extends:
- plugin:@gitlab/i18n
- plugin:no-jquery/slim
- plugin:no-jquery/deprecated-3.4
- plugin:no-unsanitized/DOM
- ./tooling/eslint-config/conditionally_ignore.js
globals:
__webpack_public_path__: true
@ -116,6 +117,14 @@ rules:
vue/multi-word-component-names: off
unicorn/prefer-dom-node-dataset:
- error
no-unsanitized/method:
- error
- escape:
methods: 'sanitize'
no-unsanitized/property:
- error
- escape:
methods: 'sanitize'
overrides:
- files:
- '{,ee/,jh/}spec/frontend*/**/*'
@ -134,6 +143,8 @@ overrides:
message: 'Prefer explicit waitForPromises (or equivalent), or jest.runAllTimers (or equivalent) to vague setImmediate calls.'
- selector: ImportSpecifier[imported.name='GlSkeletonLoading']
message: 'Migrate to GlSkeletonLoader, or import GlDeprecatedSkeletonLoading.'
no-unsanitized/method: off
no-unsanitized/property: off
- files:
- 'config/**/*'
- 'scripts/**/*'

View File

@ -482,7 +482,7 @@ gem 'net-ntp'
gem 'ssh_data', '~> 1.3'
# Spamcheck GRPC protocol definitions
gem 'spamcheck', '~> 0.1.0'
gem 'spamcheck', '~> 1.0.0'
# Gitaly GRPC protocol definitions
gem 'gitaly', '~> 15.3.0-rc4'

View File

@ -1314,7 +1314,7 @@ GEM
sorted_set (1.0.3)
rbtree
set (~> 1.0)
spamcheck (0.1.0)
spamcheck (1.0.0)
grpc (~> 1.0)
spring (2.1.1)
spring-commands-rspec (1.0.4)
@ -1746,7 +1746,7 @@ DEPENDENCIES
slack-messenger (~> 2.3.4)
snowplow-tracker (~> 0.6.1)
solargraph (~> 0.45.0)
spamcheck (~> 0.1.0)
spamcheck (~> 1.0.0)
spring (~> 2.1.0)
spring-commands-rspec (~> 1.0.4)
sprite-factory (~> 1.7)

View File

@ -165,6 +165,7 @@ export class AwardsHandler {
`;
const targetEl = this.targetContainerEl ? this.targetContainerEl : document.body;
// eslint-disable-next-line no-unsanitized/method
targetEl.insertAdjacentHTML('beforeend', emojiMenuMarkup);
this.addRemainingEmojiMenuCategories();
@ -198,6 +199,7 @@ export class AwardsHandler {
emojisInCategory,
);
requestAnimationFrame(() => {
// eslint-disable-next-line no-unsanitized/method
emojiContentElement.insertAdjacentHTML('beforeend', categoryMarkup);
resolve();
});

View File

@ -67,6 +67,7 @@ export default {
},
content() {
const el = document.createElement('div');
// eslint-disable-next-line no-unsanitized/property
el.innerHTML = this.draft.note_html;
return el.textContent;

View File

@ -22,6 +22,7 @@ class CopyCodeButton extends HTMLElement {
'data-clipboard-target': `pre#${this.for}`,
});
// eslint-disable-next-line no-unsanitized/property
button.innerHTML = spriteIcon('copy-to-clipboard');
return button;

View File

@ -91,6 +91,7 @@ class SafeMathRenderer {
`;
if (!wrapperElement.classList.contains('lazy-alert-shown')) {
// eslint-disable-next-line no-unsanitized/property
wrapperElement.innerHTML = html;
wrapperElement.append(codeElement);
wrapperElement.classList.add('lazy-alert-shown');
@ -111,6 +112,7 @@ class SafeMathRenderer {
}
try {
// eslint-disable-next-line no-unsanitized/property
displayContainer.innerHTML = this.katex.renderToString(text, {
displayMode: el.dataset.mathStyle === 'display',
throwOnError: true,

View File

@ -45,6 +45,7 @@ const loadViewer = (viewerParam) => {
viewer.dataset.loading = 'true';
return axios.get(url).then(({ data }) => {
// eslint-disable-next-line no-unsanitized/property
viewer.innerHTML = data.html;
window.requestIdleCallback(() => {

View File

@ -23,6 +23,7 @@ const wrapTextWithSpan = (el, text) => {
const wrapNodes = (text) => {
const wrapper = createSpan();
// eslint-disable-next-line no-unsanitized/property
wrapper.innerHTML = wrapSpacesWithSpans(text);
wrapper.childNodes.forEach((el) => wrapTextWithSpan(el, text));
return wrapper.childNodes;

View File

@ -13,6 +13,7 @@ const renderersByType = {
},
header(element, data) {
element.classList.add('dropdown-header');
// eslint-disable-next-line no-unsanitized/property
element.innerHTML = data.content;
return element;
@ -122,6 +123,7 @@ function assignTextToLink(el, data, options) {
const text = getLinkText(data, options);
if (options.icon || options.highlight) {
// eslint-disable-next-line no-unsanitized/property
el.innerHTML = text;
} else {
el.textContent = text;

View File

@ -81,6 +81,7 @@ export default class FilterableList {
onFilterSuccess(response, queryData) {
if (response.data.html) {
// eslint-disable-next-line no-unsanitized/property
this.listHolderElement.innerHTML = response.data.html;
}

View File

@ -75,6 +75,7 @@ export default class DropdownEmoji extends FilteredSearchDropdown {
const name = valueElement.innerText;
const emojiTag = this.glEmojiTag(name);
const emojiElement = dropdownItem.querySelector('gl-emoji');
// eslint-disable-next-line no-unsanitized/property
emojiElement.outerHTML = emojiTag;
}
});

View File

@ -107,7 +107,7 @@ class DropDown {
}
const renderableList = this.list.querySelector('ul[data-dynamic]') || this.list;
// eslint-disable-next-line no-unsanitized/property
renderableList.innerHTML = children.join('');
const listEvent = new CustomEvent('render.dl', {
@ -121,7 +121,7 @@ class DropDown {
renderChildren(data) {
const html = utils.template(this.templateString, data);
const template = document.createElement('div');
// eslint-disable-next-line no-unsanitized/property
template.innerHTML = html;
DropDown.setImagesSrc(template);
template.firstChild.style.display = data.droplab_hidden ? 'none' : 'block';

View File

@ -42,6 +42,7 @@ class HookButton extends Hook {
}
restoreInitialState() {
// eslint-disable-next-line no-unsanitized/property
this.list.list.innerHTML = this.list.initialState;
}

View File

@ -97,6 +97,7 @@ class HookInput extends Hook {
}
restoreInitialState() {
// eslint-disable-next-line no-unsanitized/property
this.list.list.innerHTML = this.list.initialState;
}

View File

@ -122,6 +122,7 @@ export default class FilteredSearchVisualTokens {
const hasOperator = Boolean(operator);
if (value) {
// eslint-disable-next-line no-unsanitized/property
li.innerHTML = FilteredSearchVisualTokens.createVisualTokenElementHTML({
canEdit,
uppercaseTokenName,
@ -138,6 +139,7 @@ export default class FilteredSearchVisualTokens {
operatorHTML = '<div class="operator"></div>';
}
// eslint-disable-next-line no-unsanitized/property
li.innerHTML = nameHTML + operatorHTML;
}
@ -160,6 +162,8 @@ export default class FilteredSearchVisualTokens {
if (!isLastVisualTokenValid && lastVisualToken.classList.contains('filtered-search-token')) {
const name = FilteredSearchVisualTokens.getLastTokenPartial();
const operator = FilteredSearchVisualTokens.getLastTokenOperator();
// eslint-disable-next-line no-unsanitized/property
lastVisualToken.innerHTML = FilteredSearchVisualTokens.createVisualTokenElementHTML({
hasOperator: Boolean(operator),
});
@ -293,6 +297,7 @@ export default class FilteredSearchVisualTokens {
const button = lastVisualToken.querySelector('.selectable');
const valueContainer = lastVisualToken.querySelector('.value-container');
button.removeChild(valueContainer);
// eslint-disable-next-line no-unsanitized/property
lastVisualToken.innerHTML = button.innerHTML;
} else if (operator) {
lastVisualToken.removeChild(operator);

View File

@ -47,6 +47,7 @@ export default class VisualTokenValue {
/* eslint-disable no-param-reassign */
tokenValueContainer.dataset.originalValue = tokenValue;
// eslint-disable-next-line no-unsanitized/property
tokenValueElement.innerHTML = `
<img class="avatar s20" src="${user.avatar_url}" alt="">
${escape(user.name)}
@ -152,6 +153,7 @@ export default class VisualTokenValue {
}
container.dataset.originalValue = value;
// eslint-disable-next-line no-unsanitized/property
element.innerHTML = Emoji.glEmojiTag(value);
});
}

View File

@ -236,11 +236,13 @@ const createFlash = function createFlash({
if (!flashContainer) return null;
// eslint-disable-next-line no-unsanitized/property
flashContainer.innerHTML = createFlashEl(message, type);
const flashEl = flashContainer.querySelector(`.flash-${type}`);
if (actionConfig) {
// eslint-disable-next-line no-unsanitized/method
flashEl.insertAdjacentHTML('beforeend', createAction(actionConfig));
if (actionConfig.clickHandler) {

View File

@ -30,6 +30,7 @@ export function addImageBadge(containerEl, { coordinate, badgeText, noteId }) {
export function addImageCommentBadge(containerEl, { coordinate, noteId }) {
const buttonEl = createImageBadge(noteId, coordinate, ['image-comment-badge']);
// eslint-disable-next-line no-unsanitized/property
buttonEl.innerHTML = spriteIcon('image-comment-dark');
containerEl.appendChild(buttonEl);

View File

@ -8,6 +8,7 @@ export function addCommentIndicator(containerEl, { x, y }) {
buttonEl.style.left = `${x}px`;
buttonEl.style.top = `${y}px`;
// eslint-disable-next-line no-unsanitized/property
buttonEl.innerHTML = spriteIcon('image-comment-dark');
containerEl.appendChild(buttonEl);

View File

@ -226,6 +226,7 @@ export default {
},
createDragIconElement() {
const container = document.createElement('div');
// eslint-disable-next-line no-unsanitized/property
container.innerHTML = `<svg class="drag-icon s14 gl-icon gl-cursor-grab gl-visibility-hidden" role="img" aria-hidden="true">
<use href="${gon.sprite_icons}#drag-vertical"></use>
</svg>`;
@ -358,6 +359,7 @@ export default {
);
button.id = `js-task-button-${index}`;
this.taskButtons.push(button.id);
// eslint-disable-next-line no-unsanitized/property
button.innerHTML = `
<svg data-testid="ellipsis_v-icon" role="img" aria-hidden="true" class="dropdown-icon gl-icon s14">
<use href="${gon.sprite_icons}#doc-new"></use>

View File

@ -13,6 +13,7 @@ const updateDescription = (descriptionHtml = '', details) => {
}
const placeholder = document.createElement('div');
// eslint-disable-next-line no-unsanitized/property
placeholder.innerHTML = descriptionHtml;
const newDetails = placeholder.getElementsByTagName('details');

View File

@ -247,6 +247,7 @@ export default class LabelsSelect {
}
linkEl.className = selectedClass.join(' ');
// eslint-disable-next-line no-unsanitized/property
linkEl.innerHTML = `${colorEl} ${escape(label.title)}`;
const listItemEl = document.createElement('li');

View File

@ -9,6 +9,7 @@ const setIframeRenderedSize = (h, w) => {
const drawDiagram = (source) => {
const element = document.getElementById('app');
const insertSvg = (svgCode) => {
// eslint-disable-next-line no-unsanitized/property
element.innerHTML = svgCode;
const height = parseInt(element.firstElementChild.getAttribute('height'), 10);

View File

@ -119,6 +119,7 @@ const getAverageCharWidth = memoize(function getAverageCharWidth(options = {}) {
div.style.left = -1000;
div.style.top = -1000;
// eslint-disable-next-line no-unsanitized/property
div.innerHTML = chars;
document.body.appendChild(div);

View File

@ -434,6 +434,7 @@ export default class MergeRequestTabs {
.get(`${source}.json`)
.then(({ data }) => {
const commitsDiv = document.querySelector('div#commits');
// eslint-disable-next-line no-unsanitized/property
commitsDiv.innerHTML = data.html;
localTimeAgo(commitsDiv.querySelectorAll('.js-timeago'));
this.commitsLoaded = true;

View File

@ -63,6 +63,8 @@ export default class PayloadPreviewer {
insertPayload(data) {
this.isInserted = true;
// eslint-disable-next-line no-unsanitized/property
this.getContainer().innerHTML = data;
this.showPayload();
}

View File

@ -200,9 +200,11 @@ export default class Todos {
});
document.dispatchEvent(event);
// eslint-disable-next-line no-unsanitized/property
document.querySelector('.js-todos-pending .js-todos-badge').innerHTML = addDelimiter(
data.count,
);
// eslint-disable-next-line no-unsanitized/property
document.querySelector('.js-todos-done .js-todos-badge').innerHTML = addDelimiter(
data.done_count,
);

View File

@ -29,6 +29,7 @@ const selectEmojiCallback = (emoji, emojiTag) => {
statusEmojiField.value = emoji;
toggleNoEmojiPlaceholder(false);
removeStatusEmoji();
// eslint-disable-next-line no-unsanitized/property
toggleEmojiMenuButton.innerHTML += emojiTag;
};
@ -74,6 +75,7 @@ Emoji.initEmojiMap()
if (hasStatusMessage) {
toggleNoEmojiPlaceholder(false);
// eslint-disable-next-line no-unsanitized/property
toggleEmojiMenuButton.innerHTML += defaultEmojiTag;
} else if (statusEmoji.dataset.name === defaultStatusEmoji) {
toggleNoEmojiPlaceholder(true);

View File

@ -8,7 +8,10 @@ const skippable = twoFactorNode ? parseBoolean(twoFactorNode.dataset.twoFactorSk
if (skippable) {
const button = `<br/><a class="btn gl-button btn-sm btn-confirm gl-mt-3" data-qa-selector="configure_it_later_button" data-method="patch" href="${twoFactorNode.dataset.two_factor_skip_url}">Configure it later</a>`;
const flashAlert = document.querySelector('.flash-alert');
if (flashAlert) flashAlert.insertAdjacentHTML('beforeend', button);
if (flashAlert) {
// eslint-disable-next-line no-unsanitized/method
flashAlert.insertAdjacentHTML('beforeend', button);
}
}
mount2faRegistration();

View File

@ -33,6 +33,7 @@ export default class UserOverviewBlock {
const containerEl = document.querySelector(this.container);
const contentList = containerEl.querySelector('.overview-content-list');
// eslint-disable-next-line no-unsanitized/property
contentList.innerHTML += html;
const loadingEl = containerEl.querySelector('.loading');

View File

@ -26,6 +26,7 @@ function setVisibilityOptions({ name, visibility, showPath, editPath }) {
if (reason) {
const optionTitle = option.querySelector('.js-visibility-level-radio span');
const optionName = optionTitle ? optionTitle.innerText.toLowerCase() : '';
// eslint-disable-next-line no-unsanitized/property
reason.innerHTML = sprintf(
__(
'This project cannot be %{visibilityLevel} because the visibility of %{openShowLink}%{name}%{closeShowLink} is %{visibility}. To make this project %{visibilityLevel}, you must first %{openEditLink}change the visibility%{closeEditLink} of the parent group.',

View File

@ -22,11 +22,14 @@ export default class Star {
starSpan.classList.remove('starred');
starSpan.textContent = s__('StarProject|Star');
starIcon.remove();
// eslint-disable-next-line no-unsanitized/method
starSpan.insertAdjacentHTML('beforebegin', spriteIcon('star-o', iconClasses));
} else {
starSpan.classList.add('starred');
starSpan.textContent = __('Unstar');
starIcon.remove();
// eslint-disable-next-line no-unsanitized/method
starSpan.insertAdjacentHTML('beforebegin', spriteIcon('star', iconClasses));
}
})

View File

@ -49,6 +49,9 @@ export default {
error,
});
},
context: {
isSingleRequest: true,
},
},
},
computed: {

View File

@ -19,6 +19,7 @@ export default class InputValidator {
setValidationMessage() {
if (this.invalidInput) {
this.inputDomElement.setCustomValidity(this.errorMessage);
// eslint-disable-next-line no-unsanitized/property
this.inputErrorMessage.innerHTML = this.errorMessage;
} else {
this.resetValidationMessage();
@ -28,6 +29,7 @@ export default class InputValidator {
resetValidationMessage() {
if (this.inputDomElement.validationMessage === this.errorMessage) {
this.inputDomElement.setCustomValidity('');
// eslint-disable-next-line no-unsanitized/property
this.inputErrorMessage.innerHTML = this.inputDomElement.title;
}
}

View File

@ -428,6 +428,7 @@ export default {
.then((res) => {
if (res.data) {
const el = document.createElement('div');
// eslint-disable-next-line no-unsanitized/property
el.innerHTML = res.data;
document.body.appendChild(el);
document.dispatchEvent(new CustomEvent('merged:UpdateActions'));

View File

@ -163,6 +163,7 @@ export default {
// resets the container HTML (replaces it with the updated noteHTML)
// calls `renderSuggestions` once the updated noteHTML is added to the DOM
// eslint-disable-next-line no-unsanitized/property
this.$refs.container.innerHTML = this.noteHtml;
this.isRendered = false;
this.renderSuggestions();

View File

@ -3,6 +3,8 @@ import { HLJS_COMMENT_SELECTOR } from '../constants';
const createWrapper = (content) => {
const span = document.createElement('span');
span.className = HLJS_COMMENT_SELECTOR;
// eslint-disable-next-line no-unsanitized/property
span.innerHTML = content;
return span.outerHTML;
};

View File

@ -10,6 +10,7 @@ export default {
mounted() {
const legacyEntry = document.querySelector(this.selector);
if (legacyEntry.tagName === 'TEMPLATE') {
// eslint-disable-next-line no-unsanitized/property
this.$el.innerHTML = legacyEntry.innerHTML;
} else {
this.source = legacyEntry.parentNode;

View File

@ -20,6 +20,7 @@ const reloadMessage = LIVE_RELOAD
? 'You have live_reload enabled, the page will reload automatically when complete.'
: 'You have live_reload disabled, the page will reload automatically in a few seconds.';
// eslint-disable-next-line no-unsanitized/property
div.innerHTML = `
<!-- https://github.com/webpack/media/blob/master/logo/icon-square-big.svg -->
<svg height="50" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 1200">

View File

@ -48,7 +48,7 @@
opacity: 0;
}
a {
a:not(.canary-badge) {
display: flex;
align-items: center;
padding: 2px 8px;
@ -61,10 +61,6 @@
}
}
.canary-badge {
margin-left: -8px;
}
.project-item-select {
right: auto;
left: 0;

View File

@ -332,9 +332,6 @@ kbd kbd {
color: #fff;
background-color: #c17d10;
}
.bg-transparent {
background-color: transparent !important;
}
.rounded-circle {
border-radius: 50% !important;
}
@ -815,20 +812,17 @@ kbd {
.navbar-gitlab .header-content .title img {
height: 24px;
}
.navbar-gitlab .header-content .title a {
.navbar-gitlab .header-content .title a:not(.canary-badge) {
display: flex;
align-items: center;
padding: 2px 8px;
margin: 4px 2px 4px -8px;
border-radius: 4px;
}
.navbar-gitlab .header-content .title a:active {
.navbar-gitlab .header-content .title a:not(.canary-badge):active {
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.6), 0 0 0 3px #1068bf;
outline: none;
}
.navbar-gitlab .header-content .title .canary-badge {
margin-left: -8px;
}
.navbar-gitlab .header-content .navbar-collapse > ul.nav > li:not(.d-none) {
margin: 0 2px;
}

View File

@ -311,9 +311,6 @@ kbd kbd {
color: #fff;
background-color: #ab6100;
}
.bg-transparent {
background-color: transparent !important;
}
.rounded-circle {
border-radius: 50% !important;
}
@ -794,20 +791,17 @@ kbd {
.navbar-gitlab .header-content .title img {
height: 24px;
}
.navbar-gitlab .header-content .title a {
.navbar-gitlab .header-content .title a:not(.canary-badge) {
display: flex;
align-items: center;
padding: 2px 8px;
margin: 4px 2px 4px -8px;
border-radius: 4px;
}
.navbar-gitlab .header-content .title a:active {
.navbar-gitlab .header-content .title a:not(.canary-badge):active {
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.6), 0 0 0 3px #63a6e9;
outline: none;
}
.navbar-gitlab .header-content .title .canary-badge {
margin-left: -8px;
}
.navbar-gitlab .header-content .navbar-collapse > ul.nav > li:not(.d-none) {
margin: 0 2px;
}

View File

@ -67,7 +67,7 @@ class JiraConnect::SubscriptionsController < JiraConnect::ApplicationController
return unless current_jira_installation.instance_url?
request.content_security_policy.directives['connect-src'] ||= []
request.content_security_policy.directives['connect-src'] << Gitlab::Utils.append_path(current_jira_installation.instance_url, '/-/jira_connect/oauth_application_ids')
request.content_security_policy.directives['connect-src'] << Gitlab::Utils.append_path(current_jira_installation.instance_url, '/-/jira_connect/oauth_application_id')
end
def create_service

View File

@ -91,7 +91,7 @@ class Snippet < ApplicationRecord
participant :notes_with_associations
attr_spammable :title, spam_title: true
attr_spammable :content, spam_description: true
attr_spammable :description, spam_description: true
attr_encrypted :secret_token,
key: Settings.attr_encrypted_db_key_base_truncated,
@ -276,13 +276,7 @@ class Snippet < ApplicationRecord
def check_for_spam?(user:)
visibility_level_changed?(to: Snippet::PUBLIC) ||
(public? && (title_changed? || content_changed?))
end
# snippets are the biggest sources of spam
override :allow_possible_spam?
def allow_possible_spam?
false
(public? && (title_changed? || description_changed?))
end
def spammable_entity_type

View File

@ -73,6 +73,15 @@ module Snippets
message
end
def file_paths_to_commit
paths = []
snippet_actions.to_commit_actions.each do |action|
paths << { path: action[:file_path] }
end
paths
end
def files_to_commit(snippet)
snippet_actions.to_commit_actions.presence || build_actions_from_params(snippet)
end

View File

@ -24,7 +24,8 @@ module Snippets
spammable: @snippet,
spam_params: spam_params,
user: current_user,
action: :create
action: :create,
extra_features: { files: file_paths_to_commit }
).execute
if save_and_commit

View File

@ -23,11 +23,14 @@ module Snippets
update_snippet_attributes(snippet)
files = snippet.all_files.map { |f| { path: f } } + file_paths_to_commit
Spam::SpamActionService.new(
spammable: snippet,
spam_params: spam_params,
user: current_user,
action: :update
action: :update,
extra_features: { files: files }
).execute
if save_and_commit(snippet)

View File

@ -4,11 +4,12 @@ module Spam
class SpamActionService
include SpamConstants
def initialize(spammable:, spam_params:, user:, action:)
def initialize(spammable:, spam_params:, user:, action:, extra_features: {})
@target = spammable
@spam_params = spam_params
@user = user
@action = action
@extra_features = extra_features
end
# rubocop:disable Metrics/AbcSize
@ -40,7 +41,7 @@ module Spam
private
attr_reader :user, :action, :target, :spam_params, :spam_log
attr_reader :user, :action, :target, :spam_params, :spam_log, :extra_features
##
# In order to be proceed to the spam check process, the target must be
@ -124,7 +125,9 @@ module Spam
SpamVerdictService.new(target: target,
user: user,
options: options,
context: context)
context: context,
extra_features: extra_features
)
end
def noteable_type

View File

@ -5,11 +5,12 @@ module Spam
include AkismetMethods
include SpamConstants
def initialize(user:, target:, options:, context: {})
def initialize(user:, target:, options:, context: {}, extra_features: {})
@target = target
@user = user
@options = options
@context = context
@extra_features = extra_features
end
def execute
@ -61,7 +62,7 @@ module Spam
private
attr_reader :user, :target, :options, :context
attr_reader :user, :target, :options, :context, :extra_features
def akismet_verdict
if akismet.spam?
@ -75,7 +76,8 @@ module Spam
return unless Gitlab::CurrentSettings.spam_check_endpoint_enabled
begin
result, attribs, _error = spamcheck_client.issue_spam?(spam_issue: target, user: user, context: context)
result, attribs, _error = spamcheck_client.spam?(spammable: target, user: user, context: context,
extra_features: extra_features)
# @TODO log if error is not nil https://gitlab.com/gitlab-org/gitlab/-/issues/329545
return [nil, attribs] unless result

View File

@ -10,10 +10,10 @@
%span.gl-sr-only GitLab
= link_to root_path, title: _('Dashboard'), id: 'logo', class: 'has-tooltip', **tracking_attrs('main_navigation', 'click_gitlab_logo_link', 'navigation') do
= brand_header_logo
.gl-display-flex.gl-align-items-center
- if Gitlab.com_and_canary?
= link_to Gitlab::Saas.canary_toggle_com_url, class: 'canary-badge bg-transparent', data: { qa_selector: 'canary_badge_link' }, target: :_blank, rel: 'noopener noreferrer' do
= gl_badge_tag({ variant: :success, size: :sm }) do
= _('Next')
= gl_badge_tag({ variant: :success, size: :sm }, { href: Gitlab::Saas.canary_toggle_com_url, data: { qa_selector: 'canary_badge_link' }, target: :_blank, rel: 'noopener noreferrer', class: 'canary-badge' }) do
= _('Next')
- if current_user
.gl-display-none.gl-sm-display-block

View File

@ -1,34 +0,0 @@
- name: "Manual iteration management" # The name of the feature to be deprecated
announcement_milestone: "14.10" # The milestone when this feature was first announced as deprecated.
announcement_date: "2022-04-22" # The date of the milestone release when this feature was first announced as deprecated. This should almost always be the 22nd of a month (YYYY-MM-22), unless you did an out of band blog post.
removal_milestone: "16.0" # The milestone when this feature is planned to be removed
removal_date: "2023-04-22" # The date of the milestone release when this feature is planned to be removed. This should almost always be the 22nd of a month (YYYY-MM-22), unless you did an out of band blog post.
breaking_change: true # If this deprecation is a breaking change, set this value to true
reporter: mcelicalderonG # GitLab username of the person reporting the deprecation
body: | # Do not modify this line, instead modify the lines below.
Manual iteration management is deprecated and only automatic iteration cadences will be supported in the future.
Creating and deleting iterations will be fully removed in 16.0. Updating all iteration fields except for
`description` will also be removed.
On the GraphQL API the following mutations will be removed:
1. `iterationCreate`
1. `iterationDelete`
The update `updateIteration` mutation will only allow updating the iteration's `description`. The following
arguments will be removed:
1. `title`
1. `dueDate`
1. `startDate`
For more information about iteration cadences, you can refer to
[the documentation of the feature](https://docs.gitlab.com/ee/user/group/iterations/#iteration-cadences).
# The following items are not published on the docs page, but may be used in the future.
stage: Plan
tiers: # (optional - may be required in the future) An array of tiers that the feature is available in currently. e.g., [Free, Silver, Gold, Core, Premium, Ultimate]
issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/356069
documentation_url: # (optional) This is a link to the current documentation page
image_url: # (optional) This is a link to a thumbnail image depicting the feature
video_url: # (optional) Use the youtube thumbnail URL with the structure of https://img.youtube.com/vi/UNIQUEID/hqdefault.jpg

View File

@ -60,7 +60,7 @@ class Gitlab::Seeder::Pipelines
::Ci::ProcessPipelineService.new(pipeline).execute
end
::Gitlab::Seeder::Ci::DailyBuildGroupReportResult.new(@project).seed if @project.last_pipeline
::Gitlab::Seeders::Ci::DailyBuildGroupReportResult.new(@project).seed if @project.last_pipeline
end
private

View File

@ -361,38 +361,6 @@ In GitLab 15.0, for Dependency Scanning, the default version of Java that the sc
</div>
<div class="deprecation removal-160 breaking-change">
### Manual iteration management
Planned removal: GitLab <span class="removal-milestone">16.0</span> (2023-04-22)
WARNING:
This is a [breaking change](https://docs.gitlab.com/ee/development/deprecation_guidelines/).
Review the details carefully before upgrading.
Manual iteration management is deprecated and only automatic iteration cadences will be supported in the future.
Creating and deleting iterations will be fully removed in 16.0. Updating all iteration fields except for
`description` will also be removed.
On the GraphQL API the following mutations will be removed:
1. `iterationCreate`
1. `iterationDelete`
The update `updateIteration` mutation will only allow updating the iteration's `description`. The following
arguments will be removed:
1. `title`
1. `dueDate`
1. `startDate`
For more information about iteration cadences, you can refer to
[the documentation of the feature](https://docs.gitlab.com/ee/user/group/iterations/#iteration-cadences).
</div>
<div class="deprecation removal-150 breaking-change">
### Outdated indices of Advanced Search migrations

View File

@ -12,11 +12,6 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> - Moved to GitLab Premium in 13.9.
> - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/221047) in GitLab 14.6. [Feature flag `group_iterations`](https://gitlab.com/gitlab-org/gitlab/-/issues/221047) removed.
WARNING:
After [Iteration Cadences](#iteration-cadences) becomes generally available,
manual iteration scheduling will be [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/356069) in GitLab 15.6.
To enhance the role of iterations as time boundaries, we will also deprecate the title field.
Iterations are a way to track issues over a period of time. This allows teams
to track velocity and volatility metrics. Iterations can be used with [milestones](../../project/milestones/index.md)
for tracking over different time periods.
@ -187,7 +182,7 @@ To group issues by label:
> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/5077) in GitLab 14.1 [with a flag](../../../administration/feature_flags.md), named `iteration_cadences`. Disabled by default.
> - [Changed](https://gitlab.com/gitlab-org/gitlab/-/issues/354977) in GitLab 15.0: All scheduled iterations must start on the same day of the week as the cadence start day. Start date of cadence cannot be edited after the first iteration starts.
> - [Enabled on GitLab.com and self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/354878) in GitLab 15.0.
> - [Changed](https://gitlab.com/gitlab-org/gitlab/-/issues/367493) in GitLab 15.3: A new automation start date can be selected for cadence. Upcoming iterations will be scheduled to start on the same day of the week as the changed start date.
> - [Changed](https://gitlab.com/gitlab-org/gitlab/-/issues/367493) in GitLab 15.4: A new automation start date can be selected for cadence. Upcoming iterations will be scheduled to start on the same day of the week as the changed start date. Iteration cadences can be manually managed by turning off the automatic scheduling feature.
Iteration cadences automate iteration scheduling. You can use them to
automate creating iterations every 1, 2, 3, or 4 weeks. You can also
@ -206,8 +201,9 @@ To create an iteration cadence:
1. On the top bar, select **Menu > Groups** and find your group.
1. On the left sidebar, select **Issues > Iterations**.
1. Select **New iteration cadence**.
1. Complete the fields.
- Enter the title and description of the iteration cadence.
1. Enter the title and description of the iteration cadence.
1. To manually manage the iteration cadence, clear the **Enable automatic scheduling** checkbox and skip the next step.
1. Complete the required fields to use automatic scheduling.
- Select the automation start date of the iteration cadence. Iterations will be scheduled to
begin on the same day of the week as the day of the week of the start date.
- From the **Duration** dropdown list, select how many weeks each iteration should last.
@ -228,7 +224,7 @@ To edit an iteration cadence:
1. On the left sidebar, select **Issues > Iterations**.
1. Select **Edit iteration cadence**.
When you edit the **Automation start date** field,
When you are using automatic scheduling and edit the **Automation start date** field,
you must set a new start date that doesn't overlap with the existing
current or past iterations.
@ -236,52 +232,23 @@ Editing **Upcoming iterations** is a non-destructive action.
If ten upcoming iterations already exist, changing the number under **Upcoming iterations** to `2`
doesn't delete the eight existing upcoming iterations.
### Delete an iteration cadence
> [Changed](https://gitlab.com/gitlab-org/gitlab/-/issues/343889) the minimum user role from Developer to Reporter in GitLab 15.0.
Prerequisites:
- You must have at least the Reporter role for a group.
Deleting an iteration cadence also deletes all iterations within that cadence.
To delete an iteration cadence:
#### Turn on automatic scheduling for manual iterations cadence
1. On the top bar, select **Menu > Groups** and find your group.
1. On the left sidebar, select **Issues > Iterations**.
1. Select the three-dot menu (**{ellipsis_v}**) > **Delete cadence** for the cadence you want to delete.
1. Select **Delete cadence** in the confirmation modal.
### Manual iteration cadences
When you **enable** the iteration cadences feature, all previously
created iterations are added to a default iteration cadence.
You can continue to add, edit, and remove iterations in
this default cadence.
#### Convert a manual cadence to use automatic scheduling
WARNING:
The upgrade is irreversible. After it's done, a new manual iteration cadence cannot be created.
Prerequisites:
- You must have created [iterations](#iterations) without cadences before enabling iteration cadences for your group.
To upgrade the iteration cadence to use the automation features:
1. On the top bar, select **Menu > Groups** and find your group.
1. On the left sidebar, select **Issues > Iterations**.
1. Select the three-dot menu (**{ellipsis_v}**) > **Edit cadence** for the cadence you want to upgrade.
1. Select the three-dot menu (**{ellipsis_v}**) > **Edit cadence** for the cadence for which you want to enable automatic scheduling.
1. Check the **Enable automatic scheduling** checkbox.
1. Complete the required fields **Duration**, **Upcoming iterations**, and **Automation start date**.
For **Automation start date**, you can select any date that doesn't overlap with the existing open iterations.
If you have upcoming iterations, the automatic scheduling adjusts them appropriately to fit
your chosen duration.
1. Select **Save changes**.
#### Converted cadences example
When you want to manage your iterations cadence manually again, edit your cadence and uncheck the **Enable automatic scheduling** checkbox.
For example, suppose it's Friday, April 15, and you have three iterations in a manual cadence:
#### Example of turning on automatic scheduling for manual iterations cadence
Suppose it's Friday, April 15, and you have three iteration in a manual iterations cadence:
- Monday, April 4 - Friday, April 8 (closed)
- Tuesday, April 12 - Friday, April 15 (ongoing)
@ -305,3 +272,20 @@ is changed to "April 18 - Sunday, April 24".
An additional upcoming iteration "April 25 - Sunday, May 1" is scheduled
to satisfy the requirement that there are at least two upcoming iterations scheduled.
### Delete an iteration cadence
> [Changed](https://gitlab.com/gitlab-org/gitlab/-/issues/343889) the minimum user role from Developer to Reporter in GitLab 15.0.
Prerequisites:
- You must have at least the Reporter role for a group.
Deleting an iteration cadence also deletes all iterations within that cadence.
To delete an iteration cadence:
1. On the top bar, select **Menu > Groups** and find your group.
1. On the left sidebar, select **Issues > Iterations**.
1. Select the three-dot menu (**{ellipsis_v}**) > **Delete cadence** for the cadence you want to delete.
1. Select **Delete cadence**.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

@ -13,15 +13,56 @@ When you have a lot of issues, it can be hard to get an overview.
With weighted issues, you can get a better idea of how much time,
value, or complexity a given issue has or costs.
You can set the weight of an issue during its creation, by changing the
value in the dropdown menu. You can set it to a non-negative integer
value from 0, 1, 2, and so on.
You can remove weight from an issue as well.
A user with a Reporter role (or above) can set the weight.
## View the issue weight
This value appears on the right sidebar of an individual issue, as well as
in the issues page next to a weight icon (**{weight}**).
You can view the issue weight on:
As an added bonus, you can see the total sum of all issues on the milestone page.
- The right sidebar of each issue.
- The issues page, next to a weight icon (**{weight}**).
- [Issue boards](../issue_board.md), next to a weight icon (**{weight}**).
- The [milestone](../milestones/index.md) page, as a total sum of issue weights.
![issue page](img/issue_weight_v13_11.png)
## Set the issue weight
Prerequisites:
- You must have at least the Reporter role for the project.
You can set the issue weight when you create or edit an issue.
You must use whole numbers (like 0, 1, 2). Negative numbers or fractions are not accepted.
When you change the weight of an issue, the new value overwrites the previous value.
### When you create an issue
To set the issue weight when you [create an issue](managing_issues.md#create-an-issue), enter a
number under **Weight**.
### From an existing issue
To set the issue weight from an existing issue:
1. Go to the issue.
1. On the right sidebar, in the **Weight** section, select **Edit**.
1. Enter the new weight.
1. Select any area outside the dropdown list.
### From an issue board
To set the issue weight when you [edit an issue from an issue board](../issue_board.md#edit-an-issue):
1. Go to your issue board.
1. Select an issue card (not its title).
1. On the right sidebar, in the **Weight** section, select **Edit**.
1. Enter the new weight.
1. Select any area outside the dropdown list.
## Remove issue weight
Prerequisites:
- You must have at least the Reporter role for the project.
To remove the issue weight, follow the same steps as when you [set the issue weight](#set-the-issue-weight),
and select **remove weight**.

View File

@ -151,48 +151,6 @@ module Gitlab
model.logger = old_loggers[connection_name]
end
end
module Ci
class DailyBuildGroupReportResult
DEFAULT_BRANCH = 'master'
COUNT_OF_DAYS = 5
def initialize(project)
@project = project
@last_pipeline = project.last_pipeline
end
def seed
COUNT_OF_DAYS.times do |count|
date = Time.now.utc - count.day
create_report(date)
end
end
private
attr_reader :project, :last_pipeline
def create_report(date)
last_pipeline.builds.uniq(&:group_name).each do |build|
::Ci::DailyBuildGroupReportResult.create(
project: project,
last_pipeline: last_pipeline,
date: date,
ref_path: last_pipeline.source_ref_path,
group_name: build.group_name,
data: {
'coverage' => rand(20..99)
},
group: project.group,
default_branch: last_pipeline.default_branch?
)
rescue ActiveRecord::RecordNotUnique
return false
end
end
end
end
end
end
# :nocov:

View File

@ -0,0 +1,47 @@
# frozen_string_literal: true
module Gitlab
module Seeders
module Ci
class DailyBuildGroupReportResult
DEFAULT_BRANCH = 'master'
COUNT_OF_DAYS = 5
def initialize(project)
@project = project
@last_pipeline = project.last_pipeline
end
def seed
COUNT_OF_DAYS.times do |count|
date = Time.now.utc - count.day
create_report(date)
end
end
private
attr_reader :project, :last_pipeline
def create_report(date)
last_pipeline.builds.uniq(&:group_name).each do |build|
::Ci::DailyBuildGroupReportResult.create(
project: project,
last_pipeline: last_pipeline,
date: date,
ref_path: last_pipeline.source_ref_path,
group_name: build.group_name,
data: {
'coverage' => rand(20..99)
},
group: project.group,
default_branch: last_pipeline.default_branch?
)
rescue ActiveRecord::RecordNotUnique
return false
end
end
end
end
end
end

View File

@ -33,33 +33,50 @@ module Gitlab
@endpoint_url = @endpoint_url.sub(URL_SCHEME_REGEX, '')
end
def issue_spam?(spam_issue:, user:, context: {})
issue = build_issue_protobuf(issue: spam_issue, user: user, context: context)
def spam?(spammable:, user:, context: {}, extra_features: {})
metadata = { 'authorization' => Gitlab::CurrentSettings.spam_check_api_key }
protobuf_args = { spammable: spammable, user: user, context: context, extra_features: extra_features }
pb, grpc_method = build_protobuf(**protobuf_args)
response = grpc_method.call(pb, metadata: metadata)
response = grpc_client.check_for_spam_issue(issue,
metadata: { 'authorization' =>
Gitlab::CurrentSettings.spam_check_api_key })
verdict = convert_verdict_to_gitlab_constant(response.verdict)
[verdict, response.extra_attributes.to_h, response.error]
end
private
def get_spammable_mappings(spammable)
case spammable
when Issue
[::Spamcheck::Issue, grpc_client.method(:check_for_spam_issue)]
when Snippet
[::Spamcheck::Snippet, grpc_client.method(:check_for_spam_snippet)]
else
raise ArgumentError, "Not a spammable type: #{spammable.class.name}"
end
end
def convert_verdict_to_gitlab_constant(verdict)
VERDICT_MAPPING.fetch(::Spamcheck::SpamVerdict::Verdict.resolve(verdict), verdict)
end
def build_issue_protobuf(issue:, user:, context:)
issue_pb = ::Spamcheck::Issue.new
issue_pb.title = issue.spam_title || ''
issue_pb.description = issue.spam_description || ''
issue_pb.created_at = convert_to_pb_timestamp(issue.created_at) if issue.created_at
issue_pb.updated_at = convert_to_pb_timestamp(issue.updated_at) if issue.updated_at
issue_pb.user_in_project = user.authorized_project?(issue.project)
issue_pb.project = build_project_protobuf(issue)
issue_pb.action = ACTION_MAPPING.fetch(context.fetch(:action)) if context.has_key?(:action)
issue_pb.user = build_user_protobuf(user)
issue_pb
def build_protobuf(spammable:, user:, context:, extra_features:)
protobuf_class, grpc_method = get_spammable_mappings(spammable)
pb = protobuf_class.new(**extra_features)
pb.title = spammable.spam_title || ''
pb.description = spammable.spam_description || ''
pb.created_at = convert_to_pb_timestamp(spammable.created_at) if spammable.created_at
pb.updated_at = convert_to_pb_timestamp(spammable.updated_at) if spammable.updated_at
pb.action = ACTION_MAPPING.fetch(context.fetch(:action)) if context.has_key?(:action)
pb.user = build_user_protobuf(user)
unless spammable.project.nil?
pb.user_in_project = user.authorized_project?(spammable.project)
pb.project = build_project_protobuf(spammable)
end
[pb, grpc_method]
end
def build_user_protobuf(user)

View File

@ -22084,9 +22084,6 @@ msgstr ""
msgid "Iterations|Cadence name"
msgstr ""
msgid "Iterations|Can be converted"
msgstr ""
msgid "Iterations|Cancel"
msgstr ""
@ -22132,6 +22129,9 @@ msgstr ""
msgid "Iterations|Edit iteration cadence"
msgstr ""
msgid "Iterations|Enable automatic scheduling"
msgstr ""
msgid "Iterations|Enable roll over"
msgstr ""
@ -22147,12 +22147,6 @@ msgstr ""
msgid "Iterations|Iterations are scheduled to start on %{weekday}s."
msgstr ""
msgid "Iterations|Learn more about automatic scheduling"
msgstr ""
msgid "Iterations|Manual management of iterations will be deprecated in GitLab 15.6. Convert your manual cadence to use automated scheduling when you are ready."
msgstr ""
msgid "Iterations|Move incomplete issues to the next iteration."
msgstr ""
@ -22210,9 +22204,6 @@ msgstr ""
msgid "Iterations|The iteration has been deleted."
msgstr ""
msgid "Iterations|This cadence can be converted to use automated scheduling"
msgstr ""
msgid "Iterations|This will delete the cadence as well as all of the iterations within it."
msgstr ""
@ -22222,9 +22213,6 @@ msgstr ""
msgid "Iterations|Title"
msgstr ""
msgid "Iterations|To convert this cadence to automatic scheduling, add a duration and number of upcoming iterations. The upgrade is irreversible."
msgstr ""
msgid "Iterations|Unable to find iteration cadence."
msgstr ""
@ -22237,9 +22225,6 @@ msgstr ""
msgid "Iterations|Upcoming iterations"
msgstr ""
msgid "Iterations|Your manual cadence can be converted to use automated scheduling"
msgstr ""
msgid "Iteration|Dates cannot overlap with other existing Iterations within this group"
msgstr ""

View File

@ -217,6 +217,7 @@
"eslint-import-resolver-jest": "3.0.2",
"eslint-import-resolver-webpack": "0.13.2",
"eslint-plugin-no-jquery": "2.7.0",
"eslint-plugin-no-unsanitized": "^4.0.1",
"gettext-extractor": "^3.5.3",
"gettext-extractor-vue": "^5.0.0",
"glob": "^7.1.6",

View File

@ -77,44 +77,4 @@ RSpec.describe Gitlab::Seeder do
end
end
end
describe ::Gitlab::Seeder::Ci::DailyBuildGroupReportResult do
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, :repository, group: group) }
let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
let_it_be(:build) { create(:ci_build, :success, pipeline: pipeline) }
subject(:build_report) do
described_class.new(project)
end
describe '#seed' do
it 'creates daily build results for the project' do
expect { build_report.seed }.to change {
Ci::DailyBuildGroupReportResult.count
}.by(Gitlab::Seeder::Ci::DailyBuildGroupReportResult::COUNT_OF_DAYS)
end
it 'matches project data with last report' do
build_report.seed
report = project.daily_build_group_report_results.last
reports_count = project.daily_build_group_report_results.count
expect(build.group_name).to eq(report.group_name)
expect(pipeline.source_ref_path).to eq(report.ref_path)
expect(pipeline.default_branch?).to eq(report.default_branch)
expect(reports_count).to eq(Gitlab::Seeder::Ci::DailyBuildGroupReportResult::COUNT_OF_DAYS)
end
it 'does not raise error on RecordNotUnique' do
build_report.seed
build_report.seed
reports_count = project.daily_build_group_report_results.count
expect(reports_count).to eq(Gitlab::Seeder::Ci::DailyBuildGroupReportResult::COUNT_OF_DAYS)
end
end
end
end

View File

@ -0,0 +1,43 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe ::Gitlab::Seeders::Ci::DailyBuildGroupReportResult do
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, :repository, group: group) }
let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
let_it_be(:build) { create(:ci_build, :success, pipeline: pipeline) }
subject(:build_report) do
described_class.new(project)
end
describe '#seed' do
it 'creates daily build results for the project' do
expect { build_report.seed }.to change {
Ci::DailyBuildGroupReportResult.count
}.by(Gitlab::Seeders::Ci::DailyBuildGroupReportResult::COUNT_OF_DAYS)
end
it 'matches project data with last report' do
build_report.seed
report = project.daily_build_group_report_results.last
reports_count = project.daily_build_group_report_results.count
expect(build.group_name).to eq(report.group_name)
expect(pipeline.source_ref_path).to eq(report.ref_path)
expect(pipeline.default_branch?).to eq(report.default_branch)
expect(reports_count).to eq(Gitlab::Seeders::Ci::DailyBuildGroupReportResult::COUNT_OF_DAYS)
end
it 'does not raise error on RecordNotUnique' do
build_report.seed
build_report.seed
reports_count = project.daily_build_group_report_results.count
expect(reports_count).to eq(Gitlab::Seeders::Ci::DailyBuildGroupReportResult::COUNT_OF_DAYS)
end
end
end

View File

@ -17,6 +17,7 @@ RSpec.describe Gitlab::Spamcheck::Client do
end
let_it_be(:issue) { create(:issue, description: 'Test issue description') }
let_it_be(:snippet) { create(:personal_snippet, :public, description: 'Test issue description') }
let(:response) do
verdict = ::Spamcheck::SpamVerdict.new
@ -26,7 +27,7 @@ RSpec.describe Gitlab::Spamcheck::Client do
verdict
end
subject { described_class.new.issue_spam?(spam_issue: issue, user: user) }
subject { described_class.new.spam?(spammable: issue, user: user) }
before do
stub_application_setting(spam_check_endpoint_url: endpoint)
@ -56,10 +57,11 @@ RSpec.describe Gitlab::Spamcheck::Client do
end
end
describe '#issue_spam?' do
shared_examples 'check for spam' do
before do
allow_next_instance_of(::Spamcheck::SpamcheckService::Stub) do |instance|
allow(instance).to receive(:check_for_spam_issue).and_return(response)
allow(instance).to receive(:check_for_spam_snippet).and_return(response)
end
end
@ -89,12 +91,26 @@ RSpec.describe Gitlab::Spamcheck::Client do
end
end
describe "#build_issue_protobuf", :aggregate_failures do
it 'builds the expected protobuf object' do
describe "#spam?", :aggregate_failures do
describe 'issue' do
subject { described_class.new.spam?(spammable: issue, user: user) }
it_behaves_like "check for spam"
end
describe 'snippet' do
subject { described_class.new.spam?(spammable: snippet, user: user, extra_features: { files: [{ path: "file.rb" }] }) }
it_behaves_like "check for spam"
end
end
describe "#build_protobuf", :aggregate_failures do
it 'builds the expected issue protobuf object' do
cxt = { action: :create }
issue_pb = described_class.new.send(:build_issue_protobuf,
issue: issue, user: user,
context: cxt)
issue_pb, _ = described_class.new.send(:build_protobuf,
spammable: issue, user: user,
context: cxt, extra_features: {})
expect(issue_pb.title).to eq issue.title
expect(issue_pb.description).to eq issue.description
expect(issue_pb.user_in_project).to be false
@ -104,6 +120,22 @@ RSpec.describe Gitlab::Spamcheck::Client do
expect(issue_pb.action).to be ::Spamcheck::Action.lookup(::Spamcheck::Action::CREATE)
expect(issue_pb.user.username).to eq user.username
end
it 'builds the expected snippet protobuf object' do
cxt = { action: :create }
snippet_pb, _ = described_class.new.send(:build_protobuf,
spammable: snippet, user: user,
context: cxt, extra_features: { files: [{ path: 'first.rb' }, { path: 'second.rb' }] })
expect(snippet_pb.title).to eq snippet.title
expect(snippet_pb.description).to eq snippet.description
expect(snippet_pb.created_at).to eq timestamp_to_protobuf_timestamp(snippet.created_at)
expect(snippet_pb.updated_at).to eq timestamp_to_protobuf_timestamp(snippet.updated_at)
expect(snippet_pb.action).to be ::Spamcheck::Action.lookup(::Spamcheck::Action::CREATE)
expect(snippet_pb.user.username).to eq user.username
expect(snippet_pb.user.username).to eq user.username
expect(snippet_pb.files.first.path).to eq 'first.rb'
expect(snippet_pb.files.last.path).to eq 'second.rb'
end
end
describe '#build_user_protobuf', :aggregate_failures do
@ -143,6 +175,19 @@ RSpec.describe Gitlab::Spamcheck::Client do
end
end
describe "#get_spammable_mappings", :aggregate_failures do
it 'is an expected spammable' do
protobuf_class, _ = described_class.new.send(:get_spammable_mappings, issue)
expect(protobuf_class).to eq ::Spamcheck::Issue
end
it 'is an unexpected spammable' do
expect { described_class.new.send(:get_spammable_mappings, 'spam') }.to raise_error(
ArgumentError, 'Not a spammable type: String'
)
end
end
private
def timestamp_to_protobuf_timestamp(timestamp)

View File

@ -256,6 +256,7 @@ RSpec.describe API::ProjectSnippets do
allow_next_instance_of(Spam::AkismetService) do |instance|
allow(instance).to receive(:spam?).and_return(true)
end
stub_feature_flags(allow_possible_spam: false)
project.add_developer(user)
end
@ -311,6 +312,8 @@ RSpec.describe API::ProjectSnippets do
allow_next_instance_of(Spam::AkismetService) do |instance|
allow(instance).to receive(:spam?).and_return(true)
end
stub_feature_flags(allow_possible_spam: false)
end
context 'when the snippet is private' do

View File

@ -340,6 +340,7 @@ RSpec.describe API::Snippets, factory_default: :keep do
allow_next_instance_of(Spam::AkismetService) do |instance|
allow(instance).to receive(:spam?).and_return(true)
end
stub_feature_flags(allow_possible_spam: false)
end
context 'when the snippet is private' do
@ -405,6 +406,7 @@ RSpec.describe API::Snippets, factory_default: :keep do
allow_next_instance_of(Spam::AkismetService) do |instance|
allow(instance).to receive(:spam?).and_return(true)
end
stub_feature_flags(allow_possible_spam: false)
end
context 'when the snippet is private' do

View File

@ -18,7 +18,7 @@ RSpec.describe JiraConnect::SubscriptionsController do
subject(:content_security_policy) { response.headers['Content-Security-Policy'] }
it { is_expected.to include('http://self-managed-gitlab.com/-/jira_connect/oauth_application_ids') }
it { is_expected.to include('http://self-managed-gitlab.com/-/jira_connect/oauth_application_id') }
context 'with no self-managed instance configured' do
let_it_be(:installation) { create(:jira_connect_installation, instance_url: '') }

View File

@ -6,6 +6,8 @@ RSpec.describe Spam::SpamActionService do
include_context 'includes Spam constants'
let(:issue) { create(:issue, project: project, author: author) }
let(:personal_snippet) { create(:personal_snippet, :public, author: author) }
let(:project_snippet) { create(:project_snippet, :public, author: author) }
let(:fake_ip) { '1.2.3.4' }
let(:fake_user_agent) { 'fake-user-agent' }
let(:fake_referer) { 'fake-http-referer' }
@ -27,6 +29,7 @@ RSpec.describe Spam::SpamActionService do
before do
issue.spam = false
personal_snippet.spam = false
end
describe 'constructor argument validation' do
@ -50,24 +53,24 @@ RSpec.describe Spam::SpamActionService do
end
end
shared_examples 'creates a spam log' do
shared_examples 'creates a spam log' do |target_type|
it do
expect { subject }
.to log_spam(title: issue.title, description: issue.description, noteable_type: 'Issue')
.to log_spam(title: target.title, description: target.description, noteable_type: target_type)
# TODO: These checks should be incorporated into the `log_spam` RSpec matcher above
new_spam_log = SpamLog.last
expect(new_spam_log.user_id).to eq(user.id)
expect(new_spam_log.title).to eq(issue.title)
expect(new_spam_log.description).to eq(issue.description)
expect(new_spam_log.title).to eq(target.title)
expect(new_spam_log.description).to eq(target.spam_description)
expect(new_spam_log.source_ip).to eq(fake_ip)
expect(new_spam_log.user_agent).to eq(fake_user_agent)
expect(new_spam_log.noteable_type).to eq('Issue')
expect(new_spam_log.noteable_type).to eq(target_type)
expect(new_spam_log.via_api).to eq(true)
end
end
describe '#execute' do
shared_examples 'execute spam action service' do |target_type|
let(:fake_captcha_verification_service) { double(:captcha_verification_service) }
let(:fake_verdict_service) { double(:spam_verdict_service) }
let(:allowlisted) { false }
@ -82,20 +85,22 @@ RSpec.describe Spam::SpamActionService do
let(:verdict_service_args) do
{
target: issue,
target: target,
user: user,
options: verdict_service_opts,
context: {
action: :create,
target_type: 'Issue'
}
target_type: target_type
},
extra_features: extra_features
}
end
let_it_be(:existing_spam_log) { create(:spam_log, user: user, recaptcha_verified: false) }
subject do
described_service = described_class.new(spammable: issue, spam_params: spam_params, user: user, action: :create)
described_service = described_class.new(spammable: target, spam_params: spam_params, extra_features:
extra_features, user: user, action: :create)
allow(described_service).to receive(:allowlisted?).and_return(allowlisted)
described_service.execute
end
@ -136,7 +141,7 @@ RSpec.describe Spam::SpamActionService do
context 'when spammable attributes have not changed' do
before do
issue.closed_at = Time.zone.now
allow(target).to receive(:has_changes_to_save?).and_return(true)
end
it 'does not create a spam log' do
@ -146,11 +151,11 @@ RSpec.describe Spam::SpamActionService do
context 'when spammable attributes have changed' do
let(:expected_service_check_response_message) do
/Check Issue spammable model for any errors or CAPTCHA requirement/
/Check #{target_type} spammable model for any errors or CAPTCHA requirement/
end
before do
issue.description = 'Lovely Spam! Wonderful Spam!'
target.description = 'Lovely Spam! Wonderful Spam!'
end
context 'when allowlisted' do
@ -170,13 +175,13 @@ RSpec.describe Spam::SpamActionService do
allow(fake_verdict_service).to receive(:execute).and_return(DISALLOW)
end
it_behaves_like 'creates a spam log'
it_behaves_like 'creates a spam log', target_type
it 'marks as spam' do
response = subject
expect(response.message).to match(expected_service_check_response_message)
expect(issue).to be_spam
expect(target).to be_spam
end
end
@ -185,13 +190,13 @@ RSpec.describe Spam::SpamActionService do
allow(fake_verdict_service).to receive(:execute).and_return(BLOCK_USER)
end
it_behaves_like 'creates a spam log'
it_behaves_like 'creates a spam log', target_type
it 'marks as spam' do
response = subject
expect(response.message).to match(expected_service_check_response_message)
expect(issue).to be_spam
expect(target).to be_spam
end
end
@ -200,20 +205,20 @@ RSpec.describe Spam::SpamActionService do
allow(fake_verdict_service).to receive(:execute).and_return(CONDITIONAL_ALLOW)
end
it_behaves_like 'creates a spam log'
it_behaves_like 'creates a spam log', target_type
it 'does not mark as spam' do
response = subject
expect(response.message).to match(expected_service_check_response_message)
expect(issue).not_to be_spam
expect(target).not_to be_spam
end
it 'marks as needing reCAPTCHA' do
response = subject
expect(response.message).to match(expected_service_check_response_message)
expect(issue).to be_needs_recaptcha
expect(target).to be_needs_recaptcha
end
end
@ -222,20 +227,20 @@ RSpec.describe Spam::SpamActionService do
allow(fake_verdict_service).to receive(:execute).and_return(OVERRIDE_VIA_ALLOW_POSSIBLE_SPAM)
end
it_behaves_like 'creates a spam log'
it_behaves_like 'creates a spam log', target_type
it 'does not mark as spam' do
response = subject
expect(response.message).to match(expected_service_check_response_message)
expect(issue).not_to be_spam
expect(target).not_to be_spam
end
it 'does not mark as needing CAPTCHA' do
response = subject
expect(response.message).to match(expected_service_check_response_message)
expect(issue).not_to be_needs_recaptcha
expect(target).not_to be_needs_recaptcha
end
end
@ -249,7 +254,7 @@ RSpec.describe Spam::SpamActionService do
end
it 'clears spam flags' do
expect(issue).to receive(:clear_spam_flags!)
expect(target).to receive(:clear_spam_flags!)
subject
end
@ -265,7 +270,7 @@ RSpec.describe Spam::SpamActionService do
end
it 'clears spam flags' do
expect(issue).to receive(:clear_spam_flags!)
expect(target).to receive(:clear_spam_flags!)
subject
end
@ -285,4 +290,27 @@ RSpec.describe Spam::SpamActionService do
end
end
end
describe '#execute' do
describe 'issue' do
let(:target) { issue }
let(:extra_features) { {} }
it_behaves_like 'execute spam action service', 'Issue'
end
describe 'project snippet' do
let(:target) { project_snippet }
let(:extra_features) { { files: [{ path: 'project.rb' }] } }
it_behaves_like 'execute spam action service', 'ProjectSnippet'
end
describe 'personal snippet' do
let(:target) { personal_snippet }
let(:extra_features) { { files: [{ path: 'personal.rb' }] } }
it_behaves_like 'execute spam action service', 'PersonalSnippet'
end
end
end

View File

@ -17,9 +17,10 @@ RSpec.describe Spam::SpamVerdictService do
let(:check_for_spam) { true }
let_it_be(:user) { create(:user) }
let_it_be(:issue) { create(:issue, author: user) }
let_it_be(:snippet) { create(:personal_snippet, :public, author: user) }
let(:service) do
described_class.new(user: user, target: issue, options: {})
described_class.new(user: user, target: target, options: {})
end
let(:attribs) do
@ -31,7 +32,7 @@ RSpec.describe Spam::SpamVerdictService do
stub_feature_flags(allow_possible_spam: false)
end
describe '#execute' do
shared_examples 'execute spam verdict service' do
subject { service.execute }
before do
@ -172,7 +173,8 @@ RSpec.describe Spam::SpamVerdictService do
end
end
describe '#akismet_verdict' do
shared_examples 'akismet verdict' do
let(:target) { issue }
subject { service.send(:akismet_verdict) }
context 'if Akismet is enabled' do
@ -227,7 +229,7 @@ RSpec.describe Spam::SpamVerdictService do
end
end
describe '#spamcheck_verdict' do
shared_examples 'spamcheck verdict' do
subject { service.send(:spamcheck_verdict) }
context 'if a Spam Check endpoint enabled and set to a URL' do
@ -254,7 +256,7 @@ RSpec.describe Spam::SpamVerdictService do
before do
allow(service).to receive(:spamcheck_client).and_return(spam_client)
allow(spam_client).to receive(:issue_spam?).and_return([verdict, attribs, error])
allow(spam_client).to receive(:spam?).and_return([verdict, attribs, error])
end
context 'if the result is a NOOP verdict' do
@ -365,7 +367,7 @@ RSpec.describe Spam::SpamVerdictService do
let(:attribs) { nil }
before do
allow(spam_client).to receive(:issue_spam?).and_raise(GRPC::Aborted)
allow(spam_client).to receive(:spam?).and_raise(GRPC::Aborted)
end
it 'returns nil' do
@ -387,7 +389,7 @@ RSpec.describe Spam::SpamVerdictService do
let(:attribs) { nil }
before do
allow(spam_client).to receive(:issue_spam?).and_raise(GRPC::DeadlineExceeded)
allow(spam_client).to receive(:spam?).and_raise(GRPC::DeadlineExceeded)
end
it 'returns nil' do
@ -416,4 +418,46 @@ RSpec.describe Spam::SpamVerdictService do
end
end
end
describe '#execute' do
describe 'issue' do
let(:target) { issue }
it_behaves_like 'execute spam verdict service'
end
describe 'snippet' do
let(:target) { snippet }
it_behaves_like 'execute spam verdict service'
end
end
describe '#akismet_verdict' do
describe 'issue' do
let(:target) { issue }
it_behaves_like 'akismet verdict'
end
describe 'snippet' do
let(:target) { snippet }
it_behaves_like 'akismet verdict'
end
end
describe '#spamcheck_verdict' do
describe 'issue' do
let(:target) { issue }
it_behaves_like 'spamcheck verdict'
end
describe 'snippet' do
let(:target) { snippet }
it_behaves_like 'spamcheck verdict'
end
end
end

View File

@ -14,7 +14,8 @@ RSpec.shared_examples 'checking spam' do
spammable: kind_of(Snippet),
spam_params: spam_params,
user: an_instance_of(User),
action: action
action: action,
extra_features: { files: an_instance_of(Array) }
}
) do |instance|
expect(instance).to receive(:execute)

View File

@ -5192,6 +5192,11 @@ eslint-plugin-no-jquery@2.7.0:
resolved "https://registry.yarnpkg.com/eslint-plugin-no-jquery/-/eslint-plugin-no-jquery-2.7.0.tgz#855f5631cf5b8e25b930cf6f06e02dd81f132e72"
integrity sha512-Aeg7dA6GTH1AcWLlBtWNzOU9efK5KpNi7b0EhBO0o0M+awyzguUUo8gF6hXGjQ9n5h8/uRtYv9zOqQkeC5CG0w==
eslint-plugin-no-unsanitized@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-no-unsanitized/-/eslint-plugin-no-unsanitized-4.0.1.tgz#e2343265467ba2270ade478cbe07bbafeaea412d"
integrity sha512-y/lAMWnPPC7RYuUdxlEL/XiCL8FehN9h9s3Kjqbp/Kv0i9NZs+IXSC2kS546Fa4Bumwy31HlVS/OdWX0Kxb5Xg==
eslint-plugin-promise@^4.2.1:
version "4.2.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-4.2.1.tgz#845fd8b2260ad8f82564c1222fce44ad71d9418a"