Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
a3ac132686
commit
01ef10900a
|
@ -189,7 +189,7 @@
|
||||||
- "config.ru"
|
- "config.ru"
|
||||||
# List explicitly all the app/ dirs that are backend (i.e. all except app/assets).
|
# List explicitly all the app/ dirs that are backend (i.e. all except app/assets).
|
||||||
- "{,ee/,jh/}{app/channels,app/controllers,app/finders,app/graphql,app/helpers,app/mailers,app/models,app/policies,app/presenters,app/serializers,app/services,app/uploaders,app/validators,app/views,app/workers}/**/*"
|
- "{,ee/,jh/}{app/channels,app/controllers,app/finders,app/graphql,app/helpers,app/mailers,app/models,app/policies,app/presenters,app/serializers,app/services,app/uploaders,app/validators,app/views,app/workers}/**/*"
|
||||||
- "{,ee/,jh/}{bin,cable,config,db,lib}/**/*"
|
- "{,ee/,jh/}{bin,cable,config,db,generator_templates,lib}/**/*"
|
||||||
- "{,ee/,jh/}spec/**/*.rb"
|
- "{,ee/,jh/}spec/**/*.rb"
|
||||||
# CI changes
|
# CI changes
|
||||||
- ".gitlab-ci.yml"
|
- ".gitlab-ci.yml"
|
||||||
|
@ -239,7 +239,7 @@
|
||||||
- "Rakefile"
|
- "Rakefile"
|
||||||
- "tests.yml"
|
- "tests.yml"
|
||||||
- "config.ru"
|
- "config.ru"
|
||||||
- "{,ee/,jh/}{app,bin,config,db,haml_lint,lib,locale,public,scripts,symbol,vendor}/**/*"
|
- "{,ee/,jh/}{app,bin,config,db,generator_templates,haml_lint,lib,locale,public,scripts,symbol,vendor}/**/*"
|
||||||
- "doc/api/graphql/reference/*" # Files in this folder are auto-generated
|
- "doc/api/graphql/reference/*" # Files in this folder are auto-generated
|
||||||
- "data/whats_new/*.yml"
|
- "data/whats_new/*.yml"
|
||||||
|
|
||||||
|
@ -264,7 +264,7 @@
|
||||||
- "Rakefile"
|
- "Rakefile"
|
||||||
- "tests.yml"
|
- "tests.yml"
|
||||||
- "config.ru"
|
- "config.ru"
|
||||||
- "{,ee/,jh/}{app,bin,config,db,haml_lint,lib,locale,public,scripts,symbol,vendor}/**/*"
|
- "{,ee/,jh/}{app,bin,config,db,generator_templates,haml_lint,lib,locale,public,scripts,symbol,vendor}/**/*"
|
||||||
- "doc/api/graphql/reference/*" # Files in this folder are auto-generated
|
- "doc/api/graphql/reference/*" # Files in this folder are auto-generated
|
||||||
- "data/whats_new/*.yml"
|
- "data/whats_new/*.yml"
|
||||||
# Backstage changes
|
# Backstage changes
|
||||||
|
@ -292,7 +292,7 @@
|
||||||
- "Rakefile"
|
- "Rakefile"
|
||||||
- "tests.yml"
|
- "tests.yml"
|
||||||
- "config.ru"
|
- "config.ru"
|
||||||
- "{,ee/,jh/}{app,bin,config,db,haml_lint,lib,locale,public,scripts,symbol,vendor}/**/*"
|
- "{,ee/,jh/}{app,bin,config,db,generator_templates,haml_lint,lib,locale,public,scripts,symbol,vendor}/**/*"
|
||||||
- "doc/api/graphql/reference/*" # Files in this folder are auto-generated
|
- "doc/api/graphql/reference/*" # Files in this folder are auto-generated
|
||||||
- "data/whats_new/*.yml"
|
- "data/whats_new/*.yml"
|
||||||
# QA changes
|
# QA changes
|
||||||
|
@ -316,7 +316,7 @@
|
||||||
- "Rakefile"
|
- "Rakefile"
|
||||||
- "tests.yml"
|
- "tests.yml"
|
||||||
- "config.ru"
|
- "config.ru"
|
||||||
- "{,ee/,jh/}{app,bin,config,db,haml_lint,lib,locale,public,scripts,symbol,vendor}/**/*"
|
- "{,ee/,jh/}{app,bin,config,db,generator_templates,haml_lint,lib,locale,public,scripts,symbol,vendor}/**/*"
|
||||||
- "doc/api/graphql/reference/*" # Files in this folder are auto-generated
|
- "doc/api/graphql/reference/*" # Files in this folder are auto-generated
|
||||||
- "data/whats_new/*.yml"
|
- "data/whats_new/*.yml"
|
||||||
# Backstage changes
|
# Backstage changes
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
9cde7b7d1ecc68f5ca3b68df5ad32e6b0bc9d661
|
ebabe4399c781ea1f4a1a43774b14489446f1f68
|
||||||
|
|
|
@ -11,6 +11,7 @@ import SidebarAssigneesWidget from '~/sidebar/components/assignees/sidebar_assig
|
||||||
import SidebarConfidentialityWidget from '~/sidebar/components/confidential/sidebar_confidentiality_widget.vue';
|
import SidebarConfidentialityWidget from '~/sidebar/components/confidential/sidebar_confidentiality_widget.vue';
|
||||||
import SidebarDateWidget from '~/sidebar/components/date/sidebar_date_widget.vue';
|
import SidebarDateWidget from '~/sidebar/components/date/sidebar_date_widget.vue';
|
||||||
import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue';
|
import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue';
|
||||||
|
import SidebarTodoWidget from '~/sidebar/components/todo_toggle/sidebar_todo_widget.vue';
|
||||||
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -24,6 +25,7 @@ export default {
|
||||||
BoardSidebarLabelsSelect,
|
BoardSidebarLabelsSelect,
|
||||||
SidebarSubscriptionsWidget,
|
SidebarSubscriptionsWidget,
|
||||||
SidebarDropdownWidget,
|
SidebarDropdownWidget,
|
||||||
|
SidebarTodoWidget,
|
||||||
MountingPortal,
|
MountingPortal,
|
||||||
SidebarWeightWidget: () =>
|
SidebarWeightWidget: () =>
|
||||||
import('ee_component/sidebar/components/weight/sidebar_weight_widget.vue'),
|
import('ee_component/sidebar/components/weight/sidebar_weight_widget.vue'),
|
||||||
|
@ -90,6 +92,15 @@ export default {
|
||||||
<template #title>
|
<template #title>
|
||||||
<h2 class="gl-my-0 gl-font-size-h2 gl-line-height-24">{{ __('Issue details') }}</h2>
|
<h2 class="gl-my-0 gl-font-size-h2 gl-line-height-24">{{ __('Issue details') }}</h2>
|
||||||
</template>
|
</template>
|
||||||
|
<template #header>
|
||||||
|
<sidebar-todo-widget
|
||||||
|
class="gl-mt-3"
|
||||||
|
:issuable-id="activeBoardItem.fullId"
|
||||||
|
:issuable-iid="activeBoardItem.iid"
|
||||||
|
:full-path="fullPath"
|
||||||
|
:issuable-type="issuableType"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
<template #default>
|
<template #default>
|
||||||
<board-sidebar-title />
|
<board-sidebar-title />
|
||||||
<sidebar-assignees-widget
|
<sidebar-assignees-widget
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import todoMarkDoneMutation from '~/graphql_shared/mutations/todo_mark_done.mutation.graphql';
|
import todoMarkDoneMutation from '~/graphql_shared/mutations/todo_mark_done.mutation.graphql';
|
||||||
import TodoButton from '~/vue_shared/components/sidebar/todo_button.vue';
|
import TodoButton from '~/vue_shared/components/sidebar/todo_toggle/todo_button.vue';
|
||||||
import createDesignTodoMutation from '../graphql/mutations/create_design_todo.mutation.graphql';
|
import createDesignTodoMutation from '../graphql/mutations/create_design_todo.mutation.graphql';
|
||||||
import getDesignQuery from '../graphql/queries/get_design.query.graphql';
|
import getDesignQuery from '../graphql/queries/get_design.query.graphql';
|
||||||
import allVersionsMixin from '../mixins/all_versions';
|
import allVersionsMixin from '../mixins/all_versions';
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import Cookies from 'js-cookie';
|
import Cookies from 'js-cookie';
|
||||||
import { fixTitle, hide } from '~/tooltips';
|
import { hide } from '~/tooltips';
|
||||||
import createFlash from './flash';
|
import createFlash from './flash';
|
||||||
import axios from './lib/utils/axios_utils';
|
import axios from './lib/utils/axios_utils';
|
||||||
import { sprintf, s__, __ } from './locale';
|
import { sprintf, s__, __ } from './locale';
|
||||||
|
@ -107,36 +107,6 @@ Sidebar.prototype.toggleTodo = function (e) {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
Sidebar.prototype.todoUpdateDone = function (data) {
|
|
||||||
const deletePath = data.delete_path ? data.delete_path : null;
|
|
||||||
const attrPrefix = deletePath ? 'mark' : 'todo';
|
|
||||||
const $todoBtns = $('.js-issuable-todo');
|
|
||||||
|
|
||||||
$(document).trigger('todo:toggle', data.count);
|
|
||||||
|
|
||||||
$todoBtns.each((i, el) => {
|
|
||||||
const $el = $(el);
|
|
||||||
const $elText = $el.find('.js-issuable-todo-inner');
|
|
||||||
|
|
||||||
$el
|
|
||||||
.removeClass('is-loading')
|
|
||||||
.enable()
|
|
||||||
.attr('aria-label', $el.data(`${attrPrefix}Text`))
|
|
||||||
.attr('title', $el.data(`${attrPrefix}Text`))
|
|
||||||
.data('deletePath', deletePath);
|
|
||||||
|
|
||||||
if ($el.hasClass('has-tooltip')) {
|
|
||||||
fixTitle(el);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof $el.data('isCollapsed') !== 'undefined') {
|
|
||||||
$elText.html($el.data(`${attrPrefix}Icon`));
|
|
||||||
} else {
|
|
||||||
$elText.text($el.data(`${attrPrefix}Text`));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
Sidebar.prototype.sidebarCollapseClicked = function (e) {
|
Sidebar.prototype.sidebarCollapseClicked = function (e) {
|
||||||
if ($(e.currentTarget).hasClass('dont-change-state')) {
|
if ($(e.currentTarget).hasClass('dont-change-state')) {
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -1,18 +1,26 @@
|
||||||
<script>
|
<script>
|
||||||
import { GlTooltipDirective } from '@gitlab/ui';
|
import { GlButton, GlIcon, GlTooltipDirective } from '@gitlab/ui';
|
||||||
import { produce } from 'immer';
|
import { produce } from 'immer';
|
||||||
import createFlash from '~/flash';
|
import createFlash from '~/flash';
|
||||||
import { __, sprintf } from '~/locale';
|
import { __, sprintf } from '~/locale';
|
||||||
import { todoQueries, TodoMutationTypes, todoMutations } from '~/sidebar/constants';
|
import { todoQueries, TodoMutationTypes, todoMutations } from '~/sidebar/constants';
|
||||||
import TodoButton from '~/vue_shared/components/sidebar/todo_button.vue';
|
import { todoLabel } from '~/vue_shared/components/sidebar/todo_toggle//utils';
|
||||||
|
import TodoButton from '~/vue_shared/components/sidebar/todo_toggle/todo_button.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
GlButton,
|
||||||
|
GlIcon,
|
||||||
TodoButton,
|
TodoButton,
|
||||||
},
|
},
|
||||||
directives: {
|
directives: {
|
||||||
GlTooltip: GlTooltipDirective,
|
GlTooltip: GlTooltipDirective,
|
||||||
},
|
},
|
||||||
|
inject: {
|
||||||
|
isClassicSidebar: {
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
issuableId: {
|
issuableId: {
|
||||||
type: String,
|
type: String,
|
||||||
|
@ -86,6 +94,12 @@ export default {
|
||||||
}
|
}
|
||||||
return TodoMutationTypes.Create;
|
return TodoMutationTypes.Create;
|
||||||
},
|
},
|
||||||
|
collapsedButtonIcon() {
|
||||||
|
return this.hasTodo ? 'todo-done' : 'todo-add';
|
||||||
|
},
|
||||||
|
tootltipTitle() {
|
||||||
|
return todoLabel(this.hasTodo);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
toggleTodo() {
|
toggleTodo() {
|
||||||
|
@ -158,7 +172,24 @@ export default {
|
||||||
:is-todo="hasTodo"
|
:is-todo="hasTodo"
|
||||||
:loading="isLoading"
|
:loading="isLoading"
|
||||||
size="small"
|
size="small"
|
||||||
|
class="hide-collapsed"
|
||||||
@click.stop.prevent="toggleTodo"
|
@click.stop.prevent="toggleTodo"
|
||||||
/>
|
/>
|
||||||
|
<gl-button
|
||||||
|
v-if="isClassicSidebar"
|
||||||
|
category="tertiary"
|
||||||
|
type="reset"
|
||||||
|
class="sidebar-collapsed-icon sidebar-collapsed-container gl-rounded-0! gl-shadow-none!"
|
||||||
|
@click.stop.prevent="toggleTodo"
|
||||||
|
>
|
||||||
|
<gl-icon
|
||||||
|
v-gl-tooltip.left.viewport
|
||||||
|
:title="tootltipTitle"
|
||||||
|
:size="16"
|
||||||
|
:class="{ 'todo-undone': hasTodo }"
|
||||||
|
:name="collapsedButtonIcon"
|
||||||
|
:aria-label="collapsedButtonIcon"
|
||||||
|
/>
|
||||||
|
</gl-button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -13,10 +13,12 @@ import issueDueDateQuery from '~/sidebar/queries/issue_due_date.query.graphql';
|
||||||
import issueReferenceQuery from '~/sidebar/queries/issue_reference.query.graphql';
|
import issueReferenceQuery from '~/sidebar/queries/issue_reference.query.graphql';
|
||||||
import issueSubscribedQuery from '~/sidebar/queries/issue_subscribed.query.graphql';
|
import issueSubscribedQuery from '~/sidebar/queries/issue_subscribed.query.graphql';
|
||||||
import issueTimeTrackingQuery from '~/sidebar/queries/issue_time_tracking.query.graphql';
|
import issueTimeTrackingQuery from '~/sidebar/queries/issue_time_tracking.query.graphql';
|
||||||
|
import issueTodoQuery from '~/sidebar/queries/issue_todo.query.graphql';
|
||||||
import mergeRequestMilestone from '~/sidebar/queries/merge_request_milestone.query.graphql';
|
import mergeRequestMilestone from '~/sidebar/queries/merge_request_milestone.query.graphql';
|
||||||
import mergeRequestReferenceQuery from '~/sidebar/queries/merge_request_reference.query.graphql';
|
import mergeRequestReferenceQuery from '~/sidebar/queries/merge_request_reference.query.graphql';
|
||||||
import mergeRequestSubscribed from '~/sidebar/queries/merge_request_subscribed.query.graphql';
|
import mergeRequestSubscribed from '~/sidebar/queries/merge_request_subscribed.query.graphql';
|
||||||
import mergeRequestTimeTrackingQuery from '~/sidebar/queries/merge_request_time_tracking.query.graphql';
|
import mergeRequestTimeTrackingQuery from '~/sidebar/queries/merge_request_time_tracking.query.graphql';
|
||||||
|
import mergeRequestTodoQuery from '~/sidebar/queries/merge_request_todo.query.graphql';
|
||||||
import todoCreateMutation from '~/sidebar/queries/todo_create.mutation.graphql';
|
import todoCreateMutation from '~/sidebar/queries/todo_create.mutation.graphql';
|
||||||
import todoMarkDoneMutation from '~/sidebar/queries/todo_mark_done.mutation.graphql';
|
import todoMarkDoneMutation from '~/sidebar/queries/todo_mark_done.mutation.graphql';
|
||||||
import updateEpicConfidentialMutation from '~/sidebar/queries/update_epic_confidential.mutation.graphql';
|
import updateEpicConfidentialMutation from '~/sidebar/queries/update_epic_confidential.mutation.graphql';
|
||||||
|
@ -216,6 +218,12 @@ export const todoQueries = {
|
||||||
[IssuableType.Epic]: {
|
[IssuableType.Epic]: {
|
||||||
query: epicTodoQuery,
|
query: epicTodoQuery,
|
||||||
},
|
},
|
||||||
|
[IssuableType.Issue]: {
|
||||||
|
query: issueTodoQuery,
|
||||||
|
},
|
||||||
|
[IssuableType.MergeRequest]: {
|
||||||
|
query: mergeRequestTodoQuery,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TodoMutationTypes = {
|
export const TodoMutationTypes = {
|
||||||
|
|
|
@ -2,6 +2,8 @@ import $ from 'jquery';
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import VueApollo from 'vue-apollo';
|
import VueApollo from 'vue-apollo';
|
||||||
import createFlash from '~/flash';
|
import createFlash from '~/flash';
|
||||||
|
import { TYPE_ISSUE, TYPE_MERGE_REQUEST } from '~/graphql_shared/constants';
|
||||||
|
import { convertToGraphQLId } from '~/graphql_shared/utils';
|
||||||
import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
|
import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
|
||||||
import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger';
|
import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger';
|
||||||
import { IssuableType } from '~/issue_show/constants';
|
import { IssuableType } from '~/issue_show/constants';
|
||||||
|
@ -19,6 +21,7 @@ import SidebarDueDateWidget from '~/sidebar/components/date/sidebar_date_widget.
|
||||||
import SidebarParticipantsWidget from '~/sidebar/components/participants/sidebar_participants_widget.vue';
|
import SidebarParticipantsWidget from '~/sidebar/components/participants/sidebar_participants_widget.vue';
|
||||||
import SidebarReferenceWidget from '~/sidebar/components/reference/sidebar_reference_widget.vue';
|
import SidebarReferenceWidget from '~/sidebar/components/reference/sidebar_reference_widget.vue';
|
||||||
import SidebarDropdownWidget from '~/sidebar/components/sidebar_dropdown_widget.vue';
|
import SidebarDropdownWidget from '~/sidebar/components/sidebar_dropdown_widget.vue';
|
||||||
|
import SidebarTodoWidget from '~/sidebar/components/todo_toggle/sidebar_todo_widget.vue';
|
||||||
import { apolloProvider } from '~/sidebar/graphql';
|
import { apolloProvider } from '~/sidebar/graphql';
|
||||||
import trackShowInviteMemberLink from '~/sidebar/track_invite_members';
|
import trackShowInviteMemberLink from '~/sidebar/track_invite_members';
|
||||||
import Translate from '../vue_shared/translate';
|
import Translate from '../vue_shared/translate';
|
||||||
|
@ -40,6 +43,40 @@ function getSidebarOptions(sidebarOptEl = document.querySelector('.js-sidebar-op
|
||||||
return JSON.parse(sidebarOptEl.innerHTML);
|
return JSON.parse(sidebarOptEl.innerHTML);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mountSidebarToDoWidget() {
|
||||||
|
const el = document.querySelector('.js-issuable-todo');
|
||||||
|
|
||||||
|
if (!el) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { projectPath, iid, id } = el.dataset;
|
||||||
|
|
||||||
|
return new Vue({
|
||||||
|
el,
|
||||||
|
apolloProvider,
|
||||||
|
components: {
|
||||||
|
SidebarTodoWidget,
|
||||||
|
},
|
||||||
|
provide: {
|
||||||
|
isClassicSidebar: true,
|
||||||
|
},
|
||||||
|
render: (createElement) =>
|
||||||
|
createElement('sidebar-todo-widget', {
|
||||||
|
props: {
|
||||||
|
fullPath: projectPath,
|
||||||
|
issuableId:
|
||||||
|
isInIssuePage() || isInDesignPage()
|
||||||
|
? convertToGraphQLId(TYPE_ISSUE, id)
|
||||||
|
: convertToGraphQLId(TYPE_MERGE_REQUEST, id),
|
||||||
|
issuableIid: iid,
|
||||||
|
issuableType:
|
||||||
|
isInIssuePage() || isInDesignPage() ? IssuableType.Issue : IssuableType.MergeRequest,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function getSidebarAssigneeAvailabilityData() {
|
function getSidebarAssigneeAvailabilityData() {
|
||||||
const sidebarAssigneeEl = document.querySelectorAll('.js-sidebar-assignee-data input');
|
const sidebarAssigneeEl = document.querySelectorAll('.js-sidebar-assignee-data input');
|
||||||
return Array.from(sidebarAssigneeEl)
|
return Array.from(sidebarAssigneeEl)
|
||||||
|
@ -497,6 +534,7 @@ export function mountSidebar(mediator) {
|
||||||
initInviteMembersModal();
|
initInviteMembersModal();
|
||||||
initInviteMembersTrigger();
|
initInviteMembersTrigger();
|
||||||
|
|
||||||
|
mountSidebarToDoWidget();
|
||||||
if (isAssigneesWidgetShown) {
|
if (isAssigneesWidgetShown) {
|
||||||
mountAssigneesComponent();
|
mountAssigneesComponent();
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
query issueTodos($fullPath: ID!, $iid: String!) {
|
||||||
|
workspace: project(fullPath: $fullPath) {
|
||||||
|
__typename
|
||||||
|
issuable: issue(iid: $iid) {
|
||||||
|
__typename
|
||||||
|
id
|
||||||
|
currentUserTodos(state: pending) {
|
||||||
|
nodes {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
query mergeRequestTodos($fullPath: ID!, $iid: String!) {
|
||||||
|
workspace: project(fullPath: $fullPath) {
|
||||||
|
__typename
|
||||||
|
issuable: mergeRequest(iid: $iid) {
|
||||||
|
__typename
|
||||||
|
id
|
||||||
|
currentUserTodos(state: pending) {
|
||||||
|
nodes {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,7 +4,7 @@ import TodoButton from './todo_button.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
component: TodoButton,
|
component: TodoButton,
|
||||||
title: 'vue_shared/components/todo_button',
|
title: 'vue_shared/components/todo_toggle/todo_button',
|
||||||
};
|
};
|
||||||
|
|
||||||
const Template = (args, { argTypes }) => ({
|
const Template = (args, { argTypes }) => ({
|
|
@ -1,6 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { GlButton } from '@gitlab/ui';
|
import { GlButton } from '@gitlab/ui';
|
||||||
import { __ } from '~/locale';
|
import { todoLabel } from './utils';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
@ -15,7 +15,7 @@ export default {
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
buttonLabel() {
|
buttonLabel() {
|
||||||
return this.isTodo ? __('Mark as done') : __('Add a to do');
|
return todoLabel(this.isTodo);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { __ } from '~/locale';
|
||||||
|
|
||||||
|
export const todoLabel = (hasTodo) => {
|
||||||
|
return hasTodo ? __('Mark as done') : __('Add a to do');
|
||||||
|
};
|
|
@ -175,7 +175,8 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.block {
|
.block,
|
||||||
|
.issuable-sidebar-header {
|
||||||
@include clearfix;
|
@include clearfix;
|
||||||
padding: $gl-padding 0;
|
padding: $gl-padding 0;
|
||||||
border-bottom: 1px solid $border-gray-normal;
|
border-bottom: 1px solid $border-gray-normal;
|
||||||
|
@ -184,11 +185,6 @@
|
||||||
width: $gutter-inner-width;
|
width: $gutter-inner-width;
|
||||||
// --
|
// --
|
||||||
|
|
||||||
&.issuable-sidebar-header {
|
|
||||||
padding-top: 0;
|
|
||||||
padding-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
border: 0;
|
border: 0;
|
||||||
}
|
}
|
||||||
|
@ -273,10 +269,6 @@
|
||||||
padding: 0 20px;
|
padding: 0 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.issuable-sidebar-header {
|
|
||||||
padding-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:not(.boards-sidebar):not([data-signed-in]):not([data-always-show-toggle]) {
|
&:not(.boards-sidebar):not([data-signed-in]):not([data-always-show-toggle]) {
|
||||||
.issuable-sidebar-header {
|
.issuable-sidebar-header {
|
||||||
display: none;
|
display: none;
|
||||||
|
@ -302,7 +294,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.gutter-toggle {
|
.gutter-toggle {
|
||||||
margin-top: 7px;
|
|
||||||
border-left: 1px solid $border-gray-normal;
|
border-left: 1px solid $border-gray-normal;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
@ -331,20 +322,21 @@
|
||||||
width: $gutter-collapsed-width;
|
width: $gutter-collapsed-width;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
||||||
.block {
|
.block,
|
||||||
|
.issuable-sidebar-header {
|
||||||
width: $gutter-collapsed-width - 2px;
|
width: $gutter-collapsed-width - 2px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border-bottom: 0;
|
border-bottom: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.block,
|
||||||
|
.gutter-toggle,
|
||||||
|
.sidebar-collapsed-container {
|
||||||
&.with-sub-blocks .sub-block:hover,
|
&.with-sub-blocks .sub-block:hover,
|
||||||
&:not(.with-sub-blocks):hover {
|
&:not(.with-sub-blocks):hover {
|
||||||
background-color: $gray-100;
|
background-color: $gray-100;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.issuable-sidebar-header {
|
|
||||||
padding-top: 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.participants {
|
.participants {
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
class Projects::MergeRequests::DiffsController < Projects::MergeRequests::ApplicationController
|
class Projects::MergeRequests::DiffsController < Projects::MergeRequests::ApplicationController
|
||||||
include DiffHelper
|
include DiffHelper
|
||||||
include RendersNotes
|
include RendersNotes
|
||||||
|
include Gitlab::Cache::Helpers
|
||||||
|
|
||||||
before_action :commit
|
before_action :commit
|
||||||
before_action :define_diff_vars
|
before_action :define_diff_vars
|
||||||
|
@ -40,7 +41,16 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
|
||||||
pagination_data: diffs.pagination_data
|
pagination_data: diffs.pagination_data
|
||||||
}
|
}
|
||||||
|
|
||||||
render json: PaginatedDiffSerializer.new(current_user: current_user).represent(diffs, options)
|
if diff_options_hash[:paths].blank? && Feature.enabled?(:diffs_batch_render_cached, project, default_enabled: :yaml)
|
||||||
|
render_cached(
|
||||||
|
diffs,
|
||||||
|
with: PaginatedDiffSerializer.new(current_user: current_user),
|
||||||
|
cache_context: -> (_) { [diff_view, params[:w], params[:expanded], params[:per_page], params[:page]] },
|
||||||
|
**options
|
||||||
|
)
|
||||||
|
else
|
||||||
|
render json: PaginatedDiffSerializer.new(current_user: current_user).represent(diffs, options)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def diffs_metadata
|
def diffs_metadata
|
||||||
|
|
|
@ -285,7 +285,7 @@ module ApplicationHelper
|
||||||
def page_class
|
def page_class
|
||||||
class_names = []
|
class_names = []
|
||||||
class_names << 'issue-boards-page gl-overflow-auto' if current_controller?(:boards)
|
class_names << 'issue-boards-page gl-overflow-auto' if current_controller?(:boards)
|
||||||
class_names << 'epic-boards-page' if current_controller?(:epic_boards)
|
class_names << 'epic-boards-page gl-overflow-auto' if current_controller?(:epic_boards)
|
||||||
class_names << 'environment-logs-page' if current_controller?(:logs)
|
class_names << 'environment-logs-page' if current_controller?(:logs)
|
||||||
class_names << 'with-performance-bar' if performance_bar_enabled?
|
class_names << 'with-performance-bar' if performance_bar_enabled?
|
||||||
class_names << system_message_class
|
class_names << system_message_class
|
||||||
|
|
|
@ -50,6 +50,10 @@ module Analytics
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def events_hash_code
|
||||||
|
Digest::SHA256.hexdigest("#{start_event.hash_code}-#{end_event.hash_code}")
|
||||||
|
end
|
||||||
|
|
||||||
def start_event_label_based?
|
def start_event_label_based?
|
||||||
start_event_identifier && start_event.label_based?
|
start_event_identifier && start_event.label_based?
|
||||||
end
|
end
|
||||||
|
|
|
@ -42,6 +42,13 @@ class DiffDiscussion < Discussion
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def cache_key
|
||||||
|
[
|
||||||
|
super,
|
||||||
|
Digest::SHA1.hexdigest(position.to_json)
|
||||||
|
].join(':')
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def get_params
|
def get_params
|
||||||
|
|
|
@ -18,12 +18,6 @@ module MergeRequests
|
||||||
end
|
end
|
||||||
|
|
||||||
def rebase
|
def rebase
|
||||||
# Ensure Gitaly isn't already running a rebase
|
|
||||||
if source_project.repository.rebase_in_progress?(merge_request.id)
|
|
||||||
log_error(exception: nil, message: 'Rebase task canceled: Another rebase is already in progress', save_message_on_model: true)
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
repository.rebase(current_user, merge_request, skip_ci: @skip_ci)
|
repository.rebase(current_user, merge_request, skip_ci: @skip_ci)
|
||||||
|
|
||||||
true
|
true
|
||||||
|
|
|
@ -10,19 +10,13 @@
|
||||||
|
|
||||||
%aside.right-sidebar.js-right-sidebar.js-issuable-sidebar{ data: { signed: { in: signed_in }, issuable_type: issuable_type }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite', 'aria-label': issuable_type }
|
%aside.right-sidebar.js-right-sidebar.js-issuable-sidebar{ data: { signed: { in: signed_in }, issuable_type: issuable_type }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite', 'aria-label': issuable_type }
|
||||||
.issuable-sidebar
|
.issuable-sidebar
|
||||||
.block.issuable-sidebar-header
|
.issuable-sidebar-header.gl-py-3
|
||||||
- if signed_in
|
|
||||||
%span.issuable-header-text.hide-collapsed.float-left
|
|
||||||
= _('To Do')
|
|
||||||
%a.gutter-toggle.float-right.js-sidebar-toggle.has-tooltip{ role: "button", href: "#", "aria-label" => _('Toggle sidebar'), title: sidebar_gutter_tooltip_text, data: { container: 'body', placement: 'left', boundary: 'viewport' } }
|
%a.gutter-toggle.float-right.js-sidebar-toggle.has-tooltip{ role: "button", href: "#", "aria-label" => _('Toggle sidebar'), title: sidebar_gutter_tooltip_text, data: { container: 'body', placement: 'left', boundary: 'viewport' } }
|
||||||
= sidebar_gutter_toggle_icon
|
= sidebar_gutter_toggle_icon
|
||||||
- if signed_in
|
- if signed_in
|
||||||
= render "shared/issuable/sidebar_todo", issuable_sidebar: issuable_sidebar
|
.js-issuable-todo{ data: { project_path: issuable_sidebar[:project_full_path], iid: issuable_sidebar[:iid], id: issuable_sidebar[:id] } }
|
||||||
|
|
||||||
= form_for issuable_type, url: issuable_sidebar[:issuable_json_path], remote: true, html: { class: 'issuable-context-form inline-update js-issuable-update' } do |f|
|
= form_for issuable_type, url: issuable_sidebar[:issuable_json_path], remote: true, html: { class: 'issuable-context-form inline-update js-issuable-update' } do |f|
|
||||||
- if signed_in
|
|
||||||
.block.todo.hide-expanded
|
|
||||||
= render "shared/issuable/sidebar_todo", issuable_sidebar: issuable_sidebar, is_collapsed: true
|
|
||||||
.block.assignee.qa-assignee-block
|
.block.assignee.qa-assignee-block
|
||||||
= render "shared/issuable/sidebar_assignees", issuable_sidebar: issuable_sidebar, assignees: assignees, signed_in: signed_in
|
= render "shared/issuable/sidebar_assignees", issuable_sidebar: issuable_sidebar, assignees: assignees, signed_in: signed_in
|
||||||
|
|
||||||
|
|
|
@ -1,15 +0,0 @@
|
||||||
- is_collapsed = local_assigns.fetch(:is_collapsed, false)
|
|
||||||
- has_todo = !!issuable_sidebar.dig(:current_user, :todo, :id)
|
|
||||||
|
|
||||||
- todo_button_data = issuable_todo_button_data(issuable_sidebar, is_collapsed)
|
|
||||||
- button_title = has_todo ? todo_button_data[:mark_text] : todo_button_data[:todo_text]
|
|
||||||
- button_icon = has_todo ? todo_button_data[:mark_icon] : todo_button_data[:todo_icon]
|
|
||||||
|
|
||||||
%button.issuable-todo-btn.js-issuable-todo{ type: 'button',
|
|
||||||
class: (is_collapsed ? 'btn-blank sidebar-collapsed-icon dont-change-state has-tooltip' : 'gl-button btn btn-default issuable-header-btn float-right'),
|
|
||||||
title: button_title,
|
|
||||||
'aria-label' => button_title,
|
|
||||||
data: todo_button_data }
|
|
||||||
%span.issuable-todo-inner.js-issuable-todo-inner<
|
|
||||||
= is_collapsed ? button_icon : button_title
|
|
||||||
= loading_icon(css_class: is_collapsed ? '' : 'gl-ml-3')
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
---
|
||||||
|
name: diffs_batch_render_cached
|
||||||
|
introduced_by_url: https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/1509
|
||||||
|
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/334762
|
||||||
|
milestone: '14.1'
|
||||||
|
type: development
|
||||||
|
group: group::code review
|
||||||
|
default_enabled: false
|
|
@ -114,7 +114,7 @@ The action is irreversible.
|
||||||
|
|
||||||
## Triggering a pipeline
|
## Triggering a pipeline
|
||||||
|
|
||||||
To trigger a job you need to send a `POST` request to the GitLab API endpoint:
|
To trigger a pipeline you need to send a `POST` request to the GitLab API endpoint:
|
||||||
|
|
||||||
```plaintext
|
```plaintext
|
||||||
POST /projects/:id/trigger/pipeline
|
POST /projects/:id/trigger/pipeline
|
||||||
|
@ -126,7 +126,7 @@ branches or tags. The `:id` of a project can be found by
|
||||||
[querying the API](../../api/projects.md) or by visiting the **CI/CD**
|
[querying the API](../../api/projects.md) or by visiting the **CI/CD**
|
||||||
settings page which provides self-explanatory examples.
|
settings page which provides self-explanatory examples.
|
||||||
|
|
||||||
When a rerun of a pipeline is triggered, jobs are marked as triggered `by API` in
|
When a rerun of a pipeline is triggered, jobs are labeled as `triggered` in
|
||||||
**CI/CD > Jobs**.
|
**CI/CD > Jobs**.
|
||||||
|
|
||||||
You can see which trigger caused a job to run by visiting the single job page.
|
You can see which trigger caused a job to run by visiting the single job page.
|
||||||
|
|
|
@ -26,39 +26,40 @@ A job is defined as a list of keywords that define the job's behavior.
|
||||||
|
|
||||||
The keywords available for jobs are:
|
The keywords available for jobs are:
|
||||||
|
|
||||||
| Keyword | Description |
|
| Keyword | Description |
|
||||||
| :-----------------------------------|:------------|
|
| :-------------------------------------------|:------------|
|
||||||
| [`after_script`](#after_script) | Override a set of commands that are executed after job. |
|
| [`after_script`](#after_script) | Override a set of commands that are executed after job. |
|
||||||
| [`allow_failure`](#allow_failure) | Allow job to fail. A failed job does not cause the pipeline to fail. |
|
| [`allow_failure`](#allow_failure) | Allow job to fail. A failed job does not cause the pipeline to fail. |
|
||||||
| [`artifacts`](#artifacts) | List of files and directories to attach to a job on success. |
|
| [`artifacts`](#artifacts) | List of files and directories to attach to a job on success. |
|
||||||
| [`before_script`](#before_script) | Override a set of commands that are executed before job. |
|
| [`before_script`](#before_script) | Override a set of commands that are executed before job. |
|
||||||
| [`cache`](#cache) | List of files that should be cached between subsequent runs. |
|
| [`cache`](#cache) | List of files that should be cached between subsequent runs. |
|
||||||
| [`coverage`](#coverage) | Code coverage settings for a given job. |
|
| [`coverage`](#coverage) | Code coverage settings for a given job. |
|
||||||
| [`dependencies`](#dependencies) | Restrict which artifacts are passed to a specific job by providing a list of jobs to fetch artifacts from. |
|
| [`dast_configuration`](#dast_configuration) | Use configuration from DAST profiles on a job level. |
|
||||||
| [`environment`](#environment) | Name of an environment to which the job deploys. |
|
| [`dependencies`](#dependencies) | Restrict which artifacts are passed to a specific job by providing a list of jobs to fetch artifacts from. |
|
||||||
| [`except`](#only--except) | Control when jobs are not created. |
|
| [`environment`](#environment) | Name of an environment to which the job deploys. |
|
||||||
| [`extends`](#extends) | Configuration entries that this job inherits from. |
|
| [`except`](#only--except) | Control when jobs are not created. |
|
||||||
| [`image`](#image) | Use Docker images. |
|
| [`extends`](#extends) | Configuration entries that this job inherits from. |
|
||||||
| [`include`](#include) | Include external YAML files. |
|
| [`image`](#image) | Use Docker images. |
|
||||||
| [`inherit`](#inherit) | Select which global defaults all jobs inherit. |
|
| [`include`](#include) | Include external YAML files. |
|
||||||
| [`interruptible`](#interruptible) | Defines if a job can be canceled when made redundant by a newer run. |
|
| [`inherit`](#inherit) | Select which global defaults all jobs inherit. |
|
||||||
| [`needs`](#needs) | Execute jobs earlier than the stage ordering. |
|
| [`interruptible`](#interruptible) | Defines if a job can be canceled when made redundant by a newer run. |
|
||||||
| [`only`](#only--except) | Control when jobs are created. |
|
| [`needs`](#needs) | Execute jobs earlier than the stage ordering. |
|
||||||
| [`pages`](#pages) | Upload the result of a job to use with GitLab Pages. |
|
| [`only`](#only--except) | Control when jobs are created. |
|
||||||
| [`parallel`](#parallel) | How many instances of a job should be run in parallel. |
|
| [`pages`](#pages) | Upload the result of a job to use with GitLab Pages. |
|
||||||
| [`release`](#release) | Instructs the runner to generate a [release](../../user/project/releases/index.md) object. |
|
| [`parallel`](#parallel) | How many instances of a job should be run in parallel. |
|
||||||
| [`resource_group`](#resource_group) | Limit job concurrency. |
|
| [`release`](#release) | Instructs the runner to generate a [release](../../user/project/releases/index.md) object. |
|
||||||
| [`retry`](#retry) | When and how many times a job can be auto-retried in case of a failure. |
|
| [`resource_group`](#resource_group) | Limit job concurrency. |
|
||||||
| [`rules`](#rules) | List of conditions to evaluate and determine selected attributes of a job, and whether or not it's created. |
|
| [`retry`](#retry) | When and how many times a job can be auto-retried in case of a failure. |
|
||||||
| [`script`](#script) | Shell script that is executed by a runner. |
|
| [`rules`](#rules) | List of conditions to evaluate and determine selected attributes of a job, and whether or not it's created. |
|
||||||
| [`secrets`](#secrets) | The CI/CD secrets the job needs. |
|
| [`script`](#script) | Shell script that is executed by a runner. |
|
||||||
| [`services`](#services) | Use Docker services images. |
|
| [`secrets`](#secrets) | The CI/CD secrets the job needs. |
|
||||||
| [`stage`](#stage) | Defines a job stage. |
|
| [`services`](#services) | Use Docker services images. |
|
||||||
| [`tags`](#tags) | List of tags that are used to select a runner. |
|
| [`stage`](#stage) | Defines a job stage. |
|
||||||
| [`timeout`](#timeout) | Define a custom job-level timeout that takes precedence over the project-wide setting. |
|
| [`tags`](#tags) | List of tags that are used to select a runner. |
|
||||||
| [`trigger`](#trigger) | Defines a downstream pipeline trigger. |
|
| [`timeout`](#timeout) | Define a custom job-level timeout that takes precedence over the project-wide setting. |
|
||||||
| [`variables`](#variables) | Define job variables on a job level. |
|
| [`trigger`](#trigger) | Defines a downstream pipeline trigger. |
|
||||||
| [`when`](#when) | When to run job. |
|
| [`variables`](#variables) | Define job variables on a job level. |
|
||||||
|
| [`when`](#when) | When to run job. |
|
||||||
|
|
||||||
### Unavailable names for jobs
|
### Unavailable names for jobs
|
||||||
|
|
||||||
|
@ -4502,6 +4503,50 @@ You can use [CI/CD variables](../variables/index.md) to configure how the runner
|
||||||
You can also use variables to configure how many times a runner
|
You can also use variables to configure how many times a runner
|
||||||
[attempts certain stages of job execution](../runners/configure_runners.md#job-stages-attempts).
|
[attempts certain stages of job execution](../runners/configure_runners.md#job-stages-attempts).
|
||||||
|
|
||||||
|
## `dast_configuration` **(ULTIMATE)**
|
||||||
|
|
||||||
|
> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/5981) in GitLab 14.1.
|
||||||
|
|
||||||
|
Use the `dast_configuration` keyword to specify a site profile and scanner profile to be used in a
|
||||||
|
CI/CD configuration. Both profiles must first have been created in the project. The job's stage must
|
||||||
|
be `dast`.
|
||||||
|
|
||||||
|
**Keyword type**: Job keyword. You can use only as part of a job.
|
||||||
|
|
||||||
|
**Possible inputs**: One each of `site_profile` and `scanner_profile`.
|
||||||
|
|
||||||
|
- Use `site_profile` to specify the site profile to be used in the job.
|
||||||
|
- Use `scanner_profile` to specify the scanner profile to be used in the job.
|
||||||
|
|
||||||
|
**Example of `dast_configuration`**:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
stages:
|
||||||
|
- build
|
||||||
|
- dast
|
||||||
|
|
||||||
|
include:
|
||||||
|
- template: DAST.gitlab-ci.yml
|
||||||
|
|
||||||
|
dast:
|
||||||
|
dast_configuration:
|
||||||
|
site_profile: "Example Co"
|
||||||
|
scanner_profile: "Quick Passive Test"
|
||||||
|
```
|
||||||
|
|
||||||
|
In this example, the `dast` job extends the `dast` configuration added with the `include:` keyword
|
||||||
|
to select a specific site profile and scanner profile.
|
||||||
|
|
||||||
|
**Additional details**:
|
||||||
|
|
||||||
|
- Settings contained in either a site profile or scanner profile take precedence over those
|
||||||
|
contained in the DAST template.
|
||||||
|
|
||||||
|
**Related topics**:
|
||||||
|
|
||||||
|
- [Site profile](../../user/application_security/dast/index.md#site-profile).
|
||||||
|
- [Scanner profile](../../user/application_security/dast/index.md#scanner-profile).
|
||||||
|
|
||||||
## YAML-specific features
|
## YAML-specific features
|
||||||
|
|
||||||
In your `.gitlab-ci.yml` file, you can use YAML-specific features like anchors (`&`), aliases (`*`),
|
In your `.gitlab-ci.yml` file, you can use YAML-specific features like anchors (`&`), aliases (`*`),
|
||||||
|
|
|
@ -47,5 +47,5 @@ To add a story:
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
- Specify the `title` field of the story as the component's file path from the `javascripts/` directory,
|
- Specify the `title` field of the story as the component's file path from the `javascripts/` directory,
|
||||||
e.g. if the component is located at `app/assets/javascripts/vue_shared/components/sidebar/todo_button.vue`, specify the `title` as
|
e.g. if the component is located at `app/assets/javascripts/vue_shared/components/sidebar/todo_toggle/todo_button.vue`, specify the `title` as
|
||||||
`vue_shared/components/To-do Button`. This will ensure the Storybook navigation maps closely to our internal directory structure.
|
`vue_shared/components/To-do Button`. This will ensure the Storybook navigation maps closely to our internal directory structure.
|
||||||
|
|
|
@ -145,6 +145,7 @@ To enable DAST to run automatically, either:
|
||||||
by [Auto DevOps](../../../topics/autodevops/index.md)).
|
by [Auto DevOps](../../../topics/autodevops/index.md)).
|
||||||
- [Include the DAST template](#include-the-dast-template) in your existing
|
- [Include the DAST template](#include-the-dast-template) in your existing
|
||||||
`.gitlab-ci.yml` file.
|
`.gitlab-ci.yml` file.
|
||||||
|
- [Configure DAST using the UI](#configure-dast-using-the-ui).
|
||||||
|
|
||||||
### DAST job order
|
### DAST job order
|
||||||
|
|
||||||
|
@ -265,6 +266,28 @@ image. Using the `DAST_VERSION` variable, you can choose how DAST updates:
|
||||||
Find the latest DAST versions on the [Releases](https://gitlab.com/security-products/dast/-/releases)
|
Find the latest DAST versions on the [Releases](https://gitlab.com/security-products/dast/-/releases)
|
||||||
page.
|
page.
|
||||||
|
|
||||||
|
#### Configure DAST using the UI
|
||||||
|
|
||||||
|
You can enable or configure DAST settings using the UI. The generated settings are formatted so they
|
||||||
|
can be conveniently pasted into the `.gitlab-ci.yml` file.
|
||||||
|
|
||||||
|
1. From the project's home page, go to **Security & Compliance > Configuration**.
|
||||||
|
1. In the **Dynamic Application Security Testing (DAST)** section, select **Enable DAST** or
|
||||||
|
**Configure DAST**.
|
||||||
|
1. Select the desired **Scanner profile**, or select **Create scanner profile** and save a
|
||||||
|
scanner profile. For more details, see [scanner profiles](#scanner-profile).
|
||||||
|
1. Select the desired **Site profile**, or select **Create site profile** and save a site
|
||||||
|
profile. For more details, see [site profiles](#site-profile).
|
||||||
|
1. Select **Generate code snippet**. A modal opens with the YAML snippet corresponding to the
|
||||||
|
options you selected.
|
||||||
|
1. Do one of the following:
|
||||||
|
1. Select **Copy code only** to copy the snippet to your clipboard.
|
||||||
|
1. Select **Copy code and open `.gitlab-ci.yml` file** to copy the snippet to your clipboard. The
|
||||||
|
CI/CD Editor then opens.
|
||||||
|
1. Paste the snippet into the `.gitlab-ci.yml` file.
|
||||||
|
1. Select the **Lint** tab to confirm the edited `.gitlab-ci.yml` file is valid.
|
||||||
|
1. Select **Commit changes**.
|
||||||
|
|
||||||
#### Crawling web applications dependent on JavaScript
|
#### Crawling web applications dependent on JavaScript
|
||||||
|
|
||||||
GitLab has released a new browser-based crawler, an add-on to DAST that uses a browser to crawl web applications for content. This crawler replaces the standard DAST Spider and Ajax Crawler, and uses the same authentication mechanisms as a normal DAST scan.
|
GitLab has released a new browser-based crawler, an add-on to DAST that uses a browser to crawl web applications for content. This crawler replaces the standard DAST Spider and Ajax Crawler, and uses the same authentication mechanisms as a normal DAST scan.
|
||||||
|
|
|
@ -115,8 +115,16 @@ permission enables an electronic signature for approvals, such as the one define
|
||||||
## Security approvals in merge requests **(ULTIMATE)**
|
## Security approvals in merge requests **(ULTIMATE)**
|
||||||
|
|
||||||
You can require that a member of your security team approves a merge request if a
|
You can require that a member of your security team approves a merge request if a
|
||||||
merge request could introduce a vulnerability. To learn more, see
|
merge request could introduce a vulnerability.
|
||||||
[Security approvals in merge requests](../../../application_security/index.md#security-approvals-in-merge-requests).
|
|
||||||
|
To learn more, see [Security approvals in merge requests](../../../application_security/index.md#security-approvals-in-merge-requests).
|
||||||
|
|
||||||
|
## Code coverage check approvals **(PREMIUM)**
|
||||||
|
|
||||||
|
You can require specific approvals if a merge request would result in a decline in code test
|
||||||
|
coverage.
|
||||||
|
|
||||||
|
To learn more, see [Coverage check approval rule](../../../../ci/pipelines/settings.md#coverage-check-approval-rule).
|
||||||
|
|
||||||
## Related links
|
## Related links
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module API
|
||||||
|
module Entities
|
||||||
|
class ProjectIntegration < Entities::ProjectIntegrationBasic
|
||||||
|
# Expose serialized properties
|
||||||
|
expose :properties do |integration, options|
|
||||||
|
# TODO: Simplify as part of https://gitlab.com/gitlab-org/gitlab/issues/29404
|
||||||
|
|
||||||
|
attributes =
|
||||||
|
if integration.data_fields_present?
|
||||||
|
integration.data_fields.as_json.keys
|
||||||
|
else
|
||||||
|
integration.properties.keys
|
||||||
|
end
|
||||||
|
|
||||||
|
attributes &= integration.api_field_names
|
||||||
|
|
||||||
|
attributes.each_with_object({}) do |attribute, hash|
|
||||||
|
hash[attribute] = integration.public_send(attribute) # rubocop:disable GitlabSecurity/PublicSend
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -2,10 +2,10 @@
|
||||||
|
|
||||||
module API
|
module API
|
||||||
module Entities
|
module Entities
|
||||||
class ProjectServiceBasic < Grape::Entity
|
class ProjectIntegrationBasic < Grape::Entity
|
||||||
expose :id, :title
|
expose :id, :title
|
||||||
expose :slug do |service|
|
expose :slug do |integration|
|
||||||
service.to_param.dasherize
|
integration.to_param.dasherize
|
||||||
end
|
end
|
||||||
expose :created_at, :updated_at, :active
|
expose :created_at, :updated_at, :active
|
||||||
expose :commit_events, :push_events, :issues_events, :confidential_issues_events
|
expose :commit_events, :push_events, :issues_events, :confidential_issues_events
|
|
@ -1,25 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module API
|
|
||||||
module Entities
|
|
||||||
class ProjectService < Entities::ProjectServiceBasic
|
|
||||||
# Expose serialized properties
|
|
||||||
expose :properties do |service, options|
|
|
||||||
# TODO: Simplify as part of https://gitlab.com/gitlab-org/gitlab/issues/29404
|
|
||||||
|
|
||||||
attributes =
|
|
||||||
if service.data_fields_present?
|
|
||||||
service.data_fields.as_json.keys
|
|
||||||
else
|
|
||||||
service.properties.keys
|
|
||||||
end
|
|
||||||
|
|
||||||
attributes &= service.api_field_names
|
|
||||||
|
|
||||||
attributes.each_with_object({}) do |attribute, hash|
|
|
||||||
hash[attribute] = service.public_send(attribute) # rubocop:disable GitlabSecurity/PublicSend
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -35,7 +35,7 @@ module API
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
TRIGGER_SERVICES = {
|
TRIGGER_INTEGRATIONS = {
|
||||||
'mattermost-slash-commands' => [
|
'mattermost-slash-commands' => [
|
||||||
{
|
{
|
||||||
name: :token,
|
name: :token,
|
||||||
|
@ -60,24 +60,24 @@ module API
|
||||||
before { authorize_admin_project }
|
before { authorize_admin_project }
|
||||||
|
|
||||||
helpers do
|
helpers do
|
||||||
def service_attributes(service)
|
def integration_attributes(integration)
|
||||||
service.fields.inject([]) do |arr, hash|
|
integration.fields.inject([]) do |arr, hash|
|
||||||
arr << hash[:name].to_sym
|
arr << hash[:name].to_sym
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
desc 'Get all active project services' do
|
desc 'Get all active project integrations' do
|
||||||
success Entities::ProjectServiceBasic
|
success Entities::ProjectIntegrationBasic
|
||||||
end
|
end
|
||||||
get ":id/services" do
|
get ":id/services" do
|
||||||
services = user_project.integrations.active
|
integrations = user_project.integrations.active
|
||||||
|
|
||||||
present services, with: Entities::ProjectServiceBasic
|
present integrations, with: Entities::ProjectIntegrationBasic
|
||||||
end
|
end
|
||||||
|
|
||||||
INTEGRATIONS.each do |slug, settings|
|
INTEGRATIONS.each do |slug, settings|
|
||||||
desc "Set #{slug} service for project"
|
desc "Set #{slug} integration for project"
|
||||||
params do
|
params do
|
||||||
settings.each do |setting|
|
settings.each do |setting|
|
||||||
if setting[:required]
|
if setting[:required]
|
||||||
|
@ -92,7 +92,7 @@ module API
|
||||||
params = declared_params(include_missing: false).merge(active: true)
|
params = declared_params(include_missing: false).merge(active: true)
|
||||||
|
|
||||||
if integration.update(params)
|
if integration.update(params)
|
||||||
present integration, with: Entities::ProjectService
|
present integration, with: Entities::ProjectIntegration
|
||||||
else
|
else
|
||||||
render_api_error!('400 Bad Request', 400)
|
render_api_error!('400 Bad Request', 400)
|
||||||
end
|
end
|
||||||
|
@ -107,14 +107,14 @@ module API
|
||||||
integration = user_project.find_or_initialize_integration(params[:slug].underscore)
|
integration = user_project.find_or_initialize_integration(params[:slug].underscore)
|
||||||
|
|
||||||
destroy_conditionally!(integration) do
|
destroy_conditionally!(integration) do
|
||||||
attrs = service_attributes(integration).index_with { nil }.merge(active: false)
|
attrs = integration_attributes(integration).index_with { nil }.merge(active: false)
|
||||||
|
|
||||||
render_api_error!('400 Bad Request', 400) unless integration.update(attrs)
|
render_api_error!('400 Bad Request', 400) unless integration.update(attrs)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
desc 'Get the integration settings for a project' do
|
desc 'Get the integration settings for a project' do
|
||||||
success Entities::ProjectService
|
success Entities::ProjectIntegration
|
||||||
end
|
end
|
||||||
params do
|
params do
|
||||||
requires :slug, type: String, values: INTEGRATIONS.keys, desc: 'The name of the service'
|
requires :slug, type: String, values: INTEGRATIONS.keys, desc: 'The name of the service'
|
||||||
|
@ -124,15 +124,15 @@ module API
|
||||||
|
|
||||||
not_found!('Service') unless integration&.persisted?
|
not_found!('Service') unless integration&.persisted?
|
||||||
|
|
||||||
present integration, with: Entities::ProjectService
|
present integration, with: Entities::ProjectIntegration
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
TRIGGER_SERVICES.each do |service_slug, settings|
|
TRIGGER_INTEGRATIONS.each do |integration_slug, settings|
|
||||||
helpers do
|
helpers do
|
||||||
def slash_command_service(project, service_slug, params)
|
def slash_command_integration(project, integration_slug, params)
|
||||||
project.integrations.active.find do |service|
|
project.integrations.active.find do |integration|
|
||||||
service.try(:token) == params[:token] && service.to_param == service_slug.underscore
|
integration.try(:token) == params[:token] && integration.to_param == integration_slug.underscore
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -141,7 +141,7 @@ module API
|
||||||
requires :id, type: String, desc: 'The ID of a project'
|
requires :id, type: String, desc: 'The ID of a project'
|
||||||
end
|
end
|
||||||
resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
|
resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
|
||||||
desc "Trigger a slash command for #{service_slug}" do
|
desc "Trigger a slash command for #{integration_slug}" do
|
||||||
detail 'Added in GitLab 8.13'
|
detail 'Added in GitLab 8.13'
|
||||||
end
|
end
|
||||||
params do
|
params do
|
||||||
|
@ -149,14 +149,14 @@ module API
|
||||||
requires setting[:name], type: setting[:type], desc: setting[:desc]
|
requires setting[:name], type: setting[:type], desc: setting[:desc]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
post ":id/services/#{service_slug.underscore}/trigger" do
|
post ":id/services/#{integration_slug.underscore}/trigger" do
|
||||||
project = find_project(params[:id])
|
project = find_project(params[:id])
|
||||||
|
|
||||||
# This is not accurate, but done to prevent leakage of the project names
|
# This is not accurate, but done to prevent leakage of the project names
|
||||||
not_found!('Service') unless project
|
not_found!('Service') unless project
|
||||||
|
|
||||||
service = slash_command_service(project, service_slug, params)
|
integration = slash_command_integration(project, integration_slug, params)
|
||||||
result = service.try(:trigger, params)
|
result = integration.try(:trigger, params)
|
||||||
|
|
||||||
if result
|
if result
|
||||||
status result[:status] || 200
|
status result[:status] || 200
|
||||||
|
|
|
@ -31,6 +31,10 @@ module Gitlab
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def hash_code
|
||||||
|
Digest::SHA256.hexdigest(self.class.identifier.to_s)
|
||||||
|
end
|
||||||
|
|
||||||
# Each StageEvent must expose a timestamp or a timestamp like expression in order to build a range query.
|
# Each StageEvent must expose a timestamp or a timestamp like expression in order to build a range query.
|
||||||
# Example: get me all the Issue records between start event end end event
|
# Example: get me all the Issue records between start event end end event
|
||||||
def timestamp_projection
|
def timestamp_projection
|
||||||
|
|
|
@ -7,9 +7,9 @@ module Gitlab
|
||||||
ERROR_MESSAGE = 'LFS objects are missing. Ensure LFS is properly set up or try a manual "git lfs push --all".'
|
ERROR_MESSAGE = 'LFS objects are missing. Ensure LFS is properly set up or try a manual "git lfs push --all".'
|
||||||
|
|
||||||
def validate!
|
def validate!
|
||||||
# This feature flag is used for disabling integrify check on some envs
|
# This feature flag is used for disabling integrity check on some envs
|
||||||
# because these costy calculations may cause performance issues
|
# because these costy calculations may cause performance issues
|
||||||
return unless Feature.enabled?(:lfs_check, default_enabled: true)
|
return unless Feature.enabled?(:lfs_check, project, default_enabled: :yaml)
|
||||||
|
|
||||||
return unless project.lfs_enabled?
|
return unless project.lfs_enabled?
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,10 @@ module Gitlab
|
||||||
diff_options: diff_options,
|
diff_options: diff_options,
|
||||||
diff_refs: commit.diff_refs)
|
diff_refs: commit.diff_refs)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def cache_key
|
||||||
|
['commit', @diffable.id]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -14,6 +14,10 @@ module Gitlab
|
||||||
def unfold_diff_lines(positions)
|
def unfold_diff_lines(positions)
|
||||||
# no-op
|
# no-op
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def cache_key
|
||||||
|
['compare', @diffable.head.id, @diffable.base.id]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -6,6 +6,8 @@ module Gitlab
|
||||||
class MergeRequestDiffBase < Base
|
class MergeRequestDiffBase < Base
|
||||||
extend ::Gitlab::Utils::Override
|
extend ::Gitlab::Utils::Override
|
||||||
|
|
||||||
|
delegate :real_size, :overflow?, :cache_key, to: :@merge_request_diff
|
||||||
|
|
||||||
def initialize(merge_request_diff, diff_options:)
|
def initialize(merge_request_diff, diff_options:)
|
||||||
@merge_request_diff = merge_request_diff
|
@merge_request_diff = merge_request_diff
|
||||||
|
|
||||||
|
@ -44,14 +46,6 @@ module Gitlab
|
||||||
diff_stats_cache.clear
|
diff_stats_cache.clear
|
||||||
end
|
end
|
||||||
|
|
||||||
def real_size
|
|
||||||
@merge_request_diff.real_size
|
|
||||||
end
|
|
||||||
|
|
||||||
def overflow?
|
|
||||||
@merge_request_diff.overflow?
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def highlight_cache
|
def highlight_cache
|
||||||
|
@ -62,7 +56,7 @@ module Gitlab
|
||||||
|
|
||||||
def diff_stats_cache
|
def diff_stats_cache
|
||||||
strong_memoize(:diff_stats_cache) do
|
strong_memoize(:diff_stats_cache) do
|
||||||
Gitlab::Diff::StatsCache.new(cachable_key: @merge_request_diff.cache_key)
|
Gitlab::Diff::StatsCache.new(cachable_key: cache_key)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -883,12 +883,6 @@ module Gitlab
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def rebase_in_progress?(rebase_id)
|
|
||||||
wrapped_gitaly_errors do
|
|
||||||
gitaly_repository_client.rebase_in_progress?(rebase_id)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def squash(user, squash_id, start_sha:, end_sha:, author:, message:)
|
def squash(user, squash_id, start_sha:, end_sha:, author:, message:)
|
||||||
wrapped_gitaly_errors do
|
wrapped_gitaly_errors do
|
||||||
gitaly_operation_client.user_squash(user, squash_id, start_sha, end_sha, author, message)
|
gitaly_operation_client.user_squash(user, squash_id, start_sha, end_sha, author, message)
|
||||||
|
|
|
@ -152,23 +152,6 @@ module Gitlab
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def rebase_in_progress?(rebase_id)
|
|
||||||
request = Gitaly::IsRebaseInProgressRequest.new(
|
|
||||||
repository: @gitaly_repo,
|
|
||||||
rebase_id: rebase_id.to_s
|
|
||||||
)
|
|
||||||
|
|
||||||
response = GitalyClient.call(
|
|
||||||
@storage,
|
|
||||||
:repository_service,
|
|
||||||
:is_rebase_in_progress,
|
|
||||||
request,
|
|
||||||
timeout: GitalyClient.fast_timeout
|
|
||||||
)
|
|
||||||
|
|
||||||
response.in_progress
|
|
||||||
end
|
|
||||||
|
|
||||||
def squash_in_progress?(squash_id)
|
def squash_in_progress?(squash_id)
|
||||||
request = Gitaly::IsSquashInProgressRequest.new(
|
request = Gitaly::IsSquashInProgressRequest.new(
|
||||||
repository: @gitaly_repo,
|
repository: @gitaly_repo,
|
||||||
|
|
|
@ -10,7 +10,8 @@ import BoardSidebarTitle from '~/boards/components/sidebar/board_sidebar_title.v
|
||||||
import { ISSUABLE } from '~/boards/constants';
|
import { ISSUABLE } from '~/boards/constants';
|
||||||
import SidebarDateWidget from '~/sidebar/components/date/sidebar_date_widget.vue';
|
import SidebarDateWidget from '~/sidebar/components/date/sidebar_date_widget.vue';
|
||||||
import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue';
|
import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue';
|
||||||
import { mockIssue, mockIssueGroupPath, mockIssueProjectPath } from '../mock_data';
|
import SidebarTodoWidget from '~/sidebar/components/todo_toggle/sidebar_todo_widget.vue';
|
||||||
|
import { mockActiveIssue, mockIssue, mockIssueGroupPath, mockIssueProjectPath } from '../mock_data';
|
||||||
|
|
||||||
describe('BoardContentSidebar', () => {
|
describe('BoardContentSidebar', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
|
@ -26,7 +27,7 @@ describe('BoardContentSidebar', () => {
|
||||||
},
|
},
|
||||||
getters: {
|
getters: {
|
||||||
activeBoardItem: () => {
|
activeBoardItem: () => {
|
||||||
return { ...mockIssue, epic: null };
|
return { ...mockActiveIssue, epic: null };
|
||||||
},
|
},
|
||||||
groupPathForActiveIssue: () => mockIssueGroupPath,
|
groupPathForActiveIssue: () => mockIssueGroupPath,
|
||||||
projectPathForActiveIssue: () => mockIssueProjectPath,
|
projectPathForActiveIssue: () => mockIssueProjectPath,
|
||||||
|
@ -110,6 +111,10 @@ describe('BoardContentSidebar', () => {
|
||||||
expect(wrapper.findComponent(GlDrawer).props('open')).toBe(true);
|
expect(wrapper.findComponent(GlDrawer).props('open')).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('renders SidebarTodoWidget', () => {
|
||||||
|
expect(wrapper.findComponent(SidebarTodoWidget).exists()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
it('renders BoardSidebarLabelsSelect', () => {
|
it('renders BoardSidebarLabelsSelect', () => {
|
||||||
expect(wrapper.findComponent(BoardSidebarLabelsSelect).exists()).toBe(true);
|
expect(wrapper.findComponent(BoardSidebarLabelsSelect).exists()).toBe(true);
|
||||||
});
|
});
|
||||||
|
@ -147,7 +152,7 @@ describe('BoardContentSidebar', () => {
|
||||||
|
|
||||||
expect(toggleBoardItem).toHaveBeenCalledTimes(1);
|
expect(toggleBoardItem).toHaveBeenCalledTimes(1);
|
||||||
expect(toggleBoardItem).toHaveBeenCalledWith(expect.any(Object), {
|
expect(toggleBoardItem).toHaveBeenCalledWith(expect.any(Object), {
|
||||||
boardItem: { ...mockIssue, epic: null },
|
boardItem: { ...mockActiveIssue, epic: null },
|
||||||
sidebarType: ISSUABLE,
|
sidebarType: ISSUABLE,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { GlAlert } from '@gitlab/ui';
|
import { GlAlert } from '@gitlab/ui';
|
||||||
import { createLocalVue, shallowMount } from '@vue/test-utils';
|
import { shallowMount } from '@vue/test-utils';
|
||||||
|
import Vue from 'vue';
|
||||||
import Draggable from 'vuedraggable';
|
import Draggable from 'vuedraggable';
|
||||||
import Vuex from 'vuex';
|
import Vuex from 'vuex';
|
||||||
import EpicsSwimlanes from 'ee_component/boards/components/epics_swimlanes.vue';
|
import EpicsSwimlanes from 'ee_component/boards/components/epics_swimlanes.vue';
|
||||||
|
@ -8,8 +9,7 @@ import BoardColumnDeprecated from '~/boards/components/board_column_deprecated.v
|
||||||
import BoardContent from '~/boards/components/board_content.vue';
|
import BoardContent from '~/boards/components/board_content.vue';
|
||||||
import { mockLists, mockListsWithModel } from '../mock_data';
|
import { mockLists, mockListsWithModel } from '../mock_data';
|
||||||
|
|
||||||
const localVue = createLocalVue();
|
Vue.use(Vuex);
|
||||||
localVue.use(Vuex);
|
|
||||||
|
|
||||||
const actions = {
|
const actions = {
|
||||||
moveList: jest.fn(),
|
moveList: jest.fn(),
|
||||||
|
@ -44,7 +44,6 @@ describe('BoardContent', () => {
|
||||||
...state,
|
...state,
|
||||||
});
|
});
|
||||||
wrapper = shallowMount(BoardContent, {
|
wrapper = shallowMount(BoardContent, {
|
||||||
localVue,
|
|
||||||
propsData: {
|
propsData: {
|
||||||
lists: mockListsWithModel,
|
lists: mockListsWithModel,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
|
|
|
@ -182,6 +182,7 @@ export const mockIssue = {
|
||||||
|
|
||||||
export const mockActiveIssue = {
|
export const mockActiveIssue = {
|
||||||
...mockIssue,
|
...mockIssue,
|
||||||
|
fullId: 'gid://gitlab/Issue/436',
|
||||||
id: 436,
|
id: 436,
|
||||||
iid: '27',
|
iid: '27',
|
||||||
subscribed: false,
|
subscribed: false,
|
||||||
|
|
|
@ -1,171 +0,0 @@
|
||||||
/* eslint-disable no-new */
|
|
||||||
import MockAdapter from 'axios-mock-adapter';
|
|
||||||
import { clone } from 'lodash';
|
|
||||||
import waitForPromises from 'helpers/wait_for_promises';
|
|
||||||
import { TEST_HOST } from 'spec/test_constants';
|
|
||||||
import axios from '~/lib/utils/axios_utils';
|
|
||||||
import Sidebar from '~/right_sidebar';
|
|
||||||
import { fixTitle } from '~/tooltips';
|
|
||||||
|
|
||||||
jest.mock('~/tooltips');
|
|
||||||
|
|
||||||
describe('Issuable right sidebar collapsed todo toggle', () => {
|
|
||||||
const fixtureName = 'issues/open-issue.html';
|
|
||||||
const jsonFixtureName = 'todos/todos.json';
|
|
||||||
let mock;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
const todoData = getJSONFixture(jsonFixtureName);
|
|
||||||
new Sidebar();
|
|
||||||
loadFixtures(fixtureName);
|
|
||||||
|
|
||||||
document.querySelector('.js-right-sidebar').classList.toggle('right-sidebar-expanded');
|
|
||||||
document.querySelector('.js-right-sidebar').classList.toggle('right-sidebar-collapsed');
|
|
||||||
|
|
||||||
mock = new MockAdapter(axios);
|
|
||||||
|
|
||||||
mock.onPost(`${TEST_HOST}/frontend-fixtures/issues-project/todos`).reply(() => {
|
|
||||||
const response = clone(todoData);
|
|
||||||
|
|
||||||
return [200, response];
|
|
||||||
});
|
|
||||||
|
|
||||||
mock.onDelete(/(.*)\/dashboard\/todos\/\d+$/).reply(() => {
|
|
||||||
const response = clone(todoData);
|
|
||||||
delete response.delete_path;
|
|
||||||
|
|
||||||
return [200, response];
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
mock.restore();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('shows add todo button', () => {
|
|
||||||
expect(document.querySelector('.js-issuable-todo.sidebar-collapsed-icon')).not.toBeNull();
|
|
||||||
|
|
||||||
expect(
|
|
||||||
document
|
|
||||||
.querySelector('.js-issuable-todo.sidebar-collapsed-icon svg')
|
|
||||||
.getAttribute('data-testid'),
|
|
||||||
).toBe('todo-add-icon');
|
|
||||||
|
|
||||||
expect(
|
|
||||||
document.querySelector('.js-issuable-todo.sidebar-collapsed-icon .todo-undone'),
|
|
||||||
).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('sets default tooltip title', () => {
|
|
||||||
expect(
|
|
||||||
document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').getAttribute('title'),
|
|
||||||
).toBe('Add a to do');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('toggle todo state', (done) => {
|
|
||||||
document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click();
|
|
||||||
|
|
||||||
setImmediate(() => {
|
|
||||||
expect(
|
|
||||||
document.querySelector('.js-issuable-todo.sidebar-collapsed-icon .todo-undone'),
|
|
||||||
).not.toBeNull();
|
|
||||||
|
|
||||||
expect(
|
|
||||||
document
|
|
||||||
.querySelector('.js-issuable-todo.sidebar-collapsed-icon svg.todo-undone')
|
|
||||||
.getAttribute('data-testid'),
|
|
||||||
).toBe('todo-done-icon');
|
|
||||||
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('toggle todo state of expanded todo toggle', (done) => {
|
|
||||||
document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click();
|
|
||||||
|
|
||||||
setImmediate(() => {
|
|
||||||
expect(
|
|
||||||
document.querySelector('.issuable-sidebar-header .js-issuable-todo').textContent.trim(),
|
|
||||||
).toBe('Mark as done');
|
|
||||||
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('toggles todo button tooltip', (done) => {
|
|
||||||
document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click();
|
|
||||||
|
|
||||||
setImmediate(() => {
|
|
||||||
const el = document.querySelector('.js-issuable-todo.sidebar-collapsed-icon');
|
|
||||||
|
|
||||||
expect(el.getAttribute('title')).toBe('Mark as done');
|
|
||||||
expect(fixTitle).toHaveBeenCalledWith(el);
|
|
||||||
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('marks todo as done', (done) => {
|
|
||||||
document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click();
|
|
||||||
|
|
||||||
waitForPromises()
|
|
||||||
.then(() => {
|
|
||||||
expect(
|
|
||||||
document.querySelector('.js-issuable-todo.sidebar-collapsed-icon .todo-undone'),
|
|
||||||
).not.toBeNull();
|
|
||||||
|
|
||||||
document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click();
|
|
||||||
})
|
|
||||||
.then(waitForPromises)
|
|
||||||
.then(() => {
|
|
||||||
expect(
|
|
||||||
document.querySelector('.js-issuable-todo.sidebar-collapsed-icon .todo-undone'),
|
|
||||||
).toBeNull();
|
|
||||||
|
|
||||||
expect(
|
|
||||||
document.querySelector('.issuable-sidebar-header .js-issuable-todo').textContent.trim(),
|
|
||||||
).toBe('Add a to do');
|
|
||||||
})
|
|
||||||
.then(done)
|
|
||||||
.catch(done.fail);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('updates aria-label to Mark as done', (done) => {
|
|
||||||
document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click();
|
|
||||||
|
|
||||||
setImmediate(() => {
|
|
||||||
expect(
|
|
||||||
document
|
|
||||||
.querySelector('.js-issuable-todo.sidebar-collapsed-icon')
|
|
||||||
.getAttribute('aria-label'),
|
|
||||||
).toBe('Mark as done');
|
|
||||||
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('updates aria-label to add todo', (done) => {
|
|
||||||
document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click();
|
|
||||||
|
|
||||||
waitForPromises()
|
|
||||||
.then(() => {
|
|
||||||
expect(
|
|
||||||
document
|
|
||||||
.querySelector('.js-issuable-todo.sidebar-collapsed-icon')
|
|
||||||
.getAttribute('aria-label'),
|
|
||||||
).toBe('Mark as done');
|
|
||||||
|
|
||||||
document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click();
|
|
||||||
})
|
|
||||||
.then(waitForPromises)
|
|
||||||
.then(() => {
|
|
||||||
expect(
|
|
||||||
document
|
|
||||||
.querySelector('.js-issuable-todo.sidebar-collapsed-icon')
|
|
||||||
.getAttribute('aria-label'),
|
|
||||||
).toBe('Add a to do');
|
|
||||||
})
|
|
||||||
.then(done)
|
|
||||||
.catch(done.fail);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -2,7 +2,7 @@ import { shallowMount, mount } from '@vue/test-utils';
|
||||||
import DesignTodoButton from '~/design_management/components/design_todo_button.vue';
|
import DesignTodoButton from '~/design_management/components/design_todo_button.vue';
|
||||||
import createDesignTodoMutation from '~/design_management/graphql/mutations/create_design_todo.mutation.graphql';
|
import createDesignTodoMutation from '~/design_management/graphql/mutations/create_design_todo.mutation.graphql';
|
||||||
import todoMarkDoneMutation from '~/graphql_shared/mutations/todo_mark_done.mutation.graphql';
|
import todoMarkDoneMutation from '~/graphql_shared/mutations/todo_mark_done.mutation.graphql';
|
||||||
import TodoButton from '~/vue_shared/components/sidebar/todo_button.vue';
|
import TodoButton from '~/vue_shared/components/sidebar/todo_toggle/todo_button.vue';
|
||||||
import mockDesign from '../mock_data/design';
|
import mockDesign from '../mock_data/design';
|
||||||
|
|
||||||
const mockDesignWithPendingTodos = {
|
const mockDesignWithPendingTodos = {
|
||||||
|
|
|
@ -66,22 +66,6 @@ describe('RightSidebar', () => {
|
||||||
assertSidebarState('collapsed');
|
assertSidebarState('collapsed');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should broadcast todo:toggle event when add todo clicked', (done) => {
|
|
||||||
const todos = getJSONFixture('todos/todos.json');
|
|
||||||
mock.onPost(/(.*)\/todos$/).reply(200, todos);
|
|
||||||
|
|
||||||
const todoToggleSpy = jest.fn();
|
|
||||||
$(document).on('todo:toggle', todoToggleSpy);
|
|
||||||
|
|
||||||
$('.issuable-sidebar-header .js-issuable-todo').click();
|
|
||||||
|
|
||||||
setImmediate(() => {
|
|
||||||
expect(todoToggleSpy.mock.calls.length).toEqual(1);
|
|
||||||
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not hide collapsed icons', () => {
|
it('should not hide collapsed icons', () => {
|
||||||
[].forEach.call(document.querySelectorAll('.sidebar-collapsed-icon'), (el) => {
|
[].forEach.call(document.querySelectorAll('.sidebar-collapsed-icon'), (el) => {
|
||||||
expect(el.querySelector('.fa, svg').classList.contains('hidden')).toBeFalsy();
|
expect(el.querySelector('.fa, svg').classList.contains('hidden')).toBeFalsy();
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { GlIcon } from '@gitlab/ui';
|
||||||
import { shallowMount } from '@vue/test-utils';
|
import { shallowMount } from '@vue/test-utils';
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import VueApollo from 'vue-apollo';
|
import VueApollo from 'vue-apollo';
|
||||||
|
@ -6,7 +7,7 @@ import waitForPromises from 'helpers/wait_for_promises';
|
||||||
import createFlash from '~/flash';
|
import createFlash from '~/flash';
|
||||||
import SidebarTodoWidget from '~/sidebar/components/todo_toggle/sidebar_todo_widget.vue';
|
import SidebarTodoWidget from '~/sidebar/components/todo_toggle/sidebar_todo_widget.vue';
|
||||||
import epicTodoQuery from '~/sidebar/queries/epic_todo.query.graphql';
|
import epicTodoQuery from '~/sidebar/queries/epic_todo.query.graphql';
|
||||||
import TodoButton from '~/vue_shared/components/sidebar/todo_button.vue';
|
import TodoButton from '~/vue_shared/components/sidebar/todo_toggle/todo_button.vue';
|
||||||
import { todosResponse, noTodosResponse } from '../../mock_data';
|
import { todosResponse, noTodosResponse } from '../../mock_data';
|
||||||
|
|
||||||
jest.mock('~/flash');
|
jest.mock('~/flash');
|
||||||
|
@ -28,6 +29,7 @@ describe('Sidebar Todo Widget', () => {
|
||||||
apolloProvider: fakeApollo,
|
apolloProvider: fakeApollo,
|
||||||
provide: {
|
provide: {
|
||||||
canUpdate: true,
|
canUpdate: true,
|
||||||
|
isClassicSidebar: true,
|
||||||
},
|
},
|
||||||
propsData: {
|
propsData: {
|
||||||
fullPath: 'group',
|
fullPath: 'group',
|
||||||
|
@ -83,4 +85,42 @@ describe('Sidebar Todo Widget', () => {
|
||||||
|
|
||||||
expect(createFlash).toHaveBeenCalled();
|
expect(createFlash).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('collapsed', () => {
|
||||||
|
const event = { stopPropagation: jest.fn(), preventDefault: jest.fn() };
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
createComponent({
|
||||||
|
todosQueryHandler: jest.fn().mockResolvedValue(noTodosResponse),
|
||||||
|
});
|
||||||
|
return waitForPromises();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows add todo icon', () => {
|
||||||
|
expect(wrapper.find(GlIcon).exists()).toBe(true);
|
||||||
|
|
||||||
|
expect(wrapper.find(GlIcon).props('name')).toBe('todo-add');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets default tooltip title', () => {
|
||||||
|
expect(wrapper.find(GlIcon).attributes('title')).toBe('Add a to do');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('when user has a to do', async () => {
|
||||||
|
createComponent({
|
||||||
|
todosQueryHandler: jest.fn().mockResolvedValue(todosResponse),
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitForPromises();
|
||||||
|
expect(wrapper.find(GlIcon).props('name')).toBe('todo-done');
|
||||||
|
expect(wrapper.find(GlIcon).attributes('title')).toBe('Mark as done');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('emits `todoUpdated` event on click on icon', async () => {
|
||||||
|
wrapper.find(GlIcon).vm.$emit('click', event);
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick();
|
||||||
|
expect(wrapper.emitted('todoUpdated')).toEqual([[false]]);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { GlButton } from '@gitlab/ui';
|
import { GlButton } from '@gitlab/ui';
|
||||||
import { shallowMount, mount } from '@vue/test-utils';
|
import { shallowMount, mount } from '@vue/test-utils';
|
||||||
import TodoButton from '~/vue_shared/components/sidebar/todo_button.vue';
|
import TodoButton from '~/vue_shared/components/sidebar/todo_toggle/todo_button.vue';
|
||||||
|
|
||||||
describe('Todo Button', () => {
|
describe('Todo Button', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
|
|
|
@ -75,4 +75,12 @@ RSpec.describe Gitlab::Diff::FileCollection::Commit do
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '#cache_key' do
|
||||||
|
subject(:cache_key) { described_class.new(diffable, diff_options: nil).cache_key }
|
||||||
|
|
||||||
|
it 'returns with the commit id' do
|
||||||
|
expect(cache_key).to eq ['commit', diffable.id]
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -15,29 +15,20 @@ RSpec.describe Gitlab::Diff::FileCollection::Compare do
|
||||||
head_commit.id)
|
head_commit.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
it_behaves_like 'diff statistics' do
|
let(:diffable) { Compare.new(raw_compare, project) }
|
||||||
let(:collection_default_args) do
|
let(:collection_default_args) do
|
||||||
{
|
{
|
||||||
project: diffable.project,
|
project: diffable.project,
|
||||||
diff_options: {},
|
diff_options: {},
|
||||||
diff_refs: diffable.diff_refs
|
diff_refs: diffable.diff_refs
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
let(:diffable) { Compare.new(raw_compare, project) }
|
it_behaves_like 'diff statistics' do
|
||||||
let(:stub_path) { '.gitignore' }
|
let(:stub_path) { '.gitignore' }
|
||||||
end
|
end
|
||||||
|
|
||||||
it_behaves_like 'sortable diff files' do
|
it_behaves_like 'sortable diff files' do
|
||||||
let(:diffable) { Compare.new(raw_compare, project) }
|
|
||||||
let(:collection_default_args) do
|
|
||||||
{
|
|
||||||
project: diffable.project,
|
|
||||||
diff_options: {},
|
|
||||||
diff_refs: diffable.diff_refs
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
let(:unsorted_diff_files_paths) do
|
let(:unsorted_diff_files_paths) do
|
||||||
[
|
[
|
||||||
'.DS_Store',
|
'.DS_Store',
|
||||||
|
@ -66,4 +57,12 @@ RSpec.describe Gitlab::Diff::FileCollection::Compare do
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '#cache_key' do
|
||||||
|
subject(:cache_key) { described_class.new(diffable, **collection_default_args).cache_key }
|
||||||
|
|
||||||
|
it 'returns with head and base' do
|
||||||
|
expect(cache_key).to eq ['compare', head_commit.id, start_commit.id]
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -25,4 +25,12 @@ RSpec.describe Gitlab::Diff::FileCollection::MergeRequestDiffBase do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '#cache_key' do
|
||||||
|
subject(:cache_key) { described_class.new(diffable, diff_options: nil).cache_key }
|
||||||
|
|
||||||
|
it 'returns cache_key from merge_request_diff' do
|
||||||
|
expect(cache_key).to eq diffable.cache_key
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -209,19 +209,6 @@ RSpec.describe Gitlab::GitalyClient::RepositoryService do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#rebase_in_progress?' do
|
|
||||||
let(:rebase_id) { 1 }
|
|
||||||
|
|
||||||
it 'sends a repository_rebase_in_progress message' do
|
|
||||||
expect_any_instance_of(Gitaly::RepositoryService::Stub)
|
|
||||||
.to receive(:is_rebase_in_progress)
|
|
||||||
.with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash))
|
|
||||||
.and_return(double(in_progress: true))
|
|
||||||
|
|
||||||
client.rebase_in_progress?(rebase_id)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe '#squash_in_progress?' do
|
describe '#squash_in_progress?' do
|
||||||
let(:squash_id) { 1 }
|
let(:squash_id) { 1 }
|
||||||
|
|
||||||
|
|
|
@ -126,4 +126,13 @@ RSpec.describe DiffDiscussion do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '#cache_key' do
|
||||||
|
it 'returns the cache key with the position sha' do
|
||||||
|
notes_sha = Digest::SHA1.hexdigest("#{diff_note.id}")
|
||||||
|
position_sha = Digest::SHA1.hexdigest(diff_note.position.to_json)
|
||||||
|
|
||||||
|
expect(subject.cache_key).to eq("#{described_class::CACHE_VERSION}:#{diff_note.latest_cached_markdown_version}:#{subject.id}:#{notes_sha}:#{diff_note.updated_at}::#{position_sha}")
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -31,7 +31,7 @@ RSpec.describe API::Services do
|
||||||
it "returns a list of all active integrations" do
|
it "returns a list of all active integrations" do
|
||||||
get api("/projects/#{project.id}/services", user)
|
get api("/projects/#{project.id}/services", user)
|
||||||
|
|
||||||
aggregate_failures 'expect successful response with all active services' do
|
aggregate_failures 'expect successful response with all active integrations' do
|
||||||
expect(response).to have_gitlab_http_status(:ok)
|
expect(response).to have_gitlab_http_status(:ok)
|
||||||
expect(json_response).to be_an Array
|
expect(json_response).to be_an Array
|
||||||
expect(json_response.count).to eq(1)
|
expect(json_response.count).to eq(1)
|
||||||
|
@ -42,40 +42,38 @@ RSpec.describe API::Services do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
Integration.available_integration_names.each do |service|
|
Integration.available_integration_names.each do |integration|
|
||||||
describe "PUT /projects/:id/services/#{service.dasherize}" do
|
describe "PUT /projects/:id/services/#{integration.dasherize}" do
|
||||||
include_context service
|
include_context integration
|
||||||
|
|
||||||
it "updates #{service} settings" do
|
it "updates #{integration} settings" do
|
||||||
put api("/projects/#{project.id}/services/#{dashed_service}", user), params: service_attrs
|
put api("/projects/#{project.id}/services/#{dashed_integration}", user), params: integration_attrs
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:ok)
|
expect(response).to have_gitlab_http_status(:ok)
|
||||||
|
|
||||||
current_service = project.integrations.first
|
current_integration = project.integrations.first
|
||||||
events = current_service.event_names.empty? ? ["foo"].freeze : current_service.event_names
|
events = current_integration.event_names.empty? ? ["foo"].freeze : current_integration.event_names
|
||||||
query_strings = []
|
query_strings = []
|
||||||
events.each do |event|
|
events.each do |event|
|
||||||
query_strings << "#{event}=#{!current_service[event]}"
|
query_strings << "#{event}=#{!current_integration[event]}"
|
||||||
end
|
end
|
||||||
query_strings = query_strings.join('&')
|
query_strings = query_strings.join('&')
|
||||||
|
|
||||||
put api("/projects/#{project.id}/services/#{dashed_service}?#{query_strings}", user), params: service_attrs
|
put api("/projects/#{project.id}/services/#{dashed_integration}?#{query_strings}", user), params: integration_attrs
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:ok)
|
expect(response).to have_gitlab_http_status(:ok)
|
||||||
expect(json_response['slug']).to eq(dashed_service)
|
expect(json_response['slug']).to eq(dashed_integration)
|
||||||
events.each do |event|
|
events.each do |event|
|
||||||
next if event == "foo"
|
next if event == "foo"
|
||||||
|
|
||||||
expect(project.integrations.first[event]).not_to eq(current_service[event]),
|
expect(project.integrations.first[event]).not_to eq(current_integration[event]),
|
||||||
"expected #{!current_service[event]} for event #{event} for service #{current_service.title}, got #{current_service[event]}"
|
"expected #{!current_integration[event]} for event #{event} for service #{current_integration.title}, got #{current_integration[event]}"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it "returns if required fields missing" do
|
it "returns if required fields missing" do
|
||||||
attrs = service_attrs
|
required_attributes = integration_attrs_list.select do |attr|
|
||||||
|
integration_klass.validators_on(attr).any? do |v|
|
||||||
required_attributes = service_attrs_list.select do |attr|
|
|
||||||
service_klass.validators_on(attr).any? do |v|
|
|
||||||
v.instance_of?(ActiveRecord::Validations::PresenceValidator) &&
|
v.instance_of?(ActiveRecord::Validations::PresenceValidator) &&
|
||||||
# exclude presence validators with conditional since those are not really required
|
# exclude presence validators with conditional since those are not really required
|
||||||
![:if, :unless].any? { |cond| v.options.include?(cond) }
|
![:if, :unless].any? { |cond| v.options.include?(cond) }
|
||||||
|
@ -85,74 +83,74 @@ RSpec.describe API::Services do
|
||||||
if required_attributes.empty?
|
if required_attributes.empty?
|
||||||
expected_code = :ok
|
expected_code = :ok
|
||||||
else
|
else
|
||||||
attrs.delete(required_attributes.sample)
|
integration_attrs.delete(required_attributes.sample)
|
||||||
expected_code = :bad_request
|
expected_code = :bad_request
|
||||||
end
|
end
|
||||||
|
|
||||||
put api("/projects/#{project.id}/services/#{dashed_service}", user), params: attrs
|
put api("/projects/#{project.id}/services/#{dashed_integration}", user), params: integration_attrs
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(expected_code)
|
expect(response).to have_gitlab_http_status(expected_code)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "DELETE /projects/:id/services/#{service.dasherize}" do
|
describe "DELETE /projects/:id/services/#{integration.dasherize}" do
|
||||||
include_context service
|
include_context integration
|
||||||
|
|
||||||
before do
|
before do
|
||||||
initialize_integration(service)
|
initialize_integration(integration)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "deletes #{service}" do
|
it "deletes #{integration}" do
|
||||||
delete api("/projects/#{project.id}/services/#{dashed_service}", user)
|
delete api("/projects/#{project.id}/services/#{dashed_integration}", user)
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:no_content)
|
expect(response).to have_gitlab_http_status(:no_content)
|
||||||
project.send(service_method).reload
|
project.send(integration_method).reload
|
||||||
expect(project.send(service_method).activated?).to be_falsey
|
expect(project.send(integration_method).activated?).to be_falsey
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "GET /projects/:id/services/#{service.dasherize}" do
|
describe "GET /projects/:id/services/#{integration.dasherize}" do
|
||||||
include_context service
|
include_context integration
|
||||||
|
|
||||||
let!(:initialized_service) { initialize_integration(service, active: true) }
|
let!(:initialized_integration) { initialize_integration(integration, active: true) }
|
||||||
|
|
||||||
let_it_be(:project2) do
|
let_it_be(:project2) do
|
||||||
create(:project, creator_id: user.id, namespace: user.namespace)
|
create(:project, creator_id: user.id, namespace: user.namespace)
|
||||||
end
|
end
|
||||||
|
|
||||||
def deactive_service!
|
def deactive_integration!
|
||||||
return initialized_service.update!(active: false) unless initialized_service.is_a?(::Integrations::Prometheus)
|
return initialized_integration.update!(active: false) unless initialized_integration.is_a?(::Integrations::Prometheus)
|
||||||
|
|
||||||
# Integrations::Prometheus sets `#active` itself within a `before_save`:
|
# Integrations::Prometheus sets `#active` itself within a `before_save`:
|
||||||
initialized_service.manual_configuration = false
|
initialized_integration.manual_configuration = false
|
||||||
initialized_service.save!
|
initialized_integration.save!
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns authentication error when unauthenticated' do
|
it 'returns authentication error when unauthenticated' do
|
||||||
get api("/projects/#{project.id}/services/#{dashed_service}")
|
get api("/projects/#{project.id}/services/#{dashed_integration}")
|
||||||
expect(response).to have_gitlab_http_status(:unauthorized)
|
expect(response).to have_gitlab_http_status(:unauthorized)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "returns all properties of active service #{service}" do
|
it "returns all properties of active service #{integration}" do
|
||||||
get api("/projects/#{project.id}/services/#{dashed_service}", user)
|
get api("/projects/#{project.id}/services/#{dashed_integration}", user)
|
||||||
|
|
||||||
expect(initialized_service).to be_active
|
expect(initialized_integration).to be_active
|
||||||
expect(response).to have_gitlab_http_status(:ok)
|
expect(response).to have_gitlab_http_status(:ok)
|
||||||
expect(json_response['properties'].keys).to match_array(service_instance.api_field_names)
|
expect(json_response['properties'].keys).to match_array(integration_instance.api_field_names)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "returns all properties of inactive integration #{service}" do
|
it "returns all properties of inactive integration #{integration}" do
|
||||||
deactive_service!
|
deactive_integration!
|
||||||
|
|
||||||
get api("/projects/#{project.id}/services/#{dashed_service}", user)
|
get api("/projects/#{project.id}/services/#{dashed_integration}", user)
|
||||||
|
|
||||||
expect(initialized_service).not_to be_active
|
expect(initialized_integration).not_to be_active
|
||||||
expect(response).to have_gitlab_http_status(:ok)
|
expect(response).to have_gitlab_http_status(:ok)
|
||||||
expect(json_response['properties'].keys).to match_array(service_instance.api_field_names)
|
expect(json_response['properties'].keys).to match_array(integration_instance.api_field_names)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "returns not found if integration does not exist" do
|
it "returns not found if integration does not exist" do
|
||||||
get api("/projects/#{project2.id}/services/#{dashed_service}", user)
|
get api("/projects/#{project2.id}/services/#{dashed_integration}", user)
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:not_found)
|
expect(response).to have_gitlab_http_status(:not_found)
|
||||||
expect(json_response['message']).to eq('404 Service Not Found')
|
expect(json_response['message']).to eq('404 Service Not Found')
|
||||||
|
@ -160,10 +158,10 @@ RSpec.describe API::Services do
|
||||||
|
|
||||||
it "returns not found if service exists but is in `Project#disabled_integrations`" do
|
it "returns not found if service exists but is in `Project#disabled_integrations`" do
|
||||||
expect_next_found_instance_of(Project) do |project|
|
expect_next_found_instance_of(Project) do |project|
|
||||||
expect(project).to receive(:disabled_integrations).at_least(:once).and_return([service])
|
expect(project).to receive(:disabled_integrations).at_least(:once).and_return([integration])
|
||||||
end
|
end
|
||||||
|
|
||||||
get api("/projects/#{project.id}/services/#{dashed_service}", user)
|
get api("/projects/#{project.id}/services/#{dashed_integration}", user)
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:not_found)
|
expect(response).to have_gitlab_http_status(:not_found)
|
||||||
expect(json_response['message']).to eq('404 Service Not Found')
|
expect(json_response['message']).to eq('404 Service Not Found')
|
||||||
|
@ -171,7 +169,7 @@ RSpec.describe API::Services do
|
||||||
|
|
||||||
it "returns error when authenticated but not a project owner" do
|
it "returns error when authenticated but not a project owner" do
|
||||||
project.add_developer(user2)
|
project.add_developer(user2)
|
||||||
get api("/projects/#{project.id}/services/#{dashed_service}", user2)
|
get api("/projects/#{project.id}/services/#{dashed_integration}", user2)
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:forbidden)
|
expect(response).to have_gitlab_http_status(:forbidden)
|
||||||
end
|
end
|
||||||
|
@ -235,8 +233,8 @@ RSpec.describe API::Services do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'Slack Service' do
|
describe 'Slack Integration' do
|
||||||
let(:service_name) { 'slack_slash_commands' }
|
let(:integration_name) { 'slack_slash_commands' }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
project.create_slack_slash_commands_integration(
|
project.create_slack_slash_commands_integration(
|
||||||
|
@ -246,7 +244,7 @@ RSpec.describe API::Services do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns status 200' do
|
it 'returns status 200' do
|
||||||
post api("/projects/#{project.id}/services/#{service_name}/trigger"), params: { token: 'token', text: 'help' }
|
post api("/projects/#{project.id}/services/#{integration_name}/trigger"), params: { token: 'token', text: 'help' }
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:ok)
|
expect(response).to have_gitlab_http_status(:ok)
|
||||||
expect(json_response['response_type']).to eq("ephemeral")
|
expect(json_response['response_type']).to eq("ephemeral")
|
||||||
|
@ -309,8 +307,8 @@ RSpec.describe API::Services do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'Hangouts Chat service' do
|
describe 'Hangouts Chat integration' do
|
||||||
let(:service_name) { 'hangouts-chat' }
|
let(:integration_name) { 'hangouts-chat' }
|
||||||
let(:params) do
|
let(:params) do
|
||||||
{
|
{
|
||||||
webhook: 'https://hook.example.com',
|
webhook: 'https://hook.example.com',
|
||||||
|
@ -326,21 +324,21 @@ RSpec.describe API::Services do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'accepts branches_to_be_notified for update', :aggregate_failures do
|
it 'accepts branches_to_be_notified for update', :aggregate_failures do
|
||||||
put api("/projects/#{project.id}/services/#{service_name}", user), params: params.merge(branches_to_be_notified: 'all')
|
put api("/projects/#{project.id}/services/#{integration_name}", user), params: params.merge(branches_to_be_notified: 'all')
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:ok)
|
expect(response).to have_gitlab_http_status(:ok)
|
||||||
expect(json_response['properties']['branches_to_be_notified']).to eq('all')
|
expect(json_response['properties']['branches_to_be_notified']).to eq('all')
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'only requires the webhook param' do
|
it 'only requires the webhook param' do
|
||||||
put api("/projects/#{project.id}/services/#{service_name}", user), params: { webhook: 'https://hook.example.com' }
|
put api("/projects/#{project.id}/services/#{integration_name}", user), params: { webhook: 'https://hook.example.com' }
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:ok)
|
expect(response).to have_gitlab_http_status(:ok)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'Pipelines Email Integration' do
|
describe 'Pipelines Email Integration' do
|
||||||
let(:service_name) { 'pipelines-email' }
|
let(:integration_name) { 'pipelines-email' }
|
||||||
|
|
||||||
context 'notify_only_broken_pipelines property was saved as a string' do
|
context 'notify_only_broken_pipelines property was saved as a string' do
|
||||||
before do
|
before do
|
||||||
|
@ -354,7 +352,7 @@ RSpec.describe API::Services do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns boolean values for notify_only_broken_pipelines' do
|
it 'returns boolean values for notify_only_broken_pipelines' do
|
||||||
get api("/projects/#{project.id}/services/#{service_name}", user)
|
get api("/projects/#{project.id}/services/#{integration_name}", user)
|
||||||
|
|
||||||
expect(json_response['properties']['notify_only_broken_pipelines']).to eq(true)
|
expect(json_response['properties']['notify_only_broken_pipelines']).to eq(true)
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,126 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'Merge Requests Diffs' do
|
||||||
|
let_it_be(:project) { create(:project, :repository) }
|
||||||
|
let_it_be(:user) { create(:user) }
|
||||||
|
let_it_be(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
project.add_maintainer(user)
|
||||||
|
sign_in(user)
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'GET diffs_batch' do
|
||||||
|
let(:headers) { {} }
|
||||||
|
|
||||||
|
shared_examples_for 'serializes diffs with expected arguments' do
|
||||||
|
it 'serializes paginated merge request diff collection' do
|
||||||
|
expect_next_instance_of(PaginatedDiffSerializer) do |instance|
|
||||||
|
expect(instance).to receive(:represent)
|
||||||
|
.with(an_instance_of(collection), expected_options)
|
||||||
|
.and_call_original
|
||||||
|
end
|
||||||
|
|
||||||
|
subject
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def collection_arguments(pagination_data = {})
|
||||||
|
{
|
||||||
|
environment: nil,
|
||||||
|
merge_request: merge_request,
|
||||||
|
diff_view: :inline,
|
||||||
|
merge_ref_head_diff: nil,
|
||||||
|
pagination_data: {
|
||||||
|
total_pages: nil
|
||||||
|
}.merge(pagination_data)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def go(extra_params = {})
|
||||||
|
params = {
|
||||||
|
namespace_id: project.namespace.to_param,
|
||||||
|
project_id: project,
|
||||||
|
id: merge_request.iid,
|
||||||
|
page: 0,
|
||||||
|
per_page: 20,
|
||||||
|
format: 'json'
|
||||||
|
}
|
||||||
|
|
||||||
|
get diffs_batch_namespace_project_json_merge_request_path(params.merge(extra_params)), headers: headers
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with caching', :use_clean_rails_memory_store_caching do
|
||||||
|
subject { go(page: 0, per_page: 5) }
|
||||||
|
|
||||||
|
context 'when the request has not been cached' do
|
||||||
|
it_behaves_like 'serializes diffs with expected arguments' do
|
||||||
|
let(:collection) { Gitlab::Diff::FileCollection::MergeRequestDiffBatch }
|
||||||
|
let(:expected_options) { collection_arguments(total_pages: 20) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the request has already been cached' do
|
||||||
|
before do
|
||||||
|
go(page: 0, per_page: 5)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not serialize diffs' do
|
||||||
|
expect_next_instance_of(PaginatedDiffSerializer) do |instance|
|
||||||
|
expect(instance).not_to receive(:represent)
|
||||||
|
end
|
||||||
|
|
||||||
|
subject
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with the different pagination option' do
|
||||||
|
subject { go(page: 5, per_page: 5) }
|
||||||
|
|
||||||
|
it_behaves_like 'serializes diffs with expected arguments' do
|
||||||
|
let(:collection) { Gitlab::Diff::FileCollection::MergeRequestDiffBatch }
|
||||||
|
let(:expected_options) { collection_arguments(total_pages: 20) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with the different diff_view' do
|
||||||
|
subject { go(page: 0, per_page: 5, view: :parallel) }
|
||||||
|
|
||||||
|
it_behaves_like 'serializes diffs with expected arguments' do
|
||||||
|
let(:collection) { Gitlab::Diff::FileCollection::MergeRequestDiffBatch }
|
||||||
|
let(:expected_options) { collection_arguments(total_pages: 20).merge(diff_view: :parallel) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with the different expanded option' do
|
||||||
|
subject { go(page: 0, per_page: 5, expanded: true ) }
|
||||||
|
|
||||||
|
it_behaves_like 'serializes diffs with expected arguments' do
|
||||||
|
let(:collection) { Gitlab::Diff::FileCollection::MergeRequestDiffBatch }
|
||||||
|
let(:expected_options) { collection_arguments(total_pages: 20) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with the different ignore_whitespace_change option' do
|
||||||
|
subject { go(page: 0, per_page: 5, w: 1) }
|
||||||
|
|
||||||
|
it_behaves_like 'serializes diffs with expected arguments' do
|
||||||
|
let(:collection) { Gitlab::Diff::FileCollection::Compare }
|
||||||
|
let(:expected_options) { collection_arguments(total_pages: 20) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the paths is given' do
|
||||||
|
subject { go(page: 0, per_page: 5, paths: %w[README CHANGELOG]) }
|
||||||
|
|
||||||
|
it 'does not use cache' do
|
||||||
|
expect(Rails.cache).not_to receive(:fetch).with(/cache:gitlab:PaginatedDiffSerializer/).and_call_original
|
||||||
|
|
||||||
|
subject
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -158,6 +158,29 @@ RSpec.describe 'merge requests discussions' do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when the diff note position changes' do
|
||||||
|
before do
|
||||||
|
# This replicates a position change wherein timestamps aren't updated
|
||||||
|
# which is why `Gitlab::Timeless.timeless` is utilized. This is the
|
||||||
|
# same approach being used in Discussions::UpdateDiffPositionService
|
||||||
|
# which is responsible for updating the positions of diff discussions
|
||||||
|
# when MR updates.
|
||||||
|
first_note.position = Gitlab::Diff::Position.new(
|
||||||
|
old_path: first_note.position.old_path,
|
||||||
|
new_path: first_note.position.new_path,
|
||||||
|
old_line: first_note.position.old_line,
|
||||||
|
new_line: first_note.position.new_line + 1,
|
||||||
|
diff_refs: first_note.position.diff_refs
|
||||||
|
)
|
||||||
|
|
||||||
|
Gitlab::Timeless.timeless(first_note, &:save)
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'cache miss' do
|
||||||
|
let(:changed_notes) { [first_note, second_note] }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'when merge_request_discussion_cache is disabled' do
|
context 'when merge_request_discussion_cache is disabled' do
|
||||||
before do
|
before do
|
||||||
stub_feature_flags(merge_request_discussion_cache: false)
|
stub_feature_flags(merge_request_discussion_cache: false)
|
||||||
|
|
|
@ -25,30 +25,6 @@ RSpec.describe MergeRequests::RebaseService do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#execute' do
|
describe '#execute' do
|
||||||
context 'when another rebase is already in progress' do
|
|
||||||
before do
|
|
||||||
allow(repository).to receive(:rebase_in_progress?).with(merge_request.id).and_return(true)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'saves the error message' do
|
|
||||||
service.execute(merge_request)
|
|
||||||
|
|
||||||
expect(merge_request.reload.merge_error).to eq 'Rebase task canceled: Another rebase is already in progress'
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns an error' do
|
|
||||||
expect(service.execute(merge_request)).to match(status: :error,
|
|
||||||
message: described_class::REBASE_ERROR)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'clears rebase_jid' do
|
|
||||||
expect { service.execute(merge_request) }
|
|
||||||
.to change { merge_request.rebase_jid }
|
|
||||||
.from(rebase_jid)
|
|
||||||
.to(nil)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
shared_examples 'sequence of failure and success' do
|
shared_examples 'sequence of failure and success' do
|
||||||
it 'properly clears the error message' do
|
it 'properly clears the error message' do
|
||||||
allow(repository).to receive(:gitaly_operation_client).and_raise('Something went wrong')
|
allow(repository).to receive(:gitaly_operation_client).and_raise('Something went wrong')
|
||||||
|
@ -150,6 +126,13 @@ RSpec.describe MergeRequests::RebaseService do
|
||||||
|
|
||||||
it_behaves_like 'a service that can execute a successful rebase'
|
it_behaves_like 'a service that can execute a successful rebase'
|
||||||
|
|
||||||
|
it 'clears rebase_jid' do
|
||||||
|
expect { service.execute(merge_request) }
|
||||||
|
.to change(merge_request, :rebase_jid)
|
||||||
|
.from(rebase_jid)
|
||||||
|
.to(nil)
|
||||||
|
end
|
||||||
|
|
||||||
context 'when skip_ci flag is set' do
|
context 'when skip_ci flag is set' do
|
||||||
let(:skip_ci) { true }
|
let(:skip_ci) { true }
|
||||||
|
|
||||||
|
|
|
@ -1,38 +1,38 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
Integration.available_integration_names.each do |service|
|
Integration.available_integration_names.each do |integration|
|
||||||
RSpec.shared_context service do
|
RSpec.shared_context integration do
|
||||||
include JiraServiceHelper if service == 'jira'
|
include JiraServiceHelper if integration == 'jira'
|
||||||
|
|
||||||
let(:dashed_service) { service.dasherize }
|
let(:dashed_integration) { integration.dasherize }
|
||||||
let(:service_method) { Project.integration_association_name(service) }
|
let(:integration_method) { Project.integration_association_name(integration) }
|
||||||
let(:service_klass) { Integration.integration_name_to_model(service) }
|
let(:integration_klass) { Integration.integration_name_to_model(integration) }
|
||||||
let(:service_instance) { service_klass.new }
|
let(:integration_instance) { integration_klass.new }
|
||||||
let(:service_fields) { service_instance.fields }
|
let(:integration_fields) { integration_instance.fields }
|
||||||
let(:service_attrs_list) { service_fields.inject([]) {|arr, hash| arr << hash[:name].to_sym } }
|
let(:integration_attrs_list) { integration_fields.inject([]) {|arr, hash| arr << hash[:name].to_sym } }
|
||||||
let(:service_attrs) do
|
let(:integration_attrs) do
|
||||||
service_attrs_list.inject({}) do |hash, k|
|
integration_attrs_list.inject({}) do |hash, k|
|
||||||
if k =~ /^(token*|.*_token|.*_key)/
|
if k =~ /^(token*|.*_token|.*_key)/
|
||||||
hash.merge!(k => 'secrettoken')
|
hash.merge!(k => 'secrettoken')
|
||||||
elsif service == 'confluence' && k == :confluence_url
|
elsif integration == 'confluence' && k == :confluence_url
|
||||||
hash.merge!(k => 'https://example.atlassian.net/wiki')
|
hash.merge!(k => 'https://example.atlassian.net/wiki')
|
||||||
elsif service == 'datadog' && k == :datadog_site
|
elsif integration == 'datadog' && k == :datadog_site
|
||||||
hash.merge!(k => 'datadoghq.com')
|
hash.merge!(k => 'datadoghq.com')
|
||||||
elsif service == 'packagist' && k == :server
|
elsif integration == 'packagist' && k == :server
|
||||||
hash.merge!(k => 'https://packagist.example.com')
|
hash.merge!(k => 'https://packagist.example.com')
|
||||||
elsif k =~ /^(.*_url|url|webhook)/
|
elsif k =~ /^(.*_url|url|webhook)/
|
||||||
hash.merge!(k => "http://example.com")
|
hash.merge!(k => "http://example.com")
|
||||||
elsif service_klass.method_defined?("#{k}?")
|
elsif integration_klass.method_defined?("#{k}?")
|
||||||
hash.merge!(k => true)
|
hash.merge!(k => true)
|
||||||
elsif service == 'irker' && k == :recipients
|
elsif integration == 'irker' && k == :recipients
|
||||||
hash.merge!(k => 'irc://irc.network.net:666/#channel')
|
hash.merge!(k => 'irc://irc.network.net:666/#channel')
|
||||||
elsif service == 'irker' && k == :server_port
|
elsif integration == 'irker' && k == :server_port
|
||||||
hash.merge!(k => 1234)
|
hash.merge!(k => 1234)
|
||||||
elsif service == 'jira' && k == :jira_issue_transition_id
|
elsif integration == 'jira' && k == :jira_issue_transition_id
|
||||||
hash.merge!(k => '1,2,3')
|
hash.merge!(k => '1,2,3')
|
||||||
elsif service == 'emails_on_push' && k == :recipients
|
elsif integration == 'emails_on_push' && k == :recipients
|
||||||
hash.merge!(k => 'foo@bar.com')
|
hash.merge!(k => 'foo@bar.com')
|
||||||
elsif service == 'slack' || service == 'mattermost' && k == :labels_to_be_notified_behavior
|
elsif integration == 'slack' || integration == 'mattermost' && k == :labels_to_be_notified_behavior
|
||||||
hash.merge!(k => "match_any")
|
hash.merge!(k => "match_any")
|
||||||
else
|
else
|
||||||
hash.merge!(k => "someword")
|
hash.merge!(k => "someword")
|
||||||
|
@ -47,24 +47,24 @@ Integration.available_integration_names.each do |service|
|
||||||
end
|
end
|
||||||
|
|
||||||
before do
|
before do
|
||||||
enable_license_for_service(service)
|
enable_license_for_integration(integration)
|
||||||
stub_jira_integration_test if service == 'jira'
|
stub_jira_integration_test if integration == 'jira'
|
||||||
end
|
end
|
||||||
|
|
||||||
def initialize_integration(integration, attrs = {})
|
def initialize_integration(integration, attrs = {})
|
||||||
record = project.find_or_initialize_integration(integration)
|
record = project.find_or_initialize_integration(integration)
|
||||||
record.attributes = attrs
|
record.attributes = attrs
|
||||||
record.properties = service_attrs
|
record.properties = integration_attrs
|
||||||
record.save!
|
record.save!
|
||||||
record
|
record
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def enable_license_for_service(service)
|
def enable_license_for_integration(integration)
|
||||||
return unless respond_to?(:stub_licensed_features)
|
return unless respond_to?(:stub_licensed_features)
|
||||||
|
|
||||||
licensed_feature = licensed_features[service]
|
licensed_feature = licensed_features[integration]
|
||||||
return unless licensed_feature
|
return unless licensed_feature
|
||||||
|
|
||||||
stub_licensed_features(licensed_feature => true)
|
stub_licensed_features(licensed_feature => true)
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
RSpec.shared_examples_for 'value stream analytics event' do
|
RSpec.shared_examples_for 'value stream analytics event' do
|
||||||
let(:params) { {} }
|
let(:params) { {} }
|
||||||
let(:instance) { described_class.new(params) }
|
let(:instance) { described_class.new(params) }
|
||||||
|
let(:expected_hash_code) { Digest::SHA256.hexdigest(instance.class.identifier.to_s) }
|
||||||
|
|
||||||
it { expect(described_class.name).to be_a_kind_of(String) }
|
it { expect(described_class.name).to be_a_kind_of(String) }
|
||||||
it { expect(described_class.identifier).to be_a_kind_of(Symbol) }
|
it { expect(described_class.identifier).to be_a_kind_of(Symbol) }
|
||||||
|
@ -19,4 +20,16 @@ RSpec.shared_examples_for 'value stream analytics event' do
|
||||||
expect(output_query).to be_a_kind_of(ActiveRecord::Relation)
|
expect(output_query).to be_a_kind_of(ActiveRecord::Relation)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '#hash_code' do
|
||||||
|
it 'returns a hash that uniquely identifies an event' do
|
||||||
|
expect(instance.hash_code).to eq(expected_hash_code)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not differ when the same object is built with the same params' do
|
||||||
|
another_instance_with_same_params = described_class.new(params)
|
||||||
|
|
||||||
|
expect(another_instance_with_same_params.hash_code).to eq(instance.hash_code)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -122,6 +122,22 @@ RSpec.shared_examples 'value stream analytics stage' do
|
||||||
expect(stage.parent_id).to eq(parent.id)
|
expect(stage.parent_id).to eq(parent.id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '#hash_code' do
|
||||||
|
it 'does not differ when the same object is built with the same params' do
|
||||||
|
stage_1 = build(factory)
|
||||||
|
stage_2 = build(factory)
|
||||||
|
|
||||||
|
expect(stage_1.events_hash_code).to eq(stage_2.events_hash_code)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'differs when the stage events are different' do
|
||||||
|
stage_1 = build(factory, start_event_identifier: :merge_request_created, end_event_identifier: :merge_request_merged)
|
||||||
|
stage_2 = build(factory, start_event_identifier: :issue_created, end_event_identifier: :issue_first_mentioned_in_commit)
|
||||||
|
|
||||||
|
expect(stage_1.events_hash_code).not_to eq(stage_2.events_hash_code)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
RSpec.shared_examples 'value stream analytics label based stage' do
|
RSpec.shared_examples 'value stream analytics label based stage' do
|
||||||
|
|
Loading…
Reference in New Issue