diff --git a/.eslintrc.yml b/.eslintrc.yml index af2f1d88938..659ed2a0010 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -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/**/*' diff --git a/Gemfile b/Gemfile index 245a57935dc..fb8e53be369 100644 --- a/Gemfile +++ b/Gemfile @@ -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' diff --git a/Gemfile.lock b/Gemfile.lock index 6f676dbef2e..d9e9b2f04a8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -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) diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js index a030797c698..a3ffb4df7b7 100644 --- a/app/assets/javascripts/awards_handler.js +++ b/app/assets/javascripts/awards_handler.js @@ -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(); }); diff --git a/app/assets/javascripts/batch_comments/components/preview_item.vue b/app/assets/javascripts/batch_comments/components/preview_item.vue index 0eb4e6e7709..71560c7de3a 100644 --- a/app/assets/javascripts/batch_comments/components/preview_item.vue +++ b/app/assets/javascripts/batch_comments/components/preview_item.vue @@ -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; diff --git a/app/assets/javascripts/behaviors/copy_code.js b/app/assets/javascripts/behaviors/copy_code.js index 6d2a4c245cc..a653769b60f 100644 --- a/app/assets/javascripts/behaviors/copy_code.js +++ b/app/assets/javascripts/behaviors/copy_code.js @@ -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; diff --git a/app/assets/javascripts/behaviors/markdown/render_math.js b/app/assets/javascripts/behaviors/markdown/render_math.js index af7aac4cf36..ac41af4df7a 100644 --- a/app/assets/javascripts/behaviors/markdown/render_math.js +++ b/app/assets/javascripts/behaviors/markdown/render_math.js @@ -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, diff --git a/app/assets/javascripts/blob/viewer/index.js b/app/assets/javascripts/blob/viewer/index.js index a0d4f7ef4f2..5ca3f131d99 100644 --- a/app/assets/javascripts/blob/viewer/index.js +++ b/app/assets/javascripts/blob/viewer/index.js @@ -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(() => { diff --git a/app/assets/javascripts/code_navigation/utils/dom_utils.js b/app/assets/javascripts/code_navigation/utils/dom_utils.js index 1a65c1a64a2..90af31b715c 100644 --- a/app/assets/javascripts/code_navigation/utils/dom_utils.js +++ b/app/assets/javascripts/code_navigation/utils/dom_utils.js @@ -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; diff --git a/app/assets/javascripts/deprecated_jquery_dropdown/render.js b/app/assets/javascripts/deprecated_jquery_dropdown/render.js index f10c2d82b61..0f612989bb4 100644 --- a/app/assets/javascripts/deprecated_jquery_dropdown/render.js +++ b/app/assets/javascripts/deprecated_jquery_dropdown/render.js @@ -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; diff --git a/app/assets/javascripts/filterable_list.js b/app/assets/javascripts/filterable_list.js index a8670caf5b2..a6781cffaec 100644 --- a/app/assets/javascripts/filterable_list.js +++ b/app/assets/javascripts/filterable_list.js @@ -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; } diff --git a/app/assets/javascripts/filtered_search/dropdown_emoji.js b/app/assets/javascripts/filtered_search/dropdown_emoji.js index 5adc074b3ce..aeea66bf51c 100644 --- a/app/assets/javascripts/filtered_search/dropdown_emoji.js +++ b/app/assets/javascripts/filtered_search/dropdown_emoji.js @@ -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; } }); diff --git a/app/assets/javascripts/filtered_search/droplab/drop_down.js b/app/assets/javascripts/filtered_search/droplab/drop_down.js index 398a7b26677..e7edc678773 100644 --- a/app/assets/javascripts/filtered_search/droplab/drop_down.js +++ b/app/assets/javascripts/filtered_search/droplab/drop_down.js @@ -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'; diff --git a/app/assets/javascripts/filtered_search/droplab/hook_button.js b/app/assets/javascripts/filtered_search/droplab/hook_button.js index c51d6167fa3..805905e7750 100644 --- a/app/assets/javascripts/filtered_search/droplab/hook_button.js +++ b/app/assets/javascripts/filtered_search/droplab/hook_button.js @@ -42,6 +42,7 @@ class HookButton extends Hook { } restoreInitialState() { + // eslint-disable-next-line no-unsanitized/property this.list.list.innerHTML = this.list.initialState; } diff --git a/app/assets/javascripts/filtered_search/droplab/hook_input.js b/app/assets/javascripts/filtered_search/droplab/hook_input.js index c523dae347f..32dfe0372bb 100644 --- a/app/assets/javascripts/filtered_search/droplab/hook_input.js +++ b/app/assets/javascripts/filtered_search/droplab/hook_input.js @@ -97,6 +97,7 @@ class HookInput extends Hook { } restoreInitialState() { + // eslint-disable-next-line no-unsanitized/property this.list.list.innerHTML = this.list.initialState; } diff --git a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js index 7143cb50ea6..0c01220a7be 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js +++ b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js @@ -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 = '
'; } + // 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); diff --git a/app/assets/javascripts/filtered_search/visual_token_value.js b/app/assets/javascripts/filtered_search/visual_token_value.js index 707add10009..0d144398531 100644 --- a/app/assets/javascripts/filtered_search/visual_token_value.js +++ b/app/assets/javascripts/filtered_search/visual_token_value.js @@ -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 = ` ${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); }); } diff --git a/app/assets/javascripts/flash.js b/app/assets/javascripts/flash.js index 5a47e76d597..edf83a33812 100644 --- a/app/assets/javascripts/flash.js +++ b/app/assets/javascripts/flash.js @@ -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) { diff --git a/app/assets/javascripts/image_diff/helpers/badge_helper.js b/app/assets/javascripts/image_diff/helpers/badge_helper.js index 5ff00394e3b..35d8ec32bdf 100644 --- a/app/assets/javascripts/image_diff/helpers/badge_helper.js +++ b/app/assets/javascripts/image_diff/helpers/badge_helper.js @@ -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); diff --git a/app/assets/javascripts/image_diff/helpers/comment_indicator_helper.js b/app/assets/javascripts/image_diff/helpers/comment_indicator_helper.js index deaef686f59..2b5cb70737f 100644 --- a/app/assets/javascripts/image_diff/helpers/comment_indicator_helper.js +++ b/app/assets/javascripts/image_diff/helpers/comment_indicator_helper.js @@ -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); diff --git a/app/assets/javascripts/issues/show/components/description.vue b/app/assets/javascripts/issues/show/components/description.vue index a6747d67611..e597144cf67 100644 --- a/app/assets/javascripts/issues/show/components/description.vue +++ b/app/assets/javascripts/issues/show/components/description.vue @@ -226,6 +226,7 @@ export default { }, createDragIconElement() { const container = document.createElement('div'); + // eslint-disable-next-line no-unsanitized/property container.innerHTML = ``; @@ -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 = `
Configure it later`; 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(); diff --git a/app/assets/javascripts/pages/users/user_overview_block.js b/app/assets/javascripts/pages/users/user_overview_block.js index a7c3c9d104d..8d2d66d812e 100644 --- a/app/assets/javascripts/pages/users/user_overview_block.js +++ b/app/assets/javascripts/pages/users/user_overview_block.js @@ -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'); diff --git a/app/assets/javascripts/projects/project_visibility.js b/app/assets/javascripts/projects/project_visibility.js index b8ac17a01f2..d1343f07f1d 100644 --- a/app/assets/javascripts/projects/project_visibility.js +++ b/app/assets/javascripts/projects/project_visibility.js @@ -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.', diff --git a/app/assets/javascripts/projects/star.js b/app/assets/javascripts/projects/star.js index 5bbace11b15..e063064663b 100644 --- a/app/assets/javascripts/projects/star.js +++ b/app/assets/javascripts/projects/star.js @@ -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)); } }) diff --git a/app/assets/javascripts/sidebar/components/participants/sidebar_participants_widget.vue b/app/assets/javascripts/sidebar/components/participants/sidebar_participants_widget.vue index a09138a708b..f266d8791fd 100644 --- a/app/assets/javascripts/sidebar/components/participants/sidebar_participants_widget.vue +++ b/app/assets/javascripts/sidebar/components/participants/sidebar_participants_widget.vue @@ -49,6 +49,9 @@ export default { error, }); }, + context: { + isSingleRequest: true, + }, }, }, computed: { diff --git a/app/assets/javascripts/validators/input_validator.js b/app/assets/javascripts/validators/input_validator.js index f37373977b8..b799976a0ba 100644 --- a/app/assets/javascripts/validators/input_validator.js +++ b/app/assets/javascripts/validators/input_validator.js @@ -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; } } diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue index 1e25143e15c..fef8b9ea7c6 100644 --- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue +++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue @@ -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')); diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue index de3eda6b04f..9b81444fc04 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue @@ -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(); diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/wrap_comments.js b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/wrap_comments.js index 5be92af5b55..8b52df83fdf 100644 --- a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/wrap_comments.js +++ b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/wrap_comments.js @@ -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; }; diff --git a/app/assets/javascripts/vue_shared/new_namespace/components/legacy_container.vue b/app/assets/javascripts/vue_shared/new_namespace/components/legacy_container.vue index d2fc2c66924..e42720bf1db 100644 --- a/app/assets/javascripts/vue_shared/new_namespace/components/legacy_container.vue +++ b/app/assets/javascripts/vue_shared/new_namespace/components/legacy_container.vue @@ -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; diff --git a/app/assets/javascripts/webpack_non_compiled_placeholder.js b/app/assets/javascripts/webpack_non_compiled_placeholder.js index af671e72129..c1baa7b8dd3 100644 --- a/app/assets/javascripts/webpack_non_compiled_placeholder.js +++ b/app/assets/javascripts/webpack_non_compiled_placeholder.js @@ -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 = ` diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 37f92d3cf3d..ffefc5fead5 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -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; diff --git a/app/assets/stylesheets/startup/startup-dark.scss b/app/assets/stylesheets/startup/startup-dark.scss index 853b421dc27..d1817e4df20 100644 --- a/app/assets/stylesheets/startup/startup-dark.scss +++ b/app/assets/stylesheets/startup/startup-dark.scss @@ -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; } diff --git a/app/assets/stylesheets/startup/startup-general.scss b/app/assets/stylesheets/startup/startup-general.scss index e4fc07d6d62..717ba631450 100644 --- a/app/assets/stylesheets/startup/startup-general.scss +++ b/app/assets/stylesheets/startup/startup-general.scss @@ -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; } diff --git a/app/controllers/jira_connect/subscriptions_controller.rb b/app/controllers/jira_connect/subscriptions_controller.rb index 623113f8413..c29a7aae082 100644 --- a/app/controllers/jira_connect/subscriptions_controller.rb +++ b/app/controllers/jira_connect/subscriptions_controller.rb @@ -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 diff --git a/app/models/snippet.rb b/app/models/snippet.rb index fd882633a44..55dcc39e72c 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -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 diff --git a/app/services/snippets/base_service.rb b/app/services/snippets/base_service.rb index 1a04c4fcedd..42e62d65ee4 100644 --- a/app/services/snippets/base_service.rb +++ b/app/services/snippets/base_service.rb @@ -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 diff --git a/app/services/snippets/create_service.rb b/app/services/snippets/create_service.rb index 6d3b63de9fd..e0bab4cd6ad 100644 --- a/app/services/snippets/create_service.rb +++ b/app/services/snippets/create_service.rb @@ -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 diff --git a/app/services/snippets/update_service.rb b/app/services/snippets/update_service.rb index 76d5063c337..067680f2abc 100644 --- a/app/services/snippets/update_service.rb +++ b/app/services/snippets/update_service.rb @@ -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) diff --git a/app/services/spam/spam_action_service.rb b/app/services/spam/spam_action_service.rb index 4fa9c0e4993..9c52e9f0cd3 100644 --- a/app/services/spam/spam_action_service.rb +++ b/app/services/spam/spam_action_service.rb @@ -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 diff --git a/app/services/spam/spam_verdict_service.rb b/app/services/spam/spam_verdict_service.rb index e73b2666c02..382545556ab 100644 --- a/app/services/spam/spam_verdict_service.rb +++ b/app/services/spam/spam_verdict_service.rb @@ -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 diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index 783733bb313..c876d82f49c 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -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 diff --git a/data/deprecations/14-10-manual-iteration-management.yml b/data/deprecations/14-10-manual-iteration-management.yml deleted file mode 100644 index f677f4fe668..00000000000 --- a/data/deprecations/14-10-manual-iteration-management.yml +++ /dev/null @@ -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 diff --git a/db/fixtures/development/14_pipelines.rb b/db/fixtures/development/14_pipelines.rb index e8fa3ee2110..03a7714fcbd 100644 --- a/db/fixtures/development/14_pipelines.rb +++ b/db/fixtures/development/14_pipelines.rb @@ -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 diff --git a/doc/update/deprecations.md b/doc/update/deprecations.md index 79ff4a09160..fdffd8113f8 100644 --- a/doc/update/deprecations.md +++ b/doc/update/deprecations.md @@ -361,38 +361,6 @@ In GitLab 15.0, for Dependency Scanning, the default version of Java that the sc -
- -### Manual iteration management - -Planned removal: GitLab 16.0 (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). - -
-
### Outdated indices of Advanced Search migrations diff --git a/doc/user/group/iterations/index.md b/doc/user/group/iterations/index.md index 530635802a6..a5102d27302 100644 --- a/doc/user/group/iterations/index.md +++ b/doc/user/group/iterations/index.md @@ -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**. diff --git a/doc/user/project/issues/img/issue_weight_v13_11.png b/doc/user/project/issues/img/issue_weight_v13_11.png deleted file mode 100644 index 842c154ea49..00000000000 Binary files a/doc/user/project/issues/img/issue_weight_v13_11.png and /dev/null differ diff --git a/doc/user/project/issues/issue_weight.md b/doc/user/project/issues/issue_weight.md index fcc53a239dc..a2020ca7a0a 100644 --- a/doc/user/project/issues/issue_weight.md +++ b/doc/user/project/issues/issue_weight.md @@ -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**. diff --git a/lib/gitlab/seeder.rb b/lib/gitlab/seeder.rb index 2450ad88bbb..ec514adafc8 100644 --- a/lib/gitlab/seeder.rb +++ b/lib/gitlab/seeder.rb @@ -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: diff --git a/lib/gitlab/seeders/ci/daily_build_group_report_result.rb b/lib/gitlab/seeders/ci/daily_build_group_report_result.rb new file mode 100644 index 00000000000..10ec65f6bf4 --- /dev/null +++ b/lib/gitlab/seeders/ci/daily_build_group_report_result.rb @@ -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 diff --git a/lib/gitlab/spamcheck/client.rb b/lib/gitlab/spamcheck/client.rb index 40b01552244..b7ac6224e5c 100644 --- a/lib/gitlab/spamcheck/client.rb +++ b/lib/gitlab/spamcheck/client.rb @@ -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) diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 568e4964fac..c98e0d3e65d 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -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 "" diff --git a/package.json b/package.json index 91ccbab5f08..fd9818f267f 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/spec/lib/gitlab/seeder_spec.rb b/spec/lib/gitlab/seeder_spec.rb index 0ad80323085..a94ae2bca7a 100644 --- a/spec/lib/gitlab/seeder_spec.rb +++ b/spec/lib/gitlab/seeder_spec.rb @@ -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 diff --git a/spec/lib/gitlab/seeders/ci/daily_build_group_report_result_spec.rb b/spec/lib/gitlab/seeders/ci/daily_build_group_report_result_spec.rb new file mode 100644 index 00000000000..4b41122d23c --- /dev/null +++ b/spec/lib/gitlab/seeders/ci/daily_build_group_report_result_spec.rb @@ -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 diff --git a/spec/lib/gitlab/spamcheck/client_spec.rb b/spec/lib/gitlab/spamcheck/client_spec.rb index 956ed2a976f..2fe978125c4 100644 --- a/spec/lib/gitlab/spamcheck/client_spec.rb +++ b/spec/lib/gitlab/spamcheck/client_spec.rb @@ -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) diff --git a/spec/requests/api/project_snippets_spec.rb b/spec/requests/api/project_snippets_spec.rb index 72519ed1683..6e2dd6e76a9 100644 --- a/spec/requests/api/project_snippets_spec.rb +++ b/spec/requests/api/project_snippets_spec.rb @@ -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 diff --git a/spec/requests/api/snippets_spec.rb b/spec/requests/api/snippets_spec.rb index 0dd6e484e8d..031bcb612f4 100644 --- a/spec/requests/api/snippets_spec.rb +++ b/spec/requests/api/snippets_spec.rb @@ -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 diff --git a/spec/requests/jira_connect/subscriptions_controller_spec.rb b/spec/requests/jira_connect/subscriptions_controller_spec.rb index d8f329f13f5..f6f21d3458f 100644 --- a/spec/requests/jira_connect/subscriptions_controller_spec.rb +++ b/spec/requests/jira_connect/subscriptions_controller_spec.rb @@ -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: '') } diff --git a/spec/services/spam/spam_action_service_spec.rb b/spec/services/spam/spam_action_service_spec.rb index bd8418d7092..4dfec9735ba 100644 --- a/spec/services/spam/spam_action_service_spec.rb +++ b/spec/services/spam/spam_action_service_spec.rb @@ -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 diff --git a/spec/services/spam/spam_verdict_service_spec.rb b/spec/services/spam/spam_verdict_service_spec.rb index 082b8f909f9..02dbc1004bf 100644 --- a/spec/services/spam/spam_verdict_service_spec.rb +++ b/spec/services/spam/spam_verdict_service_spec.rb @@ -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 diff --git a/spec/support/shared_examples/services/snippets_shared_examples.rb b/spec/support/shared_examples/services/snippets_shared_examples.rb index 7629cfa976d..65893d84798 100644 --- a/spec/support/shared_examples/services/snippets_shared_examples.rb +++ b/spec/support/shared_examples/services/snippets_shared_examples.rb @@ -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) diff --git a/yarn.lock b/yarn.lock index 615031ba714..2c8ce508616 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"