Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-04-08 18:08:29 +00:00
parent 9f9dc2bc41
commit 842ac3526c
58 changed files with 775 additions and 249 deletions

View File

@ -805,29 +805,29 @@ lib/gitlab/checks/** @proglottis @toon @zj-gitlab
/doc/user/workspace/index.md @fneill
[Authentication and Authorization]
app/**/*password* @gitlab-org/manage/authentication-and-authorization
ee/app/**/*password* @gitlab-org/manage/authentication-and-authorization
config/**/*password* @gitlab-org/manage/authentication-and-authorization
ee/config/**/*password* @gitlab-org/manage/authentication-and-authorization
lib/**/*password* @gitlab-org/manage/authentication-and-authorization
ee/lib/**/*password* @gitlab-org/manage/authentication-and-authorization
app/controllers/**/*password* @gitlab-org/manage/authentication-and-authorization
ee/app/controllers/**/*password* @gitlab-org/manage/authentication-and-authorization
/app/**/*password* @gitlab-org/manage/authentication-and-authorization
/ee/app/**/*password* @gitlab-org/manage/authentication-and-authorization
/config/**/*password* @gitlab-org/manage/authentication-and-authorization
/ee/config/**/*password* @gitlab-org/manage/authentication-and-authorization
/lib/**/*password* @gitlab-org/manage/authentication-and-authorization
/ee/lib/**/*password* @gitlab-org/manage/authentication-and-authorization
/app/controllers/**/*password* @gitlab-org/manage/authentication-and-authorization
/ee/app/controllers/**/*password* @gitlab-org/manage/authentication-and-authorization
app/**/*auth* @gitlab-org/manage/authentication-and-authorization
ee/app/**/*auth* @gitlab-org/manage/authentication-and-authorization
config/**/*auth* @gitlab-org/manage/authentication-and-authorization
ee/config/**/*auth* @gitlab-org/manage/authentication-and-authorization
lib/**/*auth* @gitlab-org/manage/authentication-and-authorization
ee/lib/**/*auth* @gitlab-org/manage/authentication-and-authorization
app/controllers/**/*auth* @gitlab-org/manage/authentication-and-authorization
ee/app/controllers/**/*auth* @gitlab-org/manage/authentication-and-authorization
/app/**/*auth* @gitlab-org/manage/authentication-and-authorization
/ee/app/**/*auth* @gitlab-org/manage/authentication-and-authorization
/config/**/*auth* @gitlab-org/manage/authentication-and-authorization
/ee/config/**/*auth* @gitlab-org/manage/authentication-and-authorization
/lib/**/*auth* @gitlab-org/manage/authentication-and-authorization
/ee/lib/**/*auth* @gitlab-org/manage/authentication-and-authorization
/app/controllers/**/*auth* @gitlab-org/manage/authentication-and-authorization
/ee/app/controllers/**/*auth* @gitlab-org/manage/authentication-and-authorization
app/**/*token* @gitlab-org/manage/authentication-and-authorization
ee/app/**/*token* @gitlab-org/manage/authentication-and-authorization
config/**/*token* @gitlab-org/manage/authentication-and-authorization
ee/config/**/*token* @gitlab-org/manage/authentication-and-authorization
lib/**/*token* @gitlab-org/manage/authentication-and-authorization
ee/lib/**/*token* @gitlab-org/manage/authentication-and-authorization
app/controllers/**/*token* @gitlab-org/manage/authentication-and-authorization
ee/app/controllers/**/*token* @gitlab-org/manage/authentication-and-authorization
/app/**/*token* @gitlab-org/manage/authentication-and-authorization
/ee/app/**/*token* @gitlab-org/manage/authentication-and-authorization
/config/**/*token* @gitlab-org/manage/authentication-and-authorization
/ee/config/**/*token* @gitlab-org/manage/authentication-and-authorization
/lib/**/*token* @gitlab-org/manage/authentication-and-authorization
/ee/lib/**/*token* @gitlab-org/manage/authentication-and-authorization
/app/controllers/**/*token* @gitlab-org/manage/authentication-and-authorization
/ee/app/controllers/**/*token* @gitlab-org/manage/authentication-and-authorization

View File

@ -1 +1 @@
14.9.0
14.10.0-rc2

View File

