Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-03-08 09:17:44 +00:00
parent 8986fe3483
commit 60028378dd
81 changed files with 820 additions and 176 deletions

View File

@ -476,7 +476,7 @@ gem 'ssh_data', '~> 1.2'
gem 'spamcheck', '~> 0.1.0'
# Gitaly GRPC protocol definitions
gem 'gitaly', '~> 14.8.0.pre.rc1'
gem 'gitaly', '~> 14.9.0.pre.rc2'
# KAS GRPC protocol definitions
gem 'kas-grpc', '~> 0.0.2'

View File

@ -455,7 +455,7 @@ GEM
rails (>= 3.2.0)
git (1.7.0)
rchardet (~> 1.8)
gitaly (14.8.0.pre.rc1)
gitaly (14.9.0.pre.rc2)
grpc (~> 1.0)
github-markup (1.7.0)
gitlab (4.16.1)
@ -1475,7 +1475,7 @@ DEPENDENCIES
gettext (~> 3.3)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3)
gitaly (~> 14.8.0.pre.rc1)
gitaly (~> 14.9.0.pre.rc2)
github-markup (~> 1.7.0)
gitlab-chronic (~> 0.10.5)
gitlab-dangerfiles (~> 2.10.2)

View File

@ -1,5 +1,6 @@
import { sortBy, cloneDeep } from 'lodash';
import { isGid } from '~/graphql_shared/utils';
import { TYPE_BOARD, TYPE_ITERATION, TYPE_MILESTONE, TYPE_USER } from '~/graphql_shared/constants';
import { isGid, convertToGraphQLId } from '~/graphql_shared/utils';
import { ListType, MilestoneIDs, AssigneeFilterType, MilestoneFilterType } from './constants';
export function getMilestone() {
@ -80,19 +81,22 @@ export function formatListsPageInfo(lists) {
}
export function fullBoardId(boardId) {
return `gid://gitlab/Board/${boardId}`;
if (!boardId) {
return null;
}
return convertToGraphQLId(TYPE_BOARD, boardId);
}
export function fullIterationId(id) {
return `gid://gitlab/Iteration/${id}`;
return convertToGraphQLId(TYPE_ITERATION, id);
}
export function fullUserId(id) {
return `gid://gitlab/User/${id}`;
return convertToGraphQLId(TYPE_USER, id);
}
export function fullMilestoneId(id) {
return `gid://gitlab/Milestone/${id}`;
return convertToGraphQLId(TYPE_MILESTONE, id);
}
export function fullLabelId(label) {

View File

@ -1,11 +1,8 @@
<script>
import { GlModal, GlAlert } from '@gitlab/ui';
import { mapGetters, mapActions, mapState } from 'vuex';
import { TYPE_USER, TYPE_ITERATION, TYPE_MILESTONE } from '~/graphql_shared/constants';
import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
import { getParameterByName, visitUrl } from '~/lib/utils/url_utility';
import { __, s__ } from '~/locale';
import { fullLabelId } from '../boards_util';
import { formType } from '../constants';
import createBoardMutation from '../graphql/board_create.mutation.graphql';
@ -158,33 +155,8 @@ export default {
groupPath: this.isGroupBoard ? this.fullPath : undefined,
};
},
issueBoardScopeMutationVariables() {
return {
weight: this.board.weight,
assigneeId: this.board.assignee?.id
? convertToGraphQLId(TYPE_USER, this.board.assignee.id)
: null,
// Temporarily converting to milestone ID due to https://gitlab.com/gitlab-org/gitlab/-/issues/344779
milestoneId: this.board.milestone?.id
? convertToGraphQLId(TYPE_MILESTONE, getIdFromGraphQLId(this.board.milestone.id))
: null,
// Temporarily converting to iteration ID due to https://gitlab.com/gitlab-org/gitlab/-/issues/344779
iterationId: this.board.iteration?.id
? convertToGraphQLId(TYPE_ITERATION, getIdFromGraphQLId(this.board.iteration.id))
: null,
};
},
boardScopeMutationVariables() {
return {
labelIds: this.board.labels.map(fullLabelId),
...(this.isIssueBoard && this.issueBoardScopeMutationVariables),
};
},
mutationVariables() {
return {
...this.baseMutationVariables,
...(this.scopedIssueBoardFeatureEnabled ? this.boardScopeMutationVariables : {}),
};
return this.baseMutationVariables;
},
},
mounted() {

View File

@ -101,6 +101,7 @@ export default {
},
update(data) {
const board = data.workspace?.board;
this.setBoardConfig(board);
return {
...board,
labels: board?.labels?.nodes,
@ -170,7 +171,7 @@ export default {
eventHub.$off('showBoardModal', this.showPage);
},
methods: {
...mapActions(['setError']),
...mapActions(['setError', 'setBoardConfig']),
showPage(page) {
this.currentPage = page;
},

View File

@ -28,6 +28,12 @@ const apolloProvider = new VueApollo({
function mountBoardApp(el) {
const { boardId, groupId, fullPath, rootPath } = el.dataset;
store.dispatch('fetchBoard', {
fullPath,
fullBoardId: fullBoardId(boardId),
boardType: el.dataset.parent,
});
store.dispatch('setInitialBoardData', {
boardId,
fullBoardId: fullBoardId(boardId),
@ -35,17 +41,6 @@ function mountBoardApp(el) {
boardType: el.dataset.parent,
disabled: parseBoolean(el.dataset.disabled) || true,
issuableType: issuableTypes.issue,
boardConfig: {
milestoneId: parseInt(el.dataset.boardMilestoneId, 10),
milestoneTitle: el.dataset.boardMilestoneTitle || '',
iterationId: parseInt(el.dataset.boardIterationId, 10),
iterationTitle: el.dataset.boardIterationTitle || '',
assigneeId: el.dataset.boardAssigneeId,
assigneeUsername: el.dataset.boardAssigneeUsername,
labels: el.dataset.labels ? JSON.parse(el.dataset.labels) : [],
labelIds: el.dataset.labelIds ? JSON.parse(el.dataset.labelIds) : [],
weight: el.dataset.boardWeight ? parseInt(el.dataset.boardWeight, 10) : null,
},
});
// eslint-disable-next-line no-new

View File

@ -36,6 +36,8 @@ import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { queryToObject } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
import { gqlClient } from '../graphql';
import projectBoardQuery from '../graphql/project_board.query.graphql';
import groupBoardQuery from '../graphql/group_board.query.graphql';
import boardLabelsQuery from '../graphql/board_labels.query.graphql';
import groupBoardMilestonesQuery from '../graphql/group_board_milestones.query.graphql';
import groupProjectsQuery from '../graphql/group_projects.query.graphql';
@ -46,10 +48,44 @@ import projectBoardMilestonesQuery from '../graphql/project_board_milestones.que
import * as types from './mutation_types';
export default {
fetchBoard: ({ commit, dispatch }, { fullPath, fullBoardId, boardType }) => {
const variables = {
fullPath,
boardId: fullBoardId,
};
return gqlClient
.query({
query: boardType === BoardType.group ? groupBoardQuery : projectBoardQuery,
variables,
})
.then(({ data }) => {
const board = data.workspace?.board;
commit(types.RECEIVE_BOARD_SUCCESS, board);
dispatch('setBoardConfig', board);
})
.catch(() => commit(types.RECEIVE_BOARD_FAILURE));
},
setInitialBoardData: ({ commit }, data) => {
commit(types.SET_INITIAL_BOARD_DATA, data);
},
setBoardConfig: ({ commit }, board) => {
const config = {
milestoneId: board.milestone?.id || null,
milestoneTitle: board.milestone?.title || null,
iterationId: board.iteration?.id || null,
iterationTitle: board.iteration?.title || null,
assigneeId: board.assignee?.id || null,
assigneeUsername: board.assignee?.username || null,
labels: board.labels?.nodes || [],
labelIds: board.labels?.nodes?.map((label) => label.id) || [],
weight: board.weight,
};
commit(types.SET_BOARD_CONFIG, config);
},
setActiveId({ commit }, { id, sidebarType }) {
commit(types.SET_ACTIVE_ID, { id, sidebarType });
},

View File

@ -1,4 +1,7 @@
export const RECEIVE_BOARD_SUCCESS = 'RECEIVE_BOARD_SUCCESS';
export const RECEIVE_BOARD_FAILURE = 'RECEIVE_BOARD_FAILURE';
export const SET_INITIAL_BOARD_DATA = 'SET_INITIAL_BOARD_DATA';
export const SET_BOARD_CONFIG = 'SET_BOARD_CONFIG';
export const SET_FILTERS = 'SET_FILTERS';
export const CREATE_LIST_SUCCESS = 'CREATE_LIST_SUCCESS';
export const CREATE_LIST_FAILURE = 'CREATE_LIST_FAILURE';

View File

@ -33,10 +33,20 @@ export const addItemToList = ({ state, listId, itemId, moveBeforeId, moveAfterId
};
export default {
[mutationTypes.RECEIVE_BOARD_SUCCESS]: (state, board) => {
state.board = {
...board,
labels: board?.labels?.nodes || [],
};
},
[mutationTypes.RECEIVE_BOARD_FAILURE]: (state) => {
state.error = s__('Boards|An error occurred while fetching the board. Please reload the page.');
},
[mutationTypes.SET_INITIAL_BOARD_DATA](state, data) {
const {
allowSubEpics,
boardConfig,
boardId,
boardType,
disabled,
@ -45,7 +55,6 @@ export default {
issuableType,
} = data;
state.allowSubEpics = allowSubEpics;
state.boardConfig = boardConfig;
state.boardId = boardId;
state.boardType = boardType;
state.disabled = disabled;
@ -54,6 +63,10 @@ export default {
state.issuableType = issuableType;
},
[mutationTypes.SET_BOARD_CONFIG](state, boardConfig) {
state.boardConfig = boardConfig;
},
[mutationTypes.RECEIVE_BOARD_LISTS_SUCCESS]: (state, lists) => {
state.boardLists = lists;
},

View File

@ -1,6 +1,7 @@
import { inactiveId, ListType } from '~/boards/constants';
export default () => ({
board: {},
boardType: null,
issuableType: null,
fullPath: null,

View File

@ -3,6 +3,7 @@ import '~/lib/utils/jquery_at_who';
import { escape as lodashEscape, sortBy, template, escapeRegExp } from 'lodash';
import * as Emoji from '~/emoji';
import axios from '~/lib/utils/axios_utils';
import { loadingIconForLegacyJS } from '~/loading_icon_for_legacy_js';
import { s__, __, sprintf } from '~/locale';
import { isUserBusy } from '~/set_status_modal/utils';
import SidebarMediator from '~/sidebar/sidebar_mediator';
@ -957,9 +958,14 @@ GfmAutoComplete.Contacts = {
return `<li><small>${firstName} ${lastName}</small> ${escape(email)}</li>`;
},
};
const loadingSpinner = loadingIconForLegacyJS({
inline: true,
classes: ['gl-mr-2'],
}).outerHTML;
GfmAutoComplete.Loading = {
template:
'<li style="pointer-events: none;"><span class="spinner align-text-bottom mr-1"></span>Loading...</li>',
template: `<li style="pointer-events: none;">${loadingSpinner}Loading...</li>`,
};
export default GfmAutoComplete;

View File

@ -1,9 +1,11 @@
export const MINIMUM_SEARCH_LENGTH = 3;
export const TYPE_BOARD = 'Board';
export const TYPE_CI_RUNNER = 'Ci::Runner';
export const TYPE_CRM_CONTACT = 'CustomerRelations::Contact';
export const TYPE_DISCUSSION = 'Discussion';
export const TYPE_EPIC = 'Epic';
export const TYPE_EPIC_BOARD = 'Boards::EpicBoard';
export const TYPE_GROUP = 'Group';
export const TYPE_ISSUE = 'Issue';
export const TYPE_ITERATION = 'Iteration';

View File

@ -9,7 +9,7 @@ const LINK_TAG_PATTERN = '[{text}](url)';
// a bullet point character (*+-) and an optional checkbox ([ ] [x])
// OR a number with a . after it and an optional checkbox ([ ] [x])
// followed by one or more whitespace characters
const LIST_LINE_HEAD_PATTERN = /^(?<indent>\s*)(?<leader>((?<isOl>[*+-])|(?<isUl>\d+\.))( \[([x ])\])?\s)(?<content>.)?/;
const LIST_LINE_HEAD_PATTERN = /^(?<indent>\s*)(?<leader>((?<isUl>[*+-])|(?<isOl>\d+\.))( \[([x ])\])?\s)(?<content>.)?/;
function selectedText(text, textarea) {
return text.substring(textarea.selectionStart, textarea.selectionEnd);
@ -31,8 +31,19 @@ function lineBefore(text, textarea, trimNewlines = true) {
return split[split.length - 1];
}
function lineAfter(text, textarea) {
return text.substring(textarea.selectionEnd).trim().split('\n')[0];
function lineAfter(text, textarea, trimNewlines = true) {
let split = text.substring(textarea.selectionEnd);
if (trimNewlines) {
split = split.trim();
} else {
// remove possible leading newline to get at the real line
split = split.replace(/^\n/, '');
}
split = split.split('\n');
return split[0];
}
function convertMonacoSelectionToAceFormat(sel) {
@ -329,6 +340,25 @@ function handleSurroundSelectedText(e, textArea) {
}
/* eslint-enable @gitlab/require-i18n-strings */
/**
* Returns the content for a new line following a list item.
*
* @param {Object} result - regex match of the current line
* @param {Object?} nextLineResult - regex match of the next line
* @returns string with the new list item
*/
function continueOlText(result, nextLineResult) {
const { indent, leader } = result.groups;
const { indent: nextIndent, isOl: nextIsOl } = nextLineResult?.groups ?? {};
const [numStr, postfix = ''] = leader.split('.');
const incrementBy = nextIsOl && nextIndent === indent ? 0 : 1;
const num = parseInt(numStr, 10) + incrementBy;
return `${indent}${num}.${postfix}`;
}
function handleContinueList(e, textArea) {
if (!gon.features?.markdownContinueLists) return;
if (!(e.key === 'Enter')) return;
@ -339,7 +369,7 @@ function handleContinueList(e, textArea) {
const result = currentLine.match(LIST_LINE_HEAD_PATTERN);
if (result) {
const { indent, content, leader } = result.groups;
const { leader, indent, content, isOl } = result.groups;
const prevLineEmpty = !content;
if (prevLineEmpty) {
@ -349,12 +379,22 @@ function handleContinueList(e, textArea) {
return;
}
const itemInsert = `${indent}${leader}`;
let itemToInsert;
if (isOl) {
const nextLine = lineAfter(textArea.value, textArea, false);
const nextLineResult = nextLine.match(LIST_LINE_HEAD_PATTERN);
itemToInsert = continueOlText(result, nextLineResult);
} else {
// isUl
itemToInsert = `${indent}${leader}`;
}
e.preventDefault();
updateText({
tag: itemInsert,
tag: itemToInsert,
textArea,
blockTag: '',
wrap: false,

View File

@ -1,6 +1,7 @@
/* eslint-disable consistent-return */
import $ from 'jquery';
import { loadingIconForLegacyJS } from '~/loading_icon_for_legacy_js';
import { spriteIcon } from '~/lib/utils/common_utils';
import FilesCommentButton from './files_comment_button';
import createFlash from './flash';
@ -10,7 +11,7 @@ import { __ } from './locale';
import syntaxHighlight from './syntax_highlight';
const WRAPPER = '<div class="diff-content"></div>';
const LOADING_HTML = '<span class="spinner"></span>';
const LOADING_HTML = loadingIconForLegacyJS().outerHTML;
const ERROR_HTML = `<div class="nothing-here-block">${spriteIcon(
'warning-solid',
's16',

View File

@ -35,16 +35,20 @@ export default {
},
highlightedContent() {
let highlightedContent;
let { language } = this;
if (this.hljs) {
if (!this.language) {
highlightedContent = this.hljs.highlightAuto(this.content).value;
if (!language) {
const hljsHighlightAuto = this.hljs.highlightAuto(this.content);
highlightedContent = hljsHighlightAuto.value;
language = hljsHighlightAuto.language;
} else if (this.languageDefinition) {
highlightedContent = this.hljs.highlight(this.content, { language: this.language }).value;
}
}
return wrapLines(highlightedContent);
return wrapLines(highlightedContent, language);
},
},
watch: {
@ -110,7 +114,7 @@ export default {
data-qa-selector="blob_viewer_file_content"
>
<line-numbers :lines="lineNumbers" />
<pre class="code gl-pb-0!"><code v-safe-html="highlightedContent"></code>
<pre class="code highlight gl-pb-0!"><code v-safe-html="highlightedContent"></code>
</pre>
</div>
</template>

View File

@ -1,11 +1,13 @@
export const wrapLines = (content) => {
export const wrapLines = (content, language) => {
const isValidLanguage = /^[a-z\d\-_]+$/.test(language); // To prevent the possibility of a vulnerability we only allow languages that contain alphanumeric characters ([a-z\d), dashes (-) or underscores (_).
return (
content &&
content
.split('\n')
.map((line, i) => {
let formattedLine;
const idAttribute = `id="LC${i + 1}"`;
const attributes = `id="LC${i + 1}" lang="${isValidLanguage ? language : ''}"`;
if (line.includes('<span class="hljs') && !line.includes('</span>')) {
/**
@ -14,9 +16,9 @@ export const wrapLines = (content) => {
* example (before): <span class="hljs-code">```bash
* example (after): <span id="LC67" class="hljs-code">```bash
*/
formattedLine = line.replace(/(?=class="hljs)/, `${idAttribute} `);
formattedLine = line.replace(/(?=class="hljs)/, `${attributes} `);
} else {
formattedLine = `<span ${idAttribute} class="line">${line}</span>`;
formattedLine = `<span ${attributes} class="line">${line}</span>`;
}
return formattedLine;

View File

@ -5,6 +5,7 @@ class ApplicationRecord < ActiveRecord::Base
include Transactions
include LegacyBulkInsert
include CrossDatabaseModification
include SensitiveSerializableHash
self.abstract_class = true

View File

@ -0,0 +1,46 @@
# frozen_string_literal: true
module SensitiveSerializableHash
extend ActiveSupport::Concern
included do
class_attribute :attributes_exempt_from_serializable_hash, default: []
end
class_methods do
def prevent_from_serialization(*keys)
self.attributes_exempt_from_serializable_hash ||= []
self.attributes_exempt_from_serializable_hash.concat keys
end
end
# Override serializable_hash to exclude sensitive attributes by default
#
# In general, prefer NOT to use serializable_hash / to_json / as_json in favor
# of serializers / entities instead which has an allowlist of attributes
def serializable_hash(options = nil)
return super unless prevent_sensitive_fields_from_serializable_hash?
return super if options && options[:unsafe_serialization_hash]
options = options.try(:dup) || {}
options[:except] = Array(options[:except]).dup
options[:except].concat self.class.attributes_exempt_from_serializable_hash
if self.class.respond_to?(:encrypted_attributes)
options[:except].concat self.class.encrypted_attributes.keys
# Per https://github.com/attr-encrypted/attr_encrypted/blob/a96693e9a2a25f4f910bf915e29b0f364f277032/lib/attr_encrypted.rb#L413
options[:except].concat self.class.encrypted_attributes.values.map { |v| v[:attribute] }
options[:except].concat self.class.encrypted_attributes.values.map { |v| "#{v[:attribute]}_iv" }
end
super(options)
end
private
def prevent_sensitive_fields_from_serializable_hash?
Feature.enabled?(:prevent_sensitive_fields_from_serializable_hash, default_enabled: :yaml)
end
end

View File

@ -8,6 +8,10 @@ module TokenAuthenticatable
@encrypted_token_authenticatable_fields ||= []
end
def token_authenticatable_fields
@token_authenticatable_fields ||= []
end
private
def add_authentication_token_field(token_field, options = {})
@ -23,6 +27,8 @@ module TokenAuthenticatable
strategy = TokenAuthenticatableStrategies::Base
.fabricate(self, token_field, options)
prevent_from_serialization(*strategy.token_fields) if respond_to?(:prevent_from_serialization)
if options.fetch(:unique, true)
define_singleton_method("find_by_#{token_field}") do |token|
strategy.find_token_authenticatable(token)
@ -82,9 +88,5 @@ module TokenAuthenticatable
@token_authenticatable_module ||=
const_set(:TokenAuthenticatable, Module.new).tap(&method(:include))
end
def token_authenticatable_fields
@token_authenticatable_fields ||= []
end
end
end

View File

@ -23,6 +23,14 @@ module TokenAuthenticatableStrategies
raise NotImplementedError
end
def token_fields
result = [token_field]
result << @expires_at_field if expirable?
result
end
# Default implementation returns the token as-is
def format_token(instance, token)
instance.send("format_#{@token_field}", token) # rubocop:disable GitlabSecurity/PublicSend

View File

@ -2,6 +2,10 @@
module TokenAuthenticatableStrategies
class Digest < Base
def token_fields
super + [token_field_name]
end
def find_token_authenticatable(token, unscoped = false)
return unless token

View File

@ -2,6 +2,10 @@
module TokenAuthenticatableStrategies
class Encrypted < Base
def token_fields
super + [encrypted_field]
end
def find_token_authenticatable(token, unscoped = false)
return if token.blank?

View File

@ -150,10 +150,10 @@ class Wiki
# the page.
#
# Returns an initialized WikiPage instance or nil
def find_page(title, version = nil)
def find_page(title, version = nil, load_content: true)
page_title, page_dir = page_title_and_dir(title)
if page = wiki.page(title: page_title, version: version, dir: page_dir)
if page = wiki.page(title: page_title, version: version, dir: page_dir, load_content: load_content)
WikiPage.new(self, page)
end
end

View File

@ -0,0 +1,8 @@
---
name: prevent_sensitive_fields_from_serializable_hash
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/81773
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/353878
milestone: '14.9'
type: development
group: group::sharding
default_enabled: false

View File

@ -5,7 +5,7 @@ def feature_mr?
end
DOCUMENTATION_UPDATE_MISSING = <<~MSG
~"feature::addition" and ~"feature::enhancement" merge requests normally have a documentation change. Consider adding a documentation update or confirming the documentation plan with the [Technical Writer counterpart](https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers).
~"feature::addition" and ~"feature::enhancement" merge requests normally have a documentation change. Consider adding a documentation update or confirming the documentation plan with the [Technical Writer counterpart](https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments).
For more information, see:
@ -36,6 +36,6 @@ markdown(<<~MARKDOWN)
The review does not need to block merging this merge request. See the:
- [Metadata for the `*.md` files](https://docs.gitlab.com/ee/development/documentation/#metadata) that you've changed. The first few lines of each `*.md` file identify the stage and group most closely associated with your docs change.
- The [Technical Writer assigned](https://about.gitlab.com/handbook/engineering/technical-writing/#designated-technical-writers) for that stage and group.
- The [Technical Writer assigned](https://about.gitlab.com/handbook/engineering/technical-writing/#assignments) for that stage and group.
- [Documentation workflows](https://docs.gitlab.com/ee/development/documentation/workflow.html) for information on when to assign a merge request for review.
MARKDOWN

View File

@ -1,7 +1,7 @@
---
stage: Enablement
group: Distribution
info: "To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers"
info: "To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments"
type: reference
---

View File

@ -1,7 +1,7 @@
---
stage: Enablement
group: Distribution
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
# Package information **(FREE SELF)**

View File

@ -1,7 +1,7 @@
---
stage: Enablement
group: Distribution
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
# Package Licensing **(FREE SELF)**

View File

@ -1,7 +1,7 @@
---
stage: Enablement
group: Distribution
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
# Omnibus based packages and images **(FREE SELF)**

View File

@ -1,7 +1,7 @@
---
stage: Enablement
group: Distribution
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
# PostgreSQL versions shipped with Omnibus GitLab **(FREE SELF)**

View File

@ -1,7 +1,7 @@
---
stage: Enablement
group: Distribution
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
# Package Signatures **(FREE SELF)**

View File

@ -1,7 +1,7 @@
---
stage: Ecosystem
group: Integrations
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
<!---

View File

@ -2,7 +2,7 @@
type: reference, howto
stage: Manage
group: Authentication and Authorization
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
# OAuth 2.0 identity provider API **(FREE)**

View File

@ -1,7 +1,7 @@
---
stage: Package
group: Package
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
comments: false
description: 'Container Registry metadata database'
---

View File

@ -1,7 +1,7 @@
---
stage: Configure
group: Configure
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
comments: false
description: 'GitLab to Kubernetes communication'
---

View File

@ -1,7 +1,7 @@
---
stage: Verify
group: Pipeline Authoring
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
type: reference
---

View File

@ -1,7 +1,7 @@
---
stage: Manage
group: Import
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
# GitLab Group Migration

View File

@ -35,7 +35,7 @@ Check these aspects both when _designing_ and _reviewing_ UI changes.
- Use clear and consistent [terminology](https://design.gitlab.com/content/terminology/).
- Check grammar and spelling.
- Consider help content and follow its [guidelines](https://design.gitlab.com/usability/helping-users/).
- Request review from the [appropriate Technical Writer](https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers),
- Request review from the [appropriate Technical Writer](https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments),
indicating any specific files or lines they should review, and how to preview
or understand the location/context of the text from the user's perspective.

View File

@ -1,7 +1,7 @@
---
stage: Manage
group: Import
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
# Export to CSV

View File

@ -390,7 +390,7 @@ persistence and is used to store session data, temporary cache information, and
chance to deploy Redis in multiple availability zones.
1. In the settings section:
1. Give the cluster a name (`gitlab-redis`) and a description.
1. For the version, select the latest of the `5.0` series (for example, `5.0.6`).
1. For the version, select the latest.
1. Leave the port as `6379` since this is what we used in our Redis security group above.
1. Select the node type (at least `cache.t3.medium`, but adjust to your needs) and the number of replicas.
1. In the advanced settings section:

View File

@ -1,7 +1,7 @@
---
stage: Secure
group: Static Analysis
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
type: index
---

View File

@ -1,7 +1,7 @@
---
stage: Enablement
group: Distribution
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
# Convert Community Edition to Enterprise Edition **(FREE SELF)**

View File

@ -1,7 +1,7 @@
---
stage: Enablement
group: Distribution
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
# Downgrade **(FREE SELF)**

View File

@ -1,7 +1,7 @@
---
stage: Enablement
group: Distribution
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
# Upgrade GitLab by using the GitLab package **(FREE SELF)**

View File

@ -1,7 +1,7 @@
---
stage: Enablement
group: Distribution
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
# Spamcheck anti-spam service **(PREMIUM SELF)**

View File

@ -1,7 +1,7 @@
---
stage: Secure
group: Dynamic Analysis
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
type: howto
---

View File

@ -1,7 +1,7 @@
---
stage: Configure
group: Configure
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
# Using a GitOps workflow for Kubernetes **(PREMIUM)**

View File

@ -1,7 +1,7 @@
---
stage: Configure
group: Configure
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
# Managing Kubernetes secrets in a GitOps workflow

View File

@ -1,7 +1,7 @@
---
stage: Configure
group: Configure
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
# Working with the agent for Kubernetes **(FREE)**

View File

@ -1,7 +1,7 @@
---
stage: Configure
group: Configure
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
# Container vulnerability scanning **(ULTIMATE)**

View File

@ -2,7 +2,7 @@
type: reference
stage: Manage
group: Optimize
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
# Value stream analytics for groups **(PREMIUM)**

View File

@ -1,7 +1,7 @@
---
stage: Protect
group: Container Security
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
# Container Host Security **(FREE)**

View File

@ -1,7 +1,7 @@
---
stage: Protect
group: Container Security
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
# Getting started with Container Host Security **(FREE)**

View File

@ -1,7 +1,7 @@
---
stage: Protect
group: Container Security
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
# Container Network Security **(FREE)**

View File

@ -1,7 +1,7 @@
---
stage: Protect
group: Container Security
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
# Getting started with Container Network Security **(FREE)**

View File

@ -1,7 +1,7 @@
---
stage: Protect
group: Container Security
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
# Protecting your deployed applications **(FREE)**

View File

@ -2,7 +2,7 @@
type: howto
stage: Fulfillment
group: Utilization
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
# Storage usage quota **(FREE)**

View File

@ -93,9 +93,9 @@ module Gitlab
end
end
def page(title:, version: nil, dir: nil)
def page(title:, version: nil, dir: nil, load_content: true)
wrapped_gitaly_errors do
gitaly_find_page(title: title, version: version, dir: dir)
gitaly_find_page(title: title, version: version, dir: dir, load_content: load_content)
end
end
@ -121,10 +121,10 @@ module Gitlab
gitaly_wiki_client.update_page(page_path, title, format, content, commit_details)
end
def gitaly_find_page(title:, version: nil, dir: nil)
def gitaly_find_page(title:, version: nil, dir: nil, load_content: true)
return unless title.present?
wiki_page, version = gitaly_wiki_client.find_page(title: title, version: version, dir: dir)
wiki_page, version = gitaly_wiki_client.find_page(title: title, version: version, dir: dir, load_content: load_content)
return unless wiki_page
Gitlab::Git::WikiPage.new(wiki_page, version)

View File

@ -64,12 +64,13 @@ module Gitlab
GitalyClient.call(@repository.storage, :wiki_service, :wiki_update_page, enum, timeout: GitalyClient.medium_timeout)
end
def find_page(title:, version: nil, dir: nil)
def find_page(title:, version: nil, dir: nil, load_content: true)
request = Gitaly::WikiFindPageRequest.new(
repository: @gitaly_repo,
title: encode_binary(title),
revision: encode_binary(version),
directory: encode_binary(dir)
directory: encode_binary(dir),
skip_content: !load_content
)
response = GitalyClient.call(@repository.storage, :wiki_service, :wiki_find_page, request, timeout: GitalyClient.fast_timeout)

View File

@ -43,7 +43,9 @@ module Gitlab
end
def write(key, value, options = nil)
backend.write(cache_key(key), value.to_json, options)
# As we use json as the serialization format, return everything from
# ActiveModel objects included encrypted values.
backend.write(cache_key(key), value.to_json(unsafe_serialization_hash: true), options)
end
def fetch(key, options = {}, &block)

View File

@ -5885,6 +5885,9 @@ msgstr ""
msgid "Boards|An error occurred while fetching the board swimlanes. Please reload the page."
msgstr ""
msgid "Boards|An error occurred while fetching the board. Please reload the page."
msgstr ""
msgid "Boards|An error occurred while generating lists. Please reload the page."
msgstr ""

View File

@ -0,0 +1,59 @@
# frozen_string_literal: true
require_relative 'gitlab_project_migration_common'
module QA
RSpec.describe 'Manage' do
describe 'Gitlab migration' do
include_context 'with gitlab project migration'
context 'with ci pipeline' do
let!(:source_project_with_readme) { true }
let(:source_pipelines) do
source_project.pipelines.map do |pipeline|
pipeline.except(:id, :web_url, :project_id)
end
end
let(:imported_pipelines) do
imported_project.pipelines.map do |pipeline|
pipeline.except(:id, :web_url, :project_id)
end
end
before do
Resource::Repository::Commit.fabricate_via_api! do |commit|
commit.api_client = api_client
commit.project = source_project
commit.commit_message = 'Add .gitlab-ci.yml'
commit.add_files(
[
{
file_path: '.gitlab-ci.yml',
content: <<~YML
test-success:
script: echo 'OK'
YML
}
]
)
end
Support::Waiter.wait_until(max_duration: 10, sleep_interval: 1) do
!source_project.pipelines.empty?
end
end
it(
'successfully imports ci pipeline',
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/354650'
) do
expect_import_finished
expect(imported_pipelines).to eq(source_pipelines)
end
end
end
end
end

View File

@ -1,9 +1,9 @@
# frozen_string_literal: true
module QA
# Disable on staging until bulk_import_projects toggle is on by default
# Disable on live envs until bulk_import_projects toggle is on by default
# Otherwise tests running in parallel can disable feature in the middle of other test
RSpec.shared_context 'with gitlab project migration', :requires_admin, except: { subdomain: :staging } do
RSpec.shared_context 'with gitlab project migration', :requires_admin, :skip_live_env do
let(:source_project_with_readme) { false }
let(:import_wait_duration) { { max_duration: 300, sleep_interval: 2 } }
let(:admin_api_client) { Runtime::API::Client.as_admin }
@ -79,8 +79,6 @@ module QA
# Log failures for easier debugging
Runtime::Logger.warn("Import failures: #{import_failures}") if example.exception && !import_failures.empty?
ensure
Runtime::Feature.disable(:bulk_import_projects)
user.remove_via_api!
end
end

View File

@ -7,10 +7,6 @@ RSpec.describe 'Copy as GFM', :js do
include RepoHelpers
include ActionView::Helpers::JavaScriptHelper
before do
stub_feature_flags(refactor_blob_viewer: false) # This stub will be removed in https://gitlab.com/gitlab-org/gitlab/-/issues/350454
end
describe 'Copying rendered GFM' do
before do
@feat = MarkdownFeature.new
@ -764,8 +760,8 @@ RSpec.describe 'Copy as GFM', :js do
context 'selecting one word of text' do
it 'copies as inline code' do
verify(
'.line[id="LC9"] .no',
'`RuntimeError`'
'.line[id="LC10"]',
'`end`'
)
end
end
@ -834,6 +830,7 @@ RSpec.describe 'Copy as GFM', :js do
end
def verify(selector, gfm, target: nil)
expect(page).to have_selector('.js-syntax-highlight')
html = html_for_selector(selector)
output_gfm = html_to_gfm(html, 'transformCodeSelection', target: target)
wait_for_requests

View File

@ -28,7 +28,7 @@ RSpec.describe 'User triggers manual job with variables', :js do
wait_for_requests
expect(build.job_variables.as_json).to contain_exactly(
expect(build.job_variables.as_json(only: [:key, :value])).to contain_exactly(
hash_including('key' => 'key_name', 'value' => 'key_value'))
end
end

View File

@ -41,6 +41,7 @@ describe('BoardsSelector', () => {
...defaultStore,
actions: {
setError: jest.fn(),
setBoardConfig: jest.fn(),
},
getters: {
isGroupBoard: () => isGroupBoard,

View File

@ -8,6 +8,37 @@ import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label
import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue';
import ReleaseToken from '~/vue_shared/components/filtered_search_bar/tokens/release_token.vue';
export const mockBoard = {
milestone: {
id: 'gid://gitlab/Milestone/114',
title: '14.9',
},
iteration: {
id: 'gid://gitlab/Iteration/124',
title: 'Iteration 9',
},
assignee: {
id: 'gid://gitlab/User/1',
username: 'admin',
},
labels: {
nodes: [{ id: 'gid://gitlab/Label/32', title: 'Deliverable' }],
},
weight: 2,
};
export const mockBoardConfig = {
milestoneId: 'gid://gitlab/Milestone/114',
milestoneTitle: '14.9',
iterationId: 'gid://gitlab/Iteration/124',
iterationTitle: 'Iteration 9',
assigneeId: 'gid://gitlab/User/1',
assigneeUsername: 'admin',
labels: [{ id: 'gid://gitlab/Label/32', title: 'Deliverable' }],
labelIds: ['gid://gitlab/Label/32'],
weight: 2,
};
export const boardObj = {
id: 1,
name: 'test',

View File

@ -32,6 +32,8 @@ import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import projectBoardMilestones from '~/boards/graphql/project_board_milestones.query.graphql';
import groupBoardMilestones from '~/boards/graphql/group_board_milestones.query.graphql';
import {
mockBoard,
mockBoardConfig,
mockLists,
mockListsById,
mockIssue,
@ -60,6 +62,52 @@ beforeEach(() => {
window.gon = { features: {} };
});
describe('fetchBoard', () => {
const payload = {
fullPath: 'gitlab-org',
fullBoardId: 'gid://gitlab/Board/1',
boardType: 'project',
};
const queryResponse = {
data: {
workspace: {
board: mockBoard,
},
},
};
it('should commit mutation RECEIVE_BOARD_SUCCESS and dispatch setBoardConfig on success', async () => {
jest.spyOn(gqlClient, 'query').mockResolvedValue(queryResponse);
await testAction({
action: actions.fetchBoard,
payload,
expectedMutations: [
{
type: types.RECEIVE_BOARD_SUCCESS,
payload: mockBoard,
},
],
expectedActions: [{ type: 'setBoardConfig', payload: mockBoard }],
});
});
it('should commit mutation RECEIVE_BOARD_FAILURE on failure', async () => {
jest.spyOn(gqlClient, 'query').mockResolvedValue(Promise.reject());
await testAction({
action: actions.fetchBoard,
payload,
expectedMutations: [
{
type: types.RECEIVE_BOARD_FAILURE,
},
],
});
});
});
describe('setInitialBoardData', () => {
it('sets data object', () => {
const mockData = {
@ -67,13 +115,21 @@ describe('setInitialBoardData', () => {
bar: 'baz',
};
return testAction(
actions.setInitialBoardData,
mockData,
{},
[{ type: types.SET_INITIAL_BOARD_DATA, payload: mockData }],
[],
);
return testAction({
action: actions.setInitialBoardData,
payload: mockData,
expectedMutations: [{ type: types.SET_INITIAL_BOARD_DATA, payload: mockData }],
});
});
});
describe('setBoardConfig', () => {
it('sets board config object from board object', () => {
return testAction({
action: actions.setBoardConfig,
payload: mockBoard,
expectedMutations: [{ type: types.SET_BOARD_CONFIG, payload: mockBoardConfig }],
});
});
});
@ -87,7 +143,7 @@ describe('setFilters', () => {
},
],
[
"and use 'assigneeWildcardId' as filter variable for 'assigneId' param",
"and use 'assigneeWildcardId' as filter variable for 'assigneeId' param",
{
filters: { assigneeId: 'None' },
filterVariables: { assigneeWildcardId: 'NONE', not: {} },

View File

@ -4,6 +4,7 @@ import * as types from '~/boards/stores/mutation_types';
import mutations from '~/boards/stores/mutations';
import defaultState from '~/boards/stores/state';
import {
mockBoard,
mockLists,
rawIssue,
mockIssue,
@ -33,6 +34,27 @@ describe('Board Store Mutations', () => {
state = defaultState();
});
describe('RECEIVE_BOARD_SUCCESS', () => {
it('Should set board to state', () => {
mutations[types.RECEIVE_BOARD_SUCCESS](state, mockBoard);
expect(state.board).toEqual({
...mockBoard,
labels: mockBoard.labels.nodes,
});
});
});
describe('RECEIVE_BOARD_FAILURE', () => {
it('Should set error in state', () => {
mutations[types.RECEIVE_BOARD_FAILURE](state);
expect(state.error).toEqual(
'An error occurred while fetching the board. Please reload the page.',
);
});
});
describe('SET_INITIAL_BOARD_DATA', () => {
it('Should set initial Boards data to state', () => {
const allowSubEpics = true;
@ -40,9 +62,6 @@ describe('Board Store Mutations', () => {
const fullPath = 'gitlab-org';
const boardType = 'group';
const disabled = false;
const boardConfig = {
milestoneTitle: 'Milestone 1',
};
const issuableType = issuableTypes.issue;
mutations[types.SET_INITIAL_BOARD_DATA](state, {
@ -51,7 +70,6 @@ describe('Board Store Mutations', () => {
fullPath,
boardType,
disabled,
boardConfig,
issuableType,
});
@ -60,11 +78,23 @@ describe('Board Store Mutations', () => {
expect(state.fullPath).toEqual(fullPath);
expect(state.boardType).toEqual(boardType);
expect(state.disabled).toEqual(disabled);
expect(state.boardConfig).toEqual(boardConfig);
expect(state.issuableType).toEqual(issuableType);
});
});
describe('SET_BOARD_CONFIG', () => {
it('Should set board config data o state', () => {
const boardConfig = {
milestoneId: 1,
milestoneTitle: 'Milestone 1',
};
mutations[types.SET_BOARD_CONFIG](state, boardConfig);
expect(state.boardConfig).toEqual(boardConfig);
});
});
describe('RECEIVE_BOARD_LISTS_SUCCESS', () => {
it('Should set boardLists to state', () => {
mutations[types.RECEIVE_BOARD_LISTS_SUCCESS](state, initialBoardListsState);

View File

@ -181,12 +181,13 @@ describe('init markdown', () => {
${'- [ ] item'} | ${'- [ ] item\n- [ ] '}
${'- [x] item'} | ${'- [x] item\n- [x] '}
${'- item\n - second'} | ${'- item\n - second\n - '}
${'1. item'} | ${'1. item\n1. '}
${'1. [ ] item'} | ${'1. [ ] item\n1. [ ] '}
${'1. [x] item'} | ${'1. [x] item\n1. [x] '}
${'108. item'} | ${'108. item\n108. '}
${'1. item'} | ${'1. item\n2. '}
${'1. [ ] item'} | ${'1. [ ] item\n2. [ ] '}
${'1. [x] item'} | ${'1. [x] item\n2. [x] '}
${'108. item'} | ${'108. item\n109. '}
${'108. item\n - second'} | ${'108. item\n - second\n - '}
${'108. item\n 1. second'} | ${'108. item\n 1. second\n 1. '}
${'108. item\n 1. second'} | ${'108. item\n 1. second\n 2. '}
${'non-item, will not change'} | ${'non-item, will not change'}
`('adds correct list continuation characters', ({ text, expected }) => {
textArea.value = text;
textArea.setSelectionRange(text.length, text.length);
@ -207,10 +208,10 @@ describe('init markdown', () => {
${'- [ ] item\n- [ ] '} | ${'- [ ] item\n'}
${'- [x] item\n- [x] '} | ${'- [x] item\n'}
${'- item\n - second\n - '} | ${'- item\n - second\n'}
${'1. item\n1. '} | ${'1. item\n'}
${'1. [ ] item\n1. [ ] '} | ${'1. [ ] item\n'}
${'1. [x] item\n1. [x] '} | ${'1. [x] item\n'}
${'108. item\n108. '} | ${'108. item\n'}
${'1. item\n2. '} | ${'1. item\n'}
${'1. [ ] item\n2. [ ] '} | ${'1. [ ] item\n'}
${'1. [x] item\n2. [x] '} | ${'1. [x] item\n'}
${'108. item\n109. '} | ${'108. item\n'}
${'108. item\n - second\n - '} | ${'108. item\n - second\n'}
${'108. item\n 1. second\n 1. '} | ${'108. item\n 1. second\n'}
`('adds correct list continuation characters', ({ text, expected }) => {
@ -243,6 +244,23 @@ describe('init markdown', () => {
expect(textArea.value).toEqual(expected);
});
it.each`
text | add_at | expected
${'1. one\n2. two\n3. three'} | ${13} | ${'1. one\n2. two\n2. \n3. three'}
${'108. item\n 5. second\n 6. six\n 7. seven'} | ${36} | ${'108. item\n 5. second\n 6. six\n 6. \n 7. seven'}
`(
'adds correct numbered continuation characters when in middle of list',
({ text, add_at, expected }) => {
textArea.value = text;
textArea.setSelectionRange(add_at, add_at);
textArea.addEventListener('keydown', keypressNoteText);
textArea.dispatchEvent(enterEvent);
expect(textArea.value).toEqual(expected);
},
);
it('does nothing if feature flag disabled', () => {
gon.features = { markdownContinueLists: false };
@ -262,8 +280,8 @@ describe('init markdown', () => {
});
describe('with selection', () => {
const text = 'initial selected value';
const selected = 'selected';
let text = 'initial selected value';
let selected = 'selected';
let selectedIndex;
beforeEach(() => {
@ -409,6 +427,46 @@ describe('init markdown', () => {
expectedText.indexOf(expectedSelectionText, 1) + expectedSelectionText.length,
);
});
it('adds block tags on line above and below selection', () => {
selected = 'this text\nis multiple\nlines';
text = `before \n${selected}\nafter `;
textArea.value = text;
selectedIndex = text.indexOf(selected);
textArea.setSelectionRange(selectedIndex, selectedIndex + selected.length);
insertMarkdownText({
textArea,
text,
tag: '',
blockTag: '***',
selected,
wrap: true,
});
expect(textArea.value).toEqual(`before \n***\n${selected}\n***\nafter `);
});
it('removes block tags on line above and below selection', () => {
selected = 'this text\nis multiple\nlines';
text = `before \n***\n${selected}\n***\nafter `;
textArea.value = text;
selectedIndex = text.indexOf(selected);
textArea.setSelectionRange(selectedIndex, selectedIndex + selected.length);
insertMarkdownText({
textArea,
text,
tag: '',
blockTag: '***',
selected,
wrap: true,
});
expect(textArea.value).toEqual(`before \n${selected}\nafter `);
});
});
});
});
@ -460,7 +518,31 @@ describe('init markdown', () => {
expect(editor.replaceSelectedText).toHaveBeenCalledWith(`***\n${selected}\n***\n`, undefined);
});
it('uses ace editor to navigate back tag length when nothing is selected', () => {
it('removes block tags on line above and below selection', () => {
const selected = 'this text\nis multiple\nlines';
const text = `before\n***\n${selected}\n***\nafter`;
editor.getSelection = jest.fn().mockReturnValue({
startLineNumber: 2,
startColumn: 1,
endLineNumber: 4,
endColumn: 2,
setSelectionRange: jest.fn(),
});
insertMarkdownText({
text,
tag: '',
blockTag: '***',
selected,
wrap: true,
editor,
});
expect(editor.replaceSelectedText).toHaveBeenCalledWith(`${selected}\n`, undefined);
});
it('uses editor to navigate back tag length when nothing is selected', () => {
editor.getSelection = jest.fn().mockReturnValue({
startLineNumber: 1,
startColumn: 1,
@ -480,7 +562,7 @@ describe('init markdown', () => {
expect(editor.moveCursor).toHaveBeenCalledWith(-1);
});
it('ace editor does not navigate back when there is selected text', () => {
it('editor does not navigate back when there is selected text', () => {
insertMarkdownText({
text: editor.getValue,
tag: '*',

View File

@ -7,6 +7,7 @@ import SourceViewer from '~/vue_shared/components/source_viewer/source_viewer.vu
import { ROUGE_TO_HLJS_LANGUAGE_MAP } from '~/vue_shared/components/source_viewer/constants';
import LineNumbers from '~/vue_shared/components/line_numbers.vue';
import waitForPromises from 'helpers/wait_for_promises';
import * as sourceViewerUtils from '~/vue_shared/components/source_viewer/utils';
jest.mock('highlight.js/lib/core');
Vue.use(VueRouter);
@ -36,6 +37,7 @@ describe('Source Viewer component', () => {
beforeEach(() => {
hljs.highlight.mockImplementation(() => ({ value: highlightedContent }));
hljs.highlightAuto.mockImplementation(() => ({ value: highlightedContent }));
jest.spyOn(sourceViewerUtils, 'wrapLines');
return createComponent();
});
@ -73,6 +75,10 @@ describe('Source Viewer component', () => {
expect(findLoadingIcon().exists()).toBe(true);
});
it('calls the wrapLines helper method with highlightedContent and mappedLanguage', () => {
expect(sourceViewerUtils.wrapLines).toHaveBeenCalledWith(highlightedContent, mappedLanguage);
});
it('renders Line Numbers', () => {
expect(findLineNumbers().props('lines')).toBe(1);
});

View File

@ -2,12 +2,25 @@ import { wrapLines } from '~/vue_shared/components/source_viewer/utils';
describe('Wrap lines', () => {
it.each`
input | output
${'line 1'} | ${'<span id="LC1" class="line">line 1</span>'}
${'line 1\nline 2'} | ${`<span id="LC1" class="line">line 1</span>\n<span id="LC2" class="line">line 2</span>`}
${'<span class="hljs-code">line 1\nline 2</span>'} | ${`<span id="LC1" class="hljs-code">line 1\n<span id="LC2" class="line">line 2</span></span>`}
${'<span class="hljs-code">```bash'} | ${'<span id="LC1" class="hljs-code">```bash'}
`('returns lines wrapped in spans containing line numbers', ({ input, output }) => {
expect(wrapLines(input)).toBe(output);
content | language | output
${'line 1'} | ${'javascript'} | ${'<span id="LC1" lang="javascript" class="line">line 1</span>'}
${'line 1\nline 2'} | ${'html'} | ${`<span id="LC1" lang="html" class="line">line 1</span>\n<span id="LC2" lang="html" class="line">line 2</span>`}
${'<span class="hljs-code">line 1\nline 2</span>'} | ${'html'} | ${`<span id="LC1" lang="html" class="hljs-code">line 1\n<span id="LC2" lang="html" class="line">line 2</span></span>`}
${'<span class="hljs-code">```bash'} | ${'bash'} | ${'<span id="LC1" lang="bash" class="hljs-code">```bash'}
${'<span class="hljs-code">```bash'} | ${'valid-language1'} | ${'<span id="LC1" lang="valid-language1" class="hljs-code">```bash'}
${'<span class="hljs-code">```bash'} | ${'valid_language2'} | ${'<span id="LC1" lang="valid_language2" class="hljs-code">```bash'}
`('returns lines wrapped in spans containing line numbers', ({ content, language, output }) => {
expect(wrapLines(content, language)).toBe(output);
});
it.each`
language
${'invalidLanguage>'}
${'"invalidLanguage"'}
${'<invalidLanguage'}
`('returns lines safely without XSS language is not valid', ({ language }) => {
expect(wrapLines('<span class="hljs-code">```bash', language)).toBe(
'<span id="LC1" lang="" class="hljs-code">```bash',
);
});
});

View File

@ -48,14 +48,26 @@ RSpec.describe Gitlab::Git::Wiki do
end
it 'returns the right page' do
expect(subject.page(title: 'page1', dir: '').url_path).to eq 'page1'
expect(subject.page(title: 'page1', dir: 'foo').url_path).to eq 'foo/page1'
page = subject.page(title: 'page1', dir: '')
expect(page.url_path).to eq 'page1'
expect(page.raw_data).to eq 'content'
page = subject.page(title: 'page1', dir: 'foo')
expect(page.url_path).to eq 'foo/page1'
expect(page.raw_data).to eq 'content foo/page1'
end
it 'returns nil for invalid arguments' do
expect(subject.page(title: '')).to be_nil
expect(subject.page(title: 'foo', version: ':')).to be_nil
end
it 'does not return content if load_content param is set to false' do
page = subject.page(title: 'page1', dir: '', load_content: false)
expect(page.url_path).to eq 'page1'
expect(page.raw_data).to be_empty
end
end
describe '#preview_slug' do

View File

@ -0,0 +1,150 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe SensitiveSerializableHash do
describe '.prevent_from_serialization' do
let(:test_class) do
Class.new do
include ActiveModel::Serialization
include SensitiveSerializableHash
attr_accessor :name, :super_secret
prevent_from_serialization :super_secret
def attributes
{ 'name' => nil, 'super_secret' => nil }
end
end
end
let(:model) { test_class.new }
it 'does not include the field in serializable_hash' do
expect(model.serializable_hash).not_to include('super_secret')
end
context 'unsafe_serialization_hash option' do
it 'includes the field in serializable_hash' do
expect(model.serializable_hash(unsafe_serialization_hash: true)).to include('super_secret')
end
end
context 'when prevent_sensitive_fields_from_serializable_hash feature flag is disabled' do
before do
stub_feature_flags(prevent_sensitive_fields_from_serializable_hash: false)
end
it 'includes the field in serializable_hash' do
expect(model.serializable_hash).to include('super_secret')
end
end
end
describe '#serializable_hash' do
shared_examples "attr_encrypted attribute" do |klass, attribute_name|
context "#{klass.name}\##{attribute_name}" do
let(:attributes) { [attribute_name, "encrypted_#{attribute_name}", "encrypted_#{attribute_name}_iv"] }
it 'has a encrypted_attributes field' do
expect(klass.encrypted_attributes).to include(attribute_name.to_sym)
end
it 'does not include the attribute in serializable_hash', :aggregate_failures do
attributes.each do |attribute|
expect(model.attributes).to include(attribute) # double-check the attribute does exist
expect(model.serializable_hash).not_to include(attribute)
expect(model.to_json).not_to include(attribute)
expect(model.as_json).not_to include(attribute)
end
end
context 'unsafe_serialization_hash option' do
it 'includes the field in serializable_hash' do
attributes.each do |attribute|
expect(model.attributes).to include(attribute) # double-check the attribute does exist
expect(model.serializable_hash(unsafe_serialization_hash: true)).to include(attribute)
expect(model.to_json(unsafe_serialization_hash: true)).to include(attribute)
expect(model.as_json(unsafe_serialization_hash: true)).to include(attribute)
end
end
end
end
end
it_behaves_like 'attr_encrypted attribute', WebHook, 'token' do
let_it_be(:model) { create(:system_hook) }
end
it_behaves_like 'attr_encrypted attribute', Ci::InstanceVariable, 'value' do
let_it_be(:model) { create(:ci_instance_variable) }
end
shared_examples "add_authentication_token_field attribute" do |klass, attribute_name, encrypted_attribute: true, digest_attribute: false|
context "#{klass.name}\##{attribute_name}" do
let(:attributes) do
if digest_attribute
["#{attribute_name}_digest"]
elsif encrypted_attribute
[attribute_name, "#{attribute_name}_encrypted"]
else
[attribute_name]
end
end
it 'has a add_authentication_token_field field' do
expect(klass.token_authenticatable_fields).to include(attribute_name.to_sym)
end
it 'does not include the attribute in serializable_hash', :aggregate_failures do
attributes.each do |attribute|
expect(model.attributes).to include(attribute) # double-check the attribute does exist
expect(model.serializable_hash).not_to include(attribute)
expect(model.to_json).not_to include(attribute)
expect(model.as_json).not_to include(attribute)
end
end
context 'unsafe_serialization_hash option' do
it 'includes the field in serializable_hash' do
attributes.each do |attribute|
expect(model.attributes).to include(attribute) # double-check the attribute does exist
expect(model.serializable_hash(unsafe_serialization_hash: true)).to include(attribute)
expect(model.to_json(unsafe_serialization_hash: true)).to include(attribute)
expect(model.as_json(unsafe_serialization_hash: true)).to include(attribute)
end
end
end
end
end
it_behaves_like 'add_authentication_token_field attribute', Ci::Runner, 'token' do
let_it_be(:model) { create(:ci_runner) }
it 'does not include token_expires_at in serializable_hash' do
attribute = 'token_expires_at'
expect(model.attributes).to include(attribute) # double-check the attribute does exist
expect(model.serializable_hash).not_to include(attribute)
expect(model.to_json).not_to include(attribute)
expect(model.as_json).not_to include(attribute)
end
end
it_behaves_like 'add_authentication_token_field attribute', ApplicationSetting, 'health_check_access_token', encrypted_attribute: false do
# health_check_access_token_encrypted column does not exist
let_it_be(:model) { create(:application_setting) }
end
it_behaves_like 'add_authentication_token_field attribute', PersonalAccessToken, 'token', encrypted_attribute: false, digest_attribute: true do
# PersonalAccessToken only has token_digest column
let_it_be(:model) { create(:personal_access_token) }
end
end
end

View File

@ -9,6 +9,12 @@ RSpec.shared_examples 'TokenAuthenticatable' do
it { is_expected.to respond_to("set_#{token_field}") }
it { is_expected.to respond_to("reset_#{token_field}!") }
end
describe 'SensitiveSerializableHash' do
it 'includes the token field in list of sensitive attributes prevented from serialization' do
expect(described_class.attributes_exempt_from_serializable_hash).to include(token_field)
end
end
end
RSpec.describe User, 'TokenAuthenticatable' do

View File

@ -6,6 +6,24 @@ RSpec.describe TokenAuthenticatableStrategies::Base do
let(:instance) { double(:instance) }
let(:field) { double(:field) }
describe '#token_fields' do
let(:strategy) { described_class.new(instance, field, options) }
let(:field) { 'some_token' }
let(:options) { {} }
it 'includes the token field' do
expect(strategy.token_fields).to contain_exactly(field)
end
context 'with expires_at option' do
let(:options) { { expires_at: true } }
it 'includes the token_expires_at field' do
expect(strategy.token_fields).to contain_exactly(field, 'some_token_expires_at')
end
end
end
describe '.fabricate' do
context 'when digest stragegy is specified' do
it 'fabricates digest strategy object' do

View File

@ -0,0 +1,18 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe TokenAuthenticatableStrategies::Digest do
let(:model) { class_double('Project') }
let(:options) { { digest: true } }
subject(:strategy) do
described_class.new(model, 'some_field', options)
end
describe '#token_fields' do
it 'includes the digest field' do
expect(strategy.token_fields).to contain_exactly('some_field', 'some_field_digest')
end
end
end

View File

@ -14,10 +14,18 @@ RSpec.describe TokenAuthenticatableStrategies::Encrypted do
Gitlab::CryptoHelper.aes256_gcm_encrypt('my-value')
end
subject do
subject(:strategy) do
described_class.new(model, 'some_field', options)
end
describe '#token_fields' do
let(:options) { { encrypted: :required } }
it 'includes the encrypted field' do
expect(strategy.token_fields).to contain_exactly('some_field', 'some_field_encrypted')
end
end
describe '#find_token_authenticatable' do
context 'when encryption is required' do
let(:options) { { encrypted: :required } }

View File

@ -175,7 +175,7 @@ RSpec.describe Ci::JobArtifacts::CreateService do
end
expect(subject[:status]).to eq(:success)
expect(job.job_variables.as_json).to contain_exactly(
expect(job.job_variables.as_json(only: [:key, :value, :source])).to contain_exactly(
hash_including('key' => 'KEY1', 'value' => 'VAR1', 'source' => 'dotenv'),
hash_including('key' => 'KEY2', 'value' => 'VAR2', 'source' => 'dotenv'))
end

View File

@ -18,7 +18,7 @@ RSpec.describe Ci::ParseDotenvArtifactService do
it 'parses the artifact' do
expect(subject[:status]).to eq(:success)
expect(build.job_variables.as_json).to contain_exactly(
expect(build.job_variables.as_json(only: [:key, :value])).to contain_exactly(
hash_including('key' => 'KEY1', 'value' => 'VAR1'),
hash_including('key' => 'KEY2', 'value' => 'VAR2'))
end
@ -57,7 +57,7 @@ RSpec.describe Ci::ParseDotenvArtifactService do
expect(subject[:status]).to eq(:success)
expect(build.job_variables.as_json).to contain_exactly(
expect(build.job_variables.as_json(only: [:key, :value])).to contain_exactly(
hash_including('key' => 'KEY1', 'value' => 'VAR4'),
hash_including('key' => 'KEY2', 'value' => 'VAR3'))
end
@ -101,7 +101,7 @@ RSpec.describe Ci::ParseDotenvArtifactService do
it 'trims the trailing space' do
subject
expect(build.job_variables.as_json).to contain_exactly(
expect(build.job_variables.as_json(only: [:key, :value])).to contain_exactly(
hash_including('key' => 'KEY1', 'value' => 'VAR1'))
end
end
@ -112,7 +112,7 @@ RSpec.describe Ci::ParseDotenvArtifactService do
it 'parses the dotenv data' do
subject
expect(build.job_variables.as_json).to contain_exactly(
expect(build.job_variables.as_json(only: [:key, :value])).to contain_exactly(
hash_including('key' => 'KEY', 'value' => 'VARCONTAINING=EQLS'))
end
end
@ -133,7 +133,7 @@ RSpec.describe Ci::ParseDotenvArtifactService do
it 'parses the dotenv data' do
subject
expect(build.job_variables.as_json).to contain_exactly(
expect(build.job_variables.as_json(only: [:key, :value])).to contain_exactly(
hash_including('key' => 'skateboard', 'value' => '🛹'))
end
end
@ -154,7 +154,7 @@ RSpec.describe Ci::ParseDotenvArtifactService do
it 'parses the dotenv data' do
subject
expect(build.job_variables.as_json).to contain_exactly(
expect(build.job_variables.as_json(only: [:key, :value])).to contain_exactly(
hash_including('key' => 'KEY1', 'value' => 'V A R 1'))
end
end
@ -165,7 +165,7 @@ RSpec.describe Ci::ParseDotenvArtifactService do
it 'parses the value as-is' do
subject
expect(build.job_variables.as_json).to contain_exactly(
expect(build.job_variables.as_json(only: [:key, :value])).to contain_exactly(
hash_including('key' => 'KEY1', 'value' => '"VAR1"'))
end
end
@ -176,7 +176,7 @@ RSpec.describe Ci::ParseDotenvArtifactService do
it 'parses the value as-is' do
subject
expect(build.job_variables.as_json).to contain_exactly(
expect(build.job_variables.as_json(only: [:key, :value])).to contain_exactly(
hash_including('key' => 'KEY1', 'value' => "'VAR1'"))
end
end
@ -187,7 +187,7 @@ RSpec.describe Ci::ParseDotenvArtifactService do
it 'parses the value as-is' do
subject
expect(build.job_variables.as_json).to contain_exactly(
expect(build.job_variables.as_json(only: [:key, :value])).to contain_exactly(
hash_including('key' => 'KEY1', 'value' => '" VAR1 "'))
end
end
@ -208,7 +208,7 @@ RSpec.describe Ci::ParseDotenvArtifactService do
it 'parses the dotenv data' do
subject
expect(build.job_variables.as_json).to contain_exactly(
expect(build.job_variables.as_json(only: [:key, :value])).to contain_exactly(
hash_including('key' => 'KEY1', 'value' => ''))
end
end
@ -250,7 +250,7 @@ RSpec.describe Ci::ParseDotenvArtifactService do
it 'does not support variable expansion in dotenv parser' do
subject
expect(build.job_variables.as_json).to contain_exactly(
expect(build.job_variables.as_json(only: [:key, :value])).to contain_exactly(
hash_including('key' => 'KEY1', 'value' => 'VAR1'),
hash_including('key' => 'KEY2', 'value' => '${KEY1}_Test'))
end
@ -284,7 +284,7 @@ RSpec.describe Ci::ParseDotenvArtifactService do
it 'does not support comment in dotenv parser' do
subject
expect(build.job_variables.as_json).to contain_exactly(
expect(build.job_variables.as_json(only: [:key, :value])).to contain_exactly(
hash_including('key' => 'KEY1', 'value' => 'VAR1 # This is variable'))
end
end

View File

@ -57,7 +57,7 @@ module Tooling
---
stage: Ecosystem
group: Integrations
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
<!---