Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-06-21 12:07:45 +00:00
parent 3ab7e70965
commit d4c968c95c
96 changed files with 743 additions and 1020 deletions

View File

@ -1057,6 +1057,8 @@
.reports:rules:package_hunter:
rules:
- if: "$PACKAGE_HUNTER_USER == null || $PACKAGE_HUNTER_USER == ''"
when: never
- <<: *if-default-branch-schedule-2-hourly
- <<: *if-merge-request
changes: ["yarn.lock"]

View File

@ -135,7 +135,7 @@ export default {
>{{ $options.i18n.newEnvironmentButtonLabel }}</gl-button
>
</div>
<gl-tabs content-class="gl-display-none">
<gl-tabs :value="activeTab" content-class="gl-display-none">
<gl-tab
v-for="(tab, idx) in tabs"
:key="idx"

View File

@ -208,6 +208,9 @@ export default {
},
];
},
activeTab() {
return this.tabs.findIndex(({ isActive }) => isActive) ?? 0;
},
},
/**

View File

@ -16,7 +16,6 @@ import IssuableByEmail from '~/issuable/components/issuable_by_email.vue';
import IssuableList from '~/issuable_list/components/issuable_list_root.vue';
import { IssuableListTabs, IssuableStates } from '~/issuable_list/constants';
import {
API_PARAM,
CREATED_DESC,
i18n,
initialPageParams,
@ -25,7 +24,7 @@ import {
PARAM_DUE_DATE,
PARAM_SORT,
PARAM_STATE,
RELATIVE_POSITION_DESC,
RELATIVE_POSITION_ASC,
TOKEN_TYPE_ASSIGNEE,
TOKEN_TYPE_AUTHOR,
TOKEN_TYPE_CONFIDENTIAL,
@ -36,12 +35,12 @@ import {
TOKEN_TYPE_MILESTONE,
TOKEN_TYPE_WEIGHT,
UPDATED_DESC,
URL_PARAM,
urlSortParams,
} from '~/issues_list/constants';
import {
convertToParams,
convertToApiParams,
convertToSearchQuery,
convertToUrlParams,
getDueDateValue,
getFilterTokens,
getSortKey,
@ -192,10 +191,10 @@ export default {
...this.apiFilterParams,
};
},
update: ({ project }) => project.issues.nodes,
update: ({ project }) => project?.issues.nodes ?? [],
result({ data }) {
this.pageInfo = data.project.issues.pageInfo;
this.totalIssues = data.project.issues.count;
this.pageInfo = data.project?.issues.pageInfo ?? {};
this.totalIssues = data.project?.issues.count ?? 0;
this.exportCsvPathWithQuery = this.getExportCsvPathWithQuery();
},
error(error) {
@ -215,32 +214,30 @@ export default {
return this.showBulkEditSidebar || !this.issues.length;
},
isManualOrdering() {
return this.sortKey === RELATIVE_POSITION_DESC;
return this.sortKey === RELATIVE_POSITION_ASC;
},
isOpenTab() {
return this.state === IssuableStates.Opened;
},
apiFilterParams() {
return convertToParams(this.filterTokens, API_PARAM);
return convertToApiParams(this.filterTokens);
},
urlFilterParams() {
return convertToParams(this.filterTokens, URL_PARAM);
return convertToUrlParams(this.filterTokens);
},
searchQuery() {
return convertToSearchQuery(this.filterTokens) || undefined;
},
searchTokens() {
let preloadedAuthors = [];
const preloadedAuthors = [];
if (gon.current_user_id) {
preloadedAuthors = [
{
id: gon.current_user_id,
name: gon.current_user_fullname,
username: gon.current_username,
avatar_url: gon.current_user_avatar_url,
},
];
preloadedAuthors.push({
id: gon.current_user_id,
name: gon.current_user_fullname,
username: gon.current_username,
avatar_url: gon.current_user_avatar_url,
});
}
const tokens = [
@ -252,6 +249,7 @@ export default {
dataType: 'user',
unique: true,
defaultAuthors: [],
operators: OPERATOR_IS_ONLY,
fetchAuthors: this.fetchUsers,
preloadedAuthors,
},
@ -280,7 +278,7 @@ export default {
title: TOKEN_TITLE_LABEL,
icon: 'labels',
token: LabelToken,
defaultLabels: [],
defaultLabels: DEFAULT_NONE_ANY,
fetchLabels: this.fetchLabels,
},
];
@ -329,6 +327,7 @@ export default {
token: EpicToken,
unique: true,
idProperty: 'id',
useIdValue: true,
fetchEpics: this.fetchEpics,
});
}
@ -346,7 +345,7 @@ export default {
return tokens;
},
showPaginationControls() {
return this.issues.length > 0;
return this.issues.length > 0 && (this.pageInfo.hasNextPage || this.pageInfo.hasPreviousPage);
},
sortOptions() {
return getSortOptions(this.hasIssueWeightsFeature, this.hasBlockedIssuesFeature);
@ -361,22 +360,12 @@ export default {
);
},
urlParams() {
const filterParams = {
...this.urlFilterParams,
};
if (filterParams.epic_id) {
filterParams.epic_id = encodeURIComponent(filterParams.epic_id);
} else if (filterParams['not[epic_id]']) {
filterParams['not[epic_id]'] = encodeURIComponent(filterParams['not[epic_id]']);
}
return {
due_date: this.dueDateFilter,
search: this.searchQuery,
sort: urlSortParams[this.sortKey],
state: this.state,
...urlSortParams[this.sortKey],
...filterParams,
...this.urlFilterParams,
};
},
},
@ -424,7 +413,10 @@ export default {
return this.fetchWithCache(this.projectMilestonesPath, 'milestones', 'title', search, true);
},
fetchIterations(search) {
return axios.get(this.projectIterationsPath, { params: { search } });
const id = Number(search);
return !search || Number.isNaN(id)
? axios.get(this.projectIterationsPath, { params: { search } })
: axios.get(this.projectIterationsPath, { params: { id } });
},
fetchUsers(search) {
return axios.get(this.autocompleteUsersPath, { params: { search } });
@ -471,6 +463,7 @@ export default {
this.state = state;
},
handleFilter(filter) {
this.pageParams = initialPageParams;
this.filterTokens = filter;
},
handleNextPage() {

View File

@ -128,21 +128,21 @@ export const CREATED_ASC = 'CREATED_ASC';
export const CREATED_DESC = 'CREATED_DESC';
export const DUE_DATE_ASC = 'DUE_DATE_ASC';
export const DUE_DATE_DESC = 'DUE_DATE_DESC';
export const LABEL_PRIORITY_ASC = 'LABEL_PRIORITY_ASC';
export const LABEL_PRIORITY_DESC = 'LABEL_PRIORITY_DESC';
export const MILESTONE_DUE_ASC = 'MILESTONE_DUE_ASC';
export const MILESTONE_DUE_DESC = 'MILESTONE_DUE_DESC';
export const POPULARITY_ASC = 'POPULARITY_ASC';
export const POPULARITY_DESC = 'POPULARITY_DESC';
export const PRIORITY_ASC = 'PRIORITY_ASC';
export const PRIORITY_DESC = 'PRIORITY_DESC';
export const RELATIVE_POSITION_DESC = 'RELATIVE_POSITION_DESC';
export const RELATIVE_POSITION_ASC = 'RELATIVE_POSITION_ASC';
export const UPDATED_ASC = 'UPDATED_ASC';
export const UPDATED_DESC = 'UPDATED_DESC';
export const WEIGHT_ASC = 'WEIGHT_ASC';
export const WEIGHT_DESC = 'WEIGHT_DESC';
const SORT_ASC = 'asc';
const SORT_DESC = 'desc';
const PRIORITY_ASC_SORT = 'priority_asc';
const CREATED_DATE_SORT = 'created_date';
const CREATED_ASC_SORT = 'created_asc';
const UPDATED_DESC_SORT = 'updated_desc';
@ -150,129 +150,30 @@ const UPDATED_ASC_SORT = 'updated_asc';
const MILESTONE_SORT = 'milestone';
const MILESTONE_DUE_DESC_SORT = 'milestone_due_desc';
const DUE_DATE_DESC_SORT = 'due_date_desc';
const LABEL_PRIORITY_ASC_SORT = 'label_priority_asc';
const POPULARITY_ASC_SORT = 'popularity_asc';
const WEIGHT_DESC_SORT = 'weight_desc';
const BLOCKING_ISSUES_DESC_SORT = 'blocking_issues_desc';
const BLOCKING_ISSUES = 'blocking_issues';
export const apiSortParams = {
[PRIORITY_DESC]: {
order_by: PRIORITY,
sort: SORT_DESC,
},
[CREATED_ASC]: {
order_by: CREATED_AT,
sort: SORT_ASC,
},
[CREATED_DESC]: {
order_by: CREATED_AT,
sort: SORT_DESC,
},
[UPDATED_ASC]: {
order_by: UPDATED_AT,
sort: SORT_ASC,
},
[UPDATED_DESC]: {
order_by: UPDATED_AT,
sort: SORT_DESC,
},
[MILESTONE_DUE_ASC]: {
order_by: MILESTONE_DUE,
sort: SORT_ASC,
},
[MILESTONE_DUE_DESC]: {
order_by: MILESTONE_DUE,
sort: SORT_DESC,
},
[DUE_DATE_ASC]: {
order_by: DUE_DATE,
sort: SORT_ASC,
},
[DUE_DATE_DESC]: {
order_by: DUE_DATE,
sort: SORT_DESC,
},
[POPULARITY_ASC]: {
order_by: POPULARITY,
sort: SORT_ASC,
},
[POPULARITY_DESC]: {
order_by: POPULARITY,
sort: SORT_DESC,
},
[LABEL_PRIORITY_DESC]: {
order_by: LABEL_PRIORITY,
sort: SORT_DESC,
},
[RELATIVE_POSITION_DESC]: {
order_by: RELATIVE_POSITION,
per_page: 100,
sort: SORT_ASC,
},
[WEIGHT_ASC]: {
order_by: WEIGHT,
sort: SORT_ASC,
},
[WEIGHT_DESC]: {
order_by: WEIGHT,
sort: SORT_DESC,
},
[BLOCKING_ISSUES_DESC]: {
order_by: BLOCKING_ISSUES,
sort: SORT_DESC,
},
};
export const urlSortParams = {
[PRIORITY_DESC]: {
sort: PRIORITY,
},
[CREATED_ASC]: {
sort: CREATED_ASC_SORT,
},
[CREATED_DESC]: {
sort: CREATED_DATE_SORT,
},
[UPDATED_ASC]: {
sort: UPDATED_ASC_SORT,
},
[UPDATED_DESC]: {
sort: UPDATED_DESC_SORT,
},
[MILESTONE_DUE_ASC]: {
sort: MILESTONE_SORT,
},
[MILESTONE_DUE_DESC]: {
sort: MILESTONE_DUE_DESC_SORT,
},
[DUE_DATE_ASC]: {
sort: DUE_DATE,
},
[DUE_DATE_DESC]: {
sort: DUE_DATE_DESC_SORT,
},
[POPULARITY_ASC]: {
sort: POPULARITY_ASC_SORT,
},
[POPULARITY_DESC]: {
sort: POPULARITY,
},
[LABEL_PRIORITY_DESC]: {
sort: LABEL_PRIORITY,
},
[RELATIVE_POSITION_DESC]: {
sort: RELATIVE_POSITION,
per_page: 100,
},
[WEIGHT_ASC]: {
sort: WEIGHT,
},
[WEIGHT_DESC]: {
sort: WEIGHT_DESC_SORT,
},
[BLOCKING_ISSUES_DESC]: {
sort: BLOCKING_ISSUES_DESC_SORT,
},
[PRIORITY_ASC]: PRIORITY_ASC_SORT,
[PRIORITY_DESC]: PRIORITY,
[CREATED_ASC]: CREATED_ASC_SORT,
[CREATED_DESC]: CREATED_DATE_SORT,
[UPDATED_ASC]: UPDATED_ASC_SORT,
[UPDATED_DESC]: UPDATED_DESC_SORT,
[MILESTONE_DUE_ASC]: MILESTONE_SORT,
[MILESTONE_DUE_DESC]: MILESTONE_DUE_DESC_SORT,
[DUE_DATE_ASC]: DUE_DATE,
[DUE_DATE_DESC]: DUE_DATE_DESC_SORT,
[POPULARITY_ASC]: POPULARITY_ASC_SORT,
[POPULARITY_DESC]: POPULARITY,
[LABEL_PRIORITY_ASC]: LABEL_PRIORITY_ASC_SORT,
[LABEL_PRIORITY_DESC]: LABEL_PRIORITY,
[RELATIVE_POSITION_ASC]: RELATIVE_POSITION,
[WEIGHT_ASC]: WEIGHT,
[WEIGHT_DESC]: WEIGHT_DESC_SORT,
[BLOCKING_ISSUES_DESC]: BLOCKING_ISSUES_DESC_SORT,
};
export const MAX_LIST_SIZE = 10;
@ -297,12 +198,7 @@ export const TOKEN_TYPE_WEIGHT = 'weight';
export const filters = {
[TOKEN_TYPE_AUTHOR]: {
[API_PARAM]: {
[OPERATOR_IS]: {
[NORMAL_FILTER]: 'author_username',
},
[OPERATOR_IS_NOT]: {
[NORMAL_FILTER]: 'not[author_username]',
},
[NORMAL_FILTER]: 'authorUsername',
},
[URL_PARAM]: {
[OPERATOR_IS]: {
@ -315,13 +211,8 @@ export const filters = {
},
[TOKEN_TYPE_ASSIGNEE]: {
[API_PARAM]: {
[OPERATOR_IS]: {
[NORMAL_FILTER]: 'assignee_username',
[SPECIAL_FILTER]: 'assignee_id',
},
[OPERATOR_IS_NOT]: {
[NORMAL_FILTER]: 'not[assignee_username]',
},
[NORMAL_FILTER]: 'assigneeUsernames',
[SPECIAL_FILTER]: 'assigneeId',
},
[URL_PARAM]: {
[OPERATOR_IS]: {
@ -336,12 +227,7 @@ export const filters = {
},
[TOKEN_TYPE_MILESTONE]: {
[API_PARAM]: {
[OPERATOR_IS]: {
[NORMAL_FILTER]: 'milestone',
},
[OPERATOR_IS_NOT]: {
[NORMAL_FILTER]: 'not[milestone]',
},
[NORMAL_FILTER]: 'milestoneTitle',
},
[URL_PARAM]: {
[OPERATOR_IS]: {
@ -354,16 +240,13 @@ export const filters = {
},
[TOKEN_TYPE_LABEL]: {
[API_PARAM]: {
[OPERATOR_IS]: {
[NORMAL_FILTER]: 'labels',
},
[OPERATOR_IS_NOT]: {
[NORMAL_FILTER]: 'not[labels]',
},
[NORMAL_FILTER]: 'labelName',
[SPECIAL_FILTER]: 'labelName',
},
[URL_PARAM]: {
[OPERATOR_IS]: {
[NORMAL_FILTER]: 'label_name[]',
[SPECIAL_FILTER]: 'label_name[]',
},
[OPERATOR_IS_NOT]: {
[NORMAL_FILTER]: 'not[label_name][]',
@ -372,10 +255,8 @@ export const filters = {
},
[TOKEN_TYPE_MY_REACTION]: {
[API_PARAM]: {
[OPERATOR_IS]: {
[NORMAL_FILTER]: 'my_reaction_emoji',
[SPECIAL_FILTER]: 'my_reaction_emoji',
},
[NORMAL_FILTER]: 'myReactionEmoji',
[SPECIAL_FILTER]: 'myReactionEmoji',
},
[URL_PARAM]: {
[OPERATOR_IS]: {
@ -386,9 +267,7 @@ export const filters = {
},
[TOKEN_TYPE_CONFIDENTIAL]: {
[API_PARAM]: {
[OPERATOR_IS]: {
[NORMAL_FILTER]: 'confidential',
},
[NORMAL_FILTER]: 'confidential',
},
[URL_PARAM]: {
[OPERATOR_IS]: {
@ -398,33 +277,23 @@ export const filters = {
},
[TOKEN_TYPE_ITERATION]: {
[API_PARAM]: {
[OPERATOR_IS]: {
[NORMAL_FILTER]: 'iteration_title',
[SPECIAL_FILTER]: 'iteration_id',
},
[OPERATOR_IS_NOT]: {
[NORMAL_FILTER]: 'not[iteration_title]',
},
[NORMAL_FILTER]: 'iterationId',
[SPECIAL_FILTER]: 'iterationWildcardId',
},
[URL_PARAM]: {
[OPERATOR_IS]: {
[NORMAL_FILTER]: 'iteration_title',
[NORMAL_FILTER]: 'iteration_id',
[SPECIAL_FILTER]: 'iteration_id',
},
[OPERATOR_IS_NOT]: {
[NORMAL_FILTER]: 'not[iteration_title]',
[NORMAL_FILTER]: 'not[iteration_id]',
},
},
},
[TOKEN_TYPE_EPIC]: {
[API_PARAM]: {
[OPERATOR_IS]: {
[NORMAL_FILTER]: 'epic_id',
[SPECIAL_FILTER]: 'epic_id',
},
[OPERATOR_IS_NOT]: {
[NORMAL_FILTER]: 'not[epic_id]',
},
[NORMAL_FILTER]: 'epicId',
[SPECIAL_FILTER]: 'epicId',
},
[URL_PARAM]: {
[OPERATOR_IS]: {
@ -438,13 +307,8 @@ export const filters = {
},
[TOKEN_TYPE_WEIGHT]: {
[API_PARAM]: {
[OPERATOR_IS]: {
[NORMAL_FILTER]: 'weight',
[SPECIAL_FILTER]: 'weight',
},
[OPERATOR_IS_NOT]: {
[NORMAL_FILTER]: 'not[weight]',
},
[NORMAL_FILTER]: 'weight',
[SPECIAL_FILTER]: 'weight',
},
[URL_PARAM]: {
[OPERATOR_IS]: {

View File

@ -1,4 +1,5 @@
import {
API_PARAM,
BLOCKING_ISSUES_DESC,
CREATED_ASC,
CREATED_DESC,
@ -6,29 +7,36 @@ import {
DUE_DATE_DESC,
DUE_DATE_VALUES,
filters,
LABEL_PRIORITY_ASC,
LABEL_PRIORITY_DESC,
MILESTONE_DUE_ASC,
MILESTONE_DUE_DESC,
NORMAL_FILTER,
POPULARITY_ASC,
POPULARITY_DESC,
PRIORITY_ASC,
PRIORITY_DESC,
RELATIVE_POSITION_DESC,
RELATIVE_POSITION_ASC,
SPECIAL_FILTER,
SPECIAL_FILTER_VALUES,
TOKEN_TYPE_ASSIGNEE,
TOKEN_TYPE_ITERATION,
UPDATED_ASC,
UPDATED_DESC,
URL_PARAM,
urlSortParams,
WEIGHT_ASC,
WEIGHT_DESC,
} from '~/issues_list/constants';
import { isPositiveInteger } from '~/lib/utils/number_utils';
import { __ } from '~/locale';
import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
import {
FILTERED_SEARCH_TERM,
OPERATOR_IS_NOT,
} from '~/vue_shared/components/filtered_search_bar/constants';
export const getSortKey = (sort) =>
Object.keys(urlSortParams).find((key) => urlSortParams[key].sort === sort);
Object.keys(urlSortParams).find((key) => urlSortParams[key] === sort);
export const getDueDateValue = (value) => (DUE_DATE_VALUES.includes(value) ? value : undefined);
@ -38,7 +46,7 @@ export const getSortOptions = (hasIssueWeightsFeature, hasBlockedIssuesFeature)
id: 1,
title: __('Priority'),
sortDirection: {
ascending: PRIORITY_DESC,
ascending: PRIORITY_ASC,
descending: PRIORITY_DESC,
},
},
@ -86,7 +94,7 @@ export const getSortOptions = (hasIssueWeightsFeature, hasBlockedIssuesFeature)
id: 7,
title: __('Label priority'),
sortDirection: {
ascending: LABEL_PRIORITY_DESC,
ascending: LABEL_PRIORITY_ASC,
descending: LABEL_PRIORITY_DESC,
},
},
@ -94,8 +102,8 @@ export const getSortOptions = (hasIssueWeightsFeature, hasBlockedIssuesFeature)
id: 8,
title: __('Manual'),
sortDirection: {
ascending: RELATIVE_POSITION_DESC,
descending: RELATIVE_POSITION_DESC,
ascending: RELATIVE_POSITION_ASC,
descending: RELATIVE_POSITION_ASC,
},
},
];
@ -128,7 +136,7 @@ export const getSortOptions = (hasIssueWeightsFeature, hasBlockedIssuesFeature)
const tokenTypes = Object.keys(filters);
const getUrlParams = (tokenType) =>
Object.values(filters[tokenType].urlParam).flatMap((filterObj) => Object.values(filterObj));
Object.values(filters[tokenType][URL_PARAM]).flatMap((filterObj) => Object.values(filterObj));
const urlParamKeys = tokenTypes.flatMap(getUrlParams);
@ -136,7 +144,7 @@ const getTokenTypeFromUrlParamKey = (urlParamKey) =>
tokenTypes.find((tokenType) => getUrlParams(tokenType).includes(urlParamKey));
const getOperatorFromUrlParamKey = (tokenType, urlParamKey) =>
Object.entries(filters[tokenType].urlParam).find(([, filterObj]) =>
Object.entries(filters[tokenType][URL_PARAM]).find(([, filterObj]) =>
Object.values(filterObj).includes(urlParamKey),
)[0];
@ -178,12 +186,36 @@ const getFilterType = (data, tokenType = '') =>
? SPECIAL_FILTER
: NORMAL_FILTER;
export const convertToParams = (filterTokens, paramType) =>
const isIterationSpecialValue = (tokenType, value) =>
tokenType === TOKEN_TYPE_ITERATION && SPECIAL_FILTER_VALUES.includes(value);
export const convertToApiParams = (filterTokens) => {
const params = {};
const not = {};
filterTokens
.filter((token) => token.type !== FILTERED_SEARCH_TERM)
.forEach((token) => {
const filterType = getFilterType(token.value.data, token.type);
const field = filters[token.type][API_PARAM][filterType];
const obj = token.value.operator === OPERATOR_IS_NOT ? not : params;
const data = isIterationSpecialValue(token.type, token.value.data)
? token.value.data.toUpperCase()
: token.value.data;
Object.assign(obj, {
[field]: obj[field] ? [obj[field], data].flat() : data,
});
});
return Object.keys(not).length ? Object.assign(params, { not }) : params;
};
export const convertToUrlParams = (filterTokens) =>
filterTokens
.filter((token) => token.type !== FILTERED_SEARCH_TERM)
.reduce((acc, token) => {
const filterType = getFilterType(token.value.data, token.type);
const param = filters[token.type][paramType][token.value.operator]?.[filterType];
const param = filters[token.type][URL_PARAM][token.value.operator]?.[filterType];
return Object.assign(acc, {
[param]: acc[param] ? [acc[param], token.value.data].flat() : token.value.data,
});

View File

@ -72,7 +72,7 @@ export default {
<template>
<div class="gl-display-flex gl-align-items-stretch">
<div
class="gl-w-grid-size-30 gl-flex-shrink-0 gl-bg-gray-10 gl-py-3 gl-px-5"
class="gl-w-grid-size-30 gl-flex-shrink-0 gl-bg-gray-10 gl-p-3"
:class="menuClass"
data-testid="menu-sidebar"
>
@ -81,7 +81,7 @@ export default {
<keep-alive-slots
v-show="activeView"
:slot-key="activeView"
class="gl-w-grid-size-40 gl-overflow-hidden gl-py-3 gl-px-5"
class="gl-w-grid-size-40 gl-overflow-hidden gl-p-3"
data-testid="menu-subview"
data-qa-selector="menu_subview_container"
>

View File

@ -56,7 +56,7 @@ export default {
}
// Current value is a string.
const [groupPath, idProperty] = this.currentValue?.split('::&');
const [groupPath, idProperty] = this.currentValue?.split(this.$options.separator);
return this.epics.find(
(epic) =>
epic.group_full_path === groupPath &&
@ -65,6 +65,9 @@ export default {
}
return null;
},
displayText() {
return `${this.activeEpic?.title}${this.$options.separator}${this.activeEpic?.iid}`;
},
},
watch: {
active: {
@ -103,8 +106,10 @@ export default {
this.fetchEpicsBySearchTerm({ epicPath, search: data });
}, DEBOUNCE_DELAY),
getEpicDisplayText(epic) {
return `${epic.title}${this.$options.separator}${epic.iid}`;
getValue(epic) {
return this.config.useIdValue
? String(epic[this.idProperty])
: `${epic.group_full_path}${this.$options.separator}${epic[this.idProperty]}`;
},
},
};
@ -118,7 +123,7 @@ export default {
@input="searchEpics"
>
<template #view="{ inputValue }">
{{ activeEpic ? getEpicDisplayText(activeEpic) : inputValue }}
{{ activeEpic ? displayText : inputValue }}
</template>
<template #suggestions>
<gl-filtered-search-suggestion
@ -131,11 +136,7 @@ export default {
<gl-dropdown-divider v-if="defaultEpics.length" />
<gl-loading-icon v-if="loading" />
<template v-else>
<gl-filtered-search-suggestion
v-for="epic in epics"
:key="epic.id"
:value="`${epic.group_full_path}::&${epic[idProperty]}`"
>
<gl-filtered-search-suggestion v-for="epic in epics" :key="epic.id" :value="getValue(epic)">
{{ epic.title }}
</gl-filtered-search-suggestion>
</template>

View File

@ -30,7 +30,6 @@ export default {
data() {
return {
iterations: this.config.initialIterations || [],
defaultIterations: this.config.defaultIterations || DEFAULT_ITERATIONS,
loading: true,
};
},
@ -39,7 +38,10 @@ export default {
return this.value.data;
},
activeIteration() {
return this.iterations.find((iteration) => iteration.title === this.currentValue);
return this.iterations.find((iteration) => iteration.id === Number(this.currentValue));
},
defaultIterations() {
return this.config.defaultIterations || DEFAULT_ITERATIONS;
},
},
watch: {
@ -99,8 +101,8 @@ export default {
<template v-else>
<gl-filtered-search-suggestion
v-for="iteration in iterations"
:key="iteration.title"
:value="iteration.title"
:key="iteration.id"
:value="String(iteration.id)"
>
{{ iteration.title }}
</gl-filtered-search-suggestion>

View File

@ -96,9 +96,6 @@ export default {
},
},
searchUsers: {
// TODO Remove error policy
// https://gitlab.com/gitlab-org/gitlab/-/issues/329750
errorPolicy: 'all',
query: searchUsers,
variables() {
return {
@ -111,28 +108,10 @@ export default {
return !this.isEditing;
},
update(data) {
// TODO Remove null filter (BE fix required)
// https://gitlab.com/gitlab-org/gitlab/-/issues/329750
return data.workspace?.users?.nodes.filter((x) => x?.user).map(({ user }) => user) || [];
},
debounce: ASSIGNEES_DEBOUNCE_DELAY,
error({ graphQLErrors }) {
// TODO This error suppression is temporary (BE fix required)
// https://gitlab.com/gitlab-org/gitlab/-/issues/329750
const isNullError = ({ message }) => {
return message === 'Cannot return null for non-nullable field GroupMember.user';
};
if (graphQLErrors?.length > 0 && graphQLErrors.every(isNullError)) {
// only null-related errors exist, suppress them.
// eslint-disable-next-line no-console
console.error(
"Suppressing the error 'Cannot return null for non-nullable field GroupMember.user'. Please see https://gitlab.com/gitlab-org/gitlab/-/issues/329750",
);
this.isSearching = false;
return;
}
error() {
this.$emit('error');
this.isSearching = false;
},

View File

@ -136,7 +136,7 @@ module Boards
def issue_params
params.require(:issue)
.permit(:title, :milestone_id, :project_id)
.merge(board_id: params[:board_id], list_id: params[:list_id], request: request)
.merge(board_id: params[:board_id], list_id: params[:list_id])
end
def serializer

View File

@ -47,31 +47,20 @@ module SpammableActions
end
end
def spammable_params
# NOTE: For the legacy reCAPTCHA implementation based on the HTML/HAML form, the
# 'g-recaptcha-response' field name comes from `Recaptcha::ClientHelper#recaptcha_tags` in the
# recaptcha gem, which is called from the HAML `_recaptcha_form.html.haml` form.
#
# It is used in the `Recaptcha::Verify#verify_recaptcha` to extract the value from `params`,
# if the `response` option is not passed explicitly.
#
# Instead of relying on this behavior, we are extracting and passing it explicitly. This will
# make it consistent with the newer, modern reCAPTCHA verification process as it will be
# implemented via the GraphQL API and in Vue components via the native reCAPTCHA Javascript API,
# which requires that the recaptcha response param be obtained and passed explicitly.
#
# It can also be expanded to multiple fields when we move to future alternative captcha
# implementations such as FriendlyCaptcha. See https://gitlab.com/gitlab-org/gitlab/-/issues/273480
# After this newer GraphQL/JS API process is fully supported by the backend, we can remove the
# check for the 'g-recaptcha-response' field and other HTML/HAML form-specific support.
captcha_response = params['g-recaptcha-response'] || params[:captcha_response]
{
request: request,
spam_log_id: params[:spam_log_id],
captcha_response: captcha_response
}
# TODO: This method is currently only needed for issue create and update. It can be removed when:
#
# 1. Issue create is is converted to a client/JS based approach instead of the legacy HAML
# `_recaptcha_form.html.haml` which is rendered via the `projects/issues/verify` template.
# In this case, which is based on the legacy reCAPTCHA implementation using the HTML/HAML form,
# the 'g-recaptcha-response' field name comes from `Recaptcha::ClientHelper#recaptcha_tags` in the
# recaptcha gem, which is called from the HAML `_recaptcha_form.html.haml` form.
# 2. Issue update is converted to use the headers-based approach, which will require adding
# support to captcha_modal_axios_interceptor.js like we have already added to
# apollo_captcha_link.js.
# In this case, the `captcha_response` field name comes from our captcha_modal_axios_interceptor.js.
def extract_legacy_spam_params_to_headers
request.headers['X-GitLab-Captcha-Response'] = params['g-recaptcha-response'] || params[:captcha_response]
request.headers['X-GitLab-Spam-Log-Id'] = params[:spam_log_id]
end
def spammable

View File

@ -129,12 +129,14 @@ class Projects::IssuesController < Projects::ApplicationController
end
def create
create_params = issue_params.merge(spammable_params).merge(
extract_legacy_spam_params_to_headers
create_params = issue_params.merge(
merge_request_to_resolve_discussions_of: params[:merge_request_to_resolve_discussions_of],
discussion_to_resolve: params[:discussion_to_resolve]
)
service = ::Issues::CreateService.new(project: project, current_user: current_user, params: create_params)
spam_params = ::Spam::SpamParams.new_from_request(request: request)
service = ::Issues::CreateService.new(project: project, current_user: current_user, params: create_params, spam_params: spam_params)
@issue = service.execute
create_vulnerability_issue_feedback(issue)
@ -334,8 +336,9 @@ class Projects::IssuesController < Projects::ApplicationController
end
def update_service
update_params = issue_params.merge(spammable_params)
::Issues::UpdateService.new(project: project, current_user: current_user, params: update_params)
extract_legacy_spam_params_to_headers
spam_params = ::Spam::SpamParams.new_from_request(request: request)
::Issues::UpdateService.new(project: project, current_user: current_user, params: issue_params, spam_params: spam_params)
end
def finder_type

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
class Projects::UsagePingController < Projects::ApplicationController
class Projects::ServicePingController < Projects::ApplicationController
before_action :authenticate_user!
feature_category :usage_ping

View File

@ -16,25 +16,6 @@ module Mutations
private
# additional_spam_params -> hash
#
# Used from a spammable mutation's #resolve method to generate
# the required additional spam/CAPTCHA params which must be merged into the params
# passed to the constructor of a service, where they can then be used in the service
# to perform spam checking via SpamActionService.
#
# Also accesses the #context of the mutation's Resolver superclass to obtain the request.
#
# Example:
#
# existing_args.merge!(additional_spam_params)
def additional_spam_params
{
api: true,
request: context[:request]
}
end
def spam_action_response(object)
fields = spam_action_response_fields(object)

View File

@ -73,7 +73,8 @@ module Mutations
project = authorized_find!(project_path)
params = build_create_issue_params(attributes.merge(author_id: current_user.id))
issue = ::Issues::CreateService.new(project: project, current_user: current_user, params: params).execute
spam_params = ::Spam::SpamParams.new_from_request(request: context[:request])
issue = ::Issues::CreateService.new(project: project, current_user: current_user, params: params, spam_params: spam_params).execute
if issue.spam?
issue.errors.add(:base, 'Spam detected.')

View File

@ -13,8 +13,11 @@ module Mutations
def resolve(project_path:, iid:, confidential:)
issue = authorized_find!(project_path: project_path, iid: iid)
project = issue.project
# Changing confidentiality affects spam checking rules, therefore we need to provide
# spam_params so a check can be performed.
spam_params = ::Spam::SpamParams.new_from_request(request: context[:request])
::Issues::UpdateService.new(project: project, current_user: current_user, params: { confidential: confidential })
::Issues::UpdateService.new(project: project, current_user: current_user, params: { confidential: confidential }, spam_params: spam_params)
.execute(issue)
{

View File

@ -31,7 +31,8 @@ module Mutations
issue = authorized_find!(project_path: project_path, iid: iid)
project = issue.project
::Issues::UpdateService.new(project: project, current_user: current_user, params: args).execute(issue)
spam_params = ::Spam::SpamParams.new_from_request(request: context[:request])
::Issues::UpdateService.new(project: project, current_user: current_user, params: args, spam_params: spam_params).execute(issue)
{
issue: issue,

View File

@ -49,7 +49,9 @@ module Mutations
process_args_for_params!(args)
service_response = ::Snippets::CreateService.new(project: project, current_user: current_user, params: args).execute
spam_params = ::Spam::SpamParams.new_from_request(request: context[:request])
service = ::Snippets::CreateService.new(project: project, current_user: current_user, params: args, spam_params: spam_params)
service_response = service.execute
# Only when the user is not an api user and the operation was successful
if !api_user? && service_response.success?
@ -81,12 +83,6 @@ module Mutations
# it's the expected key param
args[:files] = args.delete(:uploaded_files)
if Feature.enabled?(:snippet_spam)
args.merge!(additional_spam_params)
else
args[:disable_spam_action_service] = true
end
# Return nil to make it explicit that this method is mutating the args parameter, and that
# the return value is not relevant and is not to be used.
nil

View File

@ -34,7 +34,9 @@ module Mutations
process_args_for_params!(args)
service_response = ::Snippets::UpdateService.new(project: snippet.project, current_user: current_user, params: args).execute(snippet)
spam_params = ::Spam::SpamParams.new_from_request(request: context[:request])
service = ::Snippets::UpdateService.new(project: snippet.project, current_user: current_user, params: args, spam_params: spam_params)
service_response = service.execute(snippet)
# TODO: DRY this up - From here down, this is all duplicated with Mutations::Snippets::Create#resolve, except for
# `snippet.reset`, which is required in order to return the object in its non-dirty, unmodified, database state
@ -62,12 +64,6 @@ module Mutations
def process_args_for_params!(args)
convert_blob_actions_to_snippet_actions!(args)
if Feature.enabled?(:snippet_spam)
args.merge!(additional_spam_params)
else
args[:disable_spam_action_service] = true
end
# Return nil to make it explicit that this method is mutating the args parameter, and that
# the return value is not relevant and is not to be used.
nil

View File

@ -20,9 +20,7 @@ module Emails
options = service_desk_options(email_sender, 'thank_you', @issue.external_author)
.merge(subject: "Re: #{subject_base}")
mail_new_thread(@issue, options).tap do
Gitlab::Metrics::BackgroundTransaction.current&.add_event(:service_desk_thank_you_email)
end
mail_new_thread(@issue, options)
end
def service_desk_new_note_email(issue_id, note_id, recipient)
@ -33,9 +31,7 @@ module Emails
options = service_desk_options(email_sender, 'new_note', recipient)
.merge(subject: subject_base)
mail_answer_thread(@issue, options).tap do
Gitlab::Metrics::BackgroundTransaction.current&.add_event(:service_desk_new_note_email)
end
mail_answer_thread(@issue, options)
end
private

View File

@ -14,14 +14,14 @@ class Integration < ApplicationRecord
self.table_name = 'services'
INTEGRATION_NAMES = %w[
asana assembla bamboo bugzilla buildkite campfire confluence custom_issue_tracker discord
asana assembla bamboo bugzilla buildkite campfire confluence custom_issue_tracker datadog discord
drone_ci emails_on_push ewm external_wiki flowdock hangouts_chat irker jira
mattermost mattermost_slash_commands microsoft_teams packagist pipelines_email
pivotaltracker prometheus pushover redmine slack slack_slash_commands teamcity unify_circuit webex_teams youtrack
].freeze
PROJECT_SPECIFIC_INTEGRATION_NAMES = %w[
datadog jenkins
jenkins
].freeze
# Fake integrations to help with local development.
@ -54,6 +54,9 @@ class Integration < ApplicationRecord
redmine
slack slack_slash_commands
teamcity
unify_circuit
webex_teams
youtrack
].to_set.freeze
def self.renamed?(name)

View File

@ -188,9 +188,9 @@ class Project < ApplicationRecord
has_one :slack_integration, class_name: 'Integrations::Slack'
has_one :slack_slash_commands_integration, class_name: 'Integrations::SlackSlashCommands'
has_one :teamcity_integration, class_name: 'Integrations::Teamcity'
has_one :unify_circuit_service, class_name: 'Integrations::UnifyCircuit'
has_one :webex_teams_service, class_name: 'Integrations::WebexTeams'
has_one :youtrack_service, class_name: 'Integrations::Youtrack'
has_one :unify_circuit_integration, class_name: 'Integrations::UnifyCircuit'
has_one :webex_teams_integration, class_name: 'Integrations::WebexTeams'
has_one :youtrack_integration, class_name: 'Integrations::Youtrack'
has_one :root_of_fork_network,
foreign_key: 'root_project_id',
@ -1407,8 +1407,6 @@ class Project < ApplicationRecord
end
def disabled_services
return %w[datadog] unless Feature.enabled?(:datadog_ci_integration, self)
[]
end

View File

@ -30,7 +30,9 @@ module Boards
end
def create_issue(params)
::Issues::CreateService.new(project: project, current_user: current_user, params: params).execute
# NOTE: We are intentionally not doing a spam/CAPTCHA check for issues created via boards.
# See https://gitlab.com/gitlab-org/gitlab/-/issues/29400#note_598479184 for more context.
::Issues::CreateService.new(project: project, current_user: current_user, params: params, spam_params: nil).execute
end
end
end

View File

@ -7,20 +7,27 @@ module Captcha
class CaptchaVerificationService
include Recaptcha::Verify
# Currently the only value that is used out of the request by the reCAPTCHA library
# is 'remote_ip'. Therefore, we just create a struct to avoid passing the full request
# object through all the service layer objects, and instead just rely on passing only
# the required remote_ip value. This eliminates the need to couple the service layer
# to the HTTP request (for the purpose of this service, at least).
RequestStruct = Struct.new(:remote_ip)
def initialize(spam_params:)
@spam_params = spam_params
end
##
# Performs verification of a captcha response.
#
# 'captcha_response' parameter is the response from the user solving a client-side captcha.
#
# 'request' parameter is the request which submitted the captcha.
#
# NOTE: Currently only supports reCAPTCHA, and is not yet used in all places of the app in which
# captchas are verified, but these can be addressed in future MRs. See:
# https://gitlab.com/gitlab-org/gitlab/-/issues/273480
def execute(captcha_response: nil, request:)
return false unless captcha_response
def execute
return false unless spam_params.captcha_response
@request = request
@request = RequestStruct.new(spam_params.ip_address)
Gitlab::Recaptcha.load_configurations!
@ -31,11 +38,13 @@ module Captcha
# 2. We want control over the wording and i18n of the message
# 3. We want a consistent interface and behavior when adding support for other captcha
# libraries which may not support automatically adding errors to the model.
verify_recaptcha(response: captcha_response)
verify_recaptcha(response: spam_params.captcha_response)
end
private
attr_reader :spam_params
# The recaptcha library's Recaptcha::Verify#verify_recaptcha method requires that
# 'request' be a readable attribute - it doesn't support passing it as an options argument.
attr_reader :request

View File

@ -21,7 +21,8 @@ module IncidentManagement
title: title,
description: description,
issue_type: ISSUE_TYPE
}
},
spam_params: nil
).execute
return error(issue.errors.full_messages.to_sentence, issue) unless issue.valid?

View File

@ -68,7 +68,10 @@ module Issuable
end
def create_issuable(attributes)
create_issuable_class.new(project: @project, current_user: @user, params: attributes).execute
# NOTE: CSV imports are performed by workers, so we do not have a request context in order
# to create a SpamParams object to pass to the issuable create service.
spam_params = nil
create_issuable_class.new(project: @project, current_user: @user, params: attributes, spam_params: spam_params).execute
end
def email_results_to_user

View File

@ -55,9 +55,13 @@ module Issues
new_params = original_entity.serializable_hash.symbolize_keys.merge(new_params)
# spam checking is not necessary, as no new content is being created. Passing nil for
# spam_params will cause SpamActionService to skip checking and return a success response.
spam_params = nil
# Skip creation of system notes for existing attributes of the issue. The system notes of the old
# issue are copied over so we don't want to end up with duplicate notes.
CreateService.new(project: target_project, current_user: current_user, params: new_params).execute(skip_system_notes: true)
CreateService.new(project: target_project, current_user: current_user, params: new_params, spam_params: spam_params).execute(skip_system_notes: true)
end
def queue_copy_designs

View File

@ -4,10 +4,21 @@ module Issues
class CreateService < Issues::BaseService
include ResolveDiscussions
def execute(skip_system_notes: false)
@request = params.delete(:request)
@spam_params = Spam::SpamActionService.filter_spam_params!(params, @request)
# NOTE: For Issues::CreateService, we require the spam_params and do not default it to nil, because
# spam_checking is likely to be necessary. However, if there is not a request available in scope
# in the caller (for example, an issue created via email) and the required arguments to the
# SpamParams constructor are not otherwise available, spam_params: must be explicitly passed as nil.
def initialize(project:, current_user: nil, params: {}, spam_params:)
# Temporary check to ensure we are no longer passing request in params now that we have
# introduced spam_params. Raise an exception if it is present.
# Remove after https://gitlab.com/gitlab-org/gitlab/-/merge_requests/58603 is complete.
raise if params[:request]
super(project: project, current_user: current_user, params: params)
@spam_params = spam_params
end
def execute(skip_system_notes: false)
@issue = BuildService.new(project: project, current_user: current_user, params: params).execute
filter_resolve_discussion_params
@ -18,10 +29,10 @@ module Issues
def before_create(issue)
Spam::SpamActionService.new(
spammable: issue,
request: request,
spam_params: spam_params,
user: current_user,
action: :create
).execute(spam_params: spam_params)
).execute
# current_user (defined in BaseService) is not available within run_after_commit block
user = current_user
@ -64,10 +75,10 @@ module Issues
private
attr_reader :request, :spam_params
attr_reader :spam_params
def user_agent_detail_service
UserAgentDetailService.new(@issue, request)
UserAgentDetailService.new(spammable: @issue, spam_params: spam_params)
end
end
end

View File

@ -28,6 +28,7 @@ module Issues
def relate_two_issues(duplicate_issue, canonical_issue)
params = { target_issuable: canonical_issue }
IssueLinks::CreateService.new(duplicate_issue, current_user, params).execute
end
end

View File

@ -58,10 +58,13 @@ module Issues
}
new_params = original_entity.serializable_hash.symbolize_keys.merge(new_params)
# spam checking is not necessary, as no new content is being created. Passing nil for
# spam_params will cause SpamActionService to skip checking and return a success response.
spam_params = nil
# Skip creation of system notes for existing attributes of the issue. The system notes of the old
# issue are copied over so we don't want to end up with duplicate notes.
CreateService.new(project: @target_project, current_user: @current_user, params: new_params).execute(skip_system_notes: true)
CreateService.new(project: @target_project, current_user: @current_user, params: new_params, spam_params: spam_params).execute(skip_system_notes: true)
end
def queue_copy_designs

View File

@ -4,12 +4,17 @@ module Issues
class UpdateService < Issues::BaseService
extend ::Gitlab::Utils::Override
# NOTE: For Issues::UpdateService, we default the spam_params to nil, because spam_checking is not
# necessary in many cases, and we don't want to require every caller to explicitly pass it as nil
# to disable spam checking.
def initialize(project:, current_user: nil, params: {}, spam_params: nil)
super(project: project, current_user: current_user, params: params)
@spam_params = spam_params
end
def execute(issue)
handle_move_between_ids(issue)
@request = params.delete(:request)
@spam_params = Spam::SpamActionService.filter_spam_params!(params, @request)
change_issue_duplicate(issue)
move_issue_to_new_project(issue) || clone_issue(issue) || update_task_event(issue) || update(issue)
end
@ -25,10 +30,10 @@ module Issues
Spam::SpamActionService.new(
spammable: issue,
request: request,
spam_params: spam_params,
user: current_user,
action: :update
).execute(spam_params: spam_params)
).execute
end
def handle_changes(issue, options)
@ -127,7 +132,7 @@ module Issues
private
attr_reader :request, :spam_params
attr_reader :spam_params
def clone_issue(issue)
target_project = params.delete(:target_clone_project)

View File

@ -396,6 +396,7 @@ class NotificationService
recipients.each do |recipient|
mailer.service_desk_new_note_email(issue.id, note.id, recipient).deliver_later
Gitlab::Metrics::BackgroundTransaction.current&.add_event(:service_desk_new_note_email)
end
end

View File

@ -2,12 +2,14 @@
module Snippets
class CreateService < Snippets::BaseService
def execute
# NOTE: disable_spam_action_service can be removed when the ':snippet_spam' feature flag is removed.
disable_spam_action_service = params.delete(:disable_spam_action_service) == true
@request = params.delete(:request)
@spam_params = Spam::SpamActionService.filter_spam_params!(params, @request)
# NOTE: For Issues::CreateService, we require the spam_params and do not default it to nil, because
# spam_checking is likely to be necessary.
def initialize(project:, current_user: nil, params: {}, spam_params:)
super(project: project, current_user: current_user, params: params)
@spam_params = spam_params
end
def execute
@snippet = build_from_params
return invalid_params_error(@snippet) unless valid_params?
@ -18,17 +20,17 @@ module Snippets
@snippet.author = current_user
unless disable_spam_action_service
if Feature.enabled?(:snippet_spam)
Spam::SpamActionService.new(
spammable: @snippet,
request: request,
spam_params: spam_params,
user: current_user,
action: :create
).execute(spam_params: spam_params)
).execute
end
if save_and_commit
UserAgentDetailService.new(@snippet, request).create
UserAgentDetailService.new(spammable: @snippet, spam_params: spam_params).create
Gitlab::UsageDataCounters::SnippetCounter.count(:create)
move_temporary_files
@ -41,7 +43,7 @@ module Snippets
private
attr_reader :snippet, :request, :spam_params
attr_reader :snippet, :spam_params
def build_from_params
if project

View File

@ -6,12 +6,15 @@ module Snippets
UpdateError = Class.new(StandardError)
def execute(snippet)
# NOTE: disable_spam_action_service can be removed when the ':snippet_spam' feature flag is removed.
disable_spam_action_service = params.delete(:disable_spam_action_service) == true
@request = params.delete(:request)
@spam_params = Spam::SpamActionService.filter_spam_params!(params, @request)
# NOTE: For Snippets::UpdateService, we default the spam_params to nil, because spam_checking is not
# necessary in many cases, and we don't want every caller to have to explicitly pass it as nil
# to disable spam checking.
def initialize(project:, current_user: nil, params: {}, spam_params: nil)
super(project: project, current_user: current_user, params: params)
@spam_params = spam_params
end
def execute(snippet)
return invalid_params_error(snippet) unless valid_params?
if visibility_changed?(snippet) && !visibility_allowed?(visibility_level)
@ -20,13 +23,13 @@ module Snippets
update_snippet_attributes(snippet)
unless disable_spam_action_service
if Feature.enabled?(:snippet_spam)
Spam::SpamActionService.new(
spammable: snippet,
request: request,
spam_params: spam_params,
user: current_user,
action: :update
).execute(spam_params: spam_params)
).execute
end
if save_and_commit(snippet)
@ -40,7 +43,7 @@ module Snippets
private
attr_reader :request, :spam_params
attr_reader :spam_params
def visibility_changed?(snippet)
visibility_level && visibility_level.to_i != snippet.visibility_level

View File

@ -20,6 +20,7 @@ module Spam
created_at: DateTime.current,
author: owner_name,
author_email: owner_email,
# NOTE: The akismet_client needs the option to be named `:referrer`, not `:referer`
referrer: options[:referer]
}

View File

@ -4,67 +4,22 @@ module Spam
class SpamActionService
include SpamConstants
##
# Utility method to filter SpamParams from parameters, which will later be passed to #execute
# after the spammable is created/updated based on the remaining parameters.
#
# Takes a hash of parameters from an incoming request to modify a model (via a controller,
# service, or GraphQL mutation). The parameters will either be camelCase (if they are
# received directly via controller params) or underscore_case (if they have come from
# a GraphQL mutation which has converted them to underscore), or in the
# headers when using the header based flow.
#
# Deletes the parameters which are related to spam and captcha processing, and returns
# them in a SpamParams parameters object. See:
# https://refactoring.com/catalog/introduceParameterObject.html
def self.filter_spam_params!(params, request)
# NOTE: The 'captcha_response' field can be expanded to multiple fields when we move to future
# alternative captcha implementations such as FriendlyCaptcha. See
# https://gitlab.com/gitlab-org/gitlab/-/issues/273480
headers = request&.headers || {}
api = params.delete(:api)
captcha_response = read_parameter(:captcha_response, params, headers)
spam_log_id = read_parameter(:spam_log_id, params, headers)&.to_i
SpamParams.new(api: api, captcha_response: captcha_response, spam_log_id: spam_log_id)
end
def self.read_parameter(name, params, headers)
[
params.delete(name),
params.delete(name.to_s.camelize(:lower).to_sym),
headers["X-GitLab-#{name.to_s.titlecase(keep_id_suffix: true).tr(' ', '-')}"]
].compact.first
end
attr_accessor :target, :request, :options
attr_reader :spam_log
def initialize(spammable:, request:, user:, action:)
def initialize(spammable:, spam_params:, user:, action:)
@target = spammable
@request = request
@spam_params = spam_params
@user = user
@action = action
@options = {}
end
# rubocop:disable Metrics/AbcSize
def execute(spam_params:)
if request
options[:ip_address] = request.env['action_dispatch.remote_ip'].to_s
options[:user_agent] = request.env['HTTP_USER_AGENT']
options[:referer] = request.env['HTTP_REFERER']
else
# TODO: This code is never used, because we do not perform a verification if there is not a
# request. Why? Should it be deleted? Or should we check even if there is no request?
options[:ip_address] = target.ip_address
options[:user_agent] = target.user_agent
end
def execute
# If spam_params is passed as `nil`, no check will be performed. This is the easiest way to allow
# composed services which may not need to do spam checking to "opt out". For example, when
# MoveService is calling CreateService, spam checking is not necessary, as no new content is
# being created.
return ServiceResponse.success(message: 'Skipped spam check because spam_params was not present') unless spam_params
recaptcha_verified = Captcha::CaptchaVerificationService.new.execute(
captcha_response: spam_params.captcha_response,
request: request
)
recaptcha_verified = Captcha::CaptchaVerificationService.new(spam_params: spam_params).execute
if recaptcha_verified
# If it's a request which is already verified through CAPTCHA,
@ -73,10 +28,9 @@ module Spam
ServiceResponse.success(message: "CAPTCHA successfully verified")
else
return ServiceResponse.success(message: 'Skipped spam check because user was allowlisted') if allowlisted?(user)
return ServiceResponse.success(message: 'Skipped spam check because request was not present') unless request
return ServiceResponse.success(message: 'Skipped spam check because it was not required') unless check_for_spam?
perform_spam_service_check(spam_params.api)
perform_spam_service_check
ServiceResponse.success(message: "Spam check performed. Check #{target.class.name} spammable model for any errors or CAPTCHA requirement")
end
end
@ -86,7 +40,7 @@ module Spam
private
attr_reader :user, :action
attr_reader :user, :action, :target, :spam_params, :spam_log
##
# In order to be proceed to the spam check process, the target must be
@ -104,7 +58,7 @@ module Spam
##
# Performs the spam check using the spam verdict service, and modifies the target model
# accordingly based on the result.
def perform_spam_service_check(api)
def perform_spam_service_check
ensure_target_is_dirty
# since we can check for spam, and recaptcha is not verified,
@ -113,7 +67,7 @@ module Spam
case result
when CONDITIONAL_ALLOW
# at the moment, this means "ask for reCAPTCHA"
create_spam_log(api)
create_spam_log
break if target.allow_possible_spam?
@ -122,12 +76,12 @@ module Spam
# TODO: remove `unless target.allow_possible_spam?` once this flag has been passed to `SpamVerdictService`
# https://gitlab.com/gitlab-org/gitlab/-/issues/214739
target.spam! unless target.allow_possible_spam?
create_spam_log(api)
create_spam_log
when BLOCK_USER
# TODO: improve BLOCK_USER handling, non-existent until now
# https://gitlab.com/gitlab-org/gitlab/-/issues/329666
target.spam! unless target.allow_possible_spam?
create_spam_log(api)
create_spam_log
when ALLOW
target.clear_spam_flags!
when NOOP
@ -137,16 +91,21 @@ module Spam
end
end
def create_spam_log(api)
def create_spam_log
@spam_log = SpamLog.create!(
{
user_id: target.author_id,
title: target.spam_title,
description: target.spam_description,
source_ip: options[:ip_address],
user_agent: options[:user_agent],
source_ip: spam_params.ip_address,
user_agent: spam_params.user_agent,
noteable_type: noteable_type,
via_api: api
# Now, all requests are via the API, so hardcode it to true to simplify the logic and API
# of this service. See https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/2266
# for original introduction of `via_api` field.
# See discussion here about possibly deprecating this field:
# https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/2266#note_542527450
via_api: true
}
)
@ -159,9 +118,14 @@ module Spam
target_type: noteable_type
}
options = {
ip_address: spam_params.ip_address,
user_agent: spam_params.user_agent,
referer: spam_params.referer
}
SpamVerdictService.new(target: target,
user: user,
request: request,
options: options,
context: context)
end

View File

@ -3,30 +3,54 @@
module Spam
##
# This class is a Parameter Object (https://refactoring.com/catalog/introduceParameterObject.html)
# which acts as an container abstraction for multiple parameter values related to spam and
# captcha processing for a request.
# which acts as an container abstraction for multiple values related to spam and
# captcha processing for a provided HTTP request object.
#
# It is used to encapsulate these values and allow them to be passed from the Controller/GraphQL
# layers down into to the Service layer, without needing to pass the entire request and therefore
# unnecessarily couple the Service layer to the HTTP request.
#
# Values contained are:
#
# api: A boolean flag indicating if the request was submitted via the REST or GraphQL API
# captcha_response: The response resulting from the user solving a captcha. Currently it is
# a scalar reCAPTCHA response string, but it can be expanded to an object in the future to
# support other captcha implementations such as FriendlyCaptcha.
# spam_log_id: The id of a SpamLog record.
# support other captcha implementations such as FriendlyCaptcha. Obtained from
# request.headers['X-GitLab-Captcha-Response']
# spam_log_id: The id of a SpamLog record. Obtained from request.headers['X-GitLab-Spam-Log-Id']
# ip_address = The remote IP. Obtained from request.env['action_dispatch.remote_ip']
# user_agent = The user agent. Obtained from request.env['HTTP_USER_AGENT']
# referer = The HTTP referer. Obtained from request.env['HTTP_REFERER']
#
# NOTE: The presence of these values in the request is not currently enforced. If they are missing,
# then the spam check may fail, or the SpamLog or UserAgentDetail may have missing fields.
class SpamParams
attr_reader :api, :captcha_response, :spam_log_id
def self.new_from_request(request:)
self.new(
captcha_response: request.headers['X-GitLab-Captcha-Response'],
spam_log_id: request.headers['X-GitLab-Spam-Log-Id'],
ip_address: request.env['action_dispatch.remote_ip'].to_s,
user_agent: request.env['HTTP_USER_AGENT'],
referer: request.env['HTTP_REFERER']
)
end
def initialize(api:, captcha_response:, spam_log_id:)
@api = api.present?
attr_reader :captcha_response, :spam_log_id, :ip_address, :user_agent, :referer
def initialize(captcha_response:, spam_log_id:, ip_address:, user_agent:, referer:)
@captcha_response = captcha_response
@spam_log_id = spam_log_id
@ip_address = ip_address
@user_agent = user_agent
@referer = referer
end
def ==(other)
other.class <= self.class &&
other.api == api &&
other.captcha_response == captcha_response &&
other.spam_log_id == spam_log_id
other.spam_log_id == spam_log_id &&
other.ip_address == ip_address &&
other.user_agent == user_agent &&
other.referer == referer
end
end
end

View File

@ -5,9 +5,8 @@ module Spam
include AkismetMethods
include SpamConstants
def initialize(user:, target:, request:, options:, context: {})
def initialize(user:, target:, options:, context: {})
@target = target
@request = request
@user = user
@options = options
@context = context
@ -59,7 +58,7 @@ module Spam
private
attr_reader :user, :target, :request, :options, :context
attr_reader :user, :target, :options, :context
def akismet_verdict
if akismet.spam?

View File

@ -1,16 +1,21 @@
# frozen_string_literal: true
class UserAgentDetailService
attr_accessor :spammable, :request
def initialize(spammable, request)
def initialize(spammable:, spam_params:)
@spammable = spammable
@request = request
@spam_params = spam_params
end
def create
return unless request
unless spam_params&.user_agent && spam_params&.ip_address
messasge = 'Skipped UserAgentDetail creation because necessary spam_params were not provided'
return ServiceResponse.success(message: messasge)
end
spammable.create_user_agent_detail(user_agent: request.env['HTTP_USER_AGENT'], ip_address: request.env['action_dispatch.remote_ip'].to_s)
spammable.create_user_agent_detail(user_agent: spam_params.user_agent, ip_address: spam_params.ip_address)
end
private
attr_reader :spammable, :spam_params
end

View File

@ -1,8 +0,0 @@
---
name: datadog_ci_integration
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/46564
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/284088
type: development
group: group::ecosystem
default_enabled: false
milestone: '13.7'

View File

@ -547,7 +547,13 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
end
end
scope :usage_ping, controller: :usage_ping do
# TODO: Deprecated. This should be removed in 14.2.
scope :usage_ping, controller: :service_ping do
post :web_ide_clientside_preview # rubocop:todo Cop/PutProjectRoutesUnderScope
post :web_ide_pipelines_count # rubocop:todo Cop/PutProjectRoutesUnderScope
end
scope :service_ping, controller: :service_ping do
post :web_ide_clientside_preview # rubocop:todo Cop/PutProjectRoutesUnderScope
post :web_ide_pipelines_count # rubocop:todo Cop/PutProjectRoutesUnderScope
end

View File

@ -68,27 +68,3 @@ end
if helper.security_mr? && feature_flag_file_added?
fail "Feature flags are discouraged from security merge requests. Read the [security documentation](https://gitlab.com/gitlab-org/release/docs/-/blob/master/general/security/utilities/feature_flags.md) for details."
end
if feature_flag_file_added_or_removed?
new_mr_title = helper.mr_title.dup
new_mr_title << ' [RUN ALL RSPEC]' unless helper.run_all_rspec_mr?
new_mr_title << ' [RUN AS-IF-FOSS]' unless helper.run_as_if_foss_mr?
changes = {}
changes[:add_labels] = FEATURE_FLAG_LABEL unless helper.mr_has_labels?(FEATURE_FLAG_LABEL)
if new_mr_title != helper.mr_title
changes[:title] = new_mr_title
else
message "You're adding or removing a feature flag, your MR title needs to include `[RUN ALL RSPEC] [RUN AS-IF-FOSS]` (we may have updated it automatically for you and started a new MR pipeline) to ensure everything is covered."
end
if changes.any?
gitlab.api.update_merge_request(
gitlab.mr_json['project_id'],
gitlab.mr_json['iid'],
**changes
)
gitlab.api.post("/projects/#{gitlab.mr_json['project_id']}/merge_requests/#{gitlab.mr_json['iid']}/pipelines")
end
end

View File

@ -9,7 +9,8 @@ SPECIALIZATIONS = {
docs: 'documentation',
qa: 'QA',
engineering_productivity: 'Engineering Productivity',
ci_template: 'ci::templates'
ci_template: 'ci::templates',
feature_flag: 'feature flag'
}.freeze
labels_to_add = project_helper.changes_by_category.each_with_object([]) do |(category, _changes), memo|

View File

@ -176,14 +176,14 @@ Users are notified of the following events:
| Event | Sent to | Settings level |
|------------------------------|---------------------|------------------------------|
| New SSH key added | User | Security email, always sent. |
| SSH key has expired | User | Security email, always sent. |
| SSH key has expired | User | Security email, always sent. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/322637) in GitLab 13.12 |
| New email added | User | Security email, always sent. |
| Email changed | User | Security email, always sent. |
| Password changed | User | Security email, always sent when user changes their own password |
| Password changed by administrator | User | Security email, always sent when an administrator changes the password of another user |
| Two-factor authentication disabled | User | Security email, always sent. |
| New user created | User | Sent on user creation, except for OmniAuth (LDAP)|
| New SAML/SCIM user provisioned. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/276018) in GitLab 13.8 | User | Sent when a user is provisioned through SAML/SCIM |
| New SAML/SCIM user provisioned | User | Sent when a user is provisioned through SAML/SCIM. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/276018) in GitLab 13.8 |
| User added to project | User | Sent when user is added to project |
| Project access level changed | User | Sent when user project access level is changed |
| User added to group | User | Sent when user is added to group |
@ -237,10 +237,10 @@ epics:
| Close merge request | |
| Due issue | Participants and Custom notification level with this event selected |
| Failed pipeline | The author of the pipeline |
| Fixed pipeline ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/24309) in GitLab 13.1.) | The author of the pipeline. Enabled by default. |
| Fixed pipeline | The author of the pipeline. Enabled by default. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/24309) in GitLab 13.1 |
| Merge merge request | |
| Merge when pipeline succeeds ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/211961) in GitLab 13.4) | Author, Participants, Watchers, Subscribers, and Custom notification level with this event selected. `Note:` Custom notification level is ignored for Author, Watchers and Subscribers |
| Merge request [marked as ready](../project/merge_requests/drafts.md) (introduced in [GitLab 13.10](https://gitlab.com/gitlab-org/gitlab/-/issues/15332)) | Watchers and participants |
| Merge when pipeline succeeds | Author, Participants, Watchers, Subscribers, and Custom notification level with this event selected. Custom notification level is ignored for Author, Watchers and Subscribers. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/211961) in GitLab 13.4 |
| Merge request [marked as ready](../project/merge_requests/drafts.md) | Watchers and participants. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/15332) in GitLab 13.10 |
| New comment | Participants, Watchers, Subscribers, and Custom notification level with this event selected, plus anyone mentioned by `@username` in the comment, with notification level "Mention" or higher |
| New epic | |
| New issue | |

View File

@ -32,6 +32,7 @@ Click on the service links to see further configuration instructions and details
| Campfire | Connect to chat. | **{dotted-circle}** No |
| [Confluence Workspace](../../../api/services.md#confluence-service) | Replace the link to the internal wiki with a link to a Confluence Cloud Workspace. | **{dotted-circle}** No |
| [Custom issue tracker](custom_issue_tracker.md) | Use a custom issue tracker. | **{dotted-circle}** No |
| Datadog | Trace your GitLab pipelines with Datadog. | **{check-circle}** Yes |
| [Discord Notifications](discord_notifications.md) | Send notifications about project events to a Discord channel. | **{dotted-circle}** No |
| Drone CI | Run CI/CD pipelines with Drone. | **{check-circle}** Yes |
| [Emails on push](emails_on_push.md) | Send commits and diff of each push by email. | **{dotted-circle}** No |

View File

@ -577,10 +577,6 @@ module API
Gitlab::AppLogger.warn("Redis tracking event failed for event: #{event_name}, message: #{error.message}")
end
def with_api_params(&block)
yield({ api: true, request: request })
end
protected
def project_finder_params_visibility_ce

View File

@ -72,22 +72,18 @@ module API
end
def process_create_params(args)
with_api_params do |api_params|
args[:snippet_actions] = args.delete(:files)&.map do |file|
file[:action] = :create
file.symbolize_keys
end
args.merge(api_params)
args[:snippet_actions] = args.delete(:files)&.map do |file|
file[:action] = :create
file.symbolize_keys
end
args
end
def process_update_params(args)
with_api_params do |api_params|
args[:snippet_actions] = args.delete(:files)&.map(&:symbolize_keys)
args[:snippet_actions] = args.delete(:files)&.map(&:symbolize_keys)
args.merge(api_params)
end
args
end
def validate_params_for_multiple_files(snippet)

View File

@ -255,9 +255,11 @@ module API
issue_params = convert_parameters_from_legacy_format(issue_params)
begin
spam_params = ::Spam::SpamParams.new_from_request(request: request)
issue = ::Issues::CreateService.new(project: user_project,
current_user: current_user,
params: issue_params.merge(request: request, api: true)).execute
params: issue_params,
spam_params: spam_params).execute
if issue.spam?
render_api_error!({ error: 'Spam detected' }, 400)
@ -294,13 +296,15 @@ module API
issue = user_project.issues.find_by!(iid: params.delete(:issue_iid))
authorize! :update_issue, issue
update_params = declared_params(include_missing: false).merge(request: request, api: true)
update_params = declared_params(include_missing: false)
update_params = convert_parameters_from_legacy_format(update_params)
spam_params = ::Spam::SpamParams.new_from_request(request: request)
issue = ::Issues::UpdateService.new(project: user_project,
current_user: current_user,
params: update_params).execute(issue)
params: update_params,
spam_params: spam_params).execute(issue)
render_spam_error! if issue.spam?

View File

@ -75,7 +75,8 @@ module API
snippet_params = process_create_params(declared_params(include_missing: false))
service_response = ::Snippets::CreateService.new(project: user_project, current_user: current_user, params: snippet_params).execute
spam_params = ::Spam::SpamParams.new_from_request(request: request)
service_response = ::Snippets::CreateService.new(project: user_project, current_user: current_user, params: snippet_params, spam_params: spam_params).execute
snippet = service_response.payload[:snippet]
if service_response.success?
@ -116,7 +117,8 @@ module API
snippet_params = process_update_params(declared_params(include_missing: false))
service_response = ::Snippets::UpdateService.new(project: user_project, current_user: current_user, params: snippet_params).execute(snippet)
spam_params = ::Spam::SpamParams.new_from_request(request: request)
service_response = ::Snippets::UpdateService.new(project: user_project, current_user: current_user, params: snippet_params, spam_params: spam_params).execute(snippet)
snippet = service_response.payload[:snippet]
if service_response.success?

View File

@ -84,7 +84,8 @@ module API
attrs = process_create_params(declared_params(include_missing: false))
service_response = ::Snippets::CreateService.new(project: nil, current_user: current_user, params: attrs).execute
spam_params = ::Spam::SpamParams.new_from_request(request: request)
service_response = ::Snippets::CreateService.new(project: nil, current_user: current_user, params: attrs, spam_params: spam_params).execute
snippet = service_response.payload[:snippet]
if service_response.success?
@ -126,7 +127,8 @@ module API
attrs = process_update_params(declared_params(include_missing: false))
service_response = ::Snippets::UpdateService.new(project: nil, current_user: current_user, params: attrs).execute(snippet)
spam_params = ::Spam::SpamParams.new_from_request(request: request)
service_response = ::Snippets::UpdateService.new(project: nil, current_user: current_user, params: attrs, spam_params: spam_params).execute(snippet)
snippet = service_response.payload[:snippet]

View File

@ -61,7 +61,8 @@ module Gitlab
params: {
title: mail.subject,
description: message_including_reply
}
},
spam_params: nil
).execute
end

View File

@ -83,7 +83,8 @@ module Gitlab
description: message_including_template,
confidential: true,
external_author: from_address
}
},
spam_params: nil
).execute
raise InvalidIssueError unless @issue.persisted?
@ -95,6 +96,7 @@ module Gitlab
def send_thank_you_email
Notify.service_desk_thank_you_email(@issue.id).deliver_later
Gitlab::Metrics::BackgroundTransaction.current&.add_event(:service_desk_thank_you_email)
end
def message_including_template

View File

@ -33,7 +33,7 @@ module Gitlab
private
def create_issue(title:, description:)
Issues::CreateService.new(project: project, current_user: current_user, params: { title: title, description: description }).execute
Issues::CreateService.new(project: project, current_user: current_user, params: { title: title, description: description }, spam_params: nil).execute
end
def presenter(issue)

View File

@ -30,7 +30,7 @@ module Quality
labels: labels.join(',')
}
params[:closed_at] = params[:created_at] + rand(35).days if params[:state] == 'closed'
issue = ::Issues::CreateService.new(project: project, current_user: team.sample, params: params).execute
issue = ::Issues::CreateService.new(project: project, current_user: team.sample, params: params, spam_params: nil).execute
if issue.persisted?
created_issues_count += 1

View File

@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe Projects::UsagePingController do
RSpec.describe Projects::ServicePingController do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }

View File

@ -97,7 +97,7 @@ FactoryBot.define do
issue_tracker
end
factory :youtrack_service, class: 'Integrations::Youtrack' do
factory :youtrack_integration, class: 'Integrations::Youtrack' do
project
active { true }
issue_tracker

View File

@ -402,7 +402,7 @@ FactoryBot.define do
factory :youtrack_project, parent: :project do
has_external_issue_tracker { true }
youtrack_service
youtrack_integration
end
factory :jira_project, parent: :project do

View File

@ -145,7 +145,7 @@ RSpec.describe 'Contributions Calendar', :js do
describe '1 issue creation calendar activity' do
before do
Issues::CreateService.new(project: contributed_project, current_user: user, params: issue_params).execute
Issues::CreateService.new(project: contributed_project, current_user: user, params: issue_params, spam_params: nil).execute
end
it_behaves_like 'a day with activity', contribution_count: 1
@ -180,7 +180,7 @@ RSpec.describe 'Contributions Calendar', :js do
push_code_contribution
travel_to(Date.yesterday) do
Issues::CreateService.new(project: contributed_project, current_user: user, params: issue_params).execute
Issues::CreateService.new(project: contributed_project, current_user: user, params: issue_params, spam_params: nil).execute
end
end
include_context 'visit user page'

View File

@ -9,7 +9,7 @@ RSpec.describe 'Unsubscribe links', :sidekiq_might_not_need_inline do
let(:author) { create(:user) }
let(:project) { create(:project, :public) }
let(:params) { { title: 'A bug!', description: 'Fix it!', assignees: [recipient] } }
let(:issue) { Issues::CreateService.new(project: project, current_user: author, params: params).execute }
let(:issue) { Issues::CreateService.new(project: project, current_user: author, params: params, spam_params: nil).execute }
let(:mail) { ActionMailer::Base.deliveries.last }
let(:body) { Capybara::Node::Simple.new(mail.default_part_body.to_s) }

View File

@ -125,7 +125,7 @@ RSpec.describe 'Users > User browses projects on user page', :js do
end
before do
Issues::CreateService.new(project: contributed_project, current_user: user, params: { title: 'Bug in old browser' }).execute
Issues::CreateService.new(project: contributed_project, current_user: user, params: { title: 'Bug in old browser' }, spam_params: nil).execute
event = create(:push_event, project: contributed_project, author: user)
create(:push_event_payload, event: event, commit_count: 3)
end

View File

@ -1,3 +1,4 @@
import { GlTabs } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
@ -7,6 +8,7 @@ import EmptyState from '~/environments/components/empty_state.vue';
import EnableReviewAppModal from '~/environments/components/enable_review_app_modal.vue';
import EnvironmentsApp from '~/environments/components/environments_app.vue';
import axios from '~/lib/utils/axios_utils';
import * as utils from '~/lib/utils/common_utils';
import { environment, folder } from './mock_data';
describe('Environment', () => {
@ -264,4 +266,18 @@ describe('Environment', () => {
});
});
});
describe('tabs', () => {
beforeEach(() => {
mockRequest(200, { environments: [] });
jest
.spyOn(utils, 'getParameterByName')
.mockImplementation((param) => (param === 'scope' ? 'stopped' : null));
return createWrapper(true);
});
it('selects the tab for the parameter', () => {
expect(wrapper.findComponent(GlTabs).attributes('value')).toBe('1');
});
});
});

View File

@ -21,7 +21,6 @@ import IssuableList from '~/issuable_list/components/issuable_list_root.vue';
import { IssuableListTabs, IssuableStates } from '~/issuable_list/constants';
import IssuesListApp from '~/issues_list/components/issues_list_app.vue';
import {
apiSortParams,
CREATED_DESC,
DUE_DATE_OVERDUE,
PARAM_DUE_DATE,
@ -148,8 +147,8 @@ describe('IssuesListApp component', () => {
hasPreviousPage: getIssuesQueryResponse.data.project.issues.pageInfo.hasPreviousPage,
hasNextPage: getIssuesQueryResponse.data.project.issues.pageInfo.hasNextPage,
urlParams: {
sort: urlSortParams[CREATED_DESC],
state: IssuableStates.Opened,
...urlSortParams[CREATED_DESC],
},
});
});
@ -178,7 +177,7 @@ describe('IssuesListApp component', () => {
describe('csv import/export component', () => {
describe('when user is signed in', () => {
const search = '?search=refactor&state=opened&sort=created_date';
const search = '?search=refactor&sort=created_date&state=opened';
beforeEach(() => {
global.jsdom.reconfigure({ url: `${TEST_HOST}${search}` });
@ -273,13 +272,17 @@ describe('IssuesListApp component', () => {
describe('sort', () => {
it.each(Object.keys(urlSortParams))('is set as %s from the url params', (sortKey) => {
global.jsdom.reconfigure({ url: setUrlParams(urlSortParams[sortKey], TEST_HOST) });
global.jsdom.reconfigure({
url: setUrlParams({ sort: urlSortParams[sortKey] }, TEST_HOST),
});
wrapper = mountComponent();
expect(findIssuableList().props()).toMatchObject({
initialSortBy: sortKey,
urlParams: urlSortParams[sortKey],
urlParams: {
sort: urlSortParams[sortKey],
},
});
});
});
@ -640,7 +643,7 @@ describe('IssuesListApp component', () => {
});
describe('when "sort" event is emitted by IssuableList', () => {
it.each(Object.keys(apiSortParams))(
it.each(Object.keys(urlSortParams))(
'updates to the new sort when payload is `%s`',
async (sortKey) => {
wrapper = mountComponent();
@ -650,7 +653,9 @@ describe('IssuesListApp component', () => {
jest.runOnlyPendingTimers();
await nextTick();
expect(findIssuableList().props('urlParams')).toMatchObject(urlSortParams[sortKey]);
expect(findIssuableList().props('urlParams')).toMatchObject({
sort: urlSortParams[sortKey],
});
},
);
});

View File

@ -9,7 +9,7 @@ export const getIssuesQueryResponse = {
issues: {
count: 1,
pageInfo: {
hasNextPage: false,
hasNextPage: true,
hasPreviousPage: false,
startCursor: 'startcursor',
endCursor: 'endcursor',
@ -86,10 +86,10 @@ export const locationSearch = [
'not[label_name][]=drama',
'my_reaction_emoji=thumbsup',
'confidential=no',
'iteration_title=season:+%234',
'not[iteration_title]=season:+%2320',
'epic_id=gitlab-org%3A%3A%2612',
'not[epic_id]=gitlab-org%3A%3A%2634',
'iteration_id=4',
'not[iteration_id]=20',
'epic_id=12',
'not[epic_id]=34',
'weight=1',
'not[weight]=3',
].join('&');
@ -118,10 +118,10 @@ export const filteredTokens = [
{ type: 'labels', value: { data: 'drama', operator: OPERATOR_IS_NOT } },
{ type: 'my_reaction_emoji', value: { data: 'thumbsup', operator: OPERATOR_IS } },
{ type: 'confidential', value: { data: 'no', operator: OPERATOR_IS } },
{ type: 'iteration', value: { data: 'season: #4', operator: OPERATOR_IS } },
{ type: 'iteration', value: { data: 'season: #20', operator: OPERATOR_IS_NOT } },
{ type: 'epic_id', value: { data: 'gitlab-org::&12', operator: OPERATOR_IS } },
{ type: 'epic_id', value: { data: 'gitlab-org::&34', operator: OPERATOR_IS_NOT } },
{ type: 'iteration', value: { data: '4', operator: OPERATOR_IS } },
{ type: 'iteration', value: { data: '20', operator: OPERATOR_IS_NOT } },
{ type: 'epic_id', value: { data: '12', operator: OPERATOR_IS } },
{ type: 'epic_id', value: { data: '34', operator: OPERATOR_IS_NOT } },
{ type: 'weight', value: { data: '1', operator: OPERATOR_IS } },
{ type: 'weight', value: { data: '3', operator: OPERATOR_IS_NOT } },
{ type: 'filtered-search-term', value: { data: 'find' } },
@ -138,30 +138,32 @@ export const filteredTokensWithSpecialValues = [
];
export const apiParams = {
author_username: 'homer',
'not[author_username]': 'marge',
assignee_username: ['bart', 'lisa'],
'not[assignee_username]': ['patty', 'selma'],
milestone: 'season 4',
'not[milestone]': 'season 20',
labels: ['cartoon', 'tv'],
'not[labels]': ['live action', 'drama'],
my_reaction_emoji: 'thumbsup',
authorUsername: 'homer',
assigneeUsernames: ['bart', 'lisa'],
milestoneTitle: 'season 4',
labelName: ['cartoon', 'tv'],
myReactionEmoji: 'thumbsup',
confidential: 'no',
iteration_title: 'season: #4',
'not[iteration_title]': 'season: #20',
epic_id: '12',
'not[epic_id]': 'gitlab-org::&34',
iterationId: '4',
epicId: '12',
weight: '1',
'not[weight]': '3',
not: {
authorUsername: 'marge',
assigneeUsernames: ['patty', 'selma'],
milestoneTitle: 'season 20',
labelName: ['live action', 'drama'],
iterationId: '20',
epicId: '34',
weight: '3',
},
};
export const apiParamsWithSpecialValues = {
assignee_id: '123',
assignee_username: 'bart',
my_reaction_emoji: 'None',
iteration_id: 'Current',
epic_id: 'None',
assigneeId: '123',
assigneeUsernames: 'bart',
myReactionEmoji: 'None',
iterationWildcardId: 'CURRENT',
epicId: 'None',
weight: 'None',
};
@ -176,10 +178,10 @@ export const urlParams = {
'not[label_name][]': ['live action', 'drama'],
my_reaction_emoji: 'thumbsup',
confidential: 'no',
iteration_title: 'season: #4',
'not[iteration_title]': 'season: #20',
epic_id: 'gitlab-org%3A%3A%2612',
'not[epic_id]': 'gitlab-org::&34',
iteration_id: '4',
'not[iteration_id]': '20',
epic_id: '12',
'not[epic_id]': '34',
weight: '1',
'not[weight]': '3',
};

View File

@ -8,10 +8,11 @@ import {
urlParams,
urlParamsWithSpecialValues,
} from 'jest/issues_list/mock_data';
import { API_PARAM, DUE_DATE_VALUES, URL_PARAM, urlSortParams } from '~/issues_list/constants';
import { DUE_DATE_VALUES, urlSortParams } from '~/issues_list/constants';
import {
convertToParams,
convertToApiParams,
convertToSearchQuery,
convertToUrlParams,
getDueDateValue,
getFilterTokens,
getSortKey,
@ -20,7 +21,7 @@ import {
describe('getSortKey', () => {
it.each(Object.keys(urlSortParams))('returns %s given the correct inputs', (sortKey) => {
const { sort } = urlSortParams[sortKey];
const sort = urlSortParams[sortKey];
expect(getSortKey(sort)).toBe(sortKey);
});
});
@ -80,31 +81,23 @@ describe('getFilterTokens', () => {
});
});
describe('convertToParams', () => {
describe('convertToApiParams', () => {
it('returns api params given filtered tokens', () => {
expect(convertToParams(filteredTokens, API_PARAM)).toEqual({
...apiParams,
epic_id: 'gitlab-org::&12',
});
expect(convertToApiParams(filteredTokens)).toEqual(apiParams);
});
it('returns api params given filtered tokens with special values', () => {
expect(convertToParams(filteredTokensWithSpecialValues, API_PARAM)).toEqual(
apiParamsWithSpecialValues,
);
expect(convertToApiParams(filteredTokensWithSpecialValues)).toEqual(apiParamsWithSpecialValues);
});
});
describe('convertToUrlParams', () => {
it('returns url params given filtered tokens', () => {
expect(convertToParams(filteredTokens, URL_PARAM)).toEqual({
...urlParams,
epic_id: 'gitlab-org::&12',
});
expect(convertToUrlParams(filteredTokens)).toEqual(urlParams);
});
it('returns url params given filtered tokens with special values', () => {
expect(convertToParams(filteredTokensWithSpecialValues, URL_PARAM)).toEqual(
urlParamsWithSpecialValues,
);
expect(convertToUrlParams(filteredTokensWithSpecialValues)).toEqual(urlParamsWithSpecialValues);
});
});

View File

@ -7,7 +7,7 @@ import { mockIterationToken } from '../mock_data';
jest.mock('~/flash');
describe('IterationToken', () => {
const title = 'gitlab-org: #1';
const id = 123;
let wrapper;
const createComponent = ({ config = mockIterationToken, value = { data: '' } } = {}) =>
@ -28,14 +28,14 @@ describe('IterationToken', () => {
});
it('renders iteration value', async () => {
wrapper = createComponent({ value: { data: title } });
wrapper = createComponent({ value: { data: id } });
await wrapper.vm.$nextTick();
const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment);
expect(tokenSegments).toHaveLength(3); // `Iteration` `=` `gitlab-org: #1`
expect(tokenSegments.at(2).text()).toBe(title);
expect(tokenSegments.at(2).text()).toBe(id.toString());
});
it('fetches initial values', () => {
@ -43,10 +43,10 @@ describe('IterationToken', () => {
wrapper = createComponent({
config: { ...mockIterationToken, fetchIterations: fetchIterationsSpy },
value: { data: title },
value: { data: id },
});
expect(fetchIterationsSpy).toHaveBeenCalledWith(title);
expect(fetchIterationsSpy).toHaveBeenCalledWith(id);
});
it('fetches iterations on user input', () => {

View File

@ -275,48 +275,4 @@ describe('User select dropdown', () => {
expect(findEmptySearchResults().exists()).toBe(true);
});
});
// TODO Remove this test after the following issue is resolved in the backend
// https://gitlab.com/gitlab-org/gitlab/-/issues/329750
describe('temporary error suppression', () => {
beforeEach(() => {
jest.spyOn(console, 'error').mockImplementation();
});
const nullError = { message: 'Cannot return null for non-nullable field GroupMember.user' };
it.each`
mockErrors
${[nullError]}
${[nullError, nullError]}
`('does not emit errors', async ({ mockErrors }) => {
createComponent({
searchQueryHandler: jest.fn().mockResolvedValue({
errors: mockErrors,
}),
});
await waitForSearch();
expect(wrapper.emitted()).toEqual({});
// eslint-disable-next-line no-console
expect(console.error).toHaveBeenCalled();
});
it.each`
mockErrors
${[{ message: 'serious error' }]}
${[nullError, { message: 'serious error' }]}
`('emits error when non-null related errors are included', async ({ mockErrors }) => {
createComponent({
searchQueryHandler: jest.fn().mockResolvedValue({
errors: mockErrors,
}),
});
await waitForSearch();
expect(wrapper.emitted('error')).toEqual([[]]);
// eslint-disable-next-line no-console
expect(console.error).not.toHaveBeenCalled();
});
});
});

View File

@ -50,6 +50,7 @@ RSpec.describe Mutations::Issues::Create do
stub_licensed_features(multiple_issue_assignees: false, issue_weights: false)
project.add_guest(assignee1)
project.add_guest(assignee2)
stub_spam_services
end
subject { mutation.resolve(**mutation_params) }

View File

@ -17,6 +17,10 @@ RSpec.describe Mutations::Issues::SetConfidential do
subject { mutation.resolve(project_path: project.full_path, iid: issue.iid, confidential: confidential) }
before do
stub_spam_services
end
it_behaves_like 'permission level for issue mutation is correctly verified'
context 'when the user can update the issue' do

View File

@ -35,6 +35,10 @@ RSpec.describe Mutations::Issues::Update do
subject { mutation.resolve(**mutation_params) }
before do
stub_spam_services
end
it_behaves_like 'permission level for issue mutation is correctly verified'
context 'when the user can update the issue' do

View File

@ -140,7 +140,9 @@ RSpec.describe Banzai::Filter::References::ExternalIssueReferenceFilter do
end
context "youtrack project" do
let_it_be(:service) { create(:youtrack_service, project: project) }
before_all do
create(:youtrack_integration, project: project)
end
before do
project.update!(issues_enabled: false)

View File

@ -50,6 +50,15 @@ RSpec.describe Gitlab::Email::Handler::ServiceDeskHandler do
it 'sends thank you email' do
expect { receiver.execute }.to have_enqueued_job.on_queue('mailers')
end
it 'adds metric events for incoming and reply emails' do
metric_transaction = double('Gitlab::Metrics::WebTransaction', increment: true, observe: true)
allow(::Gitlab::Metrics::BackgroundTransaction).to receive(:current).and_return(metric_transaction)
expect(metric_transaction).to receive(:add_event).with(:receive_email_service_desk, anything)
expect(metric_transaction).to receive(:add_event).with(:service_desk_thank_you_email)
receiver.execute
end
end
context 'when everything is fine' do

View File

@ -381,14 +381,14 @@ project:
- microsoft_teams_integration
- mattermost_integration
- hangouts_chat_integration
- unify_circuit_service
- unify_circuit_integration
- buildkite_integration
- bamboo_integration
- teamcity_integration
- pushover_integration
- jira_integration
- redmine_integration
- youtrack_service
- youtrack_integration
- custom_issue_tracker_integration
- bugzilla_integration
- ewm_integration
@ -557,7 +557,7 @@ project:
- alert_management_alerts
- repository_storage_moves
- freeze_periods
- webex_teams_service
- webex_teams_integration
- build_report_results
- vulnerability_statistic
- vulnerability_historical_statistics

View File

@ -115,16 +115,6 @@ RSpec.describe Emails::ServiceDesk do
end
end
shared_examples 'notification with metric event' do |event_type|
it 'adds metric event' do
metric_transaction = double('Gitlab::Metrics::WebTransaction', increment: true, observe: true)
allow(::Gitlab::Metrics::BackgroundTransaction).to receive(:current).and_return(metric_transaction)
expect(metric_transaction).to receive(:add_event).with(event_type)
subject.content_type
end
end
describe '.service_desk_thank_you_email' do
let_it_be(:reply_in_subject) { true }
let_it_be(:default_text) do
@ -134,7 +124,6 @@ RSpec.describe Emails::ServiceDesk do
subject { ServiceEmailClass.service_desk_thank_you_email(issue.id) }
it_behaves_like 'read template from repository', 'thank_you'
it_behaves_like 'notification with metric event', :service_desk_thank_you_email
context 'handling template markdown' do
context 'with a simple text' do
@ -175,7 +164,6 @@ RSpec.describe Emails::ServiceDesk do
subject { ServiceEmailClass.service_desk_new_note_email(issue.id, note.id, email) }
it_behaves_like 'read template from repository', 'new_note'
it_behaves_like 'notification with metric event', :service_desk_new_note_email
context 'handling template markdown' do
context 'with a simple text' do

View File

@ -26,11 +26,11 @@ RSpec.describe MigrateIssueTrackersData do
services.create!(type: 'BugzillaService', properties: properties, category: 'issue_tracker')
end
let!(:youtrack_service) do
let!(:youtrack_integration) do
services.create!(type: 'YoutrackService', properties: properties, category: 'issue_tracker')
end
let!(:youtrack_service_empty) do
let!(:youtrack_integration_empty) do
services.create!(type: 'YoutrackService', properties: '', category: 'issue_tracker')
end
@ -56,7 +56,7 @@ RSpec.describe MigrateIssueTrackersData do
migrate!
expect(migration_name).to be_scheduled_delayed_migration(3.minutes, jira_integration.id, bugzilla_integration.id)
expect(migration_name).to be_scheduled_delayed_migration(6.minutes, youtrack_service.id, gitlab_service.id)
expect(migration_name).to be_scheduled_delayed_migration(6.minutes, youtrack_integration.id, gitlab_service.id)
expect(BackgroundMigrationWorker.jobs.size).to eq(2)
end
end

View File

@ -26,11 +26,11 @@ RSpec.describe RescheduleMigrateIssueTrackersData do
services.create!(id: 12, type: 'BugzillaService', properties: properties, category: 'issue_tracker')
end
let!(:youtrack_service) do
let!(:youtrack_integration) do
services.create!(id: 13, type: 'YoutrackService', properties: properties, category: 'issue_tracker')
end
let!(:youtrack_service_empty) do
let!(:youtrack_integration_empty) do
services.create!(id: 14, type: 'YoutrackService', properties: '', category: 'issue_tracker')
end
@ -57,7 +57,7 @@ RSpec.describe RescheduleMigrateIssueTrackersData do
migrate!
expect(migration_name).to be_scheduled_delayed_migration(3.minutes, jira_integration.id, bugzilla_integration.id)
expect(migration_name).to be_scheduled_delayed_migration(6.minutes, youtrack_service.id, gitlab_service.id)
expect(migration_name).to be_scheduled_delayed_migration(6.minutes, youtrack_integration.id, gitlab_service.id)
expect(BackgroundMigrationWorker.jobs.size).to eq(2)
end
end

View File

@ -74,7 +74,7 @@ RSpec.describe Integrations::MicrosoftTeams do
context 'with issue events' do
let(:opts) { { title: 'Awesome issue', description: 'please fix' } }
let(:issues_sample_data) do
service = Issues::CreateService.new(project: project, current_user: user, params: opts)
service = Issues::CreateService.new(project: project, current_user: user, params: opts, spam_params: nil)
issue = service.execute
service.hook_data(issue, 'open')
end

View File

@ -39,8 +39,8 @@ RSpec.describe Project, factory_default: :keep do
it { is_expected.to have_one(:microsoft_teams_integration) }
it { is_expected.to have_one(:mattermost_integration) }
it { is_expected.to have_one(:hangouts_chat_integration) }
it { is_expected.to have_one(:unify_circuit_service) }
it { is_expected.to have_one(:webex_teams_service) }
it { is_expected.to have_one(:unify_circuit_integration) }
it { is_expected.to have_one(:webex_teams_integration) }
it { is_expected.to have_one(:packagist_integration) }
it { is_expected.to have_one(:pushover_integration) }
it { is_expected.to have_one(:asana_integration) }
@ -62,7 +62,7 @@ RSpec.describe Project, factory_default: :keep do
it { is_expected.to have_one(:teamcity_integration) }
it { is_expected.to have_one(:jira_integration) }
it { is_expected.to have_one(:redmine_integration) }
it { is_expected.to have_one(:youtrack_service) }
it { is_expected.to have_one(:youtrack_integration) }
it { is_expected.to have_one(:custom_issue_tracker_integration) }
it { is_expected.to have_one(:bugzilla_integration) }
it { is_expected.to have_one(:ewm_integration) }
@ -5870,26 +5870,6 @@ RSpec.describe Project, factory_default: :keep do
end
end
describe '#disabled_services' do
subject { build(:project).disabled_services }
context 'without datadog_ci_integration' do
before do
stub_feature_flags(datadog_ci_integration: false)
end
it { is_expected.to include('datadog') }
end
context 'with datadog_ci_integration' do
before do
stub_feature_flags(datadog_ci_integration: true)
end
it { is_expected.not_to include('datadog') }
end
end
describe '#find_or_initialize_service' do
it 'avoids N+1 database queries' do
allow(Integration).to receive(:available_services_names).and_return(%w[prometheus pushover])

View File

@ -17,7 +17,6 @@ RSpec.describe 'Creating a Snippet' do
let(:actions) { [{ action: action }.merge(file_1), { action: action }.merge(file_2)] }
let(:project_path) { nil }
let(:uploaded_files) { nil }
let(:spam_mutation_vars) { {} }
let(:mutation_vars) do
{
description: description,
@ -26,7 +25,7 @@ RSpec.describe 'Creating a Snippet' do
project_path: project_path,
uploaded_files: uploaded_files,
blob_actions: actions
}.merge(spam_mutation_vars)
}
end
let(:mutation) do
@ -77,21 +76,6 @@ RSpec.describe 'Creating a Snippet' do
expect(mutation_response['snippet']).to be_nil
end
context 'when snippet_spam flag is disabled' do
before do
stub_feature_flags(snippet_spam: false)
end
it 'passes disable_spam_action_service param to service' do
expect(::Snippets::CreateService)
.to receive(:new)
.with(project: anything, current_user: anything, params: hash_including(disable_spam_action_service: true))
.and_call_original
subject
end
end
end
shared_examples 'creates snippet' do
@ -121,15 +105,6 @@ RSpec.describe 'Creating a Snippet' do
it_behaves_like 'snippet edit usage data counters'
it_behaves_like 'a mutation which can mutate a spammable' do
let(:captcha_response) { 'abc123' }
let(:spam_log_id) { 1234 }
let(:spam_mutation_vars) do
{
captcha_response: captcha_response,
spam_log_id: spam_log_id
}
end
let(:service) { Snippets::CreateService }
end
end
@ -190,7 +165,7 @@ RSpec.describe 'Creating a Snippet' do
it do
expect(::Snippets::CreateService).to receive(:new)
.with(project: nil, current_user: user, params: hash_including(files: expected_value))
.with(project: nil, current_user: user, params: hash_including(files: expected_value), spam_params: instance_of(::Spam::SpamParams))
.and_return(double(execute: creation_response))
subject

View File

@ -16,7 +16,6 @@ RSpec.describe 'Updating a Snippet' do
let(:updated_file) { 'CHANGELOG' }
let(:deleted_file) { 'README' }
let(:snippet_gid) { GitlabSchema.id_from_object(snippet).to_s }
let(:spam_mutation_vars) { {} }
let(:mutation_vars) do
{
id: snippet_gid,
@ -27,7 +26,7 @@ RSpec.describe 'Updating a Snippet' do
{ action: :update, filePath: updated_file, content: updated_content },
{ action: :delete, filePath: deleted_file }
]
}.merge(spam_mutation_vars)
}
end
let(:mutation) do
@ -82,21 +81,6 @@ RSpec.describe 'Updating a Snippet' do
end
end
context 'when snippet_spam flag is disabled' do
before do
stub_feature_flags(snippet_spam: false)
end
it 'passes disable_spam_action_service param to service' do
expect(::Snippets::UpdateService)
.to receive(:new)
.with(project: anything, current_user: anything, params: hash_including(disable_spam_action_service: true))
.and_call_original
subject
end
end
context 'when there are ActiveRecord validation errors' do
let(:updated_title) { '' }
@ -125,15 +109,6 @@ RSpec.describe 'Updating a Snippet' do
end
it_behaves_like 'a mutation which can mutate a spammable' do
let(:captcha_response) { 'abc123' }
let(:spam_log_id) { 1234 }
let(:spam_mutation_vars) do
{
captcha_response: captcha_response,
spam_log_id: spam_log_id
}
end
let(:service) { Snippets::UpdateService }
end

View File

@ -770,13 +770,23 @@ RSpec.describe 'project routing' do
end
end
describe Projects::UsagePingController, 'routing' do
it 'routes to usage_ping#web_ide_clientside_preview' do
expect(post('/gitlab/gitlabhq/usage_ping/web_ide_clientside_preview')).to route_to('projects/usage_ping#web_ide_clientside_preview', namespace_id: 'gitlab', project_id: 'gitlabhq')
describe Projects::ServicePingController, 'routing' do
describe 'deprecated routing' do
it 'routes to service_ping#web_ide_clientside_preview' do
expect(post('/gitlab/gitlabhq/usage_ping/web_ide_clientside_preview')).to route_to('projects/service_ping#web_ide_clientside_preview', namespace_id: 'gitlab', project_id: 'gitlabhq')
end
it 'routes to service_ping#web_ide_pipelines_count' do
expect(post('/gitlab/gitlabhq/usage_ping/web_ide_pipelines_count')).to route_to('projects/service_ping#web_ide_pipelines_count', namespace_id: 'gitlab', project_id: 'gitlabhq')
end
end
it 'routes to usage_ping#web_ide_pipelines_count' do
expect(post('/gitlab/gitlabhq/usage_ping/web_ide_pipelines_count')).to route_to('projects/usage_ping#web_ide_pipelines_count', namespace_id: 'gitlab', project_id: 'gitlabhq')
it 'routes to service_ping#web_ide_clientside_preview' do
expect(post('/gitlab/gitlabhq/service_ping/web_ide_clientside_preview')).to route_to('projects/service_ping#web_ide_clientside_preview', namespace_id: 'gitlab', project_id: 'gitlabhq')
end
it 'routes to service_ping#web_ide_pipelines_count' do
expect(post('/gitlab/gitlabhq/service_ping/web_ide_pipelines_count')).to route_to('projects/service_ping#web_ide_pipelines_count', namespace_id: 'gitlab', project_id: 'gitlabhq')
end
end

View File

@ -4,21 +4,31 @@ require 'spec_helper'
RSpec.describe Captcha::CaptchaVerificationService do
describe '#execute' do
let(:captcha_response) { nil }
let(:request) { double(:request) }
let(:service) { described_class.new }
let(:captcha_response) { 'abc123' }
let(:fake_ip) { '1.2.3.4' }
let(:spam_params) do
::Spam::SpamParams.new(
captcha_response: captcha_response,
spam_log_id: double,
ip_address: fake_ip,
user_agent: double,
referer: double
)
end
subject { service.execute(captcha_response: captcha_response, request: request) }
let(:service) { described_class.new(spam_params: spam_params) }
subject { service.execute }
context 'when there is no captcha_response' do
let(:captcha_response) { nil }
it 'returns false' do
expect(subject).to eq(false)
end
end
context 'when there is a captcha_response' do
let(:captcha_response) { 'abc123' }
before do
expect(Gitlab::Recaptcha).to receive(:load_configurations!)
end
@ -29,10 +39,12 @@ RSpec.describe Captcha::CaptchaVerificationService do
expect(subject).to eq(true)
end
it 'has a request method which returns the request' do
it 'has a request method which returns an object with the ip address #remote_ip' do
subject
expect(service.send(:request)).to eq(request)
request_struct = service.send(:request)
expect(request_struct).to respond_to(:remote_ip)
expect(request_struct.remote_ip).to eq(fake_ip)
end
end
end

View File

@ -8,11 +8,17 @@ RSpec.describe Issues::CreateService do
let_it_be_with_reload(:project) { create(:project) }
let_it_be(:user) { create(:user) }
let(:spam_params) { double }
describe '#execute' do
let_it_be(:assignee) { create(:user) }
let_it_be(:milestone) { create(:milestone, project: project) }
let(:issue) { described_class.new(project: project, current_user: user, params: opts).execute }
let(:issue) { described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute }
before do
stub_spam_services
end
context 'when params are valid' do
let_it_be(:labels) { create_pair(:label, project: project) }
@ -44,7 +50,7 @@ RSpec.describe Issues::CreateService do
end
context 'when skip_system_notes is true' do
let(:issue) { described_class.new(project: project, current_user: user, params: opts).execute(skip_system_notes: true) }
let(:issue) { described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute(skip_system_notes: true) }
it 'does not call Issuable::CommonSystemNotesService' do
expect(Issuable::CommonSystemNotesService).not_to receive(:new)
@ -92,7 +98,7 @@ RSpec.describe Issues::CreateService do
let_it_be(:non_member) { create(:user) }
it 'filters out params that cannot be set without the :set_issue_metadata permission' do
issue = described_class.new(project: project, current_user: non_member, params: opts).execute
issue = described_class.new(project: project, current_user: non_member, params: opts, spam_params: spam_params).execute
expect(issue).to be_persisted
expect(issue.title).to eq('Awesome issue')
@ -104,7 +110,7 @@ RSpec.describe Issues::CreateService do
end
it 'can create confidential issues' do
issue = described_class.new(project: project, current_user: non_member, params: { confidential: true }).execute
issue = described_class.new(project: project, current_user: non_member, params: { confidential: true }, spam_params: spam_params).execute
expect(issue.confidential).to be_truthy
end
@ -113,7 +119,7 @@ RSpec.describe Issues::CreateService do
it 'moves the issue to the end, in an asynchronous worker' do
expect(IssuePlacementWorker).to receive(:perform_async).with(be_nil, Integer)
described_class.new(project: project, current_user: user, params: opts).execute
described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute
end
context 'when label belongs to project group' do
@ -200,7 +206,7 @@ RSpec.describe Issues::CreateService do
it 'invalidates open issues counter for assignees when issue is assigned' do
project.add_maintainer(assignee)
described_class.new(project: project, current_user: user, params: opts).execute
described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute
expect(assignee.assigned_open_issues_count).to eq 1
end
@ -226,7 +232,7 @@ RSpec.describe Issues::CreateService do
expect(project).to receive(:execute_hooks).with(an_instance_of(Hash), :issue_hooks)
expect(project).to receive(:execute_services).with(an_instance_of(Hash), :issue_hooks)
described_class.new(project: project, current_user: user, params: opts).execute
described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute
end
it 'executes confidential issue hooks when issue is confidential' do
@ -235,7 +241,7 @@ RSpec.describe Issues::CreateService do
expect(project).to receive(:execute_hooks).with(an_instance_of(Hash), :confidential_issue_hooks)
expect(project).to receive(:execute_services).with(an_instance_of(Hash), :confidential_issue_hooks)
described_class.new(project: project, current_user: user, params: opts).execute
described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute
end
context 'after_save callback to store_mentions' do
@ -279,7 +285,7 @@ RSpec.describe Issues::CreateService do
it 'removes assignee when user id is invalid' do
opts = { title: 'Title', description: 'Description', assignee_ids: [-1] }
issue = described_class.new(project: project, current_user: user, params: opts).execute
issue = described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute
expect(issue.assignees).to be_empty
end
@ -287,7 +293,7 @@ RSpec.describe Issues::CreateService do
it 'removes assignee when user id is 0' do
opts = { title: 'Title', description: 'Description', assignee_ids: [0] }
issue = described_class.new(project: project, current_user: user, params: opts).execute
issue = described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute
expect(issue.assignees).to be_empty
end
@ -296,7 +302,7 @@ RSpec.describe Issues::CreateService do
project.add_maintainer(assignee)
opts = { title: 'Title', description: 'Description', assignee_ids: [assignee.id] }
issue = described_class.new(project: project, current_user: user, params: opts).execute
issue = described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute
expect(issue.assignees).to eq([assignee])
end
@ -314,7 +320,7 @@ RSpec.describe Issues::CreateService do
project.update!(visibility_level: level)
opts = { title: 'Title', description: 'Description', assignee_ids: [assignee.id] }
issue = described_class.new(project: project, current_user: user, params: opts).execute
issue = described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute
expect(issue.assignees).to be_empty
end
@ -324,7 +330,7 @@ RSpec.describe Issues::CreateService do
end
it_behaves_like 'issuable record that supports quick actions' do
let(:issuable) { described_class.new(project: project, current_user: user, params: params).execute }
let(:issuable) { described_class.new(project: project, current_user: user, params: params, spam_params: spam_params).execute }
end
context 'Quick actions' do
@ -364,14 +370,14 @@ RSpec.describe Issues::CreateService do
let(:opts) { { discussion_to_resolve: discussion.id, merge_request_to_resolve_discussions_of: merge_request.iid } }
it 'resolves the discussion' do
described_class.new(project: project, current_user: user, params: opts).execute
described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute
discussion.first_note.reload
expect(discussion.resolved?).to be(true)
end
it 'added a system note to the discussion' do
described_class.new(project: project, current_user: user, params: opts).execute
described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute
reloaded_discussion = MergeRequest.find(merge_request.id).discussions.first
@ -379,7 +385,7 @@ RSpec.describe Issues::CreateService do
end
it 'assigns the title and description for the issue' do
issue = described_class.new(project: project, current_user: user, params: opts).execute
issue = described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute
expect(issue.title).not_to be_nil
expect(issue.description).not_to be_nil
@ -391,7 +397,8 @@ RSpec.describe Issues::CreateService do
merge_request_to_resolve_discussions_of: merge_request,
description: nil,
title: nil
}).execute
},
spam_params: spam_params).execute
expect(issue.description).to be_nil
expect(issue.title).to be_nil
@ -402,14 +409,14 @@ RSpec.describe Issues::CreateService do
let(:opts) { { merge_request_to_resolve_discussions_of: merge_request.iid } }
it 'resolves the discussion' do
described_class.new(project: project, current_user: user, params: opts).execute
described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute
discussion.first_note.reload
expect(discussion.resolved?).to be(true)
end
it 'added a system note to the discussion' do
described_class.new(project: project, current_user: user, params: opts).execute
described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute
reloaded_discussion = MergeRequest.find(merge_request.id).discussions.first
@ -417,7 +424,7 @@ RSpec.describe Issues::CreateService do
end
it 'assigns the title and description for the issue' do
issue = described_class.new(project: project, current_user: user, params: opts).execute
issue = described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute
expect(issue.title).not_to be_nil
expect(issue.description).not_to be_nil
@ -429,7 +436,8 @@ RSpec.describe Issues::CreateService do
merge_request_to_resolve_discussions_of: merge_request,
description: nil,
title: nil
}).execute
},
spam_params: spam_params).execute
expect(issue.description).to be_nil
expect(issue.title).to be_nil
@ -438,47 +446,27 @@ RSpec.describe Issues::CreateService do
end
context 'checking spam' do
let(:request) { double(:request, headers: nil) }
let(:api) { true }
let(:captcha_response) { 'abc123' }
let(:spam_log_id) { 1 }
let(:params) do
{
title: 'Spam issue',
request: request,
api: api,
captcha_response: captcha_response,
spam_log_id: spam_log_id
title: 'Spam issue'
}
end
subject do
described_class.new(project: project, current_user: user, params: params)
end
before do
allow_next_instance_of(UserAgentDetailService) do |instance|
allow(instance).to receive(:create)
end
described_class.new(project: project, current_user: user, params: params, spam_params: spam_params)
end
it 'executes SpamActionService' do
spam_params = Spam::SpamParams.new(
api: api,
captcha_response: captcha_response,
spam_log_id: spam_log_id
)
expect_next_instance_of(
Spam::SpamActionService,
{
spammable: an_instance_of(Issue),
request: request,
user: user,
spammable: kind_of(Issue),
spam_params: spam_params,
user: an_instance_of(User),
action: :create
}
) do |instance|
expect(instance).to receive(:execute).with(spam_params: spam_params)
expect(instance).to receive(:execute)
end
subject.execute

View File

@ -376,42 +376,32 @@ RSpec.describe NotificationService, :mailer do
let(:subject) { NotificationService.new }
let(:mailer) { double(deliver_later: true) }
def should_email!
expect(Notify).to receive(:service_desk_new_note_email)
.with(issue.id, note.id, issue.external_author)
end
def should_not_email!
expect(Notify).not_to receive(:service_desk_new_note_email)
end
def execute!
subject.new_note(note)
end
def self.it_should_email!
it 'sends the email' do
should_email!
execute!
end
end
def self.it_should_not_email!
it 'doesn\'t send the email' do
should_not_email!
execute!
end
end
let(:issue) { create(:issue, author: User.support_bot) }
let(:project) { issue.project }
let(:note) { create(:note, noteable: issue, project: project) }
context 'do not exist' do
it_should_not_email!
shared_examples 'notification with exact metric events' do |number_of_events|
it 'adds metric event' do
metric_transaction = double('Gitlab::Metrics::WebTransaction', increment: true, observe: true)
allow(::Gitlab::Metrics::BackgroundTransaction).to receive(:current).and_return(metric_transaction)
expect(metric_transaction).to receive(:add_event).with(:service_desk_new_note_email).exactly(number_of_events).times
subject.new_note(note)
end
end
shared_examples 'no participants are notified' do
it 'does not send the email' do
expect(Notify).not_to receive(:service_desk_new_note_email)
subject.new_note(note)
end
it_behaves_like 'notification with exact metric events', 0
end
it_behaves_like 'no participants are notified'
context 'do exist and note not confidential' do
let!(:issue_email_participant) { issue.issue_email_participants.create!(email: 'service.desk@example.com') }
@ -420,7 +410,14 @@ RSpec.describe NotificationService, :mailer do
project.update!(service_desk_enabled: true)
end
it_should_email!
it 'sends the email' do
expect(Notify).to receive(:service_desk_new_note_email)
.with(issue.id, note.id, issue.external_author)
subject.new_note(note)
end
it_behaves_like 'notification with exact metric events', 1
end
context 'do exist and note is confidential' do
@ -432,7 +429,7 @@ RSpec.describe NotificationService, :mailer do
project.update!(service_desk_enabled: true)
end
it_should_not_email!
it_behaves_like 'no participants are notified'
end
end

View File

@ -19,8 +19,9 @@ RSpec.describe Snippets::CreateService do
let(:extra_opts) { {} }
let(:creator) { admin }
let(:spam_params) { double }
subject { described_class.new(project: project, current_user: creator, params: opts).execute }
subject { described_class.new(project: project, current_user: creator, params: opts, spam_params: spam_params).execute }
let(:snippet) { subject.payload[:snippet] }
@ -301,6 +302,10 @@ RSpec.describe Snippets::CreateService do
end
end
before do
stub_spam_services
end
context 'when ProjectSnippet' do
let_it_be(:project) { create(:project) }

View File

@ -20,7 +20,9 @@ RSpec.describe Snippets::UpdateService do
let(:extra_opts) { {} }
let(:options) { base_opts.merge(extra_opts) }
let(:updater) { user }
let(:service) { Snippets::UpdateService.new(project: project, current_user: updater, params: options) }
let(:spam_params) { double }
let(:service) { Snippets::UpdateService.new(project: project, current_user: updater, params: options, spam_params: spam_params) }
subject { service.execute(snippet) }
@ -721,6 +723,10 @@ RSpec.describe Snippets::UpdateService do
end
end
before do
stub_spam_services
end
context 'when Project Snippet' do
let_it_be(:project) { create(:project) }
let!(:snippet) { create(:project_snippet, :repository, author: user, project: project) }

View File

@ -59,7 +59,7 @@ RSpec.describe Spam::AkismetService do
it_behaves_like 'no activity if Akismet is not enabled', :spam?, :check
context 'if Akismet is enabled' do
it 'correctly transforms options for the akismet client' do
it 'correctly transforms options for the akismet client, including spelling of referrer key' do
expected_check_params = {
type: 'comment',
text: text,

View File

@ -5,15 +5,20 @@ require 'spec_helper'
RSpec.describe Spam::SpamActionService do
include_context 'includes Spam constants'
let(:request) { double(:request, env: env, headers: {}) }
let(:issue) { create(:issue, project: project, author: user) }
let(:fake_ip) { '1.2.3.4' }
let(:fake_user_agent) { 'fake-user-agent' }
let(:fake_referer) { 'fake-http-referer' }
let(:env) do
{ 'action_dispatch.remote_ip' => fake_ip,
'HTTP_USER_AGENT' => fake_user_agent,
'HTTP_REFERER' => fake_referer }
let(:captcha_response) { 'abc123' }
let(:spam_log_id) { existing_spam_log.id }
let(:spam_params) do
::Spam::SpamParams.new(
captcha_response: captcha_response,
spam_log_id: spam_log_id,
ip_address: fake_ip,
user_agent: fake_user_agent,
referer: fake_referer
)
end
let_it_be(:project) { create(:project, :public) }
@ -23,32 +28,33 @@ RSpec.describe Spam::SpamActionService do
issue.spam = false
end
shared_examples 'only checks for spam if a request is provided' do
context 'when request is missing' do
let(:request) { nil }
it "doesn't check as spam" do
expect(fake_verdict_service).not_to receive(:execute)
response = subject
expect(response.message).to match(/request was not present/)
expect(issue).not_to be_spam
end
describe 'constructor argument validation' do
subject do
described_service = described_class.new(spammable: issue, spam_params: spam_params, user: user, action: :create)
described_service.execute
end
context 'when request exists' do
it 'creates a spam log' do
expect { subject }
.to log_spam(title: issue.title, description: issue.description, noteable_type: 'Issue')
context 'when spam_params is nil' do
let(:spam_params) { nil }
let(:expected_service_params_not_present_message) do
/Skipped spam check because spam_params was not present/
end
it "returns success with a messaage" do
response = subject
expect(response.message).to match(expected_service_params_not_present_message)
expect(issue).not_to be_spam
end
end
end
shared_examples 'creates a spam log' do
it do
expect { subject }.to change(SpamLog, :count).by(1)
expect { subject }
.to log_spam(title: issue.title, description: issue.description, noteable_type: 'Issue')
# TODO: These checks should be incorporated into the `log_spam` RSpec matcher above
new_spam_log = SpamLog.last
expect(new_spam_log.user_id).to eq(user.id)
expect(new_spam_log.title).to eq(issue.title)
@ -56,25 +62,14 @@ RSpec.describe Spam::SpamActionService do
expect(new_spam_log.source_ip).to eq(fake_ip)
expect(new_spam_log.user_agent).to eq(fake_user_agent)
expect(new_spam_log.noteable_type).to eq('Issue')
expect(new_spam_log.via_api).to eq(false)
expect(new_spam_log.via_api).to eq(true)
end
end
describe '#execute' do
let(:request) { double(:request, env: env, headers: nil) }
let(:fake_captcha_verification_service) { double(:captcha_verification_service) }
let(:fake_verdict_service) { double(:spam_verdict_service) }
let(:allowlisted) { false }
let(:api) { nil }
let(:captcha_response) { 'abc123' }
let(:spam_log_id) { existing_spam_log.id }
let(:spam_params) do
::Spam::SpamParams.new(
api: api,
captcha_response: captcha_response,
spam_log_id: spam_log_id
)
end
let(:verdict_service_opts) do
{
@ -88,7 +83,6 @@ RSpec.describe Spam::SpamActionService do
{
target: issue,
user: user,
request: request,
options: verdict_service_opts,
context: {
action: :create,
@ -100,40 +94,20 @@ RSpec.describe Spam::SpamActionService do
let_it_be(:existing_spam_log) { create(:spam_log, user: user, recaptcha_verified: false) }
subject do
described_service = described_class.new(spammable: issue, request: request, user: user, action: :create)
described_service = described_class.new(spammable: issue, spam_params: spam_params, user: user, action: :create)
allow(described_service).to receive(:allowlisted?).and_return(allowlisted)
described_service.execute(spam_params: spam_params)
described_service.execute
end
before do
allow(Captcha::CaptchaVerificationService).to receive(:new) { fake_captcha_verification_service }
allow(Captcha::CaptchaVerificationService).to receive(:new).with(spam_params: spam_params) { fake_captcha_verification_service }
allow(Spam::SpamVerdictService).to receive(:new).with(verdict_service_args).and_return(fake_verdict_service)
end
context 'when the captcha params are passed in the headers' do
let(:request) { double(:request, env: env, headers: headers) }
let(:spam_params) { Spam::SpamActionService.filter_spam_params!({ api: api }, request) }
let(:headers) do
{
'X-GitLab-Captcha-Response' => captcha_response,
'X-GitLab-Spam-Log-Id' => spam_log_id
}
end
it 'extracts the headers correctly' do
expect(fake_captcha_verification_service)
.to receive(:execute).with(captcha_response: captcha_response, request: request).and_return(true)
expect(SpamLog)
.to receive(:verify_recaptcha!).with(user_id: user.id, id: spam_log_id)
subject
end
end
context 'when captcha response verification returns true' do
before do
allow(fake_captcha_verification_service)
.to receive(:execute).with(captcha_response: captcha_response, request: request).and_return(true)
.to receive(:execute).and_return(true)
end
it "doesn't check with the SpamVerdictService" do
@ -156,7 +130,7 @@ RSpec.describe Spam::SpamActionService do
context 'when captcha response verification returns false' do
before do
allow(fake_captcha_verification_service)
.to receive(:execute).with(captcha_response: captcha_response, request: request).and_return(false)
.to receive(:execute).and_return(false)
end
context 'when spammable attributes have not changed' do
@ -200,8 +174,6 @@ RSpec.describe Spam::SpamActionService do
stub_feature_flags(allow_possible_spam: false)
end
it_behaves_like 'only checks for spam if a request is provided'
it 'marks as spam' do
response = subject
@ -211,8 +183,6 @@ RSpec.describe Spam::SpamActionService do
end
context 'when allow_possible_spam feature flag is true' do
it_behaves_like 'only checks for spam if a request is provided'
it 'does not mark as spam' do
response = subject
@ -232,8 +202,6 @@ RSpec.describe Spam::SpamActionService do
stub_feature_flags(allow_possible_spam: false)
end
it_behaves_like 'only checks for spam if a request is provided'
it 'marks as spam' do
response = subject
@ -243,8 +211,6 @@ RSpec.describe Spam::SpamActionService do
end
context 'when allow_possible_spam feature flag is true' do
it_behaves_like 'only checks for spam if a request is provided'
it 'does not mark as spam' do
response = subject
@ -264,8 +230,6 @@ RSpec.describe Spam::SpamActionService do
stub_feature_flags(allow_possible_spam: false)
end
it_behaves_like 'only checks for spam if a request is provided'
it_behaves_like 'creates a spam log'
it 'does not mark as spam' do
@ -284,8 +248,6 @@ RSpec.describe Spam::SpamActionService do
end
context 'when allow_possible_spam feature flag is true' do
it_behaves_like 'only checks for spam if a request is provided'
it_behaves_like 'creates a spam log'
it 'does not mark as needing reCAPTCHA' do
@ -334,37 +296,10 @@ RSpec.describe Spam::SpamActionService do
allow(fake_verdict_service).to receive(:execute).and_return(ALLOW)
end
context 'when the request is nil' do
let(:request) { nil }
let(:issue_ip_address) { '1.2.3.4' }
let(:issue_user_agent) { 'lynx' }
let(:verdict_service_opts) do
{
ip_address: issue_ip_address,
user_agent: issue_user_agent
}
end
it 'assembles the options with information from the request' do
expect(Spam::SpamVerdictService).to receive(:new).with(verdict_service_args)
before do
allow(issue).to receive(:ip_address) { issue_ip_address }
allow(issue).to receive(:user_agent) { issue_user_agent }
end
it 'assembles the options with information from the spammable' do
# TODO: This code untestable, because we do not perform a verification if there is not a
# request. See corresponding comment in code
# expect(Spam::SpamVerdictService).to receive(:new).with(verdict_service_args)
subject
end
end
context 'when the request is present' do
it 'assembles the options with information from the request' do
expect(Spam::SpamVerdictService).to receive(:new).with(verdict_service_args)
subject
end
subject
end
end
end

View File

@ -0,0 +1,40 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Spam::SpamParams do
describe '.new_from_request' do
let(:captcha_response) { 'abc123' }
let(:spam_log_id) { 42 }
let(:ip_address) { '0.0.0.0' }
let(:user_agent) { 'Lynx' }
let(:referer) { 'http://localhost' }
let(:headers) do
{
'X-GitLab-Captcha-Response' => captcha_response,
'X-GitLab-Spam-Log-Id' => spam_log_id
}
end
let(:env) do
{
'action_dispatch.remote_ip' => ip_address,
'HTTP_USER_AGENT' => user_agent,
'HTTP_REFERER' => referer
}
end
let(:request) {double(:request, headers: headers, env: env)}
it 'constructs from a request' do
expected = ::Spam::SpamParams.new(
captcha_response: captcha_response,
spam_log_id: spam_log_id,
ip_address: ip_address,
user_agent: user_agent,
referer: referer
)
expect(described_class.new_from_request(request: request)).to eq(expected)
end
end
end

View File

@ -14,13 +14,11 @@ RSpec.describe Spam::SpamVerdictService do
'HTTP_REFERER' => fake_referer }
end
let(:request) { double(:request, env: env) }
let(:check_for_spam) { true }
let_it_be(:user) { create(:user) }
let_it_be(:issue) { create(:issue, author: user) }
let(:service) do
described_class.new(user: user, target: issue, request: request, options: {})
described_class.new(user: user, target: issue, options: {})
end
let(:attribs) do

View File

@ -190,6 +190,7 @@ RSpec.configure do |config|
config.include RailsHelpers
config.include SidekiqMiddleware
config.include StubActionCableConnection, type: :channel
config.include StubSpamServices
include StubFeatureFlags

View File

@ -0,0 +1,23 @@
# frozen_string_literal: true
module StubSpamServices
def stub_spam_services
allow(::Spam::SpamParams).to receive(:new_from_request) do
::Spam::SpamParams.new(
captcha_response: double(:captcha_response),
spam_log_id: double(:spam_log_id),
ip_address: double(:ip_address),
user_agent: double(:user_agent),
referer: double(:referer)
)
end
allow_next_instance_of(::Spam::SpamActionService) do |service|
allow(service).to receive(:execute)
end
allow_next_instance_of(::UserAgentDetailService) do |service|
allow(service).to receive(:create)
end
end
end

View File

@ -3,17 +3,13 @@
require 'spec_helper'
RSpec.shared_examples 'a mutation which can mutate a spammable' do
describe "#additional_spam_params" do
it 'passes additional spam params to the service' do
describe "#spam_params" do
it 'passes spam params to the service constructor' do
args = [
project: anything,
current_user: anything,
params: hash_including(
api: true,
request: instance_of(ActionDispatch::Request),
captcha_response: captcha_response,
spam_log_id: spam_log_id
)
params: anything,
spam_params: instance_of(::Spam::SpamParams)
]
expect(service).to receive(:new).with(*args).and_call_original

View File

@ -57,7 +57,7 @@ RSpec.shared_examples 'has spam protection' do
context 'and no CAPTCHA is required' do
let(:render_captcha) { false }
it 'does not return a to-level error' do
it 'does not return a top-level error' do
send_request
expect(graphql_errors).to be_blank

View File

@ -163,7 +163,7 @@ RSpec.shared_examples "chat integration" do |integration_name|
context "with issue events" do
let(:opts) { { title: "Awesome issue", description: "please fix" } }
let(:sample_data) do
service = Issues::CreateService.new(project: project, current_user: user, params: opts)
service = Issues::CreateService.new(project: project, current_user: user, params: opts, spam_params: nil)
issue = service.execute
service.hook_data(issue, "open")
end

View File

@ -1,23 +1,6 @@
# frozen_string_literal: true
RSpec.shared_examples 'checking spam' do
let(:request) { double(:request, headers: headers) }
let(:headers) { nil }
let(:api) { true }
let(:captcha_response) { 'abc123' }
let(:spam_log_id) { 1 }
let(:disable_spam_action_service) { false }
let(:extra_opts) do
{
request: request,
api: api,
captcha_response: captcha_response,
spam_log_id: spam_log_id,
disable_spam_action_service: disable_spam_action_service
}
end
before do
allow_next_instance_of(UserAgentDetailService) do |instance|
allow(instance).to receive(:create)
@ -25,67 +8,26 @@ RSpec.shared_examples 'checking spam' do
end
it 'executes SpamActionService' do
spam_params = Spam::SpamParams.new(
api: api,
captcha_response: captcha_response,
spam_log_id: spam_log_id
)
expect_next_instance_of(
Spam::SpamActionService,
{
spammable: kind_of(Snippet),
request: request,
spam_params: spam_params,
user: an_instance_of(User),
action: action
}
) do |instance|
expect(instance).to receive(:execute).with(spam_params: spam_params)
expect(instance).to receive(:execute)
end
subject
end
context 'when CAPTCHA arguments are passed in the headers' do
let(:headers) do
{
'X-GitLab-Spam-Log-Id' => spam_log_id,
'X-GitLab-Captcha-Response' => captcha_response
}
context 'when snippet_spam flag is disabled' do
before do
stub_feature_flags(snippet_spam: false)
end
let(:extra_opts) do
{
request: request,
api: api,
disable_spam_action_service: disable_spam_action_service
}
end
it 'executes the SpamActionService correctly' do
spam_params = Spam::SpamParams.new(
api: api,
captcha_response: captcha_response,
spam_log_id: spam_log_id
)
expect_next_instance_of(
Spam::SpamActionService,
{
spammable: kind_of(Snippet),
request: request,
user: an_instance_of(User),
action: action
}
) do |instance|
expect(instance).to receive(:execute).with(spam_params: spam_params)
end
subject
end
end
context 'when spam action service is disabled' do
let(:disable_spam_action_service) { true }
it 'request parameter is not passed to the service' do
expect(Spam::SpamActionService).not_to receive(:new)