@ -23,6 +23,11 @@ export default {
required: false,
default: null,
},
wrapTextNodes: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
...mapState([
@ -37,6 +42,7 @@ export default {
const initialData = {
blobs: [{ path: this.blobPath, codeNavigationPath: this.codeNavigationPath }],
definitionPathPrefix: this.pathPrefix,
wrapTextNodes: this.wrapTextNodes,
};
this.setInitialData(initialData);
}

View File

@ -22,7 +22,7 @@ export default {
...d,
definitionLineNumber: parseInt(d.definition_path?.split('#L').pop() || 0, 10),
};
addInteractionClass(path, d);
addInteractionClass({ path, d, wrapTextNodes: state.wrapTextNodes });
}
return acc;
}, {});
@ -34,7 +34,9 @@ export default {
},
showBlobInteractionZones({ state }, path) {
if (state.data && state.data[path]) {
Object.values(state.data[path]).forEach((d) => addInteractionClass(path, d));
Object.values(state.data[path]).forEach((d) =>
addInteractionClass({ path, d, wrapTextNodes: state.wrapTextNodes }),
);
}
},
showDefinition({ commit, state }, { target: el }) {

View File

@ -1,9 +1,10 @@
import * as types from './mutation_types';
export default {
[types.SET_INITIAL_DATA](state, { blobs, definitionPathPrefix }) {
[types.SET_INITIAL_DATA](state, { blobs, definitionPathPrefix, wrapTextNodes }) {
state.blobs = blobs;
state.definitionPathPrefix = definitionPathPrefix;
state.wrapTextNodes = wrapTextNodes;
},
[types.REQUEST_DATA](state) {
state.loading = true;

View File

@ -2,6 +2,7 @@ export default () => ({
blobs: [],
loading: false,
data: null,
wrapTextNodes: false,
currentDefinition: null,
currentDefinitionPosition: null,
currentBlobPath: null,

View File

@ -0,0 +1,31 @@
const TEXT_NODE = 3;
const isTextNode = ({ nodeType }) => nodeType === TEXT_NODE;
const isBlank = (str) => !str || /^\s*$/.test(str);
const isMatch = (s1, s2) => !isBlank(s1) && s1.trim() === s2.trim();
const createSpan = (content) => {
const span = document.createElement('span');
span.innerText = content;
return span;
};
const wrapSpacesWithSpans = (text) => text.replace(/ /g, createSpan(' ').outerHTML);
const wrapTextWithSpan = (el, text) => {
if (isTextNode(el) && isMatch(el.textContent, text)) {
const newEl = createSpan(text.trim());
el.replaceWith(newEl);
}
};
const wrapNodes = (text) => {
const wrapper = createSpan();
wrapper.innerHTML = wrapSpacesWithSpans(text);
wrapper.childNodes.forEach((el) => wrapTextWithSpan(el, text));
return wrapper.childNodes;
};
export { wrapNodes, isTextNode };

View File

@ -1,9 +1,11 @@
import { wrapNodes, isTextNode } from './dom_utils';
export const cachedData = new Map();
export const getCurrentHoverElement = () => cachedData.get('current');
export const setCurrentHoverElement = (el) => cachedData.set('current', el);
export const addInteractionClass = (path, d) => {
export const addInteractionClass = ({ path, d, wrapTextNodes }) => {
const lineNumber = d.start_line + 1;
const lines = document
.querySelector(`[data-path="${path}"]`)
@ -12,13 +14,24 @@ export const addInteractionClass = (path, d) => {
lines.forEach((line) => {
let charCount = 0;
if (wrapTextNodes) {
line.childNodes.forEach((elm) => {
if (isTextNode(elm)) {
// Highlight.js does not wrap all text nodes by default
// We need all text nodes to be wrapped in order to append code nav attributes
elm.replaceWith(...wrapNodes(elm.textContent));
}
});
}
const el = [...line.childNodes].find(({ textContent }) => {
if (charCount === d.start_char) return true;
charCount += textContent.length;
return false;
});
if (el) {
if (el && !isTextNode(el)) {
el.setAttribute('data-char-index', d.start_char);
el.setAttribute('data-line-index', d.start_line);
el.classList.add('cursor-pointer', 'code-navigation', 'js-code-navigation');

View File

@ -358,7 +358,7 @@ export default {
:loading="redirecting"
:disabled="redirecting"
category="primary"
variant="success"
variant="confirm"
:href="newIncidentPath"
@click="navigateToCreateNewIncident"
>

View File

@ -171,6 +171,7 @@ export default {
data-testid="cancel-button"
icon="cancel"
:title="$options.CANCEL"
:aria-label="$options.CANCEL"
:disabled="cancelBtnDisabled"
@click="cancelJob()"
/>
@ -182,6 +183,7 @@ export default {
v-gl-modal-directive="$options.playJobModalId"
icon="play"
:title="$options.ACTIONS_START_NOW"
:aria-label="$options.ACTIONS_START_NOW"
data-testid="play-scheduled"
/>
<gl-modal
@ -196,6 +198,7 @@ export default {
<gl-button
icon="time-out"
:title="$options.ACTIONS_UNSCHEDULE"
:aria-label="$options.ACTIONS_UNSCHEDULE"
:disabled="unscheduleBtnDisabled"
data-testid="unschedule"
@click="unscheduleJob()"
@ -207,6 +210,7 @@ export default {
v-if="manualJobPlayable"
icon="play"
:title="$options.ACTIONS_PLAY"
:aria-label="$options.ACTIONS_PLAY"
:disabled="playManualBtnDisabled"
data-testid="play"
@click="playJob()"
@ -215,6 +219,7 @@ export default {
v-else-if="isRetryable"
icon="repeat"
:title="$options.ACTIONS_RETRY"
:aria-label="$options.ACTIONS_RETRY"
:method="currentJobMethod"
:disabled="retryBtnDisabled"
data-testid="retry"
@ -226,6 +231,7 @@ export default {
v-if="shouldDisplayArtifacts"
icon="download"
:title="$options.ACTIONS_DOWNLOAD_ARTIFACTS"
:aria-label="$options.ACTIONS_DOWNLOAD_ARTIFACTS"
:href="artifactDownloadPath"
rel="nofollow"
download

View File

@ -66,6 +66,7 @@ export default {
text: this.secondaryText,
attributes: {
variant: this.secondaryVariant,
category: 'secondary',
},
};
},

View File

@ -357,7 +357,13 @@ export default {
}) {
if (shouldConfirm && isDirty) {
const msg = __('Are you sure you want to cancel editing this comment?');
const confirmed = await confirmAction(msg);
const confirmed = await confirmAction(msg, {
primaryBtnText: __('Cancel editing'),
primaryBtnVariant: 'danger',
secondaryBtnVariant: 'default',
secondaryBtnText: __('Continue editing'),
hideCancel: true,
});
if (!confirmed) return;
}
this.$refs.noteBody.resetAutoSave();

View File

@ -185,7 +185,7 @@ export default {
<gl-button
class="mr-auto js-no-auto-disable"
category="primary"
variant="success"
variant="confirm"
type="submit"
:disabled="isFormSubmissionDisabled"
data-testid="submit-button"

View File

@ -238,7 +238,7 @@ export default {
:href="newReleasePath"
:aria-describedby="shouldRenderEmptyState && 'releases-description'"
category="primary"
variant="success"
variant="confirm"
>{{ $options.i18n.newRelease }}</gl-button
>
</div>

View File

@ -301,6 +301,7 @@ export default {
:code-navigation-path="blobInfo.codeNavigationPath"
:blob-path="blobInfo.path"
:path-prefix="blobInfo.projectBlobPathRoot"
:wrap-text-nodes="glFeatures.highlightJs"
/>
</div>
</div>

View File

@ -84,7 +84,7 @@ export default {
<gl-form-input id="user-list-name" v-model="name" data-testid="user-list-name" required />
</gl-form-group>
<div :class="$options.classes.actionContainer">
<gl-button variant="success" data-testid="save-user-list" @click="submit">
<gl-button variant="confirm" data-testid="save-user-list" @click="submit">
{{ saveButtonLabel }}
</gl-button>
<gl-button :href="cancelPath" data-testid="user-list-cancel">

View File

@ -1,6 +1,7 @@
<script>
import { GlSafeHtmlDirective, GlLoadingIcon } from '@gitlab/ui';
import LineHighlighter from '~/blob/line_highlighter';
import eventHub from '~/notes/event_hub';
import { ROUGE_TO_HLJS_LANGUAGE_MAP, LINES_PER_CHUNK } from './constants';
import Chunk from './components/chunk.vue';
@ -102,6 +103,8 @@ export default {
Object.assign(chunk, { language, content: highlightedContent, isHighlighted: true });
this.selectLine();
this.$nextTick(() => eventHub.$emit('showBlobInteractionZones', this.blob.path));
},
highlight(content, language) {
let detectedLanguage = language;
@ -153,6 +156,7 @@ export default {
class="file-content code js-syntax-highlight blob-content gl-display-flex gl-flex-direction-column gl-overflow-auto"
:class="$options.userColorScheme"
data-type="simple"
:data-path="blob.path"
data-qa-selector="blob_viewer_file_content"
>
<chunk

View File

@ -35,6 +35,8 @@ class Note < ApplicationRecord
contact: :read_crm_contact
}.freeze
NON_DIFF_NOTE_TYPES = ['Note', 'DiscussionNote', nil].freeze
# Aliases to make application_helper#edited_time_ago_with_tooltip helper work properly with notes.
# See https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/10392/diffs#note_28719102
alias_attribute :last_edited_by, :updated_by
@ -97,6 +99,9 @@ class Note < ApplicationRecord
validates :author, presence: true
validates :discussion_id, presence: true, format: { with: /\A\h{40}\z/ }
validate :ensure_confidentiality_discussion_compliance
validate :ensure_noteable_can_have_confidential_note
validate :ensure_note_type_can_be_confidential
validate :ensure_confidentiality_not_changed, on: :update
validate unless: [:for_commit?, :importing?, :skip_project_check?] do |note|
@ -143,7 +148,7 @@ class Note < ApplicationRecord
scope :diff_notes, -> { where(type: %w(LegacyDiffNote DiffNote)) }
scope :new_diff_notes, -> { where(type: 'DiffNote') }
scope :non_diff_notes, -> { where(type: ['Note', 'DiscussionNote', nil]) }
scope :non_diff_notes, -> { where(type: NON_DIFF_NOTE_TYPES) }
scope :with_associations, -> do
# FYI noteable cannot be loaded for LegacyDiffNote for commits
@ -460,7 +465,7 @@ class Note < ApplicationRecord
# and all its notes and if we don't care about the discussion's resolvability status.
def discussion
strong_memoize(:discussion) do
full_discussion = self.noteable.notes.find_discussion(self.discussion_id) if part_of_discussion?
full_discussion = self.noteable.notes.find_discussion(self.discussion_id) if self.noteable && part_of_discussion?
full_discussion || to_discussion
end
end
@ -727,6 +732,35 @@ class Note < ApplicationRecord
errors.add(:confidential, _('can not be changed for existing notes'))
end
def ensure_confidentiality_discussion_compliance
return if start_of_discussion?
if discussion.first_note.confidential? != confidential?
errors.add(:confidential, _('reply should have same confidentiality as top-level note'))
end
ensure
clear_memoization(:discussion)
end
def ensure_noteable_can_have_confidential_note
return unless confidential?
return if noteable_can_have_confidential_note?
errors.add(:confidential, _('can not be set for this resource'))
end
def ensure_note_type_can_be_confidential
return unless confidential?
return if NON_DIFF_NOTE_TYPES.include?(type)
errors.add(:confidential, _('can not be set for this type of note'))
end
def noteable_can_have_confidential_note?
for_issue?
end
end
Note.prepend_mod_with('Note')

View File

@ -7,8 +7,8 @@ class UserCustomAttribute < ApplicationRecord
validates :key, uniqueness: { scope: [:user_id] }
def self.upsert_custom_attributes(custom_attributes)
created_at = Date.today
updated_at = Date.today
created_at = DateTime.now
updated_at = DateTime.now
custom_attributes.map! do |custom_attribute|
custom_attribute.merge({ created_at: created_at, updated_at: updated_at })

View File

@ -7,16 +7,14 @@ module Ci
include ::Gitlab::LoopHelpers
BATCH_SIZE = 100
LOOP_LIMIT = 500
LOOP_TIMEOUT = 5.minutes
SMALL_LOOP_LIMIT = 100
LARGE_LOOP_LIMIT = 500
EXCLUSIVE_LOCK_KEY = 'expired_job_artifacts:destroy:lock'
LOCK_TIMEOUT = 6.minutes
EXCLUSIVE_LOCK_KEY = 'expired_job_artifacts:destroy:lock'
def initialize
@removed_artifacts_count = 0
@start_at = Time.current
@loop_limit = ::Feature.enabled?(:ci_artifact_fast_removal_large_loop_limit, default_enabled: :yaml) ? LARGE_LOOP_LIMIT : SMALL_LOOP_LIMIT
end
##
@ -42,7 +40,7 @@ module Ci
private
def destroy_unlocked_job_artifacts
loop_until(timeout: LOOP_TIMEOUT, limit: @loop_limit) do
loop_until(timeout: LOOP_TIMEOUT, limit: LOOP_LIMIT) do
artifacts = Ci::JobArtifact.expired_before(@start_at).artifact_unlocked.limit(BATCH_SIZE)
service_response = destroy_batch(artifacts)
@removed_artifacts_count += service_response[:destroyed_artifacts_count]
@ -59,7 +57,7 @@ module Ci
@removed_artifacts_count += service_response[:destroyed_artifacts_count]
break if loop_timeout?
break if index >= @loop_limit
break if index >= LOOP_LIMIT
end
end

View File

@ -1,16 +1,12 @@
.settings-content
= form_for @application_setting, url: ci_cd_admin_application_settings_path(anchor: 'js-ci-cd-settings'), html: { class: 'fieldset-form' } do |f|
= gitlab_ui_form_for @application_setting, url: ci_cd_admin_application_settings_path(anchor: 'js-ci-cd-settings'), html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
.form-group
.form-check
= f.check_box :auto_devops_enabled, class: 'form-check-input'
= f.label :auto_devops_enabled, class: 'form-check-label' do
= s_('CICD|Default to Auto DevOps pipeline for all projects')
.form-text.text-muted
= s_('CICD|The Auto DevOps pipeline runs by default in all projects with no CI/CD configuration file.')
= link_to _('What is Auto DevOps?'), help_page_path('topics/autodevops/index.md'), target: '_blank', rel: 'noopener noreferrer'
- devops_help_link_url = help_page_path('topics/autodevops/index.md')
- devops_help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: devops_help_link_url }
= f.gitlab_ui_checkbox_component :auto_devops_enabled, s_('CICD|Default to Auto DevOps pipeline for all projects'), help_text: s_('CICD|The Auto DevOps pipeline runs by default in all projects with no CI/CD configuration file. %{link_start}What is Auto DevOps?%{link_end}').html_safe % { link_start: devops_help_link_start, link_end: '</a>'.html_safe }
.form-group
= f.label :auto_devops_domain, s_('AdminSettings|Auto DevOps domain'), class: 'label-bold'
= f.text_field :auto_devops_domain, class: 'form-control gl-form-input', placeholder: 'example.com'
@ -19,12 +15,7 @@
= link_to _('Learn more.'), help_page_path('topics/autodevops/stages.md', anchor: 'auto-review-apps'), target: '_blank', rel: 'noopener noreferrer'
.form-group
.form-check
= f.check_box :shared_runners_enabled, class: 'form-check-input'
= f.label :shared_runners_enabled, class: 'form-check-label' do
= s_("AdminSettings|Enable shared runners for new projects")
.form-text.text-muted
= s_("AdminSettings|All new projects can use the instance's shared runners by default.")
= f.gitlab_ui_checkbox_component :shared_runners_enabled, s_("AdminSettings|Enable shared runners for new projects"), help_text: s_("AdminSettings|All new projects can use the instance's shared runners by default.")
= render_if_exists 'admin/application_settings/shared_runners_minutes_setting', form: f
@ -45,12 +36,7 @@
= html_escape(_("Set the default expiration time for job artifacts in all projects. Set to %{code_open}0%{code_close} to never expire artifacts by default. If no unit is written, it defaults to seconds. For example, these are all equivalent: %{code_open}3600%{code_close}, %{code_open}60 minutes%{code_close}, or %{code_open}one hour%{code_close}.")) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe }
= link_to _('Learn more.'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'default-artifacts-expiration')
.form-group
.form-check
= f.check_box :keep_latest_artifact, class: 'form-check-input'
= f.label :keep_latest_artifact, class: 'form-check-label' do
= s_('AdminSettings|Keep the latest artifacts for all jobs in the latest successful pipelines')
.form-text.text-muted
= s_('AdminSettings|The latest artifacts for all jobs in the most recent successful pipelines in each project are stored and do not expire.')
= f.gitlab_ui_checkbox_component :keep_latest_artifact, s_('AdminSettings|Keep the latest artifacts for all jobs in the latest successful pipelines'), help_text: s_('AdminSettings|The latest artifacts for all jobs in the most recent successful pipelines in each project are stored and do not expire.')
.form-group
= f.label :archive_builds_in_human_readable, _('Archive jobs'), class: 'label-bold'
= f.text_field :archive_builds_in_human_readable, class: 'form-control gl-form-input'
@ -58,12 +44,7 @@
= html_escape(_("Jobs older than the configured time are considered expired and are archived. Archived jobs can no longer be retried. Leave empty to never archive jobs automatically. The default unit is in days, but you can use other units, for example %{code_open}15 days%{code_close}, %{code_open}1 month%{code_close}, %{code_open}2 years%{code_close}. Minimum value is 1 day.")) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe }
= link_to _('Learn more.'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'archive-jobs')
.form-group
.form-check
= f.check_box :protected_ci_variables, class: 'form-check-input'
= f.label :protected_ci_variables, class: 'form-check-label' do
= s_('AdminSettings|Protect CI/CD variables by default')
.form-text.text-muted
= s_('AdminSettings|New CI/CD variables in projects and groups default to protected.')
= f.gitlab_ui_checkbox_component :protected_ci_variables, s_('AdminSettings|Protect CI/CD variables by default'), help_text: s_('AdminSettings|New CI/CD variables in projects and groups default to protected.')
.form-group
= f.label :ci_config_path, _('Default CI/CD configuration file'), class: 'label-bold'
= f.text_field :default_ci_config_path, class: 'form-control gl-form-input', placeholder: '.gitlab-ci.yml'
@ -71,12 +52,7 @@
= _("The default CI/CD configuration file and path for new projects.").html_safe
= link_to sprite_icon('question-o'), help_page_path('ci/pipelines/settings', anchor: 'specify-a-custom-cicd-configuration-file'), target: '_blank', rel: 'noopener noreferrer'
.form-group
.form-check
= f.check_box :suggest_pipeline_enabled, class: 'form-check-input'
= f.label :suggest_pipeline_enabled, class: 'form-check-label' do
= s_('AdminSettings|Enable pipeline suggestion banner')
.form-text.text-muted
= s_('AdminSettings|Display a banner on merge requests in projects with no pipelines to initiate steps to add a .gitlab-ci.yml file.')
= f.gitlab_ui_checkbox_component :suggest_pipeline_enabled, s_('AdminSettings|Enable pipeline suggestion banner'), help_text: s_('AdminSettings|Display a banner on merge requests in projects with no pipelines to initiate steps to add a .gitlab-ci.yml file.')
= f.submit _('Save changes'), class: "gl-button btn btn-confirm"

View File

@ -93,18 +93,16 @@
%p
= _('Manage Web IDE features.')
.settings-content
= form_for @application_setting, url: general_admin_application_settings_path(anchor: "#js-web-ide-settings"), html: { class: 'fieldset-form', id: 'web-ide-settings' } do |f|
= gitlab_ui_form_for @application_setting, url: general_admin_application_settings_path(anchor: "#js-web-ide-settings"), html: { class: 'fieldset-form', id: 'web-ide-settings' } do |f|
= form_errors(@application_setting)
%fieldset
.form-group
.form-check
= f.check_box :web_ide_clientside_preview_enabled, class: 'form-check-input'
= f.label :web_ide_clientside_preview_enabled, class: 'form-check-label' do
= s_('IDE|Live Preview')
%span.form-text.text-muted
- link_start = '<a href="%{url}">'.html_safe % { url: help_page_path('user/project/web_ide/index', anchor: 'enable-live-preview') }
= s_('Preview JavaScript projects in the Web IDE with CodeSandbox Live Preview. %{link_start}Learn more.%{link_end} ').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
- link_start = '<a href="%{url}">'.html_safe % { url: help_page_path('user/project/web_ide/index', anchor: 'enable-live-preview') }
= f.gitlab_ui_checkbox_component :web_ide_clientside_preview_enabled,
s_('IDE|Live Preview'),
help_text: s_('Preview JavaScript projects in the Web IDE with CodeSandbox Live Preview. %{link_start}Learn more.%{link_end} ').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
= f.submit _('Save changes'), class: "gl-button btn btn-confirm"
= render_if_exists 'admin/application_settings/maintenance_mode_settings_form'

View File

@ -40,7 +40,7 @@
%span.light= _('Secondary email:')
%strong
= render partial: 'shared/email_with_badge', locals: { email: email.email, verified: email.confirmed? }
= link_to remove_email_admin_user_path(@user, email), data: { confirm: _("Are you sure you want to remove %{email}?") % { email: email.email } }, method: :delete, class: "btn btn-sm btn-danger gl-button btn-icon float-right", title: _('Remove secondary email'), id: "remove_email_#{email.id}" do
= link_to remove_email_admin_user_path(@user, email), data: { confirm: _("Are you sure you want to remove %{email}?") % { email: email.email }, 'confirm-btn-variant': 'danger' }, method: :delete, class: "btn btn-sm btn-danger gl-button btn-icon float-right", title: _('Remove secondary email'), id: "remove_email_#{email.id}" do
= sprite_icon('close', size: 16, css_class: 'gl-icon')
%li
%span.light ID:
@ -56,7 +56,7 @@
%strong{ class: @user.two_factor_enabled? ? 'cgreen' : 'cred' }
- if @user.two_factor_enabled?
= _('Enabled')
= link_to _('Disable'), disable_two_factor_admin_user_path(@user), data: { confirm: _('Are you sure?') }, method: :patch, class: 'btn gl-button btn-sm btn-danger float-right', title: _('Disable Two-factor Authentication')
= link_to _('Disable'), disable_two_factor_admin_user_path(@user), aria: { label: _('Disable') }, data: { confirm: _('Are you sure?'), 'confirm-btn-variant': 'danger' }, method: :patch, class: 'btn gl-button btn-sm btn-danger float-right', title: _('Disable Two-factor Authentication')
- else
= _('Disabled')

View File

@ -1,8 +0,0 @@
---
name: ci_artifact_fast_removal_large_loop_limit
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/76504
rollout_issue_url:
milestone: '14.6'
type: development
group: group::pipeline execution
default_enabled: false

View File

@ -321,13 +321,13 @@ The following package managers use lockfiles that GitLab analyzers are capable o
| Package Manager | Supported File Format Versions | Tested Versions |
| ------ | ------ | ------ |
| Bundler | N/A | [1.17.3](https://gitlab.com/gitlab-org/security-products/tests/ruby-bundler/-/blob/master/Gemfile.lock#L118), [2.1.4](https://gitlab.com/gitlab-org/security-products/tests/ruby-bundler/-/blob/bundler2-FREEZE/Gemfile.lock#L118) |
| Composer | N/A | [1.x](https://gitlab.com/gitlab-org/security-products/tests/php-composer/-/blob/master/composer.lock) |
| Conan | 0.4 | [1.x](https://gitlab.com/gitlab-org/security-products/tests/c-conan/-/blob/master/conan.lock) |
| Go | N/A | [1.x](https://gitlab.com/gitlab-org/security-products/tests/go-modules/-/blob/master/go.mod) |
| NuGet | v1 | [4.9](https://gitlab.com/gitlab-org/security-products/tests/csharp-nuget-dotnetcore/-/blob/master/src/web.api/packages.lock.json#L2) |
| npm | v1, v2 | [6.x](https://gitlab.com/gitlab-org/security-products/tests/js-npm/-/blob/master/package-lock.json#L4), [7.x](https://gitlab.com/gitlab-org/security-products/tests/js-npm/-/blob/lockfile-v2-FREEZE/package-lock.json#L4) |
| yarn | v1 | [1.x](https://gitlab.com/gitlab-org/security-products/tests/js-yarn/-/blob/master/yarn.lock) |
| Bundler | N/A | [1.17.3](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium/-/blob/master/qa/fixtures/ruby-bundler/main/Gemfile.lock#L118), [2.1.4](https://gitlab.com/gitlab-org/security-products/tests/ruby-bundler/-/blob/bundler2-FREEZE/Gemfile.lock#L118) |
| Composer | N/A | [1.x](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium/-/blob/master/qa/fixtures/php-composer/main/composer.lock) |
| Conan | 0.4 | [1.x](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium/-/blob/master/qa/fixtures/c-conan/main/conan.lock) |
| Go | N/A | [1.x](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium/-/blob/master/qa/fixtures/go-modules/main/go.sum) |
| NuGet | v1 | [4.9](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium/-/blob/master/qa/fixtures/csharp-nuget-dotnetcore/main/src/web.api/packages.lock.json#L2) |
| npm | v1, v2 | [6.x](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium/-/blob/master/qa/fixtures/js-npm/main/package-lock.json#L4), [7.x](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium/-/blob/master/qa/fixtures/js-npm/lockfileVersion2/package-lock.json#L4) |
| yarn | v1 | [1.x](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium/-/blob/master/qa/fixtures/js-yarn/main/yarn.lock#L2) |
#### Obtaining dependency information by running a package manager to generate a parsable file
@ -339,19 +339,19 @@ To support the following package managers, the GitLab analyzers proceed in two s
| Package Manager | Pre-installed Versions | Tested Versions |
| ------ | ------ | ------ |
| Bundler | [2.1.4](https://gitlab.com/gitlab-org/security-products/analyzers/bundler-audit/-/blob/v2.11.3/Dockerfile#L15)<sup><b><a href="#exported-dependency-information-notes-1">1</a></b></sup> | [1.17.3](https://gitlab.com/gitlab-org/security-products/tests/ruby-bundler/-/blob/master/Gemfile.lock#L118), [2.1.4](https://gitlab.com/gitlab-org/security-products/tests/ruby-bundler/-/blob/bundler2-FREEZE/Gemfile.lock#L118) |
| sbt | [1.6.1](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.24.6/config/.tool-versions#L4) | [1.0.4](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.24.6/.gitlab-ci.yml#L330), [1.1.4](https://gitlab.com/gitlab-org/security-products/tests/scala-sbt-multiproject/-/blob/main/project/build.properties#L1), [1.1.6](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.24.6/.gitlab-ci.yml#L339), [1.2.8](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.24.6/.gitlab-ci.yml#L348), [1.3.12](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.24.6/.gitlab-ci.yml#L357), [1.4.6](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.24.6/.gitlab-ci.yml#L366), [1.6.1](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.24.6/.gitlab-ci.yml#L384) |
| Maven | [3.6.3](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.23.0/config/.tool-versions#L3) | [3.6.3](https://gitlab.com/gitlab-org/security-products/tests/java-maven/-/blob/master/pom.xml#L3) |
| Gradle | [6.7.1](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.23.0/config/.tool-versions#L5)<sup><b><a href="#exported-dependency-information-notes-2">2</a></b></sup>, [7.3.3](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.26.0/config/.tool-versions#L5)<sup><b><a href="#exported-dependency-information-notes-2">2</a></b></sup> | [5.6.4](https://gitlab.com/gitlab-org/security-products/tests/java-gradle/-/blob/master/gradle/wrapper/gradle-wrapper.properties#L3), [6.5](https://gitlab.com/gitlab-org/security-products/tests/java-gradle/-/blob/java-14/gradle/wrapper/gradle-wrapper.properties#L3), [6.7-rc-1](https://gitlab.com/gitlab-org/security-products/tests/java-gradle/-/blob/java-15/gradle/wrapper/gradle-wrapper.properties#L3), [6.7.1](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.27.1/.gitlab-ci.yml#L289-297)<sup><b><a href="#exported-dependency-information-notes-3">3</a></b></sup>, [6.9](https://gitlab.com/gitlab-org/security-products/tests/java-gradle/-/blob/java-14-gradle-6-9/gradle/wrapper/gradle-wrapper.properties#L3), [7.0-rc-2](https://gitlab.com/gitlab-org/security-products/tests/java-gradle/-/blob/java-16/gradle/wrapper/gradle-wrapper.properties#L3), [7.3](https://gitlab.com/gitlab-org/security-products/tests/java-gradle/-/blob/java-14-gradle-7-3/gradle/wrapper/gradle-wrapper.properties#L3), [7.3.3](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.27.1/.gitlab-ci.yml#L299-317)<sup><b><a href="#exported-dependency-information-notes-3">3</a></b></sup> |
| setuptools | [50.3.2](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium/-/blob/v2.29.9/Dockerfile#L27) | [57.5.0](https://gitlab.com/gitlab-org/security-products/tests/python-setuptools/-/blob/main/setup.py) |
| pip | [20.2.4](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium/-/blob/v2.29.9/Dockerfile#L26) | [20.x](https://gitlab.com/gitlab-org/security-products/tests/python-pip/-/blob/master/requirements.txt) |
| Pipenv | [2018.11.26](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-python/-/blob/v2.18.4/requirements.txt#L13) | [2018.11.26](https://gitlab.com/gitlab-org/security-products/tests/python-pipenv/-/blob/pipfile-lock-FREEZE/Pipfile.lock#L6)<sup><b><a href="#exported-dependency-information-notes-4">4</a></b></sup>, [2018.11.26](https://gitlab.com/gitlab-org/security-products/tests/python-pipenv/-/blob/master/Pipfile) |
| sbt | [1.6.1](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.24.6/config/.tool-versions#L4) | [1.0.4](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.28.1/spec/image_spec.rb#L443-447), [1.1.6](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.28.1/spec/image_spec.rb#L449-453), [1.2.8](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.28.1/spec/image_spec.rb#L455-459), [1.3.12](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.28.1/spec/image_spec.rb#L461-465), [1.4.6](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.28.1/spec/image_spec.rb#L467-471), [1.5.8](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.28.1/spec/image_spec.rb#L473-477), [1.6.1](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.28.1/spec/image_spec.rb#L479-483) |
| Maven | [3.6.3](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.28.1/spec/image_spec.rb#L95-97) | [3.6.3](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.28.1/spec/image_spec.rb#L95-97) |
| Gradle | [6.7.1](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.23.0/config/.tool-versions#L5)<sup><b><a href="#exported-dependency-information-notes-2">2</a></b></sup>, [7.3.3](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.26.0/config/.tool-versions#L5)<sup><b><a href="#exported-dependency-information-notes-2">2</a></b></sup> | [5.6.4](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.28.1/spec/image_spec.rb#L319-323), [6.7](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.28.1/spec/image_spec.rb#L286-288)<sup><b><a href="#exported-dependency-information-notes-3">3</a></b></sup>, [6.9](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.28.1/spec/image_spec.rb#L331-335), [7.3](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.28.1/spec/image_spec.rb#L300-302)<sup><b><a href="#exported-dependency-information-notes-3">3</a></b></sup> |
| setuptools | [50.3.2](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium/-/blob/v2.29.9/Dockerfile#L27) | [57.5.0](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-python/-/blob/v2.22.0/spec/image_spec.rb#L224-247) |
| pip | [20.2.4](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium/-/blob/v2.29.9/Dockerfile#L26) | [20.x](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-python/-/blob/v2.22.0/spec/image_spec.rb#L77-91) |
| Pipenv | [2018.11.26](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-python/-/blob/v2.18.4/requirements.txt#L13) | [2018.11.26](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-python/-/blob/v2.22.0/spec/image_spec.rb#L168-191)<sup><b><a href="#exported-dependency-information-notes-4">4</a></b></sup>, [2018.11.26](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-python/-/blob/v2.22.0/spec/image_spec.rb#L143-166) |
<!-- markdownlint-disable MD044 -->
<ol>
<li>
<a id="exported-dependency-information-notes-1"></a>
<p>
The pre-installed version of <code>Bundler</code> is only used for the <a href="https://gitlab.com/gitlab-org/security-products/analyzers/bundler-audit">bundler-audit</a> analyzer, and is not used for <a href="https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium">gemnasium</a>.
The pre-installed and tested version of <code>Bundler</code> is only used for the <a href="https://gitlab.com/gitlab-org/security-products/analyzers/bundler-audit">bundler-audit</a> analyzer, and is not used for <a href="https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium">gemnasium</a>.
</p>
</li>
<li>

View File

@ -326,18 +326,13 @@ run tests:
The following [`.gitlab-ci.yml`](../../../ci/yaml/index.md) example for Go uses:
- [`go test`](https://go.dev/doc/tutorial/add-a-test) to run tests.
- [`gocover-cobertura`](https://github.com/t-yuki/gocover-cobertura) to convert Go's coverage profile into the Cobertura XML format.
- [`gocover-cobertura`](https://github.com/boumenot/gocover-cobertura) to convert Go's coverage profile into the Cobertura XML format.
This example assumes that [Go modules](https://go.dev/ref/mod) are being used.
Using Go modules causes paths within the coverage profile to be prefixed with your
project's module identifier, which can be found in the `go.mod` file. This
prefix must be removed for GitLab to parse the Cobertura XML file correctly. You can use the following `sed` command to remove the prefix:
```shell
sed -i 's;filename=\"<YOUR_MODULE_ID>/;filename=\";g' coverage.xml
```
Replace the `gitlab.com/my-group/my-project` placeholder in the following example with your own module identifier to make it work.
This example assumes that [Go modules](https://go.dev/ref/mod)
are being used. Please note that the `-covermode count` option does not work with the `-race` flag.
If you want to generate code coverage while also using the `-race` flag, you must switch to
`-covermode atomic` which is slower than `-covermode count`. See [this blog post](https://go.dev/blog/cover)
for more details.
```yaml
run tests:
@ -345,9 +340,9 @@ run tests:
image: golang:1.17
script:
- go install
- go test . -coverprofile=coverage.txt -covermode count
- go run github.com/t-yuki/gocover-cobertura < coverage.txt > coverage.xml
- sed -i 's;filename=\"gitlab.com/my-group/my-project/;filename=\";g' coverage.xml
- go test ./... -coverprofile=coverage.txt -covermode count
- go get github.com/boumenot/gocover-cobertura
- go run github.com/boumenot/gocover-cobertura < coverage.txt > coverage.xml
artifacts:
reports:
cobertura: coverage.xml

View File

@ -30,7 +30,7 @@ module Gitlab
user: deployment.deployed_by.hook_attrs,
user_url: Gitlab::UrlBuilder.build(deployment.deployed_by),
commit_url: commit_url,
commit_title: deployment.commit&.title,
commit_title: deployment.commit_title,
ref: deployment.ref
}
end

View File

@ -6630,7 +6630,7 @@ msgstr ""
msgid "CICD|Select projects that can be accessed by API requests authenticated with this project's CI_JOB_TOKEN CI/CD variable."
msgstr ""
msgid "CICD|The Auto DevOps pipeline runs by default in all projects with no CI/CD configuration file."
msgid "CICD|The Auto DevOps pipeline runs by default in all projects with no CI/CD configuration file. %{link_start}What is Auto DevOps?%{link_end}"
msgstr ""
msgid "CICD|The Auto DevOps pipeline runs if no alternative CI configuration file is found."
@ -6798,6 +6798,9 @@ msgstr ""
msgid "Cancel and close"
msgstr ""
msgid "Cancel editing"
msgstr ""
msgid "Cancel index deletion"
msgstr ""
@ -9979,6 +9982,9 @@ msgstr ""
msgid "Continue"
msgstr ""
msgid "Continue editing"
msgstr ""
msgid "Continue to the next step"
msgstr ""
@ -22131,6 +22137,9 @@ msgstr ""
msgid "Lead Time"
msgstr ""
msgid "Lead Time for Changes"
msgstr ""
msgid "Lead time"
msgstr ""
@ -42376,9 +42385,6 @@ msgstr ""
msgid "What does this command do?"
msgstr ""
msgid "What is Auto DevOps?"
msgstr ""
msgid "What is Markdown?"
msgstr ""
@ -43950,6 +43956,12 @@ msgstr ""
msgid "can not be changed for existing notes"
msgstr ""
msgid "can not be set for this resource"
msgstr ""
msgid "can not be set for this type of note"
msgstr ""
msgid "can only be changed by a group admin."
msgstr ""
@ -45432,6 +45444,9 @@ msgid_plural "replies"
msgstr[0] ""
msgstr[1] ""
msgid "reply should have same confidentiality as top-level note"
msgstr ""
msgid "repositories"
msgstr ""

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
module QA
RSpec.describe 'Release' do
RSpec.describe 'Verify' do
describe 'Multi-project pipelines' do
let(:downstream_job_name) { 'downstream_job' }
let(:executor) { "qa-runner-#{SecureRandom.hex(4)}" }

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
module QA
RSpec.describe 'Release', :runner, :reliable do
RSpec.describe 'Verify', :runner, :reliable do
describe 'Parent-child pipelines dependent relationship' do
let!(:project) do
Resource::Project.fabricate_via_api! do |project|

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
module QA
RSpec.describe 'Release', :runner, :reliable do
RSpec.describe 'Verify', :runner, :reliable do
describe 'Parent-child pipelines independent relationship' do
let!(:project) do
Resource::Project.fabricate_via_api! do |project|

View File

@ -5,7 +5,7 @@ module QA
# Static address variables declared for mapping environment to logging URLs
STAGING_ADDRESS = 'https://staging.gitlab.com'
STAGING_REF_ADDRESS = 'https://staging-ref.gitlab.com'
PRODUCTION_ADDRESS = 'https://www.gitlab.com'
PRODUCTION_ADDRESS = 'https://gitlab.com'
PRE_PROD_ADDRESS = 'https://pre.gitlab.com'
SENTRY_ENVIRONMENTS = {
staging: 'https://sentry.gitlab.net/gitlab/staginggitlabcom/?environment=gstg',

View File

@ -83,7 +83,7 @@ RSpec.describe QA::Support::Loglinking do
describe '.logging_environment' do
let(:staging_address) { 'https://staging.gitlab.com' }
let(:staging_ref_address) { 'https://staging-ref.gitlab.com' }
let(:production_address) { 'https://www.gitlab.com' }
let(:production_address) { 'https://gitlab.com' }
let(:pre_prod_address) { 'https://pre.gitlab.com' }
let(:logging_env_array) do
[

View File

@ -149,7 +149,7 @@ class MappingTest
end
end
results = tests.map { |test| MappingTest.new(test) }
results = tests.map { |test| MappingTest.new(**test) }
failed_tests = results.select(&:failed?)
if failed_tests.any?

View File

@ -423,7 +423,21 @@ RSpec.describe Projects::NotesController do
end
context 'when creating a confidential note' do
let(:extra_request_params) { { format: :json } }
let(:project) { create(:project) }
let(:note_params) do
{ note: note_text, noteable_id: issue.id, noteable_type: 'Issue' }.merge(extra_note_params)
end
let(:request_params) do
{
note: note_params,
namespace_id: project.namespace,
project_id: project,
target_type: 'issue',
target_id: issue.id,
format: :json
}
end
context 'when `confidential` parameter is not provided' do
it 'sets `confidential` to `false` in JSON response' do

View File

@ -302,6 +302,56 @@ FactoryBot.define do
end
end
# Bandit reports are correctly de-duplicated when ran in the same pipeline
# as a corresponding semgrep report.
# This report does not include signature tracking.
trait :sast_bandit do
file_type { :sast }
file_format { :raw }
after(:build) do |artifact, _|
artifact.file = fixture_file_upload(
Rails.root.join('spec/fixtures/security_reports/master/gl-sast-report-bandit.json'), 'application/json')
end
end
# Equivalent Semgrep report for :sast_bandit report.
# This report includes signature tracking.
trait :sast_semgrep_for_bandit do
file_type { :sast }
file_format { :raw }
after(:build) do |artifact, _|
artifact.file = fixture_file_upload(
Rails.root.join('spec/fixtures/security_reports/master/gl-sast-report-semgrep-for-bandit.json'), 'application/json')
end
end
# Gosec reports are not correctly de-duplicated when ran in the same pipeline
# as a corresponding semgrep report.
# This report includes signature tracking.
trait :sast_gosec do
file_type { :sast }
file_format { :raw }
after(:build) do |artifact, _|
artifact.file = fixture_file_upload(
Rails.root.join('spec/fixtures/security_reports/master/gl-sast-report-gosec.json'), 'application/json')
end
end
# Equivalent Semgrep report for :sast_gosec report.
# This report includes signature tracking.
trait :sast_semgrep_for_gosec do
file_type { :sast }
file_format { :raw }
after(:build) do |artifact, _|
artifact.file = fixture_file_upload(
Rails.root.join('spec/fixtures/security_reports/master/gl-sast-report-semgrep-for-gosec.json'), 'application/json')
end
end
trait :common_security_report do
file_format { :raw }
file_type { :dependency_scanning }

View File

@ -169,7 +169,7 @@ RSpec.describe 'Merge request > User posts notes', :js do
end
page.within('.modal') do
click_button('OK', match: :first)
click_button('Cancel editing', match: :first)
end
expect(find('.js-note-text').text).to eq ''

View File

@ -0,0 +1,43 @@
{
"version": "14.0.4",
"vulnerabilities": [
{
"id": "985a5666dcae22adef5ac12f8a8a2dacf9b9b481ae5d87cd0ac1712b0fd64864",
"category": "sast",
"message": "Deserialization of Untrusted Data",
"description": "Avoid using `load()`. `PyYAML.load` can create arbitrary Python\nobjects. A malicious actor could exploit this to run arbitrary\ncode. Use `safe_load()` instead.\n",
"cve": "",
"severity": "Critical",
"scanner": {
"id": "bandit",
"name": "Bandit"
},
"location": {
"file": "app/app.py",
"start_line": 39
},
"identifiers": [
{
"type": "bandit_test_id",
"name": "Bandit Test ID B506",
"value": "B506"
}
]
}
],
"scan": {
"scanner": {
"id": "bandit",
"name": "Bandit",
"url": "https://github.com/PyCQA/bandit",
"vendor": {
"name": "GitLab"
},
"version": "1.7.1"
},
"type": "sast",
"start_time": "2022-03-11T00:21:49",
"end_time": "2022-03-11T00:21:50",
"status": "success"
}
}

View File

@ -0,0 +1,68 @@
{
"version": "14.0.4",
"vulnerabilities": [
{
"id": "2e5656ff30e2e7cc93c36b4845c8a689ddc47fdbccf45d834c67442fbaa89be0",
"category": "sast",
"name": "Key Exchange without Entity Authentication",
"message": "Use of ssh InsecureIgnoreHostKey should be audited",
"description": "The software performs a key exchange with an actor without verifying the identity of that actor.",
"cve": "og.go:8:7: func foo() {\n8: \t_ = ssh.InsecureIgnoreHostKey()\n9: }\n:CWE-322",
"severity": "Medium",
"confidence": "High",
"raw_source_code_extract": "7: func foo() {\n8: \t_ = ssh.InsecureIgnoreHostKey()\n9: }\n",
"scanner": {
"id": "gosec",
"name": "Gosec"
},
"location": {
"file": "og.go",
"start_line": 8
},
"identifiers": [
{
"type": "gosec_rule_id",
"name": "Gosec Rule ID G106",
"value": "G106"
},
{
"type": "CWE",
"name": "CWE-322",
"value": "322",
"url": "https://cwe.mitre.org/data/definitions/322.html"
}
],
"tracking": {
"type": "source",
"items": [
{
"file": "og.go",
"line_start": 8,
"line_end": 8,
"signatures": [
{
"algorithm": "scope_offset",
"value": "og.go|foo[0]:1"
}
]
}
]
}
}
],
"scan": {
"scanner": {
"id": "gosec",
"name": "Gosec",
"url": "https://github.com/securego/gosec",
"vendor": {
"name": "GitLab"
},
"version": "2.10.0"
},
"type": "sast",
"start_time": "2022-03-15T20:33:12",
"end_time": "2022-03-15T20:33:17",
"status": "success"
}
}

View File

@ -0,0 +1,71 @@
{
"version": "14.0.4",
"vulnerabilities": [
{
"id": "985a5666dcae22adef5ac12f8a8a2dacf9b9b481ae5d87cd0ac1712b0fd64864",
"category": "sast",
"message": "Deserialization of Untrusted Data",
"description": "Avoid using `load()`. `PyYAML.load` can create arbitrary Python\nobjects. A malicious actor could exploit this to run arbitrary\ncode. Use `safe_load()` instead.\n",
"cve": "",
"severity": "Critical",
"scanner": {
"id": "semgrep",
"name": "Semgrep"
},
"location": {
"file": "app/app.py",
"start_line": 39
},
"identifiers": [
{
"type": "semgrep_id",
"name": "bandit.B506",
"value": "bandit.B506",
"url": "https://semgrep.dev/r/gitlab.bandit.B506"
},
{
"type": "cwe",
"name": "CWE-502",
"value": "502",
"url": "https://cwe.mitre.org/data/definitions/502.html"
},
{
"type": "bandit_test_id",
"name": "Bandit Test ID B506",
"value": "B506"
}
],
"tracking": {
"type": "source",
"items": [
{
"file": "app/app.py",
"line_start": 39,
"line_end": 39,
"signatures": [
{
"algorithm": "scope_offset",
"value": "app/app.py|yaml_hammer[0]:13"
}
]
}
]
}
}
],
"scan": {
"scanner": {
"id": "semgrep",
"name": "Semgrep",
"url": "https://github.com/returntocorp/semgrep",
"vendor": {
"name": "GitLab"
},
"version": "0.82.0"
},
"type": "sast",
"start_time": "2022-03-11T18:48:16",
"end_time": "2022-03-11T18:48:22",
"status": "success"
}
}

View File

@ -0,0 +1,70 @@
{
"version": "14.0.4",
"vulnerabilities": [
{
"id": "79f6537b7ec83c7717f5bd1a4f12645916caafefe2e4359148d889855505aa67",
"category": "sast",
"message": "Key Exchange without Entity Authentication",
"description": "Audit the use of ssh.InsecureIgnoreHostKey\n",
"cve": "",
"severity": "Medium",
"scanner": {
"id": "semgrep",
"name": "Semgrep"
},
"location": {
"file": "og.go",
"start_line": 8
},
"identifiers": [
{
"type": "semgrep_id",
"name": "gosec.G106-1",
"value": "gosec.G106-1"
},
{
"type": "cwe",
"name": "CWE-322",
"value": "322",
"url": "https://cwe.mitre.org/data/definitions/322.html"
},
{
"type": "gosec_rule_id",
"name": "Gosec Rule ID G106",
"value": "G106"
}
],
"tracking": {
"type": "source",
"items": [
{
"file": "og.go",
"line_start": 8,
"line_end": 8,
"signatures": [
{
"algorithm": "scope_offset",
"value": "og.go|foo[0]:1"
}
]
}
]
}
}
],
"scan": {
"scanner": {
"id": "semgrep",
"name": "Semgrep",
"url": "https://github.com/returntocorp/semgrep",
"vendor": {
"name": "GitLab"
},
"version": "0.82.0"
},
"type": "sast",
"start_time": "2022-03-15T20:36:58",
"end_time": "2022-03-15T20:37:05",
"status": "success"
}
}

View File

@ -38,12 +38,17 @@ describe('Code navigation app component', () => {
const codeNavigationPath = 'code/nav/path.js';
const path = 'blob/path.js';
const definitionPathPrefix = 'path/prefix';
const wrapTextNodes = true;
factory({}, { codeNavigationPath, blobPath: path, pathPrefix: definitionPathPrefix });
factory(
{},
{ codeNavigationPath, blobPath: path, pathPrefix: definitionPathPrefix, wrapTextNodes },
);
expect(setInitialData).toHaveBeenCalledWith(expect.anything(), {
blobs: [{ codeNavigationPath, path }],
definitionPathPrefix,
wrapTextNodes,
});
});

View File

@ -7,13 +7,15 @@ import axios from '~/lib/utils/axios_utils';
jest.mock('~/code_navigation/utils');
describe('Code navigation actions', () => {
const wrapTextNodes = true;
describe('setInitialData', () => {
it('commits SET_INITIAL_DATA', (done) => {
testAction(
actions.setInitialData,
{ projectPath: 'test' },
{ projectPath: 'test', wrapTextNodes },
{},
[{ type: 'SET_INITIAL_DATA', payload: { projectPath: 'test' } }],
[{ type: 'SET_INITIAL_DATA', payload: { projectPath: 'test', wrapTextNodes } }],
[],
done,
);
@ -30,7 +32,7 @@ describe('Code navigation actions', () => {
const codeNavigationPath =
'gitlab-org/gitlab-shell/-/jobs/1114/artifacts/raw/lsif/cmd/check/main.go.json';
const state = { blobs: [{ path: 'index.js', codeNavigationPath }] };
const state = { blobs: [{ path: 'index.js', codeNavigationPath }], wrapTextNodes };
beforeEach(() => {
window.gon = { api_version: '1' };
@ -109,10 +111,14 @@ describe('Code navigation actions', () => {
[],
)
.then(() => {
expect(addInteractionClass).toHaveBeenCalledWith('index.js', {
start_line: 0,
start_char: 0,
hover: { value: '123' },
expect(addInteractionClass).toHaveBeenCalledWith({
path: 'index.js',
d: {
start_line: 0,
start_char: 0,
hover: { value: '123' },
},
wrapTextNodes,
});
})
.then(done)
@ -144,14 +150,19 @@ describe('Code navigation actions', () => {
data: {
'index.js': { '0:0': 'test', '1:1': 'console.log' },
},
wrapTextNodes,
};
actions.showBlobInteractionZones({ state }, 'index.js');
expect(addInteractionClass).toHaveBeenCalled();
expect(addInteractionClass.mock.calls.length).toBe(2);
expect(addInteractionClass.mock.calls[0]).toEqual(['index.js', 'test']);
expect(addInteractionClass.mock.calls[1]).toEqual(['index.js', 'console.log']);
expect(addInteractionClass.mock.calls[0]).toEqual([
{ path: 'index.js', d: 'test', wrapTextNodes },
]);
expect(addInteractionClass.mock.calls[1]).toEqual([
{ path: 'index.js', d: 'console.log', wrapTextNodes },
]);
});
it('does not call addInteractionClass when no data exists', () => {

View File

@ -13,10 +13,12 @@ describe('Code navigation mutations', () => {
mutations.SET_INITIAL_DATA(state, {
blobs: ['test'],
definitionPathPrefix: 'https://test.com/blob/main',
wrapTextNodes: true,
});
expect(state.blobs).toEqual(['test']);
expect(state.definitionPathPrefix).toBe('https://test.com/blob/main');
expect(state.wrapTextNodes).toBe(true);
});
});

View File

@ -45,14 +45,42 @@ describe('addInteractionClass', () => {
${0} | ${0} | ${0}
${0} | ${8} | ${2}
${1} | ${0} | ${0}
${1} | ${0} | ${0}
`(
'it sets code navigation attributes for line $line and character $char',
({ line, char, index }) => {
addInteractionClass('index.js', { start_line: line, start_char: char });
addInteractionClass({ path: 'index.js', d: { start_line: line, start_char: char } });
expect(document.querySelectorAll(`#LC${line + 1} span`)[index].classList).toContain(
'js-code-navigation',
);
},
);
describe('wrapTextNodes', () => {
beforeEach(() => {
setFixtures(
'<div data-path="index.js"><div class="blob-content"><div id="LC1" class="line"> Text </div></div></div>',
);
});
const params = { path: 'index.js', d: { start_line: 0, start_char: 0 } };
const findAllSpans = () => document.querySelectorAll('#LC1 span');
it('does not wrap text nodes by default', () => {
addInteractionClass(params);
const spans = findAllSpans();
expect(spans.length).toBe(0);
});
it('wraps text nodes if wrapTextNodes is true', () => {
addInteractionClass({ ...params, wrapTextNodes: true });
const spans = findAllSpans();
expect(spans.length).toBe(3);
expect(spans[0].textContent).toBe(' ');
expect(spans[1].textContent).toBe('Text');
expect(spans[2].textContent).toBe(' ');
});
});
});

View File

@ -258,6 +258,7 @@ describe('Blob content viewer component', () => {
codeNavigationPath: simpleViewerMock.codeNavigationPath,
blobPath: simpleViewerMock.path,
pathPrefix: simpleViewerMock.projectBlobPathRoot,
wrapTextNodes: true,
});
});

View File

@ -7,6 +7,7 @@ import Chunk from '~/vue_shared/components/source_viewer/components/chunk.vue';
import { ROUGE_TO_HLJS_LANGUAGE_MAP } from '~/vue_shared/components/source_viewer/constants';
import waitForPromises from 'helpers/wait_for_promises';
import LineHighlighter from '~/blob/line_highlighter';
import eventHub from '~/notes/event_hub';
jest.mock('~/blob/line_highlighter');
jest.mock('highlight.js/lib/core');
@ -30,7 +31,8 @@ describe('Source Viewer component', () => {
const chunk1 = generateContent('// Some source code 1', 70);
const chunk2 = generateContent('// Some source code 2', 70);
const content = chunk1 + chunk2;
const DEFAULT_BLOB_DATA = { language, rawTextBlob: content };
const path = 'some/path.js';
const DEFAULT_BLOB_DATA = { language, rawTextBlob: content, path };
const highlightedContent = `<span data-testid='test-highlighted' id='LC1'>${content}</span><span id='LC2'></span>`;
const createComponent = async (blob = {}) => {
@ -47,6 +49,7 @@ describe('Source Viewer component', () => {
hljs.highlight.mockImplementation(() => ({ value: highlightedContent }));
hljs.highlightAuto.mockImplementation(() => ({ value: highlightedContent }));
jest.spyOn(window, 'requestIdleCallback').mockImplementation(execImmediately);
jest.spyOn(eventHub, '$emit');
return createComponent();
});
@ -102,6 +105,11 @@ describe('Source Viewer component', () => {
});
});
it('emits showBlobInteractionZones on the eventHub when chunk appears', () => {
findChunks().at(0).vm.$emit('appear');
expect(eventHub.$emit).toBeCalledWith('showBlobInteractionZones', path);
});
describe('LineHighlighter', () => {
it('instantiates the lineHighlighter class', async () => {
expect(LineHighlighter).toHaveBeenCalledWith({ scrollBehavior: 'auto' });

View File

@ -134,6 +134,74 @@ RSpec.describe Note do
expect(existing_note.errors[:confidential]).to include('can not be changed for existing notes')
end
end
context 'for a new note' do
let_it_be(:noteable) { create(:issue) }
let(:note_params) { { confidential: true, noteable: noteable, project: noteable.project } }
subject { build(:note, **note_params) }
it 'allows to create a confidential note for an issue' do
expect(subject).to be_valid
end
context 'when noteable is not allowed to have confidential notes' do
let_it_be(:noteable) { create(:merge_request) }
it 'can not be set confidential' do
expect(subject).not_to be_valid
expect(subject.errors[:confidential]).to include('can not be set for this resource')
end
end
context 'when note type is not allowed to be confidential' do
let(:note_params) { { type: 'DiffNote', confidential: true, noteable: noteable, project: noteable.project } }
it 'can not be set confidential' do
expect(subject).not_to be_valid
expect(subject.errors[:confidential]).to include('can not be set for this type of note')
end
end
context 'when the note is a discussion note' do
let(:note_params) { { type: 'DiscussionNote', confidential: true, noteable: noteable, project: noteable.project } }
it { is_expected.to be_valid }
end
context 'when replying to a note' do
let(:note_params) { { confidential: true, noteable: noteable, project: noteable.project } }
subject { build(:discussion_note, discussion_id: original_note.discussion_id, **note_params) }
context 'when the note is reply to a confidential note' do
let_it_be(:original_note) { create(:note, confidential: true, noteable: noteable, project: noteable.project) }
it { is_expected.to be_valid }
end
context 'when the note is reply to a public note' do
let_it_be(:original_note) { create(:note, noteable: noteable, project: noteable.project) }
it 'can not be set confidential' do
expect(subject).not_to be_valid
expect(subject.errors[:confidential]).to include('reply should have same confidentiality as top-level note')
end
end
context 'when reply note is public but discussion is confidential' do
let_it_be(:original_note) { create(:note, confidential: true, noteable: noteable, project: noteable.project) }
let(:note_params) { { noteable: noteable, project: noteable.project } }
it 'can not be set confidential' do
expect(subject).not_to be_valid
expect(subject.errors[:confidential]).to include('reply should have same confidentiality as top-level note')
end
end
end
end
end
end
@ -1199,8 +1267,8 @@ RSpec.describe Note do
end
describe "#discussion" do
let!(:note1) { create(:discussion_note_on_merge_request) }
let!(:note2) { create(:diff_note_on_merge_request, project: note1.project, noteable: note1.noteable) }
let_it_be(:note1) { create(:discussion_note_on_merge_request) }
let_it_be(:note2) { create(:diff_note_on_merge_request, project: note1.project, noteable: note1.noteable) }
context 'when the note is part of a discussion' do
subject { create(:discussion_note_on_merge_request, project: note1.project, noteable: note1.noteable, in_reply_to: note1) }

View File

@ -359,39 +359,6 @@ RSpec.describe NotePolicy do
expect(permissions(assignee, confidential_note)).to be_disallowed(:admin_note, :reposition_note, :resolve_note)
end
end
context 'for merge requests' do
let(:merge_request) { create(:merge_request, source_project: project, author: author, assignees: [assignee]) }
let(:confidential_note) { create(:note, :confidential, project: project, noteable: merge_request) }
it_behaves_like 'confidential notes permissions'
it 'allows noteable assignees to read all notes' do
expect(permissions(assignee, confidential_note)).to be_allowed(:read_note, :award_emoji)
expect(permissions(assignee, confidential_note)).to be_disallowed(:admin_note, :reposition_note, :resolve_note)
end
end
context 'for project snippets' do
let(:project_snippet) { create(:project_snippet, project: project, author: author) }
let(:confidential_note) { create(:note, :confidential, project: project, noteable: project_snippet) }
it_behaves_like 'confidential notes permissions'
end
context 'for personal snippets' do
let(:personal_snippet) { create(:personal_snippet, author: author) }
let(:confidential_note) { create(:note, :confidential, project: nil, noteable: personal_snippet) }
it 'allows snippet author to read and resolve all notes' do
expect(permissions(author, confidential_note)).to be_allowed(:read_note, :resolve_note, :award_emoji)
expect(permissions(author, confidential_note)).to be_disallowed(:admin_note, :reposition_note)
end
it 'does not allow maintainers to read confidential notes and replies' do
expect(permissions(maintainer, confidential_note)).to be_disallowed(:read_note, :admin_note, :reposition_note, :resolve_note, :award_emoji)
end
end
end
end
end

View File

@ -17,8 +17,7 @@ RSpec.describe 'Adding a Note' do
noteable_id: GitlabSchema.id_from_object(noteable).to_s,
discussion_id: (GitlabSchema.id_from_object(discussion).to_s if discussion),
merge_request_diff_head_sha: head_sha.presence,
body: body,
confidential: true
body: body
}
graphql_mutation(:create_note, variables)
@ -49,7 +48,6 @@ RSpec.describe 'Adding a Note' do
post_graphql_mutation(mutation, current_user: current_user)
expect(mutation_response['note']['body']).to eq('Body text')
expect(mutation_response['note']['confidential']).to eq(true)
end
describe 'creating Notes in reply to a discussion' do
@ -79,6 +77,25 @@ RSpec.describe 'Adding a Note' do
end
end
context 'for an issue' do
let(:noteable) { create(:issue, project: project) }
let(:mutation) do
variables = {
noteable_id: GitlabSchema.id_from_object(noteable).to_s,
body: body,
confidential: true
}
graphql_mutation(:create_note, variables)
end
before do
project.add_developer(current_user)
end
it_behaves_like 'a Note mutation with confidential notes'
end
context 'when body only contains quick actions' do
let(:head_sha) { noteable.diff_head_sha }
let(:body) { '/merge' }

View File

@ -877,5 +877,35 @@ RSpec.describe API::Issues do
expect(response).to have_gitlab_http_status(:not_found)
end
context 'with a confidential note' do
let!(:note) do
create(
:note,
:confidential,
project: project,
noteable: issue,
author: create(:user)
)
end
it 'returns a full list of participants' do
get api("/projects/#{project.id}/issues/#{issue.iid}/participants", user)
expect(response).to have_gitlab_http_status(:ok)
participant_ids = json_response.map { |el| el['id'] }
expect(participant_ids).to match_array([issue.author_id, note.author_id])
end
context 'when user cannot see a confidential note' do
it 'returns a limited list of participants' do
get api("/projects/#{project.id}/issues/#{issue.iid}/participants", create(:user))
expect(response).to have_gitlab_http_status(:ok)
participant_ids = json_response.map { |el| el['id'] }
expect(participant_ids).to match_array([issue.author_id])
end
end
end
end
end

View File

@ -22,7 +22,7 @@ RSpec.describe API::Notes do
let!(:issue) { create(:issue, project: project, author: user) }
let!(:issue_note) { create(:note, noteable: issue, project: project, author: user) }
it_behaves_like "noteable API", 'projects', 'issues', 'iid' do
it_behaves_like "noteable API with confidential notes", 'projects', 'issues', 'iid' do
let(:parent) { project }
let(:noteable) { issue }
let(:note) { issue_note }

View File

@ -31,7 +31,7 @@ RSpec.describe Ci::JobArtifacts::DestroyAllExpiredService, :clean_gitlab_redis_s
end
before do
stub_const("#{described_class}::LARGE_LOOP_LIMIT", 1)
stub_const("#{described_class}::LOOP_LIMIT", 1)
# This artifact-with-file is created before the control execution to ensure
# that the DeletedObject operations are accounted for in the query count.
@ -130,7 +130,7 @@ RSpec.describe Ci::JobArtifacts::DestroyAllExpiredService, :clean_gitlab_redis_s
let!(:artifact) { create(:ci_job_artifact, :expired, job: job, locked: job.pipeline.locked) }
before do
stub_const("#{described_class}::LARGE_LOOP_LIMIT", 10)
stub_const("#{described_class}::LOOP_LIMIT", 10)
end
context 'when the import fails' do
@ -200,8 +200,7 @@ RSpec.describe Ci::JobArtifacts::DestroyAllExpiredService, :clean_gitlab_redis_s
context 'when loop reached loop limit' do
before do
stub_feature_flags(ci_artifact_fast_removal_large_loop_limit: false)
stub_const("#{described_class}::SMALL_LOOP_LIMIT", 1)
stub_const("#{described_class}::LOOP_LIMIT", 1)
end
it 'destroys one artifact' do

View File

@ -106,7 +106,8 @@ RSpec.describe Notes::CreateService do
type: 'DiffNote',
noteable_type: 'MergeRequest',
noteable_id: merge_request.id,
position: position.to_h)
position: position.to_h,
confidential: false)
end
before do
@ -141,7 +142,8 @@ RSpec.describe Notes::CreateService do
type: 'DiffNote',
noteable_type: 'MergeRequest',
noteable_id: merge_request.id,
position: position.to_h)
position: position.to_h,
confidential: false)
expect(merge_request).not_to receive(:diffs)
@ -173,7 +175,8 @@ RSpec.describe Notes::CreateService do
type: 'DiffNote',
noteable_type: 'MergeRequest',
noteable_id: merge_request.id,
position: position.to_h)
position: position.to_h,
confidential: false)
end
it 'note is associated with a note diff file' do
@ -201,7 +204,8 @@ RSpec.describe Notes::CreateService do
type: 'DiffNote',
noteable_type: 'MergeRequest',
noteable_id: merge_request.id,
position: position.to_h)
position: position.to_h,
confidential: false)
end
it 'note is not associated with a note diff file' do
@ -230,7 +234,8 @@ RSpec.describe Notes::CreateService do
type: 'DiffNote',
noteable_type: 'MergeRequest',
noteable_id: merge_request.id,
position: image_position.to_h)
position: image_position.to_h,
confidential: false)
end
it 'note is not associated with a note diff file' do
@ -306,7 +311,7 @@ RSpec.describe Notes::CreateService do
let_it_be(:merge_request) { create(:merge_request, source_project: project, labels: [bug_label]) }
let(:issuable) { merge_request }
let(:note_params) { opts.merge(noteable_type: 'MergeRequest', noteable_id: merge_request.id) }
let(:note_params) { opts.merge(noteable_type: 'MergeRequest', noteable_id: merge_request.id, confidential: false) }
let(:merge_request_quick_actions) do
[
QuickAction.new(

View File

@ -99,7 +99,7 @@ module LoginHelpers
fill_in "user_password", with: (password || "12345678")
check 'user_remember_me' if remember
click_button "Sign in"
find('[data-testid="sign-in-button"]:enabled').click
if two_factor_auth
fill_in "user_otp_attempt", with: user.reload.current_otp

View File

@ -85,3 +85,14 @@ RSpec.shared_examples 'a Note mutation when there are rate limit validation erro
end
end
end
RSpec.shared_examples 'a Note mutation with confidential notes' do
it_behaves_like 'a Note mutation that creates a Note'
it 'returns a Note with confidentiality enabled' do
post_graphql_mutation(mutation, current_user: current_user)
expect(mutation_response).to have_key('note')
expect(mutation_response['note']['confidential']).to eq(true)
end
end

View File

@ -28,34 +28,4 @@ RSpec.shared_examples 'issuable participants endpoint' do
expect(response).to have_gitlab_http_status(:not_found)
end
context 'with a confidential note' do
let!(:note) do
create(
:note,
:confidential,
project: project,
noteable: entity,
author: create(:user)
)
end
it 'returns a full list of participants' do
get api("/projects/#{project.id}/#{area}/#{entity.iid}/participants", user)
expect(response).to have_gitlab_http_status(:ok)
participant_ids = json_response.map { |el| el['id'] }
expect(participant_ids).to match_array([entity.author_id, note.author_id])
end
context 'when user cannot see a confidential note' do
it 'returns a limited list of participants' do
get api("/projects/#{project.id}/#{area}/#{entity.iid}/participants", create(:user))
expect(response).to have_gitlab_http_status(:ok)
participant_ids = json_response.map { |el| el['id'] }
expect(participant_ids).to match_array([entity.author_id])
end
end
end
end

View File

@ -142,15 +142,6 @@ RSpec.shared_examples 'noteable API' do |parent_type, noteable_type, id_name|
expect(json_response['author']['username']).to eq(user.username)
end
it "creates a confidential note if confidential is set to true" do
post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user), params: { body: 'hi!', confidential: true }
expect(response).to have_gitlab_http_status(:created)
expect(json_response['body']).to eq('hi!')
expect(json_response['confidential']).to be_truthy
expect(json_response['author']['username']).to eq(user.username)
end
it "returns a 400 bad request error if body not given" do
post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user)
@ -312,26 +303,22 @@ RSpec.shared_examples 'noteable API' do |parent_type, noteable_type, id_name|
put api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes/#{note.id}", user), params: params
end
context 'when eveything is ok' do
before do
subject
end
context 'when only body param is present' do
let(:params) { { body: 'Hello!' } }
it 'returns modified note' do
it 'updates the note text' do
subject
expect(note.reload.note).to eq('Hello!')
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['body']).to eq('Hello!')
end
it 'updates the note' do
expect(note.reload.note).to eq('Hello!')
expect(note.confidential).to be_falsey
end
end
context 'when also confidential param is set' do
let(:params) { { body: 'Hello!', confidential: true } }
context 'when confidential param is present' do
let(:params) { { confidential: true } }
it 'fails to update the note' do
it 'does not allow to change confidentiality' do
expect { subject }.not_to change { note.reload.note }
expect(response).to have_gitlab_http_status(:bad_request)
@ -376,3 +363,24 @@ RSpec.shared_examples 'noteable API' do |parent_type, noteable_type, id_name|
end
end
end
RSpec.shared_examples 'noteable API with confidential notes' do |parent_type, noteable_type, id_name|
it_behaves_like 'noteable API', parent_type, noteable_type, id_name
describe "POST /#{parent_type}/:id/#{noteable_type}/:noteable_id/notes" do
let(:params) { { body: 'hi!' } }
subject do
post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user), params: params
end
it "creates a confidential note if confidential is set to true" do
post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user), params: params.merge(confidential: true)
expect(response).to have_gitlab_http_status(:created)
expect(json_response['body']).to eq('hi!')
expect(json_response['confidential']).to be_truthy
expect(json_response['author']['username']).to eq(user.username)
end
end
end