Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
c40b751771
commit
3c5195204b
|
|
@ -193,6 +193,6 @@ overrides:
|
|||
'@graphql-eslint/no-unused-fragments': error
|
||||
'@graphql-eslint/no-duplicate-fields': error
|
||||
- files:
|
||||
- 'spec/contracts/consumer/**/*'
|
||||
- '{,ee/}spec/contracts/consumer/**/*'
|
||||
rules:
|
||||
'@gitlab/require-i18n-strings': off
|
||||
|
|
|
|||
|
|
@ -11,9 +11,7 @@ const messageHtml = `
|
|||
<ul>
|
||||
<li>${s__("AdminUsers|The user can't log in.")}</li>
|
||||
<li>${s__("AdminUsers|The user can't access git repositories.")}</li>
|
||||
<li>${s__(
|
||||
'AdminUsers|Issues and merge requests authored by this user are hidden from other users.',
|
||||
)}</li>
|
||||
<li>${s__('AdminUsers|Issues authored by this user are hidden from other users.')}</li>
|
||||
</ul>
|
||||
<p>${s__('AdminUsers|You can unban their account in the future. Their data remains intact.')}</p>
|
||||
<p>${sprintf(
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<script>
|
||||
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { sprintf, __ } from '~/locale';
|
||||
import { __ } from '~/locale';
|
||||
import { IssuableType, WorkspaceType } from '~/issues/constants';
|
||||
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue';
|
||||
|
|
@ -40,9 +40,7 @@ export default {
|
|||
iconName: 'spam',
|
||||
visible: this.hidden,
|
||||
dataTestId: 'hidden',
|
||||
tooltip: sprintf(__('This %{issuable} is hidden because its author has been banned'), {
|
||||
issuable: this.getNoteableData.targetType.replace('_', ' '),
|
||||
}),
|
||||
tooltip: __('This issue is hidden because its author has been banned'),
|
||||
},
|
||||
];
|
||||
},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,229 @@
|
|||
<script>
|
||||
/**
|
||||
* Common component to render a system note, icon and user information.
|
||||
*
|
||||
* This component need not be used with any store neither has any vuex dependency
|
||||
*
|
||||
* @example
|
||||
* <system-note
|
||||
* :note="{
|
||||
* id: String,
|
||||
* author: Object,
|
||||
* createdAt: String,
|
||||
* bodyHtml: String,
|
||||
* systemNoteIconName: String
|
||||
* }"
|
||||
* />
|
||||
*/
|
||||
import { GlButton, GlSkeletonLoader, GlTooltipDirective, GlIcon } from '@gitlab/ui';
|
||||
import $ from 'jquery';
|
||||
import SafeHtml from '~/vue_shared/directives/safe_html';
|
||||
import descriptionVersionHistoryMixin from 'ee_else_ce/notes/mixins/description_version_history';
|
||||
import '~/behaviors/markdown/render_gfm';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import { getLocationHash } from '~/lib/utils/url_utility';
|
||||
import { __ } from '~/locale';
|
||||
import NoteHeader from '~/notes/components/note_header.vue';
|
||||
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
|
||||
|
||||
const MAX_VISIBLE_COMMIT_LIST_COUNT = 3;
|
||||
|
||||
export default {
|
||||
i18n: {
|
||||
deleteButtonLabel: __('Remove description history'),
|
||||
},
|
||||
name: 'SystemNote',
|
||||
components: {
|
||||
GlIcon,
|
||||
NoteHeader,
|
||||
TimelineEntryItem,
|
||||
GlButton,
|
||||
GlSkeletonLoader,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
SafeHtml,
|
||||
},
|
||||
mixins: [descriptionVersionHistoryMixin, glFeatureFlagsMixin()],
|
||||
props: {
|
||||
note: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
expanded: false,
|
||||
lines: [],
|
||||
showLines: false,
|
||||
loadingDiff: false,
|
||||
isLoadingDescriptionVersion: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
targetNoteHash() {
|
||||
return getLocationHash();
|
||||
},
|
||||
descriptionVersions() {
|
||||
return [];
|
||||
},
|
||||
noteAnchorId() {
|
||||
return `note_${this.note.id}`;
|
||||
},
|
||||
isTargetNote() {
|
||||
return this.targetNoteHash === this.noteAnchorId;
|
||||
},
|
||||
toggleIcon() {
|
||||
return this.expanded ? 'chevron-up' : 'chevron-down';
|
||||
},
|
||||
// following 2 methods taken from code in `collapseLongCommitList` of notes.js:
|
||||
actionTextHtml() {
|
||||
return $(this.note.bodyHtml).unwrap().html();
|
||||
},
|
||||
hasMoreCommits() {
|
||||
return $(this.note.bodyHtml).filter('ul').children().length > MAX_VISIBLE_COMMIT_LIST_COUNT;
|
||||
},
|
||||
descriptionVersion() {
|
||||
return this.descriptionVersions[this.note.description_version_id];
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
$(this.$refs['gfm-content']).renderGFM();
|
||||
},
|
||||
methods: {
|
||||
fetchDescriptionVersion() {},
|
||||
softDeleteDescriptionVersion() {},
|
||||
|
||||
async toggleDiff() {
|
||||
this.showLines = !this.showLines;
|
||||
|
||||
if (!this.lines.length) {
|
||||
this.loadingDiff = true;
|
||||
const { data } = await axios.get(this.note.outdated_line_change_path);
|
||||
|
||||
this.lines = data.map((l) => ({
|
||||
...l,
|
||||
rich_text: l.rich_text.replace(/^[+ -]/, ''),
|
||||
}));
|
||||
this.loadingDiff = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
safeHtmlConfig: {
|
||||
ADD_TAGS: ['use'], // to support icon SVGs
|
||||
},
|
||||
userColorSchemeClass: window.gon.user_color_scheme,
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<timeline-entry-item
|
||||
:id="noteAnchorId"
|
||||
:class="{ target: isTargetNote, 'pr-0': shouldShowDescriptionVersion }"
|
||||
class="note system-note note-wrapper"
|
||||
>
|
||||
<div class="timeline-icon"><gl-icon :name="note.systemNoteIconName" /></div>
|
||||
<div class="timeline-content">
|
||||
<div class="note-header">
|
||||
<note-header
|
||||
:author="note.author"
|
||||
:created-at="note.createdAt"
|
||||
:note-id="note.id"
|
||||
:is-system-note="true"
|
||||
>
|
||||
<span ref="gfm-content" v-safe-html="actionTextHtml"></span>
|
||||
<template
|
||||
v-if="canSeeDescriptionVersion || note.outdated_line_change_path"
|
||||
#extra-controls
|
||||
>
|
||||
·
|
||||
<gl-button
|
||||
v-if="canSeeDescriptionVersion"
|
||||
variant="link"
|
||||
:icon="descriptionVersionToggleIcon"
|
||||
data-testid="compare-btn"
|
||||
class="gl-vertical-align-text-bottom gl-font-sm!"
|
||||
@click="toggleDescriptionVersion"
|
||||
>{{ __('Compare with previous version') }}</gl-button
|
||||
>
|
||||
<gl-button
|
||||
v-if="note.outdated_line_change_path"
|
||||
:icon="showLines ? 'chevron-up' : 'chevron-down'"
|
||||
variant="link"
|
||||
data-testid="outdated-lines-change-btn"
|
||||
class="gl-vertical-align-text-bottom gl-font-sm!"
|
||||
@click="toggleDiff"
|
||||
>
|
||||
{{ __('Compare changes') }}
|
||||
</gl-button>
|
||||
</template>
|
||||
</note-header>
|
||||
</div>
|
||||
<div class="note-body">
|
||||
<div
|
||||
v-safe-html="note.bodyHtml"
|
||||
:class="{ 'system-note-commit-list': hasMoreCommits, 'hide-shade': expanded }"
|
||||
class="note-text md"
|
||||
></div>
|
||||
<div v-if="hasMoreCommits" class="flex-list">
|
||||
<div class="system-note-commit-list-toggler flex-row" @click="expanded = !expanded">
|
||||
<gl-icon :name="toggleIcon" :size="8" class="gl-mr-2" />
|
||||
<span>{{ __('Toggle commit list') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="shouldShowDescriptionVersion" class="description-version pt-2">
|
||||
<pre v-if="isLoadingDescriptionVersion" class="loading-state">
|
||||
<gl-skeleton-loader />
|
||||
</pre>
|
||||
<pre v-else v-safe-html="descriptionVersion" class="wrapper mt-2"></pre>
|
||||
<gl-button
|
||||
v-if="displayDeleteButton"
|
||||
v-gl-tooltip
|
||||
:title="$options.i18n.deleteButtonLabel"
|
||||
:aria-label="$options.i18n.deleteButtonLabel"
|
||||
variant="default"
|
||||
category="tertiary"
|
||||
icon="remove"
|
||||
class="delete-description-history"
|
||||
data-testid="delete-description-version-button"
|
||||
@click="deleteDescriptionVersion"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="lines.length && showLines"
|
||||
class="diff-content outdated-lines-wrapper gl-border-solid gl-border-1 gl-border-gray-200 gl-mt-4 gl-rounded-small gl-overflow-hidden"
|
||||
>
|
||||
<table
|
||||
:class="$options.userColorSchemeClass"
|
||||
class="code js-syntax-highlight"
|
||||
data-testid="outdated-lines"
|
||||
>
|
||||
<tr v-for="line in lines" v-once :key="line.line_code" class="line_holder">
|
||||
<td
|
||||
:class="line.type"
|
||||
class="diff-line-num old_line gl-border-bottom-0! gl-border-top-0! gl-border-0! gl-rounded-0!"
|
||||
>
|
||||
{{ line.old_line }}
|
||||
</td>
|
||||
<td
|
||||
:class="line.type"
|
||||
class="diff-line-num new_line gl-border-bottom-0! gl-border-top-0!"
|
||||
>
|
||||
{{ line.new_line }}
|
||||
</td>
|
||||
<td
|
||||
:class="line.type"
|
||||
class="line_content gl-display-table-cell! gl-border-0! gl-rounded-0!"
|
||||
v-html="line.rich_text /* eslint-disable-line vue/no-v-html */"
|
||||
></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div v-else-if="showLines" class="mt-4">
|
||||
<gl-skeleton-loader />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</timeline-entry-item>
|
||||
</template>
|
||||
|
|
@ -30,6 +30,7 @@ import {
|
|||
WIDGET_TYPE_ITERATION,
|
||||
WORK_ITEM_TYPE_VALUE_ISSUE,
|
||||
WORK_ITEM_TYPE_VALUE_OBJECTIVE,
|
||||
WIDGET_TYPE_NOTES,
|
||||
} from '../constants';
|
||||
|
||||
import workItemDatesSubscription from '../graphql/work_item_dates.subscription.graphql';
|
||||
|
|
@ -49,6 +50,7 @@ import WorkItemDueDate from './work_item_due_date.vue';
|
|||
import WorkItemAssignees from './work_item_assignees.vue';
|
||||
import WorkItemLabels from './work_item_labels.vue';
|
||||
import WorkItemMilestone from './work_item_milestone.vue';
|
||||
import WorkItemNotes from './work_item_notes.vue';
|
||||
|
||||
export default {
|
||||
i18n,
|
||||
|
|
@ -75,6 +77,7 @@ export default {
|
|||
WorkItemIteration: () => import('ee_component/work_items/components/work_item_iteration.vue'),
|
||||
WorkItemMilestone,
|
||||
WorkItemTree,
|
||||
WorkItemNotes,
|
||||
},
|
||||
mixins: [glFeatureFlagMixin()],
|
||||
inject: ['fullPath'],
|
||||
|
|
@ -258,6 +261,9 @@ export default {
|
|||
workItemMilestone() {
|
||||
return this.isWidgetPresent(WIDGET_TYPE_MILESTONE);
|
||||
},
|
||||
workItemNotes() {
|
||||
return this.isWidgetPresent(WIDGET_TYPE_NOTES);
|
||||
},
|
||||
fetchByIid() {
|
||||
return this.glFeatures.useIidInWorkItemsPath && parseBoolean(getParameterByName('iid_path'));
|
||||
},
|
||||
|
|
@ -428,7 +434,7 @@ export default {
|
|||
<div class="gl-display-flex gl-align-items-center" data-testid="work-item-body">
|
||||
<ul
|
||||
v-if="parentWorkItem"
|
||||
class="list-unstyled gl-display-flex gl-mr-auto gl-max-w-26 gl-md-max-w-50p gl-min-w-0 gl-mb-0"
|
||||
class="list-unstyled gl-display-flex gl-mr-auto gl-max-w-26 gl-md-max-w-50p gl-min-w-0 gl-mb-0 gl-z-index-0"
|
||||
data-testid="work-item-parent"
|
||||
>
|
||||
<li class="gl-ml-n4 gl-display-flex gl-align-items-center gl-overflow-hidden">
|
||||
|
|
@ -589,6 +595,17 @@ export default {
|
|||
@addWorkItemChild="addChild"
|
||||
@removeChild="removeChild"
|
||||
/>
|
||||
<template v-if="workItemsMvc2Enabled">
|
||||
<work-item-notes
|
||||
v-if="workItemNotes"
|
||||
:work-item-id="workItem.id"
|
||||
:query-variables="queryVariables"
|
||||
:full-path="fullPath"
|
||||
:fetch-by-iid="fetchByIid"
|
||||
class="gl-pt-5"
|
||||
@error="updateError = $event"
|
||||
/>
|
||||
</template>
|
||||
<gl-empty-state
|
||||
v-if="error"
|
||||
:title="$options.i18n.fetchErrorTitle"
|
||||
|
|
|
|||
|
|
@ -139,6 +139,7 @@ export default {
|
|||
size="lg"
|
||||
modal-id="work-item-detail-modal"
|
||||
header-class="gl-p-0 gl-pb-2!"
|
||||
scrollable
|
||||
@hide="closeModal"
|
||||
>
|
||||
<gl-alert v-if="error" variant="danger" @dismiss="error = false">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,109 @@
|
|||
<script>
|
||||
import { GlSkeletonLoader } from '@gitlab/ui';
|
||||
import { s__ } from '~/locale';
|
||||
import SystemNote from '~/work_items/components/notes/system_note.vue';
|
||||
import { i18n, DEFAULT_PAGE_SIZE_NOTES } from '~/work_items/constants';
|
||||
import { getWorkItemNotesQuery } from '~/work_items/utils';
|
||||
|
||||
export default {
|
||||
i18n: {
|
||||
ACTIVITY_LABEL: s__('WorkItem|Activity'),
|
||||
},
|
||||
loader: {
|
||||
repeat: 10,
|
||||
width: 1000,
|
||||
height: 40,
|
||||
},
|
||||
components: {
|
||||
SystemNote,
|
||||
GlSkeletonLoader,
|
||||
},
|
||||
props: {
|
||||
workItemId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
queryVariables: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
fullPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
fetchByIid: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
areNotesLoading() {
|
||||
return this.$apollo.queries.workItemNotes.loading;
|
||||
},
|
||||
notes() {
|
||||
return this.workItemNotes?.nodes;
|
||||
},
|
||||
pageInfo() {
|
||||
return this.workItemNotes?.pageInfo;
|
||||
},
|
||||
},
|
||||
apollo: {
|
||||
workItemNotes: {
|
||||
query() {
|
||||
return getWorkItemNotesQuery(this.fetchByIid);
|
||||
},
|
||||
context: {
|
||||
isSingleRequest: true,
|
||||
},
|
||||
variables() {
|
||||
return {
|
||||
...this.queryVariables,
|
||||
pageSize: DEFAULT_PAGE_SIZE_NOTES,
|
||||
};
|
||||
},
|
||||
update(data) {
|
||||
const workItemWidgets = this.fetchByIid
|
||||
? data.workspace?.workItems?.nodes[0]?.widgets
|
||||
: data.workItem?.widgets;
|
||||
return workItemWidgets.find((widget) => widget.type === 'NOTES').discussions || [];
|
||||
},
|
||||
skip() {
|
||||
return !this.queryVariables.id && !this.queryVariables.iid;
|
||||
},
|
||||
error() {
|
||||
this.$emit('error', i18n.fetchError);
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="gl-border-t gl-mt-5">
|
||||
<label class="gl-mb-0">{{ $options.i18n.ACTIVITY_LABEL }}</label>
|
||||
<div v-if="areNotesLoading" class="gl-mt-5">
|
||||
<gl-skeleton-loader
|
||||
v-for="index in $options.loader.repeat"
|
||||
:key="index"
|
||||
:width="$options.loader.width"
|
||||
:height="$options.loader.height"
|
||||
preserve-aspect-ratio="xMinYMax meet"
|
||||
>
|
||||
<circle cx="20" cy="20" r="16" />
|
||||
<rect width="500" x="45" y="15" height="10" rx="4" />
|
||||
</gl-skeleton-loader>
|
||||
</div>
|
||||
<div v-else class="issuable-discussion gl-mb-5 work-item-notes">
|
||||
<template v-if="notes && notes.length">
|
||||
<ul class="notes main-notes-list timeline">
|
||||
<system-note
|
||||
v-for="note in notes"
|
||||
:key="note.notes.nodes[0].id"
|
||||
:note="note.notes.nodes[0]"
|
||||
/>
|
||||
</ul>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -19,6 +19,7 @@ export const WIDGET_TYPE_WEIGHT = 'WEIGHT';
|
|||
export const WIDGET_TYPE_HIERARCHY = 'HIERARCHY';
|
||||
export const WIDGET_TYPE_MILESTONE = 'MILESTONE';
|
||||
export const WIDGET_TYPE_ITERATION = 'ITERATION';
|
||||
export const WIDGET_TYPE_NOTES = 'NOTES';
|
||||
|
||||
export const WORK_ITEM_TYPE_ENUM_INCIDENT = 'INCIDENT';
|
||||
export const WORK_ITEM_TYPE_ENUM_ISSUE = 'ISSUE';
|
||||
|
|
@ -145,3 +146,4 @@ export const FORM_TYPES = {
|
|||
};
|
||||
|
||||
export const DEFAULT_PAGE_SIZE_ASSIGNEES = 10;
|
||||
export const DEFAULT_PAGE_SIZE_NOTES = 100;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
#import "~/graphql_shared/fragments/user.fragment.graphql"
|
||||
|
||||
fragment Discussion on Note {
|
||||
id
|
||||
body
|
||||
bodyHtml
|
||||
systemNoteIconName
|
||||
createdAt
|
||||
author {
|
||||
...User
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
#import "~/graphql_shared/fragments/page_info.fragment.graphql"
|
||||
#import "~/work_items/graphql/discussion.fragment.graphql"
|
||||
|
||||
query workItemNotes($id: WorkItemID!, $after: String, $pageSize: Int) {
|
||||
workItem(id: $id) {
|
||||
id
|
||||
iid
|
||||
widgets {
|
||||
... on WorkItemWidgetNotes {
|
||||
type
|
||||
discussions(first: $pageSize, after: $after, filter: ONLY_ACTIVITY) {
|
||||
pageInfo {
|
||||
...PageInfo
|
||||
}
|
||||
nodes {
|
||||
id
|
||||
notes {
|
||||
nodes {
|
||||
...Discussion
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
#import "~/graphql_shared/fragments/page_info.fragment.graphql"
|
||||
#import "~/work_items/graphql/discussion.fragment.graphql"
|
||||
|
||||
query workItemNotesByIid($fullPath: ID!, $iid: String, $after: String, $pageSize: Int) {
|
||||
workspace: project(fullPath: $fullPath) {
|
||||
id
|
||||
workItems(iid: $iid) {
|
||||
nodes {
|
||||
id
|
||||
iid
|
||||
widgets {
|
||||
... on WorkItemWidgetNotes {
|
||||
type
|
||||
discussions(first: $pageSize, after: $after, filter: ONLY_ACTIVITY) {
|
||||
pageInfo {
|
||||
...PageInfo
|
||||
}
|
||||
nodes {
|
||||
id
|
||||
notes {
|
||||
nodes {
|
||||
...Discussion
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -79,4 +79,7 @@ fragment WorkItemWidgets on WorkItemWidget {
|
|||
...MilestoneFragment
|
||||
}
|
||||
}
|
||||
... on WorkItemWidgetNotes {
|
||||
type
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,12 @@
|
|||
import workItemQuery from './graphql/work_item.query.graphql';
|
||||
import workItemByIidQuery from './graphql/work_item_by_iid.query.graphql';
|
||||
import workItemNotesIdQuery from './graphql/work_item_notes.query.graphql';
|
||||
import workItemNotesByIidQuery from './graphql/work_item_notes_by_iid.query.graphql';
|
||||
|
||||
export function getWorkItemQuery(isFetchedByIid) {
|
||||
return isFetchedByIid ? workItemByIidQuery : workItemQuery;
|
||||
}
|
||||
|
||||
export function getWorkItemNotesQuery(isFetchedByIid) {
|
||||
return isFetchedByIid ? workItemNotesByIidQuery : workItemNotesIdQuery;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,10 +13,6 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont
|
|||
@issuable =
|
||||
@merge_request ||=
|
||||
merge_request_includes(@project.merge_requests).find_by_iid!(params[:id])
|
||||
|
||||
return render_404 unless can?(current_user, :read_merge_request, @issuable)
|
||||
|
||||
@issuable
|
||||
end
|
||||
|
||||
def merge_request_includes(association)
|
||||
|
|
|
|||
|
|
@ -248,10 +248,7 @@ class IssuableFinder
|
|||
end
|
||||
|
||||
def init_collection
|
||||
return klass.all if params.user_can_see_all_issuables?
|
||||
|
||||
# Only admins and auditors can see hidden issuables, for other users we filter out hidden issuables
|
||||
klass.without_hidden
|
||||
klass.all
|
||||
end
|
||||
|
||||
def default_or_simple_sort?
|
||||
|
|
|
|||
|
|
@ -195,11 +195,6 @@ class IssuableFinder
|
|||
project || group
|
||||
end
|
||||
|
||||
def user_can_see_all_issuables?
|
||||
Ability.allowed?(current_user, :read_all_resources)
|
||||
end
|
||||
strong_memoize_attr :user_can_see_all_issuables?, :user_can_see_all_issuables
|
||||
|
||||
private
|
||||
|
||||
def projects_public_or_visible_to_user
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ class IssuesFinder < IssuableFinder
|
|||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def with_confidentiality_access_check
|
||||
return model_class.all if params.user_can_see_all_issuables?
|
||||
return model_class.all if params.user_can_see_all_issues?
|
||||
|
||||
# Only admins can see hidden issues, so for non-admins, we filter out any hidden issues
|
||||
issues = model_class.without_hidden
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ class IssuesFinder
|
|||
if parent
|
||||
Ability.allowed?(current_user, :read_confidential_issues, parent)
|
||||
else
|
||||
user_can_see_all_issuables?
|
||||
user_can_see_all_issues?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -54,6 +54,12 @@ class IssuesFinder
|
|||
|
||||
current_user.blank?
|
||||
end
|
||||
|
||||
def user_can_see_all_issues?
|
||||
strong_memoize(:user_can_see_all_issues) do
|
||||
Ability.allowed?(current_user, :read_all_resources)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ module ResolvesMergeRequests
|
|||
end
|
||||
|
||||
def unconditional_includes
|
||||
[:target_project, :author]
|
||||
[:target_project]
|
||||
end
|
||||
|
||||
def preloads
|
||||
|
|
|
|||
|
|
@ -275,7 +275,7 @@ module IssuablesHelper
|
|||
zoomMeetingUrl: ZoomMeeting.canonical_meeting_url(issuable),
|
||||
sentryIssueIdentifier: SentryIssue.find_by(issue: issuable)&.sentry_issue_identifier, # rubocop:disable CodeReuse/ActiveRecord
|
||||
iid: issuable.iid.to_s,
|
||||
isHidden: issuable_hidden?(issuable),
|
||||
isHidden: issue_hidden?(issuable),
|
||||
canCreateIncident: create_issue_type_allowed?(issuable.project, :incident)
|
||||
}
|
||||
end
|
||||
|
|
@ -372,20 +372,6 @@ module IssuablesHelper
|
|||
end
|
||||
end
|
||||
|
||||
def issuable_hidden?(issuable)
|
||||
Feature.enabled?(:ban_user_feature_flag) && issuable.hidden?
|
||||
end
|
||||
|
||||
def hidden_issuable_icon(issuable)
|
||||
return unless issuable_hidden?(issuable)
|
||||
|
||||
title = format(_('This %{issuable} is hidden because its author has been banned'),
|
||||
issuable: issuable.human_class_name)
|
||||
content_tag(:span, class: 'has-tooltip', title: title) do
|
||||
sprite_icon('spam', css_class: 'gl-vertical-align-text-bottom')
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def sidebar_gutter_collapsed?
|
||||
|
|
|
|||
|
|
@ -70,6 +70,18 @@ module IssuesHelper
|
|||
sprite_icon('eye-slash', css_class: 'gl-vertical-align-text-bottom') if issue.confidential?
|
||||
end
|
||||
|
||||
def issue_hidden?(issue)
|
||||
Feature.enabled?(:ban_user_feature_flag) && issue.hidden?
|
||||
end
|
||||
|
||||
def hidden_issue_icon(issue)
|
||||
return unless issue_hidden?(issue)
|
||||
|
||||
content_tag(:span, class: 'has-tooltip', title: _('This issue is hidden because its author has been banned')) do
|
||||
sprite_icon('spam', css_class: 'gl-vertical-align-text-bottom')
|
||||
end
|
||||
end
|
||||
|
||||
def award_user_list(awards, current_user, limit: 10)
|
||||
names = awards.map do |award|
|
||||
award.user == current_user ? 'You' : award.user.name
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ class BulkImports::Tracker < ApplicationRecord
|
|||
entity_scope = where(bulk_import_entity_id: entity_id)
|
||||
next_stage_scope = entity_scope.with_status(:created).select('MIN(stage)')
|
||||
|
||||
entity_scope.where(stage: next_stage_scope)
|
||||
entity_scope.where(stage: next_stage_scope).with_status(:created)
|
||||
}
|
||||
|
||||
def self.stage_running?(entity_id, stage)
|
||||
|
|
|
|||
|
|
@ -144,14 +144,6 @@ module Issuable
|
|||
includes(*associations)
|
||||
end
|
||||
|
||||
scope :without_hidden, -> {
|
||||
if Feature.enabled?(:ban_user_feature_flag)
|
||||
where.not(author_id: Users::BannedUser.all.select(:user_id))
|
||||
else
|
||||
all
|
||||
end
|
||||
}
|
||||
|
||||
attr_mentionable :title, pipeline: :single_line
|
||||
attr_mentionable :description
|
||||
|
||||
|
|
@ -235,10 +227,6 @@ module Issuable
|
|||
issuable_severity&.severity || IssuableSeverity::DEFAULT
|
||||
end
|
||||
|
||||
def hidden?
|
||||
author&.banned?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def description_max_length_for_new_records_is_valid
|
||||
|
|
|
|||
|
|
@ -178,6 +178,14 @@ class Issue < ApplicationRecord
|
|||
|
||||
scope :confidential_only, -> { where(confidential: true) }
|
||||
|
||||
scope :without_hidden, -> {
|
||||
if Feature.enabled?(:ban_user_feature_flag)
|
||||
where.not(author_id: Users::BannedUser.all.select(:user_id))
|
||||
else
|
||||
all
|
||||
end
|
||||
}
|
||||
|
||||
scope :counts_by_state, -> { reorder(nil).group(:state_id).count }
|
||||
|
||||
scope :service_desk, -> { where(author: ::User.support_bot) }
|
||||
|
|
@ -650,6 +658,10 @@ class Issue < ApplicationRecord
|
|||
end
|
||||
end
|
||||
|
||||
def hidden?
|
||||
author&.banned?
|
||||
end
|
||||
|
||||
# Necessary until all issues are backfilled and we add a NOT NULL constraint on the DB
|
||||
def work_item_type
|
||||
super || WorkItems::Type.default_by_type(issue_type)
|
||||
|
|
|
|||
|
|
@ -16,9 +16,6 @@ class IssuablePolicy < BasePolicy
|
|||
|
||||
condition(:is_incident) { @subject.incident? }
|
||||
|
||||
desc "Issuable is hidden"
|
||||
condition(:hidden, scope: :subject) { @subject.hidden? }
|
||||
|
||||
rule { can?(:guest_access) & assignee_or_author & ~is_incident }.policy do
|
||||
enable :read_issue
|
||||
enable :update_issue
|
||||
|
|
|
|||
|
|
@ -21,6 +21,9 @@ class IssuePolicy < IssuablePolicy
|
|||
desc "Issue is confidential"
|
||||
condition(:confidential, scope: :subject) { @subject.confidential? }
|
||||
|
||||
desc "Issue is hidden"
|
||||
condition(:hidden, scope: :subject) { @subject.hidden? }
|
||||
|
||||
desc "Issue is persisted"
|
||||
condition(:persisted, scope: :subject) { @subject.persisted? }
|
||||
|
||||
|
|
|
|||
|
|
@ -29,10 +29,6 @@ class MergeRequestPolicy < IssuablePolicy
|
|||
enable :update_subscription
|
||||
end
|
||||
|
||||
rule { hidden & ~admin }.policy do
|
||||
prevent :read_merge_request
|
||||
end
|
||||
|
||||
condition(:can_merge) { @subject.can_be_merged_by?(@user) }
|
||||
|
||||
rule { can_merge }.policy do
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
- if issue.confidential?
|
||||
%span.has-tooltip{ title: _('Confidential') }
|
||||
= confidential_icon(issue)
|
||||
= hidden_issuable_icon(issue)
|
||||
= hidden_issue_icon(issue)
|
||||
= link_to issue.title, issue_path(issue), class: 'js-prefetch-document'
|
||||
= render_if_exists 'projects/issues/subepic_flag', issue: issue
|
||||
- if issue.tasks?
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@
|
|||
.issuable-main-info
|
||||
.merge-request-title.title
|
||||
%span.merge-request-title-text.js-onboarding-mr-item
|
||||
= hidden_issuable_icon(merge_request)
|
||||
= link_to merge_request.title, merge_request_path(merge_request), class: 'js-prefetch-document'
|
||||
- if merge_request.tasks?
|
||||
%span.task-status.d-none.d-sm-inline-block
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
.detail-page-header.border-bottom-0.pt-0.pb-0.gl-display-block{ class: "gl-md-display-flex! #{'is-merge-request' if moved_mr_sidebar_enabled? && !fluid_layout}" }
|
||||
.detail-page-header-body
|
||||
.issuable-meta.gl-display-flex
|
||||
#js-issuable-header-warnings{ data: { hidden: issuable_hidden?(@merge_request).to_s } }
|
||||
#js-issuable-header-warnings
|
||||
%h1.title.page-title.gl-font-size-h-display.gl-my-0.gl-display-inline-block{ data: { qa_selector: 'title_content' } }
|
||||
= markdown_field(@merge_request, :title)
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
%span.gl-display-none.gl-sm-display-block.gl-ml-2
|
||||
= _('Open')
|
||||
|
||||
#js-issuable-header-warnings{ data: { hidden: issuable_hidden?(issuable).to_s } }
|
||||
#js-issuable-header-warnings{ data: { hidden: issue_hidden?(issuable).to_s } }
|
||||
= issuable_meta(issuable, @project)
|
||||
|
||||
%a.btn.gl-button.btn-default.btn-icon.float-right.gl-display-block.d-sm-none.gutter-toggle.issuable-gutter-toggle.js-sidebar-toggle{ href: "#" }
|
||||
|
|
|
|||
|
|
@ -74,6 +74,8 @@ module BulkImports
|
|||
source_version: source_version,
|
||||
importer: 'gitlab_migration'
|
||||
)
|
||||
|
||||
entity.fail_op!
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
|||
|
|
@ -4,11 +4,15 @@ module BulkImports
|
|||
class ExportRequestWorker
|
||||
include ApplicationWorker
|
||||
|
||||
data_consistency :always
|
||||
|
||||
idempotent!
|
||||
worker_has_external_dependencies!
|
||||
data_consistency :always
|
||||
feature_category :importers
|
||||
sidekiq_options dead: false, retry: 5
|
||||
worker_has_external_dependencies!
|
||||
|
||||
sidekiq_retries_exhausted do |msg, exception|
|
||||
new.perform_failure(exception, msg['args'].first)
|
||||
end
|
||||
|
||||
def perform(entity_id)
|
||||
entity = BulkImports::Entity.find(entity_id)
|
||||
|
|
@ -18,26 +22,12 @@ module BulkImports
|
|||
request_export(entity)
|
||||
|
||||
BulkImports::EntityWorker.perform_async(entity_id)
|
||||
rescue BulkImports::NetworkError => e
|
||||
if e.retriable?(entity)
|
||||
retry_request(e, entity)
|
||||
else
|
||||
log_exception(e,
|
||||
{
|
||||
bulk_import_entity_id: entity.id,
|
||||
bulk_import_id: entity.bulk_import_id,
|
||||
bulk_import_entity_type: entity.source_type,
|
||||
source_full_path: entity.source_full_path,
|
||||
message: "Request to export #{entity.source_type} failed",
|
||||
source_version: entity.bulk_import.source_version_info.to_s,
|
||||
importer: 'gitlab_migration'
|
||||
}
|
||||
)
|
||||
|
||||
BulkImports::Failure.create(failure_attributes(e, entity))
|
||||
|
||||
entity.fail_op!
|
||||
end
|
||||
|
||||
def perform_failure(exception, entity_id)
|
||||
entity = BulkImports::Entity.find(entity_id)
|
||||
|
||||
log_and_fail(exception, entity)
|
||||
end
|
||||
|
||||
private
|
||||
|
|
@ -104,22 +94,6 @@ module BulkImports
|
|||
end
|
||||
end
|
||||
|
||||
def retry_request(exception, entity)
|
||||
log_exception(exception,
|
||||
{
|
||||
message: 'Retrying export request',
|
||||
bulk_import_entity_id: entity.id,
|
||||
bulk_import_id: entity.bulk_import_id,
|
||||
bulk_import_entity_type: entity.source_type,
|
||||
source_full_path: entity.source_full_path,
|
||||
source_version: entity.bulk_import.source_version_info.to_s,
|
||||
importer: 'gitlab_migration'
|
||||
}
|
||||
)
|
||||
|
||||
self.class.perform_in(2.seconds, entity.id)
|
||||
end
|
||||
|
||||
def logger
|
||||
@logger ||= Gitlab::Import::Logger.build
|
||||
end
|
||||
|
|
@ -129,5 +103,23 @@ module BulkImports
|
|||
|
||||
logger.error(structured_payload(payload))
|
||||
end
|
||||
|
||||
def log_and_fail(exception, entity)
|
||||
log_exception(exception,
|
||||
{
|
||||
bulk_import_entity_id: entity.id,
|
||||
bulk_import_id: entity.bulk_import_id,
|
||||
bulk_import_entity_type: entity.source_type,
|
||||
source_full_path: entity.source_full_path,
|
||||
message: "Request to export #{entity.source_type} failed",
|
||||
source_version: entity.bulk_import.source_version_info.to_s,
|
||||
importer: 'gitlab_migration'
|
||||
}
|
||||
)
|
||||
|
||||
BulkImports::Failure.create(failure_attributes(exception, entity))
|
||||
|
||||
entity.fail_op!
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
module BulkImports
|
||||
class PipelineWorker # rubocop:disable Scalability/IdempotentWorker
|
||||
include ApplicationWorker
|
||||
include ExclusiveLeaseGuard
|
||||
|
||||
FILE_EXTRACTION_PIPELINE_PERFORM_DELAY = 10.seconds
|
||||
|
||||
|
|
@ -10,44 +11,24 @@ module BulkImports
|
|||
feature_category :importers
|
||||
sidekiq_options retry: false, dead: false
|
||||
worker_has_external_dependencies!
|
||||
deduplicate :until_executing
|
||||
|
||||
def perform(pipeline_tracker_id, stage, entity_id)
|
||||
@pipeline_tracker = ::BulkImports::Tracker
|
||||
.with_status(:enqueued)
|
||||
.find_by_id(pipeline_tracker_id)
|
||||
@entity = ::BulkImports::Entity.find(entity_id)
|
||||
@pipeline_tracker = ::BulkImports::Tracker.find(pipeline_tracker_id)
|
||||
|
||||
if pipeline_tracker.present?
|
||||
@entity = @pipeline_tracker.entity
|
||||
|
||||
logger.info(
|
||||
structured_payload(
|
||||
bulk_import_entity_id: entity.id,
|
||||
bulk_import_id: entity.bulk_import_id,
|
||||
bulk_import_entity_type: entity.source_type,
|
||||
source_full_path: entity.source_full_path,
|
||||
pipeline_name: pipeline_tracker.pipeline_name,
|
||||
message: 'Pipeline starting',
|
||||
source_version: source_version,
|
||||
importer: 'gitlab_migration'
|
||||
)
|
||||
)
|
||||
try_obtain_lease do
|
||||
if pipeline_tracker.enqueued?
|
||||
logger.info(log_attributes(message: 'Pipeline starting'))
|
||||
|
||||
run
|
||||
else
|
||||
@entity = ::BulkImports::Entity.find(entity_id)
|
||||
message = "Pipeline in #{pipeline_tracker.human_status_name} state instead of expected enqueued state"
|
||||
|
||||
logger.error(
|
||||
structured_payload(
|
||||
bulk_import_entity_id: entity_id,
|
||||
bulk_import_id: entity.bulk_import_id,
|
||||
bulk_import_entity_type: entity.source_type,
|
||||
source_full_path: entity.source_full_path,
|
||||
pipeline_tracker_id: pipeline_tracker_id,
|
||||
message: 'Unstarted pipeline not found',
|
||||
source_version: source_version,
|
||||
importer: 'gitlab_migration'
|
||||
)
|
||||
)
|
||||
logger.error(log_attributes(message: message))
|
||||
|
||||
fail_tracker(StandardError.new(message)) unless pipeline_tracker.finished? || pipeline_tracker.skipped?
|
||||
end
|
||||
end
|
||||
|
||||
ensure
|
||||
|
|
@ -83,29 +64,9 @@ module BulkImports
|
|||
def fail_tracker(exception)
|
||||
pipeline_tracker.update!(status_event: 'fail_op', jid: jid)
|
||||
|
||||
log_exception(exception,
|
||||
{
|
||||
bulk_import_entity_id: entity.id,
|
||||
bulk_import_id: entity.bulk_import_id,
|
||||
bulk_import_entity_type: entity.source_type,
|
||||
source_full_path: entity.source_full_path,
|
||||
pipeline_name: pipeline_tracker.pipeline_name,
|
||||
message: 'Pipeline failed',
|
||||
source_version: source_version,
|
||||
importer: 'gitlab_migration'
|
||||
}
|
||||
)
|
||||
log_exception(exception, log_attributes(message: 'Pipeline failed'))
|
||||
|
||||
Gitlab::ErrorTracking.track_exception(
|
||||
exception,
|
||||
bulk_import_entity_id: entity.id,
|
||||
bulk_import_id: entity.bulk_import_id,
|
||||
bulk_import_entity_type: entity.source_type,
|
||||
source_full_path: entity.source_full_path,
|
||||
pipeline_name: pipeline_tracker.pipeline_name,
|
||||
source_version: source_version,
|
||||
importer: 'gitlab_migration'
|
||||
)
|
||||
Gitlab::ErrorTracking.track_exception(exception, log_attributes)
|
||||
|
||||
BulkImports::Failure.create(
|
||||
bulk_import_entity_id: entity.id,
|
||||
|
|
@ -171,18 +132,7 @@ module BulkImports
|
|||
end
|
||||
|
||||
def retry_tracker(exception)
|
||||
log_exception(exception,
|
||||
{
|
||||
bulk_import_entity_id: entity.id,
|
||||
bulk_import_id: entity.bulk_import_id,
|
||||
bulk_import_entity_type: entity.source_type,
|
||||
source_full_path: entity.source_full_path,
|
||||
pipeline_name: pipeline_tracker.pipeline_name,
|
||||
message: "Retrying pipeline",
|
||||
source_version: source_version,
|
||||
importer: 'gitlab_migration'
|
||||
}
|
||||
)
|
||||
log_exception(exception, log_attributes(message: "Retrying pipeline"))
|
||||
|
||||
pipeline_tracker.update!(status_event: 'retry', jid: jid)
|
||||
|
||||
|
|
@ -190,29 +140,43 @@ module BulkImports
|
|||
end
|
||||
|
||||
def skip_tracker
|
||||
logger.info(
|
||||
structured_payload(
|
||||
bulk_import_entity_id: entity.id,
|
||||
bulk_import_id: entity.bulk_import_id,
|
||||
bulk_import_entity_type: entity.source_type,
|
||||
source_full_path: entity.source_full_path,
|
||||
pipeline_name: pipeline_tracker.pipeline_name,
|
||||
message: 'Skipping pipeline due to failed entity',
|
||||
source_version: source_version,
|
||||
importer: 'gitlab_migration'
|
||||
)
|
||||
)
|
||||
logger.info(log_attributes(message: 'Skipping pipeline due to failed entity'))
|
||||
|
||||
pipeline_tracker.update!(status_event: 'skip', jid: jid)
|
||||
end
|
||||
|
||||
def log_attributes(extra = {})
|
||||
structured_payload(
|
||||
{
|
||||
bulk_import_entity_id: entity.id,
|
||||
bulk_import_id: entity.bulk_import_id,
|
||||
bulk_import_entity_type: entity.source_type,
|
||||
source_full_path: entity.source_full_path,
|
||||
pipeline_tracker_id: pipeline_tracker.id,
|
||||
pipeline_name: pipeline_tracker.pipeline_name,
|
||||
pipeline_tracker_state: pipeline_tracker.human_status_name,
|
||||
source_version: source_version,
|
||||
importer: 'gitlab_migration'
|
||||
}.merge(extra)
|
||||
)
|
||||
end
|
||||
|
||||
def log_exception(exception, payload)
|
||||
Gitlab::ExceptionLogFormatter.format!(exception, payload)
|
||||
|
||||
logger.error(structured_payload(payload))
|
||||
end
|
||||
|
||||
def time_since_entity_created
|
||||
Time.zone.now - entity.created_at
|
||||
end
|
||||
|
||||
def lease_timeout
|
||||
30
|
||||
end
|
||||
|
||||
def lease_key
|
||||
"gitlab:bulk_imports:pipeline_worker:#{pipeline_tracker.id}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ The contracts themselves are stored in [`/spec/contracts/contracts`](https://git
|
|||
|
||||
Before running the consumer tests, go to `spec/contracts/consumer` and run `npm install`. To run all the consumer tests, you just need to run `npm test -- /specs`. Otherwise, to run a specific spec file, replace `/specs` with the specific spec filename.
|
||||
|
||||
You can also run tests from the root directory of the project, using the command `yarn jest:contract`.
|
||||
|
||||
### Run the provider tests
|
||||
|
||||
Before running the provider tests, make sure your GDK (GitLab Development Kit) is fully set up and running. You can follow the setup instructions detailed in the [GDK repository](https://gitlab.com/gitlab-org/gitlab-development-kit/-/tree/main). To run the provider tests, you use Rake tasks that can be found in [`./lib/tasks/contracts`](https://gitlab.com/gitlab-org/gitlab/-/tree/master/lib/tasks/contracts). To get a list of all the Rake tasks related to the provider tests, run `bundle exec rake -T contracts`. For example:
|
||||
|
|
|
|||
|
|
@ -223,7 +223,7 @@ On self-managed GitLab, by default this feature is available.
|
|||
To hide the feature, ask an administrator to [disable the feature flag](../../administration/feature_flags.md) named `ban_user_feature_flag`.
|
||||
On GitLab.com, this feature is available to GitLab.com administrators only.
|
||||
|
||||
GitLab administrators can ban and unban users. Banned users are blocked, and their issues and merge requests are hidden.
|
||||
GitLab administrators can ban and unban users. Banned users are blocked, and their issues are hidden.
|
||||
The banned user's comments are still displayed. Hiding a banned user's comments is [tracked in this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/327356).
|
||||
|
||||
### Ban a user
|
||||
|
|
|
|||
|
|
@ -63,6 +63,10 @@ The following items are changed when they are imported:
|
|||
|
||||
## User assignment
|
||||
|
||||
Prerequisite:
|
||||
|
||||
- Authentication token with administrator access.
|
||||
|
||||
When issues and pull requests are importing, the importer tries to find the author's email address
|
||||
with a confirmed email address in the GitLab user database. If no such user is available, the
|
||||
project creator is set as the author. The importer appends a note in the comment to mark the
|
||||
|
|
|
|||
|
|
@ -91,7 +91,10 @@ If you are using a self-managed GitLab instance or if you are importing from Git
|
|||
|
||||
### Use a GitHub token
|
||||
|
||||
NOTE:
|
||||
Prerequisite:
|
||||
|
||||
- Authentication token with administrator access.
|
||||
|
||||
Using a personal access token to import projects is not recommended. If you are a GitLab.com user,
|
||||
you can use a personal access token to import your project from GitHub, but this method cannot
|
||||
associate all user activity (such as issues and pull requests) with matching GitLab users.
|
||||
|
|
@ -225,7 +228,7 @@ When they are imported, supported GitHub branch protection rules are mapped to e
|
|||
- Project-wide GitLab settings.
|
||||
|
||||
| GitHub rule | GitLab rule | Introduced in |
|
||||
|:------------------------------------------------------------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------------------------------------------------------------------|
|
||||
| :---------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------ |
|
||||
| **Require conversation resolution before merging** for the project's default branch | **All threads must be resolved** [project setting](../../discussions/index.md#prevent-merge-unless-all-threads-are-resolved) | [GitLab 15.5](https://gitlab.com/gitlab-org/gitlab/-/issues/371110) |
|
||||
| **Require a pull request before merging** | **No one** option in the **Allowed to push** list of [branch protection settings](../protected_branches.md#configure-a-protected-branch) | [GitLab 15.5](https://gitlab.com/gitlab-org/gitlab/-/issues/370951) |
|
||||
| **Require signed commits** for the project's default branch | **Reject unsigned commits** GitLab [push rule](../repository/push_rules.md#prevent-unintended-consequences) **(PREMIUM)** | [GitLab 15.5](https://gitlab.com/gitlab-org/gitlab/-/issues/370949) |
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ module.exports = (path, options = {}) => {
|
|||
]);
|
||||
}
|
||||
|
||||
const glob = `${path}/**/*_spec.js`;
|
||||
const glob = `${path}/**/*@([._])spec.js`;
|
||||
let testMatch = [`<rootDir>/${glob}`];
|
||||
if (IS_EE) {
|
||||
testMatch.push(`<rootDir>/ee/${glob}`);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
module.exports = () => {
|
||||
return {
|
||||
modulePaths: ['<rootDir>/spec/contracts/consumer/node_modules/'],
|
||||
roots: ['spec/contracts/consumer', 'ee/spec/contracts/consumer'],
|
||||
};
|
||||
};
|
||||
|
|
@ -3298,7 +3298,7 @@ msgstr ""
|
|||
msgid "AdminUsers|Is using seat"
|
||||
msgstr ""
|
||||
|
||||
msgid "AdminUsers|Issues and merge requests authored by this user are hidden from other users."
|
||||
msgid "AdminUsers|Issues authored by this user are hidden from other users."
|
||||
msgstr ""
|
||||
|
||||
msgid "AdminUsers|It's you!"
|
||||
|
|
@ -42070,9 +42070,6 @@ msgstr ""
|
|||
msgid "This %{issuableDisplayName} is locked. Only project members can comment."
|
||||
msgstr ""
|
||||
|
||||
msgid "This %{issuable} is hidden because its author has been banned"
|
||||
msgstr ""
|
||||
|
||||
msgid "This %{issuable} is locked. Only %{strong_open}project members%{strong_close} can comment."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -46875,6 +46872,9 @@ msgstr ""
|
|||
msgid "WorkItem|%{workItemType} deleted"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Activity"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Add"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
"jest-debug": "node --inspect-brk node_modules/.bin/jest --runInBand",
|
||||
"jest:ci": "jest --config jest.config.js --ci --coverage --testSequencer ./scripts/frontend/parallel_ci_sequencer.js",
|
||||
"jest:ci:minimal": "jest --config jest.config.js --ci --coverage --findRelatedTests $(cat $RSPEC_CHANGED_FILES_PATH) --passWithNoTests --testSequencer ./scripts/frontend/parallel_ci_sequencer.js",
|
||||
"jest:contract": "PACT_DO_NOT_TRACK=true jest --config jest.config.contract.js --runInBand",
|
||||
"jest:integration": "jest --config jest.config.integration.js",
|
||||
"lint:eslint": "node scripts/frontend/eslint.js",
|
||||
"lint:eslint:fix": "node scripts/frontend/eslint.js --fix",
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
source 'https://rubygems.org'
|
||||
|
||||
gem 'gitlab-qa', '~> 8', '>= 8.13.1', require: 'gitlab/qa'
|
||||
gem 'gitlab-qa', '~> 8', '>= 8.14.0', require: 'gitlab/qa'
|
||||
gem 'activesupport', '~> 6.1.4.7' # This should stay in sync with the root's Gemfile
|
||||
gem 'allure-rspec', '~> 2.20.0'
|
||||
gem 'capybara', '~> 3.38.0'
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ GEM
|
|||
gitlab (4.18.0)
|
||||
httparty (~> 0.18)
|
||||
terminal-table (>= 1.5.1)
|
||||
gitlab-qa (8.13.1)
|
||||
gitlab-qa (8.14.0)
|
||||
activesupport (~> 6.1)
|
||||
gitlab (~> 4.18.0)
|
||||
http (~> 5.0)
|
||||
|
|
@ -310,7 +310,7 @@ DEPENDENCIES
|
|||
faraday-retry (~> 2.0)
|
||||
fog-core (= 2.1.0)
|
||||
fog-google (~> 1.19)
|
||||
gitlab-qa (~> 8, >= 8.13.1)
|
||||
gitlab-qa (~> 8, >= 8.14.0)
|
||||
influxdb-client (~> 2.8)
|
||||
knapsack (~> 4.0)
|
||||
nokogiri (~> 1.13, >= 1.13.10)
|
||||
|
|
|
|||
37
qa/README.md
37
qa/README.md
|
|
@ -88,6 +88,43 @@ bundle exec bin/qa Test::Instance::All {GDK IP ADDRESS}
|
|||
- Note: If you want to run tests requiring SSH against GDK, you will need to [modify your GDK setup](https://gitlab.com/gitlab-org/gitlab-qa/blob/master/docs/run_qa_against_gdk.md).
|
||||
- Note: If this is your first time running GDK, you can use the password pre-set for `root`. [See supported GitLab environment variables](https://gitlab.com/gitlab-org/gitlab-qa/-/blob/master/docs/what_tests_can_be_run.md#supported-gitlab-environment-variables). If you have changed your `root` password, use that when exporting `GITLAB_INITIAL_ROOT_PASSWORD`.
|
||||
|
||||
#### Run the end-to-end tests on GitLab in Docker
|
||||
|
||||
1. [GitLab can be installed in Docker](https://docs.gitlab.com/ee/install/docker.html). You can use the following command to start an instance that you can visit at `http://127.0.0.1`:
|
||||
|
||||
```
|
||||
docker run \
|
||||
--hostname 127.0.0.1 \
|
||||
--publish 80:80 --publish 22:22 \
|
||||
--name gitlab \
|
||||
--shm-size 256m \
|
||||
--env GITLAB_OMNIBUS_CONFIG="gitlab_rails['initial_root_password']='5iveL\!fe';" \
|
||||
gitlab/gitlab-ee:nightly
|
||||
```
|
||||
|
||||
Notes:
|
||||
- If you are on a Mac with [Apple Silicon](https://support.apple.com/en-us/HT211814), you will also need to add: `--platform=linux/amd64`
|
||||
- If you are on Windows, please be aware that [Docker Desktop must be set to use Linux containers](https://learn.microsoft.com/en-us/virtualization/windowscontainers/quick-start/quick-start-windows-10-linux#run-your-first-linux-container).
|
||||
|
||||
|
||||
2. Navigate to the QA folder and run the following commands.
|
||||
|
||||
```bash
|
||||
cd gitlab/qa
|
||||
bundle install
|
||||
export WEBDRIVER_HEADLESS=false
|
||||
export GITLAB_INITIAL_ROOT_PASSWORD=5iveL\!fe
|
||||
export QA_GITLAB_URL="http://127.0.0.1"
|
||||
```
|
||||
|
||||
3. Most tests that do not require special setup could then be run with the following command.
|
||||
|
||||
```bash
|
||||
bundle exec rspec <path/to/spec.rb>
|
||||
```
|
||||
|
||||
- Note: See the section above for situations that might require adjustment to the commands or to the configuration of the GitLab instance. [You can find more information in the documentation](https://docs.gitlab.com/ee/install/docker.html).
|
||||
|
||||
#### Running EE tests
|
||||
|
||||
When running EE tests you'll need to have a license available. GitLab engineers can [request a license](https://about.gitlab.com/handbook/developer-onboarding/#working-on-gitlab-ee).
|
||||
|
|
|
|||
|
|
@ -79,9 +79,12 @@ module QA
|
|||
super
|
||||
end
|
||||
|
||||
# @param name [Symbol] name of the data_qa_selector element
|
||||
# @param page [Class] a target page class to check existence of (class must inherit from QA::Page::Base)
|
||||
# @param kwargs [Hash] keyword arguments to pass to Capybara finder
|
||||
def click_element(name, page = nil, **kwargs)
|
||||
msg = ["clicking :#{highlight_element(name)}"]
|
||||
msg << ", expecting to be at #{page.class}" if page
|
||||
msg << "and ensuring #{page} is present" if page
|
||||
|
||||
log(msg.join(' '), :info)
|
||||
log("with args #{kwargs}")
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ require 'capybara/dsl'
|
|||
RSpec.describe QA::Support::Page::Logging do
|
||||
let(:page) { double.as_null_object }
|
||||
let(:logger) { Gitlab::QA::TestLogger.logger(level: ::Logger::DEBUG, source: 'QA Tests') }
|
||||
let(:page_class) { class_double('QA::Page::TestPage') }
|
||||
|
||||
before do
|
||||
allow(QA::Runtime::Logger).to receive(:logger).and_return(logger)
|
||||
|
|
@ -66,6 +67,14 @@ RSpec.describe QA::Support::Page::Logging do
|
|||
.to output(/clicking :element/).to_stdout_from_any_process
|
||||
end
|
||||
|
||||
it 'logs click_element with a page' do
|
||||
allow(page_class).to receive(:validate_elements_present!).and_return(true)
|
||||
allow(page_class).to receive(:to_s).and_return('QA::Page::TestPage')
|
||||
|
||||
expect { subject.click_element(:element, page_class) }
|
||||
.to output(/clicking :element and ensuring QA::Page::TestPage is present/).to_stdout_from_any_process
|
||||
end
|
||||
|
||||
it 'logs fill_element' do
|
||||
expect { subject.fill_element(:element, 'foo') }
|
||||
.to output(/filling :element with "foo"/).to_stdout_from_any_process
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
14.17.5
|
||||
|
|
@ -22,5 +22,8 @@
|
|||
"devDependencies": {
|
||||
"@babel/preset-env": "^7.18.2",
|
||||
"babel-jest": "^28.1.1"
|
||||
},
|
||||
"config": {
|
||||
"pact_do_not_track": true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,13 @@
|
|||
require 'spec_helper'
|
||||
require 'zeitwerk'
|
||||
require_relative 'helpers/users_helper'
|
||||
require_relative('../../../ee/spec/contracts/provider/spec_helper') if Gitlab.ee?
|
||||
require Rails.root.join("spec/support/helpers/rails_helpers.rb")
|
||||
require Rails.root.join("spec/support/helpers/stub_env.rb")
|
||||
|
||||
# Opt out of telemetry collection. We can't allow all engineers, and users who install GitLab from source, to be
|
||||
# automatically enrolled in sending data on their usage without their knowledge.
|
||||
ENV['PACT_DO_NOT_TRACK'] = 'true'
|
||||
|
||||
RSpec.configure do |config|
|
||||
config.include Devise::Test::IntegrationHelpers
|
||||
|
|
@ -19,6 +26,8 @@ end
|
|||
|
||||
Pact.configure do |config|
|
||||
config.include FactoryBot::Syntax::Methods
|
||||
config.include RailsHelpers
|
||||
config.include StubENV
|
||||
end
|
||||
|
||||
module SpecHelper
|
||||
|
|
|
|||
|
|
@ -7,23 +7,22 @@ FactoryBot.define do
|
|||
stage { 0 }
|
||||
has_next_page { false }
|
||||
sequence(:pipeline_name) { |n| "pipeline_name_#{n}" }
|
||||
sequence(:jid) { |n| "bulk_import_entity_#{n}" }
|
||||
|
||||
trait :started do
|
||||
status { 1 }
|
||||
|
||||
sequence(:jid) { |n| "bulk_import_entity_#{n}" }
|
||||
end
|
||||
|
||||
trait :finished do
|
||||
status { 2 }
|
||||
|
||||
sequence(:jid) { |n| "bulk_import_entity_#{n}" }
|
||||
end
|
||||
|
||||
trait :failed do
|
||||
status { -1 }
|
||||
end
|
||||
|
||||
sequence(:jid) { |n| "bulk_import_entity_#{n}" }
|
||||
trait :skipped do
|
||||
status { -2 }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,26 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'Admin views hidden merge request', feature_category: :code_review do
|
||||
context 'when signed in as admin and viewing a hidden merge request', :js do
|
||||
let_it_be(:admin) { create(:admin) }
|
||||
let_it_be(:author) { create(:user, :banned) }
|
||||
let_it_be(:project) { create(:project, :repository) }
|
||||
let!(:merge_request) { create(:merge_request, source_project: project, author: author) }
|
||||
|
||||
before do
|
||||
sign_in(admin)
|
||||
gitlab_enable_admin_mode_sign_in(admin)
|
||||
visit(project_merge_request_path(project, merge_request))
|
||||
end
|
||||
|
||||
it 'shows a hidden merge request icon' do
|
||||
page.within('.detail-page-header-body') do
|
||||
tooltip = format(_('This %{issuable} is hidden because its author has been banned'), issuable: 'merge request')
|
||||
expect(page).to have_css("div[data-testid='hidden'][title='#{tooltip}']")
|
||||
expect(page).to have_css('svg[data-testid="spam-icon"]')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'Admin views hidden merge requests', feature_category: :code_review do
|
||||
context 'when signed in as admin and viewing a hidden merge request' do
|
||||
let_it_be(:admin) { create(:admin) }
|
||||
let_it_be(:author) { create(:user, :banned) }
|
||||
let_it_be(:project) { create(:project) }
|
||||
let!(:merge_request) { create(:merge_request, source_project: project, author: author) }
|
||||
|
||||
before do
|
||||
sign_in(admin)
|
||||
gitlab_enable_admin_mode_sign_in(admin)
|
||||
visit(project_merge_requests_path(project))
|
||||
end
|
||||
|
||||
it 'shows a hidden merge request icon' do
|
||||
page.within("#merge_request_#{merge_request.id}") do
|
||||
tooltip = format(_('This %{issuable} is hidden because its author has been banned'), issuable: 'merge request')
|
||||
expect(page).to have_css("span[title='#{tooltip}']")
|
||||
expect(page).to have_css('svg[data-testid="spam-icon"]')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -57,7 +57,6 @@ describe('IssuableHeaderWarnings', () => {
|
|||
beforeEach(() => {
|
||||
store.getters.getNoteableData.confidential = confidentialStatus;
|
||||
store.getters.getNoteableData.discussion_locked = lockStatus;
|
||||
store.getters.getNoteableData.targetType = issuableType;
|
||||
|
||||
createComponent({ store, provide: { hidden: hiddenStatus } });
|
||||
});
|
||||
|
|
@ -85,7 +84,7 @@ describe('IssuableHeaderWarnings', () => {
|
|||
|
||||
if (hiddenStatus) {
|
||||
expect(hiddenIcon.attributes('title')).toBe(
|
||||
`This ${issuableType} is hidden because its author has been banned`,
|
||||
'This issue is hidden because its author has been banned',
|
||||
);
|
||||
expect(getBinding(hiddenIcon.element, 'gl-tooltip')).not.toBeUndefined();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,111 @@
|
|||
import { GlIcon } from '@gitlab/ui';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import $ from 'jquery';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import WorkItemSystemNote from '~/work_items/components/notes/system_note.vue';
|
||||
import NoteHeader from '~/notes/components/note_header.vue';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
|
||||
describe('system note component', () => {
|
||||
let wrapper;
|
||||
let props;
|
||||
let mock;
|
||||
|
||||
const findTimelineIcon = () => wrapper.findComponent(GlIcon);
|
||||
const findSystemNoteMessage = () => wrapper.findComponent(NoteHeader);
|
||||
const findOutdatedLineButton = () =>
|
||||
wrapper.findComponent('[data-testid="outdated-lines-change-btn"]');
|
||||
const findOutdatedLines = () => wrapper.findComponent('[data-testid="outdated-lines"]');
|
||||
|
||||
const createComponent = (propsData = {}) => {
|
||||
wrapper = shallowMount(WorkItemSystemNote, {
|
||||
propsData,
|
||||
slots: {
|
||||
'extra-controls':
|
||||
'<gl-button data-testid="outdated-lines-change-btn">Compare with last version</gl-button>',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
props = {
|
||||
note: {
|
||||
id: '1424',
|
||||
author: {
|
||||
id: 1,
|
||||
name: 'Root',
|
||||
username: 'root',
|
||||
state: 'active',
|
||||
avatarUrl: 'path',
|
||||
path: '/root',
|
||||
},
|
||||
bodyHtml: '<p dir="auto">closed</p>',
|
||||
systemNoteIconName: 'status_closed',
|
||||
createdAt: '2017-08-02T10:51:58.559Z',
|
||||
},
|
||||
};
|
||||
|
||||
mock = new MockAdapter(axios);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore();
|
||||
});
|
||||
|
||||
it('should render a list item with correct id', () => {
|
||||
createComponent(props);
|
||||
|
||||
expect(wrapper.attributes('id')).toBe(`note_${props.note.id}`);
|
||||
});
|
||||
|
||||
// Note: The test case below is to handle a use case related to vuex store but since this does not
|
||||
// have a vuex store , disabling it now will be fixing it in the next iteration
|
||||
// eslint-disable-next-line jest/no-disabled-tests
|
||||
it.skip('should render target class is note is target note', () => {
|
||||
createComponent(props);
|
||||
|
||||
expect(wrapper.classes()).toContain('target');
|
||||
});
|
||||
|
||||
it('should render svg icon', () => {
|
||||
createComponent(props);
|
||||
|
||||
expect(findTimelineIcon().exists()).toBe(true);
|
||||
});
|
||||
|
||||
// Redcarpet Markdown renderer wraps text in `<p>` tags
|
||||
// we need to strip them because they break layout of commit lists in system notes:
|
||||
// https://gitlab.com/gitlab-org/gitlab-foss/uploads/b07a10670919254f0220d3ff5c1aa110/jqzI.png
|
||||
it('removes wrapping paragraph from note HTML', () => {
|
||||
createComponent(props);
|
||||
|
||||
expect(findSystemNoteMessage().html()).toContain('<span>closed</span>');
|
||||
});
|
||||
|
||||
it('should renderGFM onMount', () => {
|
||||
const renderGFMSpy = jest.spyOn($.fn, 'renderGFM');
|
||||
|
||||
createComponent(props);
|
||||
|
||||
expect(renderGFMSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// eslint-disable-next-line jest/no-disabled-tests
|
||||
it.skip('renders outdated code lines', async () => {
|
||||
mock
|
||||
.onGet('/outdated_line_change_path')
|
||||
.reply(200, [
|
||||
{ rich_text: 'console.log', type: 'new', line_code: '123', old_line: null, new_line: 1 },
|
||||
]);
|
||||
|
||||
createComponent({
|
||||
note: { ...props.note, outdated_line_change_path: '/outdated_line_change_path' },
|
||||
});
|
||||
|
||||
await findOutdatedLineButton().vm.$emit('click');
|
||||
await waitForPromises();
|
||||
|
||||
expect(findOutdatedLines().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
import { GlSkeletonLoader } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import Vue from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import SystemNote from '~/work_items/components/notes/system_note.vue';
|
||||
import WorkItemNotes from '~/work_items/components/work_item_notes.vue';
|
||||
import workItemNotesQuery from '~/work_items/graphql/work_item_notes.query.graphql';
|
||||
import workItemNotesByIidQuery from '~/work_items/graphql/work_item_notes_by_iid.query.graphql';
|
||||
import { WIDGET_TYPE_NOTES } from '~/work_items/constants';
|
||||
import {
|
||||
mockWorkItemNotesResponse,
|
||||
workItemQueryResponse,
|
||||
mockWorkItemNotesByIidResponse,
|
||||
} from '../mock_data';
|
||||
|
||||
const mockWorkItemId = workItemQueryResponse.data.workItem.id;
|
||||
const mockNotesWidgetResponse = mockWorkItemNotesResponse.data.workItem.widgets.find(
|
||||
(widget) => widget.type === WIDGET_TYPE_NOTES,
|
||||
);
|
||||
|
||||
const mockNotesByIidWidgetResponse = mockWorkItemNotesByIidResponse.data.workspace.workItems.nodes[0].widgets.find(
|
||||
(widget) => widget.type === WIDGET_TYPE_NOTES,
|
||||
);
|
||||
|
||||
describe('WorkItemNotes component', () => {
|
||||
let wrapper;
|
||||
|
||||
Vue.use(VueApollo);
|
||||
|
||||
const findAllSystemNotes = () => wrapper.findAllComponents(SystemNote);
|
||||
const findActivityLabel = () => wrapper.find('label');
|
||||
const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
|
||||
const workItemNotesQueryHandler = jest.fn().mockResolvedValue(mockWorkItemNotesResponse);
|
||||
const workItemNotesByIidQueryHandler = jest
|
||||
.fn()
|
||||
.mockResolvedValue(mockWorkItemNotesByIidResponse);
|
||||
|
||||
const createComponent = ({ workItemId = mockWorkItemId, fetchByIid = false } = {}) => {
|
||||
wrapper = shallowMount(WorkItemNotes, {
|
||||
apolloProvider: createMockApollo([
|
||||
[workItemNotesQuery, workItemNotesQueryHandler],
|
||||
[workItemNotesByIidQuery, workItemNotesByIidQueryHandler],
|
||||
]),
|
||||
propsData: {
|
||||
workItemId,
|
||||
queryVariables: {
|
||||
id: workItemId,
|
||||
},
|
||||
fullPath: 'test-path',
|
||||
fetchByIid,
|
||||
},
|
||||
provide: {
|
||||
glFeatures: {
|
||||
useIidInWorkItemsPath: fetchByIid,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
it('renders activity label', () => {
|
||||
expect(findActivityLabel().exists()).toBe(true);
|
||||
});
|
||||
|
||||
describe('when notes are loading', () => {
|
||||
it('renders skeleton loader', () => {
|
||||
expect(findSkeletonLoader().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('does not render system notes', () => {
|
||||
expect(findAllSystemNotes().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when notes have been loaded', () => {
|
||||
it('does not render skeleton loader', () => {
|
||||
expect(findSkeletonLoader().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders system notes to the length of the response', async () => {
|
||||
await waitForPromises();
|
||||
expect(findAllSystemNotes()).toHaveLength(mockNotesWidgetResponse.discussions.nodes.length);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the notes are fetched by `iid`', () => {
|
||||
beforeEach(async () => {
|
||||
createComponent({ workItemId: mockWorkItemId, fetchByIid: true });
|
||||
await waitForPromises();
|
||||
});
|
||||
|
||||
it('shows the notes list', () => {
|
||||
expect(findAllSystemNotes()).toHaveLength(
|
||||
mockNotesByIidWidgetResponse.discussions.nodes.length,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1394,3 +1394,288 @@ export const projectWorkItemResponse = {
|
|||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const mockWorkItemNotesResponse = {
|
||||
data: {
|
||||
workItem: {
|
||||
id: 'gid://gitlab/WorkItem/600',
|
||||
iid: '60',
|
||||
widgets: [
|
||||
{
|
||||
__typename: 'WorkItemWidgetIteration',
|
||||
},
|
||||
{
|
||||
__typename: 'WorkItemWidgetWeight',
|
||||
},
|
||||
{
|
||||
__typename: 'WorkItemWidgetAssignees',
|
||||
},
|
||||
{
|
||||
__typename: 'WorkItemWidgetLabels',
|
||||
},
|
||||
{
|
||||
__typename: 'WorkItemWidgetDescription',
|
||||
},
|
||||
{
|
||||
__typename: 'WorkItemWidgetHierarchy',
|
||||
},
|
||||
{
|
||||
__typename: 'WorkItemWidgetStartAndDueDate',
|
||||
},
|
||||
{
|
||||
__typename: 'WorkItemWidgetMilestone',
|
||||
},
|
||||
{
|
||||
type: 'NOTES',
|
||||
discussions: {
|
||||
pageInfo: {
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
startCursor: null,
|
||||
endCursor: null,
|
||||
__typename: 'PageInfo',
|
||||
},
|
||||
nodes: [
|
||||
{
|
||||
id:
|
||||
'gid://gitlab/IndividualNoteDiscussion/8bbc4890b6ff0f2cde93a5a0947cd2b8a13d3b6e',
|
||||
notes: {
|
||||
nodes: [
|
||||
{
|
||||
id: 'gid://gitlab/Note/2428',
|
||||
body: 'added #31 as parent issue',
|
||||
bodyHtml:
|
||||
'<p data-sourcepos="1:1-1:25" dir="auto">added <a href="/flightjs/Flight/-/issues/31" data-reference-type="issue" data-original="#31" data-link="false" data-link-reference="false" data-project="6" data-issue="224" data-project-path="flightjs/Flight" data-iid="31" data-issue-type="issue" data-container=body data-placement="top" title="Perferendis est quae totam quia laborum tempore ut voluptatem." class="gfm gfm-issue">#31</a> as parent issue</p>',
|
||||
systemNoteIconName: 'link',
|
||||
createdAt: '2022-11-14T04:18:59Z',
|
||||
author: {
|
||||
avatarUrl:
|
||||
'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
|
||||
id: 'gid://gitlab/User/1',
|
||||
name: 'Administrator',
|
||||
username: 'root',
|
||||
webUrl: 'http://127.0.0.1:3000/root',
|
||||
__typename: 'UserCore',
|
||||
},
|
||||
__typename: 'Note',
|
||||
},
|
||||
],
|
||||
__typename: 'NoteConnection',
|
||||
},
|
||||
__typename: 'Discussion',
|
||||
},
|
||||
{
|
||||
id:
|
||||
'gid://gitlab/IndividualNoteDiscussion/7b08b89a728a5ceb7de8334246837ba1d07270dc',
|
||||
notes: {
|
||||
nodes: [
|
||||
{
|
||||
id: 'gid://gitlab/MilestoneNote/not-persisted',
|
||||
body: 'changed milestone to %5',
|
||||
bodyHtml:
|
||||
'<p data-sourcepos="1:1-1:23" dir="auto">changed milestone to <a href="/flightjs/Flight/-/milestones/5" data-reference-type="milestone" data-original="%5" data-link="false" data-link-reference="false" data-project="6" data-milestone="30" data-container=body data-placement="top" title="" class="gfm gfm-milestone has-tooltip">%v4.0</a></p>',
|
||||
systemNoteIconName: 'clock',
|
||||
createdAt: '2022-11-14T04:18:59Z',
|
||||
author: {
|
||||
avatarUrl:
|
||||
'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
|
||||
id: 'gid://gitlab/User/1',
|
||||
name: 'Administrator',
|
||||
username: 'root',
|
||||
webUrl: 'http://127.0.0.1:3000/root',
|
||||
__typename: 'UserCore',
|
||||
},
|
||||
__typename: 'Note',
|
||||
},
|
||||
],
|
||||
__typename: 'NoteConnection',
|
||||
},
|
||||
__typename: 'Discussion',
|
||||
},
|
||||
{
|
||||
id:
|
||||
'gid://gitlab/IndividualNoteDiscussion/0f2f195ec0d1ef95ee9d5b10446b8e96a7d83864',
|
||||
notes: {
|
||||
nodes: [
|
||||
{
|
||||
id: 'gid://gitlab/WeightNote/not-persisted',
|
||||
body: 'changed weight to 89',
|
||||
bodyHtml: '<p dir="auto">changed weight to <strong>89</strong></p>',
|
||||
systemNoteIconName: 'weight',
|
||||
createdAt: '2022-11-25T07:16:20Z',
|
||||
author: {
|
||||
avatarUrl:
|
||||
'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
|
||||
id: 'gid://gitlab/User/1',
|
||||
name: 'Administrator',
|
||||
username: 'root',
|
||||
webUrl: 'http://127.0.0.1:3000/root',
|
||||
__typename: 'UserCore',
|
||||
},
|
||||
__typename: 'Note',
|
||||
},
|
||||
],
|
||||
__typename: 'NoteConnection',
|
||||
},
|
||||
__typename: 'Discussion',
|
||||
},
|
||||
],
|
||||
__typename: 'DiscussionConnection',
|
||||
},
|
||||
__typename: 'WorkItemWidgetNotes',
|
||||
},
|
||||
],
|
||||
__typename: 'WorkItem',
|
||||
},
|
||||
},
|
||||
};
|
||||
export const mockWorkItemNotesByIidResponse = {
|
||||
data: {
|
||||
workspace: {
|
||||
id: 'gid://gitlab/Project/6',
|
||||
workItems: {
|
||||
nodes: [
|
||||
{
|
||||
id: 'gid://gitlab/WorkItem/600',
|
||||
iid: '51',
|
||||
widgets: [
|
||||
{
|
||||
__typename: 'WorkItemWidgetIteration',
|
||||
},
|
||||
{
|
||||
__typename: 'WorkItemWidgetWeight',
|
||||
},
|
||||
{
|
||||
__typename: 'WorkItemWidgetHealthStatus',
|
||||
},
|
||||
{
|
||||
__typename: 'WorkItemWidgetAssignees',
|
||||
},
|
||||
{
|
||||
__typename: 'WorkItemWidgetLabels',
|
||||
},
|
||||
{
|
||||
__typename: 'WorkItemWidgetDescription',
|
||||
},
|
||||
{
|
||||
__typename: 'WorkItemWidgetHierarchy',
|
||||
},
|
||||
{
|
||||
__typename: 'WorkItemWidgetStartAndDueDate',
|
||||
},
|
||||
{
|
||||
__typename: 'WorkItemWidgetMilestone',
|
||||
},
|
||||
{
|
||||
type: 'NOTES',
|
||||
discussions: {
|
||||
pageInfo: {
|
||||
hasNextPage: true,
|
||||
hasPreviousPage: false,
|
||||
startCursor: null,
|
||||
endCursor:
|
||||
'eyJjcmVhdGVkX2F0IjoiMjAyMi0xMS0xNCAwNDoxOTowMC4wOTkxMTcwMDAgKzAwMDAiLCJpZCI6IjQyNyIsIl9rZCI6Im4ifQ==',
|
||||
__typename: 'PageInfo',
|
||||
},
|
||||
nodes: [
|
||||
{
|
||||
id:
|
||||
'gid://gitlab/IndividualNoteDiscussion/8bbc4890b6ff0f2cde93a5a0947cd2b8a13d3b6e',
|
||||
notes: {
|
||||
nodes: [
|
||||
{
|
||||
id: 'gid://gitlab/Note/2428',
|
||||
body: 'added #31 as parent issue',
|
||||
bodyHtml:
|
||||
'\u003cp data-sourcepos="1:1-1:25" dir="auto"\u003eadded \u003ca href="/flightjs/Flight/-/issues/31" data-reference-type="issue" data-original="#31" data-link="false" data-link-reference="false" data-project="6" data-issue="224" data-project-path="flightjs/Flight" data-iid="31" data-issue-type="issue" data-container="body" data-placement="top" title="Perferendis est quae totam quia laborum tempore ut voluptatem." class="gfm gfm-issue"\u003e#31\u003c/a\u003e as parent issue\u003c/p\u003e',
|
||||
systemNoteIconName: 'link',
|
||||
createdAt: '2022-11-14T04:18:59Z',
|
||||
author: {
|
||||
id: 'gid://gitlab/User/1',
|
||||
avatarUrl:
|
||||
'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
|
||||
name: 'Administrator',
|
||||
username: 'root',
|
||||
webUrl: 'http://127.0.0.1:3000/root',
|
||||
__typename: 'UserCore',
|
||||
},
|
||||
__typename: 'Note',
|
||||
},
|
||||
],
|
||||
__typename: 'NoteConnection',
|
||||
},
|
||||
__typename: 'Discussion',
|
||||
},
|
||||
{
|
||||
id:
|
||||
'gid://gitlab/IndividualNoteDiscussion/7b08b89a728a5ceb7de8334246837ba1d07270dc',
|
||||
notes: {
|
||||
nodes: [
|
||||
{
|
||||
id:
|
||||
'gid://gitlab/MilestoneNote/7b08b89a728a5ceb7de8334246837ba1d07270dc',
|
||||
body: 'changed milestone to %5',
|
||||
bodyHtml:
|
||||
'\u003cp data-sourcepos="1:1-1:23" dir="auto"\u003echanged milestone to \u003ca href="/flightjs/Flight/-/milestones/5" data-reference-type="milestone" data-original="%5" data-link="false" data-link-reference="false" data-project="6" data-milestone="30" data-container="body" data-placement="top" title="" class="gfm gfm-milestone has-tooltip"\u003e%v4.0\u003c/a\u003e\u003c/p\u003e',
|
||||
systemNoteIconName: 'clock',
|
||||
createdAt: '2022-11-14T04:18:59Z',
|
||||
author: {
|
||||
id: 'gid://gitlab/User/1',
|
||||
avatarUrl:
|
||||
'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
|
||||
name: 'Administrator',
|
||||
username: 'root',
|
||||
webUrl: 'http://127.0.0.1:3000/root',
|
||||
__typename: 'UserCore',
|
||||
},
|
||||
__typename: 'Note',
|
||||
},
|
||||
],
|
||||
__typename: 'NoteConnection',
|
||||
},
|
||||
__typename: 'Discussion',
|
||||
},
|
||||
{
|
||||
id:
|
||||
'gid://gitlab/IndividualNoteDiscussion/addbc177f7664699a135130ab05ffb78c57e4db3',
|
||||
notes: {
|
||||
nodes: [
|
||||
{
|
||||
id:
|
||||
'gid://gitlab/IterationNote/addbc177f7664699a135130ab05ffb78c57e4db3',
|
||||
body: 'changed iteration to *iteration:5352',
|
||||
bodyHtml:
|
||||
'\u003cp data-sourcepos="1:1-1:36" dir="auto"\u003echanged iteration to \u003ca href="/groups/flightjs/-/iterations/5352" data-reference-type="iteration" data-original="*iteration:5352" data-link="false" data-link-reference="false" data-project="6" data-iteration="5352" data-container="body" data-placement="top" title="Iteration" class="gfm gfm-iteration has-tooltip"\u003eEt autem debitis nam suscipit eos ut. Jul 13, 2022 - Jul 19, 2022\u003c/a\u003e\u003c/p\u003e',
|
||||
systemNoteIconName: 'iteration',
|
||||
createdAt: '2022-11-14T04:19:00Z',
|
||||
author: {
|
||||
id: 'gid://gitlab/User/1',
|
||||
avatarUrl:
|
||||
'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
|
||||
name: 'Administrator',
|
||||
username: 'root',
|
||||
webUrl: 'http://127.0.0.1:3000/root',
|
||||
__typename: 'UserCore',
|
||||
},
|
||||
__typename: 'Note',
|
||||
},
|
||||
],
|
||||
__typename: 'NoteConnection',
|
||||
},
|
||||
__typename: 'Discussion',
|
||||
},
|
||||
],
|
||||
__typename: 'DiscussionConnection',
|
||||
},
|
||||
__typename: 'WorkItemWidgetNotes',
|
||||
},
|
||||
],
|
||||
__typename: 'WorkItem',
|
||||
},
|
||||
],
|
||||
__typename: 'WorkItemConnection',
|
||||
},
|
||||
__typename: 'Project',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -629,66 +629,4 @@ RSpec.describe IssuablesHelper do
|
|||
expect(helper.sidebar_milestone_tooltip_label(milestone)).to eq('<img onerror=alert(1)><br/>Milestone')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#issuable_hidden?' do
|
||||
let_it_be(:issuable) { build(:issue) }
|
||||
|
||||
context 'when issuable is hidden' do
|
||||
let_it_be(:banned_user) { build(:user, :banned) }
|
||||
let_it_be(:hidden_issuable) { build(:issue, author: banned_user) }
|
||||
|
||||
context 'when `ban_user_feature_flag` feature flag is enabled' do
|
||||
it 'returns `true`' do
|
||||
expect(helper.issuable_hidden?(hidden_issuable)).to eq(true)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when `ban_user_feature_flag` feature flag is disabled' do
|
||||
before do
|
||||
stub_feature_flags(ban_user_feature_flag: false)
|
||||
end
|
||||
|
||||
it 'returns `false`' do
|
||||
expect(helper.issuable_hidden?(hidden_issuable)).to eq(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when issuable is not hidden' do
|
||||
it 'returns `false`' do
|
||||
expect(helper.issuable_hidden?(issuable)).to eq(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#hidden_issuable_icon' do
|
||||
let_it_be(:banned_user) { build(:user, :banned) }
|
||||
let_it_be(:hidden_issuable) { build(:issue, author: banned_user) }
|
||||
let_it_be(:issuable) { build(:issue) }
|
||||
let_it_be(:mock_svg) { '<svg></svg>'.html_safe }
|
||||
|
||||
before do
|
||||
allow(helper).to receive(:sprite_icon).and_return(mock_svg)
|
||||
end
|
||||
|
||||
context 'when issuable is hidden' do
|
||||
it 'returns icon with tooltip' do
|
||||
expect(helper.hidden_issuable_icon(hidden_issuable)).to eq("<span class=\"has-tooltip\" title=\"This issue is hidden because its author has been banned\">#{mock_svg}</span>")
|
||||
end
|
||||
|
||||
context 'when issuable is a merge request' do
|
||||
let_it_be(:hidden_issuable) { build(:merge_request, author: banned_user) }
|
||||
|
||||
it 'returns icon with tooltip' do
|
||||
expect(helper.hidden_issuable_icon(hidden_issuable)).to eq("<span class=\"has-tooltip\" title=\"This merge request is hidden because its author has been banned\">#{mock_svg}</span>")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when issuable is not hidden' do
|
||||
it 'returns `nil`' do
|
||||
expect(helper.hidden_issuable_icon(issuable)).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -508,4 +508,55 @@ RSpec.describe IssuesHelper do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#issue_hidden?' do
|
||||
context 'when issue is hidden' do
|
||||
let_it_be(:banned_user) { build(:user, :banned) }
|
||||
let_it_be(:hidden_issue) { build(:issue, author: banned_user) }
|
||||
|
||||
context 'when `ban_user_feature_flag` feature flag is enabled' do
|
||||
it 'returns `true`' do
|
||||
expect(helper.issue_hidden?(hidden_issue)).to eq(true)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when `ban_user_feature_flag` feature flag is disabled' do
|
||||
before do
|
||||
stub_feature_flags(ban_user_feature_flag: false)
|
||||
end
|
||||
|
||||
it 'returns `false`' do
|
||||
expect(helper.issue_hidden?(hidden_issue)).to eq(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when issue is not hidden' do
|
||||
it 'returns `false`' do
|
||||
expect(helper.issue_hidden?(issue)).to eq(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#hidden_issue_icon' do
|
||||
let_it_be(:banned_user) { build(:user, :banned) }
|
||||
let_it_be(:hidden_issue) { build(:issue, author: banned_user) }
|
||||
let_it_be(:mock_svg) { '<svg></svg>'.html_safe }
|
||||
|
||||
before do
|
||||
allow(helper).to receive(:sprite_icon).and_return(mock_svg)
|
||||
end
|
||||
|
||||
context 'when issue is hidden' do
|
||||
it 'returns icon with tooltip' do
|
||||
expect(helper.hidden_issue_icon(hidden_issue)).to eq("<span class=\"has-tooltip\" title=\"This issue is hidden because its author has been banned\">#{mock_svg}</span>")
|
||||
end
|
||||
end
|
||||
|
||||
context 'when issue is not hidden' do
|
||||
it 'returns `nil`' do
|
||||
expect(helper.hidden_issue_icon(issue)).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -54,13 +54,16 @@ RSpec.describe BulkImports::Tracker, type: :model do
|
|||
|
||||
it 'returns the not started pipeline trackers from the minimum stage number' do
|
||||
stage_1_tracker = create(:bulk_import_tracker, entity: entity, stage: 1)
|
||||
stage_1_finished_tracker = create(:bulk_import_tracker, :finished, entity: entity, stage: 1)
|
||||
stage_1_failed_tracker = create(:bulk_import_tracker, :failed, entity: entity, stage: 1)
|
||||
stage_1_skipped_tracker = create(:bulk_import_tracker, :skipped, entity: entity, stage: 1)
|
||||
stage_2_tracker = create(:bulk_import_tracker, entity: entity, stage: 2)
|
||||
|
||||
expect(described_class.next_pipeline_trackers_for(entity.id))
|
||||
.to include(stage_1_tracker)
|
||||
|
||||
expect(described_class.next_pipeline_trackers_for(entity.id))
|
||||
.not_to include(stage_2_tracker)
|
||||
.not_to include(stage_2_tracker, stage_1_finished_tracker, stage_1_failed_tracker, stage_1_skipped_tracker)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -337,53 +337,6 @@ RSpec.describe Issuable do
|
|||
it { expect(MergeRequest.to_ability_name).to eq("merge_request") }
|
||||
end
|
||||
|
||||
describe '.without_hidden' do
|
||||
let_it_be(:banned_user) { create(:user, :banned) }
|
||||
|
||||
where(issuable_type: [:issue, :merge_request])
|
||||
|
||||
with_them do
|
||||
let!(:public_issuable) { create(issuable_type, :closed) }
|
||||
let!(:hidden_issuable) { create(issuable_type, :closed, author: banned_user) }
|
||||
|
||||
subject { issuable_type.to_s.classify.constantize.without_hidden }
|
||||
|
||||
it 'only returns public issuables' do
|
||||
expect(subject).to contain_exactly(public_issuable)
|
||||
end
|
||||
|
||||
context 'when feature flag is disabled' do
|
||||
before do
|
||||
stub_feature_flags(ban_user_feature_flag: false)
|
||||
end
|
||||
|
||||
it 'returns public and hidden issuables' do
|
||||
expect(subject).to contain_exactly(public_issuable, hidden_issuable)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#hidden?' do
|
||||
let_it_be(:author) { create(:user) }
|
||||
|
||||
where(issuable_type: [:issue, :merge_request])
|
||||
|
||||
with_them do
|
||||
let(:issuable) { build_stubbed(issuable_type, author: author) }
|
||||
|
||||
subject { issuable.hidden? }
|
||||
|
||||
it { is_expected.to eq(false) }
|
||||
|
||||
context 'when the author is banned' do
|
||||
let_it_be(:author) { create(:user, :banned) }
|
||||
|
||||
it { is_expected.to eq(true) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#sort_by_attribute" do
|
||||
let(:project) { create(:project) }
|
||||
|
||||
|
|
|
|||
|
|
@ -1432,6 +1432,26 @@ RSpec.describe Issue do
|
|||
end
|
||||
end
|
||||
|
||||
describe '.without_hidden' do
|
||||
let_it_be(:banned_user) { create(:user, :banned) }
|
||||
let_it_be(:public_issue) { create(:issue, project: reusable_project) }
|
||||
let_it_be(:hidden_issue) { create(:issue, project: reusable_project, author: banned_user) }
|
||||
|
||||
it 'only returns without_hidden issues' do
|
||||
expect(described_class.without_hidden).to eq([public_issue])
|
||||
end
|
||||
|
||||
context 'when feature flag is disabled' do
|
||||
before do
|
||||
stub_feature_flags(ban_user_feature_flag: false)
|
||||
end
|
||||
|
||||
it 'returns public and hidden issues' do
|
||||
expect(described_class.without_hidden).to contain_exactly(public_issue, hidden_issue)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.by_project_id_and_iid' do
|
||||
let_it_be(:issue_a) { create(:issue, project: reusable_project) }
|
||||
let_it_be(:issue_b) { create(:issue, iid: issue_a.iid) }
|
||||
|
|
|
|||
|
|
@ -461,20 +461,4 @@ RSpec.describe MergeRequestPolicy do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the author of the merge request is banned' do
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:admin) { create(:user, :admin) }
|
||||
let_it_be(:author) { create(:user, :banned) }
|
||||
let_it_be(:project) { create(:project, :public) }
|
||||
let_it_be(:hidden_merge_request) { create(:merge_request, source_project: project, author: author) }
|
||||
|
||||
it 'does not allow non-admin user to read the merge_request' do
|
||||
expect(permissions(user, hidden_merge_request)).not_to be_allowed(:read_merge_request)
|
||||
end
|
||||
|
||||
it 'allows admin to read the merge_request', :enable_admin_mode do
|
||||
expect(permissions(admin, hidden_merge_request)).to be_allowed(:read_merge_request)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ RSpec.describe Projects::MergeRequestsController, feature_category: :source_code
|
|||
let_it_be(:user) { merge_request.author }
|
||||
|
||||
describe 'GET #show' do
|
||||
context 'when logged in' do
|
||||
before do
|
||||
login_as(user)
|
||||
end
|
||||
|
|
@ -20,21 +19,6 @@ RSpec.describe Projects::MergeRequestsController, feature_category: :source_code
|
|||
end
|
||||
end
|
||||
|
||||
context 'when the author of the merge request is banned' do
|
||||
let_it_be(:user) { create(:user, :banned) }
|
||||
let_it_be(:project) { create(:project, :public) }
|
||||
let_it_be(:merge_request) { create(:merge_request, source_project: project, author: user) }
|
||||
|
||||
subject { response }
|
||||
|
||||
before do
|
||||
get project_merge_request_path(project, merge_request)
|
||||
end
|
||||
|
||||
it { is_expected.to have_gitlab_http_status(:not_found) }
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET #discussions' do
|
||||
let_it_be(:discussion) { create(:discussion_note_on_merge_request, noteable: merge_request, project: project) }
|
||||
let_it_be(:discussion_reply) { create(:discussion_note_on_merge_request, noteable: merge_request, project: project, in_reply_to: discussion) }
|
||||
|
|
|
|||
|
|
@ -114,6 +114,8 @@ RSpec.describe BulkImports::EntityWorker do
|
|||
)
|
||||
|
||||
subject
|
||||
|
||||
expect(entity.reload.failed?).to eq(true)
|
||||
end
|
||||
|
||||
context 'in first stage' do
|
||||
|
|
|
|||
|
|
@ -2,9 +2,10 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe BulkImports::ExportRequestWorker do
|
||||
RSpec.describe BulkImports::ExportRequestWorker, feature_category: :importers do
|
||||
let_it_be(:bulk_import) { create(:bulk_import) }
|
||||
let_it_be(:config) { create(:bulk_import_configuration, bulk_import: bulk_import) }
|
||||
let_it_be(:entity) { create(:bulk_import_entity, bulk_import: bulk_import) }
|
||||
let_it_be(:version_url) { 'https://gitlab.example/api/v4/version' }
|
||||
|
||||
let(:response_double) { double(code: 200, success?: true, parsed_response: {}) }
|
||||
|
|
@ -31,73 +32,6 @@ RSpec.describe BulkImports::ExportRequestWorker do
|
|||
perform_multiple(job_args)
|
||||
end
|
||||
|
||||
context 'when network error is raised' do
|
||||
let(:exception) { BulkImports::NetworkError.new('Export error') }
|
||||
|
||||
before do
|
||||
allow_next_instance_of(BulkImports::Clients::HTTP) do |client|
|
||||
allow(client).to receive(:post).and_raise(exception).twice
|
||||
end
|
||||
end
|
||||
|
||||
context 'when error is retriable' do
|
||||
it 'logs retry request and reenqueues' do
|
||||
allow(exception).to receive(:retriable?).twice.and_return(true)
|
||||
|
||||
expect_next_instance_of(Gitlab::Import::Logger) do |logger|
|
||||
expect(logger).to receive(:error).with(
|
||||
a_hash_including(
|
||||
'bulk_import_entity_id' => entity.id,
|
||||
'bulk_import_id' => entity.bulk_import_id,
|
||||
'bulk_import_entity_type' => entity.source_type,
|
||||
'source_full_path' => entity.source_full_path,
|
||||
'exception.backtrace' => anything,
|
||||
'exception.class' => 'BulkImports::NetworkError',
|
||||
'exception.message' => 'Export error',
|
||||
'message' => 'Retrying export request',
|
||||
'importer' => 'gitlab_migration',
|
||||
'source_version' => entity.bulk_import.source_version_info.to_s
|
||||
)
|
||||
).twice
|
||||
end
|
||||
|
||||
expect(described_class).to receive(:perform_in).twice.with(2.seconds, entity.id)
|
||||
|
||||
perform_multiple(job_args)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when error is not retriable' do
|
||||
it 'logs export failure and marks entity as failed' do
|
||||
allow(exception).to receive(:retriable?).twice.and_return(false)
|
||||
|
||||
expect_next_instance_of(Gitlab::Import::Logger) do |logger|
|
||||
expect(logger).to receive(:error).with(
|
||||
a_hash_including(
|
||||
'bulk_import_entity_id' => entity.id,
|
||||
'bulk_import_id' => entity.bulk_import_id,
|
||||
'bulk_import_entity_type' => entity.source_type,
|
||||
'source_full_path' => entity.source_full_path,
|
||||
'exception.backtrace' => anything,
|
||||
'exception.class' => 'BulkImports::NetworkError',
|
||||
'exception.message' => 'Export error',
|
||||
'message' => "Request to export #{entity.source_type} failed",
|
||||
'importer' => 'gitlab_migration',
|
||||
'source_version' => entity.bulk_import.source_version_info.to_s
|
||||
)
|
||||
).twice
|
||||
end
|
||||
|
||||
perform_multiple(job_args)
|
||||
|
||||
failure = entity.failures.last
|
||||
|
||||
expect(failure.pipeline_class).to eq('ExportRequestWorker')
|
||||
expect(failure.exception_message).to eq('Export error')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when source id is nil' do
|
||||
let(:entity_source_id) { 'gid://gitlab/Model/1234567' }
|
||||
|
||||
|
|
@ -179,4 +113,24 @@ RSpec.describe BulkImports::ExportRequestWorker do
|
|||
it_behaves_like 'requests relations export for api resource'
|
||||
end
|
||||
end
|
||||
|
||||
describe '#sidekiq_retries_exhausted' do
|
||||
it 'logs export failure and marks entity as failed' do
|
||||
entity = create(:bulk_import_entity, bulk_import: bulk_import)
|
||||
error = 'Exhausted error!'
|
||||
|
||||
expect_next_instance_of(Gitlab::Import::Logger) do |logger|
|
||||
expect(logger)
|
||||
.to receive(:error)
|
||||
.with(hash_including('message' => "Request to export #{entity.source_type} failed"))
|
||||
end
|
||||
|
||||
described_class
|
||||
.sidekiq_retries_exhausted_block
|
||||
.call({ 'args' => [entity.id] }, StandardError.new(error))
|
||||
|
||||
expect(entity.reload.failed?).to eq(true)
|
||||
expect(entity.failures.last.exception_message).to eq(error)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe BulkImports::PipelineWorker do
|
||||
RSpec.describe BulkImports::PipelineWorker, feature_category: :importers do
|
||||
let(:pipeline_class) do
|
||||
Class.new do
|
||||
def initialize(_); end
|
||||
|
|
@ -19,6 +19,15 @@ RSpec.describe BulkImports::PipelineWorker do
|
|||
let_it_be(:config) { create(:bulk_import_configuration, bulk_import: bulk_import) }
|
||||
let_it_be(:entity) { create(:bulk_import_entity, bulk_import: bulk_import) }
|
||||
|
||||
let(:pipeline_tracker) do
|
||||
create(
|
||||
:bulk_import_tracker,
|
||||
entity: entity,
|
||||
pipeline_name: 'FakePipeline',
|
||||
status_event: 'enqueue'
|
||||
)
|
||||
end
|
||||
|
||||
before do
|
||||
stub_const('FakePipeline', pipeline_class)
|
||||
|
||||
|
|
@ -60,45 +69,12 @@ RSpec.describe BulkImports::PipelineWorker do
|
|||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'successfully runs the pipeline' do
|
||||
let(:pipeline_tracker) do
|
||||
create(
|
||||
:bulk_import_tracker,
|
||||
entity: entity,
|
||||
pipeline_name: 'FakePipeline',
|
||||
status_event: 'enqueue'
|
||||
)
|
||||
end
|
||||
end
|
||||
it_behaves_like 'successfully runs the pipeline'
|
||||
|
||||
context 'when the pipeline cannot be found' do
|
||||
it 'logs the error' do
|
||||
pipeline_tracker = create(
|
||||
:bulk_import_tracker,
|
||||
:finished,
|
||||
entity: entity,
|
||||
pipeline_name: 'FakePipeline'
|
||||
)
|
||||
|
||||
expect_next_instance_of(Gitlab::Import::Logger) do |logger|
|
||||
expect(logger)
|
||||
.to receive(:error)
|
||||
.with(
|
||||
hash_including(
|
||||
'pipeline_tracker_id' => pipeline_tracker.id,
|
||||
'bulk_import_entity_id' => entity.id,
|
||||
'bulk_import_id' => entity.bulk_import_id,
|
||||
'bulk_import_entity_type' => entity.source_type,
|
||||
'source_full_path' => entity.source_full_path,
|
||||
'source_version' => entity.bulk_import.source_version_info.to_s,
|
||||
'message' => 'Unstarted pipeline not found'
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
expect(BulkImports::EntityWorker)
|
||||
.to receive(:perform_async)
|
||||
.with(entity.id, pipeline_tracker.stage)
|
||||
context 'when exclusive lease cannot be obtained' do
|
||||
it 'does not run the pipeline' do
|
||||
expect(subject).to receive(:try_obtain_lease).and_return(false)
|
||||
expect(subject).not_to receive(:run)
|
||||
|
||||
subject.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
|
||||
end
|
||||
|
|
@ -145,13 +121,15 @@ RSpec.describe BulkImports::PipelineWorker do
|
|||
.to receive(:track_exception)
|
||||
.with(
|
||||
instance_of(StandardError),
|
||||
bulk_import_entity_id: entity.id,
|
||||
bulk_import_id: entity.bulk_import.id,
|
||||
bulk_import_entity_type: entity.source_type,
|
||||
source_full_path: entity.source_full_path,
|
||||
pipeline_name: pipeline_tracker.pipeline_name,
|
||||
importer: 'gitlab_migration',
|
||||
source_version: entity.bulk_import.source_version_info.to_s
|
||||
hash_including(
|
||||
'bulk_import_entity_id' => entity.id,
|
||||
'bulk_import_id' => entity.bulk_import.id,
|
||||
'bulk_import_entity_type' => entity.source_type,
|
||||
'source_full_path' => entity.source_full_path,
|
||||
'pipeline_name' => pipeline_tracker.pipeline_name,
|
||||
'importer' => 'gitlab_migration',
|
||||
'source_version' => entity.bulk_import.source_version_info.to_s
|
||||
)
|
||||
)
|
||||
|
||||
expect(BulkImports::EntityWorker)
|
||||
|
|
@ -179,6 +157,111 @@ RSpec.describe BulkImports::PipelineWorker do
|
|||
expect(pipeline_tracker.jid).to eq('jid')
|
||||
end
|
||||
|
||||
shared_examples 'successfully runs the pipeline' do
|
||||
it 'runs the given pipeline successfully' do
|
||||
expect_next_instance_of(Gitlab::Import::Logger) do |logger|
|
||||
expect(logger)
|
||||
.to receive(:info)
|
||||
.with(
|
||||
hash_including(
|
||||
'pipeline_name' => 'FakePipeline',
|
||||
'bulk_import_id' => entity.bulk_import_id,
|
||||
'bulk_import_entity_id' => entity.id,
|
||||
'bulk_import_entity_type' => entity.source_type,
|
||||
'source_full_path' => entity.source_full_path
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
expect(BulkImports::EntityWorker)
|
||||
.to receive(:perform_async)
|
||||
.with(entity.id, pipeline_tracker.stage)
|
||||
|
||||
allow(subject).to receive(:jid).and_return('jid')
|
||||
|
||||
subject.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
|
||||
|
||||
pipeline_tracker.reload
|
||||
|
||||
expect(pipeline_tracker.status_name).to eq(:finished)
|
||||
expect(pipeline_tracker.jid).to eq('jid')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when enqueued pipeline cannot be found' do
|
||||
shared_examples 'logs the error' do
|
||||
it 'logs the error' do
|
||||
expect_next_instance_of(Gitlab::Import::Logger) do |logger|
|
||||
status = pipeline_tracker.human_status_name
|
||||
|
||||
expect(logger)
|
||||
.to receive(:error)
|
||||
.with(
|
||||
hash_including(
|
||||
'bulk_import_entity_id' => entity.id,
|
||||
'bulk_import_id' => entity.bulk_import_id,
|
||||
'bulk_import_entity_type' => entity.source_type,
|
||||
'pipeline_tracker_id' => pipeline_tracker.id,
|
||||
'pipeline_tracker_state' => status,
|
||||
'pipeline_name' => pipeline_tracker.pipeline_name,
|
||||
'source_full_path' => entity.source_full_path,
|
||||
'source_version' => entity.bulk_import.source_version_info.to_s,
|
||||
'importer' => 'gitlab_migration',
|
||||
'message' => "Pipeline in #{status} state instead of expected enqueued state"
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
expect(BulkImports::EntityWorker)
|
||||
.to receive(:perform_async)
|
||||
.with(entity.id, pipeline_tracker.stage)
|
||||
|
||||
subject.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when pipeline is finished' do
|
||||
let(:pipeline_tracker) do
|
||||
create(
|
||||
:bulk_import_tracker,
|
||||
:finished,
|
||||
entity: entity,
|
||||
pipeline_name: 'FakePipeline'
|
||||
)
|
||||
end
|
||||
|
||||
include_examples 'logs the error'
|
||||
end
|
||||
|
||||
context 'when pipeline is skipped' do
|
||||
let(:pipeline_tracker) do
|
||||
create(
|
||||
:bulk_import_tracker,
|
||||
:skipped,
|
||||
entity: entity,
|
||||
pipeline_name: 'FakePipeline'
|
||||
)
|
||||
end
|
||||
|
||||
include_examples 'logs the error'
|
||||
end
|
||||
|
||||
context 'when tracker is started' do
|
||||
it 'marks tracker as failed' do
|
||||
pipeline_tracker = create(
|
||||
:bulk_import_tracker,
|
||||
:started,
|
||||
entity: entity,
|
||||
pipeline_name: 'FakePipeline'
|
||||
)
|
||||
|
||||
subject.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
|
||||
|
||||
expect(pipeline_tracker.reload.failed?).to eq(true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when entity is failed' do
|
||||
it 'marks tracker as skipped and logs the skip' do
|
||||
pipeline_tracker = create(
|
||||
|
|
|
|||
|
|
@ -139,6 +139,7 @@ RSpec.describe 'Every Sidekiq worker' do
|
|||
'BuildQueueWorker' => 3,
|
||||
'BuildSuccessWorker' => 3,
|
||||
'BulkImportWorker' => false,
|
||||
'BulkImports::ExportRequestWorker' => 5,
|
||||
'BulkImports::EntityWorker' => false,
|
||||
'BulkImports::PipelineWorker' => false,
|
||||
'Chaos::CpuSpinWorker' => 3,
|
||||
|
|
|
|||
Loading…
Reference in New Issue