Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
fcef382cb9
commit
442a79b733
|
|
@ -1 +1 @@
|
|||
64625df11e8add7e64cce44a47984512e5f42d72
|
||||
c89fdf6bb2dc9f652f5c724caf13d3bde76e9d90
|
||||
|
|
|
|||
|
|
@ -442,10 +442,10 @@ const Api = {
|
|||
});
|
||||
},
|
||||
|
||||
applySuggestion(id) {
|
||||
applySuggestion(id, message) {
|
||||
const url = Api.buildUrl(Api.applySuggestionPath).replace(':id', encodeURIComponent(id));
|
||||
|
||||
return axios.put(url);
|
||||
return axios.put(url, { commit_message: message });
|
||||
},
|
||||
|
||||
applySuggestionBatch(ids) {
|
||||
|
|
|
|||
|
|
@ -124,6 +124,11 @@ export default {
|
|||
required: false,
|
||||
default: false,
|
||||
},
|
||||
defaultSuggestionCommitMessage: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
mrReviews: {
|
||||
type: Object,
|
||||
required: false,
|
||||
|
|
@ -268,6 +273,7 @@ export default {
|
|||
dismissEndpoint: this.dismissEndpoint,
|
||||
showSuggestPopover: this.showSuggestPopover,
|
||||
viewDiffsFileByFile: fileByFile(this.fileByFileUserPreference),
|
||||
defaultSuggestionCommitMessage: this.defaultSuggestionCommitMessage,
|
||||
mrReviews: this.mrReviews || {},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -83,6 +83,7 @@ export default function initDiffsApp(store) {
|
|||
showSuggestPopover: parseBoolean(dataset.showSuggestPopover),
|
||||
showWhitespaceDefault: parseBoolean(dataset.showWhitespaceDefault),
|
||||
viewDiffsFileByFile: parseBoolean(dataset.fileByFileDefault),
|
||||
defaultSuggestionCommitMessage: dataset.defaultSuggestionCommitMessage,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -123,6 +124,7 @@ export default function initDiffsApp(store) {
|
|||
dismissEndpoint: this.dismissEndpoint,
|
||||
showSuggestPopover: this.showSuggestPopover,
|
||||
fileByFileUserPreference: this.viewDiffsFileByFile,
|
||||
defaultSuggestionCommitMessage: this.defaultSuggestionCommitMessage,
|
||||
mrReviews: getReviewsForMergeRequest(mrPath),
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ export const setBaseConfig = ({ commit }, options) => {
|
|||
projectPath,
|
||||
dismissEndpoint,
|
||||
showSuggestPopover,
|
||||
defaultSuggestionCommitMessage,
|
||||
viewDiffsFileByFile,
|
||||
mrReviews,
|
||||
} = options;
|
||||
|
|
@ -73,6 +74,7 @@ export const setBaseConfig = ({ commit }, options) => {
|
|||
projectPath,
|
||||
dismissEndpoint,
|
||||
showSuggestPopover,
|
||||
defaultSuggestionCommitMessage,
|
||||
viewDiffsFileByFile,
|
||||
mrReviews,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -45,5 +45,6 @@ export default () => ({
|
|||
fileFinderVisible: false,
|
||||
dismissEndpoint: '',
|
||||
showSuggestPopover: true,
|
||||
defaultSuggestionCommitMessage: '',
|
||||
mrReviews: {},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ export default {
|
|||
projectPath,
|
||||
dismissEndpoint,
|
||||
showSuggestPopover,
|
||||
defaultSuggestionCommitMessage,
|
||||
viewDiffsFileByFile,
|
||||
mrReviews,
|
||||
} = options;
|
||||
|
|
@ -47,6 +48,7 @@ export default {
|
|||
projectPath,
|
||||
dismissEndpoint,
|
||||
showSuggestPopover,
|
||||
defaultSuggestionCommitMessage,
|
||||
viewDiffsFileByFile,
|
||||
mrReviews,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ export default {
|
|||
...mapState({
|
||||
batchSuggestionsInfo: (state) => state.notes.batchSuggestionsInfo,
|
||||
}),
|
||||
...mapState('diffs', ['defaultSuggestionCommitMessage']),
|
||||
noteBody() {
|
||||
return this.note.note;
|
||||
},
|
||||
|
|
@ -98,12 +99,16 @@ export default {
|
|||
formCancelHandler(shouldConfirm, isDirty) {
|
||||
this.$emit('cancelForm', shouldConfirm, isDirty);
|
||||
},
|
||||
applySuggestion({ suggestionId, flashContainer, callback = () => {} }) {
|
||||
applySuggestion({ suggestionId, flashContainer, callback = () => {}, message }) {
|
||||
const { discussion_id: discussionId, id: noteId } = this.note;
|
||||
|
||||
return this.submitSuggestion({ discussionId, noteId, suggestionId, flashContainer }).then(
|
||||
callback,
|
||||
);
|
||||
return this.submitSuggestion({
|
||||
discussionId,
|
||||
noteId,
|
||||
suggestionId,
|
||||
flashContainer,
|
||||
message,
|
||||
}).then(callback);
|
||||
},
|
||||
applySuggestionBatch({ flashContainer }) {
|
||||
return this.submitSuggestionBatch({ flashContainer });
|
||||
|
|
@ -130,6 +135,7 @@ export default {
|
|||
:note-html="note.note_html"
|
||||
:line-type="lineType"
|
||||
:help-page-path="helpPagePath"
|
||||
:default-commit-message="defaultSuggestionCommitMessage"
|
||||
@apply="applySuggestion"
|
||||
@applyBatch="applySuggestionBatch"
|
||||
@addToBatch="addSuggestionToBatch"
|
||||
|
|
|
|||
|
|
@ -559,7 +559,7 @@ export const updateResolvableDiscussionsCounts = ({ commit }) =>
|
|||
|
||||
export const submitSuggestion = (
|
||||
{ commit, dispatch },
|
||||
{ discussionId, suggestionId, flashContainer },
|
||||
{ discussionId, suggestionId, flashContainer, message },
|
||||
) => {
|
||||
const dispatchResolveDiscussion = () =>
|
||||
dispatch('resolveDiscussion', { discussionId }).catch(() => {});
|
||||
|
|
@ -567,7 +567,7 @@ export const submitSuggestion = (
|
|||
commit(types.SET_RESOLVING_DISCUSSION, true);
|
||||
dispatch('stopPolling');
|
||||
|
||||
return Api.applySuggestion(suggestionId)
|
||||
return Api.applySuggestion(suggestionId, message)
|
||||
.then(dispatchResolveDiscussion)
|
||||
.catch((err) => {
|
||||
const defaultMessage = __(
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
<script>
|
||||
import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
|
||||
import getPipelineDetails from 'shared_queries/pipelines/get_pipeline_details.query.graphql';
|
||||
import { __ } from '~/locale';
|
||||
import { DEFAULT, LOAD_FAILURE } from '../../constants';
|
||||
import getPipelineDetails from '../../graphql/queries/get_pipeline_details.query.graphql';
|
||||
import PipelineGraph from './graph_component.vue';
|
||||
import { unwrapPipelineData, toggleQueryPollingByVisibility, reportToSentry } from './utils';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import getPipelineDetails from '../../graphql/queries/get_pipeline_details.query.graphql';
|
||||
import getPipelineDetails from 'shared_queries/pipelines/get_pipeline_details.query.graphql';
|
||||
import LinkedPipeline from './linked_pipeline.vue';
|
||||
import { LOAD_FAILURE } from '../../constants';
|
||||
import { UPSTREAM } from './constants';
|
||||
|
|
|
|||
|
|
@ -1,17 +0,0 @@
|
|||
fragment LinkedPipelineData on Pipeline {
|
||||
id
|
||||
iid
|
||||
path
|
||||
status: detailedStatus {
|
||||
group
|
||||
label
|
||||
icon
|
||||
}
|
||||
sourceJob {
|
||||
name
|
||||
}
|
||||
project {
|
||||
name
|
||||
fullPath
|
||||
}
|
||||
}
|
||||
|
|
@ -36,7 +36,7 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="m-3 ml-7" :class="messageClass">
|
||||
<div class="gl-m-3 gl-ml-7" :class="messageClass">
|
||||
<slot></slot>
|
||||
<gl-link v-if="helpPath" :href="helpPath" target="_blank">
|
||||
<gl-icon :size="16" name="question-o" class="align-middle" />
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ export default {
|
|||
};
|
||||
</script>
|
||||
<template>
|
||||
<section class="mr-widget-help font-italic">
|
||||
<section class="gl-py-3 gl-pr-3 gl-pl-5 gl-ml-7 mr-widget-help gl-font-style-italic">
|
||||
<template v-if="missingBranch">
|
||||
{{ missingBranchInfo }}
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ export default {
|
|||
};
|
||||
</script>
|
||||
<template>
|
||||
<section class="mr-info-list mr-links">
|
||||
<section class="mr-info-list gl-ml-7 gl-pb-5">
|
||||
<p v-if="relatedLinks.closing">{{ closesText }} <span v-html="relatedLinks.closing"></span></p>
|
||||
<p v-if="relatedLinks.mentioned">
|
||||
{{ s__('mrWidget|Mentions') }} <span v-html="relatedLinks.mentioned"></span>
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<p v-once class="mr-info-list mr-links gl-mb-0">
|
||||
<p v-once class="mr-info-list gl-ml-7 gl-pb-5 gl-mb-0">
|
||||
<span class="status-text">
|
||||
<gl-sprintf :message="$options.i18n.removesBranchText">
|
||||
<template #strong="{ content }">
|
||||
|
|
|
|||
|
|
@ -83,6 +83,7 @@ export default {
|
|||
:aria-label="ariaLabel"
|
||||
category="tertiary"
|
||||
class="commit-edit-toggle gl-mr-3"
|
||||
size="small"
|
||||
:icon="collapseIcon"
|
||||
@click.stop="toggle()"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -159,13 +159,13 @@ export default {
|
|||
<div class="rebase-state-find-class-convention media media-body space-children">
|
||||
<span
|
||||
v-if="rebaseInProgress || isMakingRequest"
|
||||
class="gl-font-weight-bold"
|
||||
class="gl-font-weight-bold gl-ml-0!"
|
||||
data-testid="rebase-message"
|
||||
>{{ __('Rebase in progress') }}</span
|
||||
>
|
||||
<span
|
||||
v-if="!rebaseInProgress && !canPushToSourceBranch"
|
||||
class="gl-font-weight-bold"
|
||||
class="gl-font-weight-bold gl-ml-0!"
|
||||
data-testid="rebase-message"
|
||||
v-html="fastForwardMergeText"
|
||||
></span>
|
||||
|
|
@ -181,12 +181,17 @@ export default {
|
|||
>
|
||||
{{ __('Rebase') }}
|
||||
</gl-button>
|
||||
<span v-if="!rebasingError" class="gl-font-weight-bold" data-testid="rebase-message">{{
|
||||
__(
|
||||
'Fast-forward merge is not possible. Rebase the source branch onto the target branch.',
|
||||
)
|
||||
}}</span>
|
||||
<span v-else class="gl-font-weight-bold danger" data-testid="rebase-message">{{
|
||||
<span
|
||||
v-if="!rebasingError"
|
||||
class="gl-font-weight-bold gl-ml-0!"
|
||||
data-testid="rebase-message"
|
||||
>{{
|
||||
__(
|
||||
'Fast-forward merge is not possible. Rebase the source branch onto the target branch.',
|
||||
)
|
||||
}}</span
|
||||
>
|
||||
<span v-else class="gl-font-weight-bold danger gl-ml-0!" data-testid="rebase-message">{{
|
||||
rebasingError
|
||||
}}</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -26,7 +26,11 @@ export default () => {
|
|||
|
||||
registerExtension(issueExtension);
|
||||
|
||||
const vm = new Vue({ ...MrWidgetOptions, apolloProvider });
|
||||
const vm = new Vue({
|
||||
el: '#js-vue-mr-widget',
|
||||
...MrWidgetOptions,
|
||||
apolloProvider,
|
||||
});
|
||||
|
||||
window.gl.mrWidget = {
|
||||
checkStatus: vm.checkStatus,
|
||||
|
|
|
|||
|
|
@ -48,7 +48,6 @@ import GroupedAccessibilityReportsApp from '../reports/accessibility_report/grou
|
|||
import getStateQuery from './queries/get_state.query.graphql';
|
||||
|
||||
export default {
|
||||
el: '#js-vue-mr-widget',
|
||||
// False positive i18n lint: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/25
|
||||
// eslint-disable-next-line @gitlab/require-i18n-strings
|
||||
name: 'MRWidget',
|
||||
|
|
|
|||
|
|
@ -1,117 +0,0 @@
|
|||
<script>
|
||||
import $ from 'jquery';
|
||||
import { GlButton } from '@gitlab/ui';
|
||||
|
||||
const buttonVariants = ['danger', 'primary', 'success', 'warning'];
|
||||
const sizeVariants = ['sm', 'md', 'lg', 'xl'];
|
||||
|
||||
export default {
|
||||
name: 'DeprecatedModal2', // use GlModal instead
|
||||
|
||||
components: {
|
||||
GlButton,
|
||||
},
|
||||
props: {
|
||||
id: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
modalSize: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'md',
|
||||
validator: (value) => sizeVariants.includes(value),
|
||||
},
|
||||
headerTitleText: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
footerPrimaryButtonVariant: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'primary',
|
||||
validator: (value) => buttonVariants.includes(value),
|
||||
},
|
||||
footerPrimaryButtonText: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
modalSizeClass() {
|
||||
return this.modalSize === 'md' ? '' : `modal-${this.modalSize}`;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
$(this.$el).on('shown.bs.modal', this.opened).on('hidden.bs.modal', this.closed);
|
||||
},
|
||||
beforeDestroy() {
|
||||
$(this.$el).off('shown.bs.modal', this.opened).off('hidden.bs.modal', this.closed);
|
||||
},
|
||||
methods: {
|
||||
emitCancel(event) {
|
||||
this.$emit('cancel', event);
|
||||
},
|
||||
emitSubmit(event) {
|
||||
this.$emit('submit', event);
|
||||
},
|
||||
opened() {
|
||||
this.$emit('open');
|
||||
},
|
||||
closed() {
|
||||
this.$emit('closed');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :id="id" class="modal fade" tabindex="-1" role="dialog">
|
||||
<div :class="modalSizeClass" class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header gl-pr-4">
|
||||
<slot name="header">
|
||||
<h4 class="modal-title">
|
||||
<slot name="title"> {{ headerTitleText }} </slot>
|
||||
</h4>
|
||||
<gl-button
|
||||
:aria-label="s__('Modal|Close')"
|
||||
variant="default"
|
||||
category="tertiary"
|
||||
size="small"
|
||||
icon="close"
|
||||
class="js-modal-close-action"
|
||||
data-dismiss="modal"
|
||||
@click="emitCancel($event)"
|
||||
/>
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
<div class="modal-body"><slot></slot></div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<slot name="footer">
|
||||
<gl-button
|
||||
class="js-modal-cancel-action qa-modal-cancel-button"
|
||||
data-dismiss="modal"
|
||||
@click="emitCancel($event)"
|
||||
>
|
||||
{{ s__('Modal|Cancel') }}
|
||||
</gl-button>
|
||||
<gl-button
|
||||
:class="`btn-${footerPrimaryButtonVariant}`"
|
||||
class="js-modal-primary-action qa-modal-primary-button"
|
||||
data-dismiss="modal"
|
||||
@click="emitSubmit($event)"
|
||||
>
|
||||
{{ footerPrimaryButtonText }}
|
||||
</gl-button>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
<script>
|
||||
import { GlDropdown, GlDropdownForm, GlFormTextarea, GlButton } from '@gitlab/ui';
|
||||
import { __, sprintf } from '~/locale';
|
||||
|
||||
export default {
|
||||
components: { GlDropdown, GlDropdownForm, GlFormTextarea, GlButton },
|
||||
|
|
@ -10,7 +9,7 @@ export default {
|
|||
required: false,
|
||||
default: false,
|
||||
},
|
||||
fileName: {
|
||||
defaultCommitMessage: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
|
@ -18,18 +17,11 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
message: null,
|
||||
buttonText: __('Apply suggestion'),
|
||||
headerText: __('Apply suggestion commit message'),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
placeholderText() {
|
||||
return sprintf(__('Apply suggestion on %{fileName}'), { fileName: this.fileName });
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onApply() {
|
||||
this.$emit('apply', this.message || this.placeholderText);
|
||||
this.$emit('apply', this.message);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -37,18 +29,26 @@ export default {
|
|||
|
||||
<template>
|
||||
<gl-dropdown
|
||||
:text="buttonText"
|
||||
:header-text="headerText"
|
||||
:text="__('Apply suggestion')"
|
||||
:disabled="disabled"
|
||||
boundary="window"
|
||||
right
|
||||
menu-class="gl-w-full! gl-pb-0!"
|
||||
menu-class="gl-w-full!"
|
||||
@shown="$refs.commitMessage.$el.focus()"
|
||||
>
|
||||
<gl-dropdown-form class="gl-m-3!">
|
||||
<gl-form-textarea v-model="message" :placeholder="placeholderText" />
|
||||
<gl-dropdown-form class="gl-px-4! gl-m-0!">
|
||||
<label for="commit-message">{{ __('Commit message') }}</label>
|
||||
<gl-form-textarea
|
||||
id="commit-message"
|
||||
ref="commitMessage"
|
||||
v-model="message"
|
||||
:placeholder="defaultCommitMessage"
|
||||
submit-on-enter
|
||||
@submit="onApply"
|
||||
/>
|
||||
<gl-button
|
||||
class="gl-w-quarter! gl-mt-3 gl-text-center! float-right"
|
||||
category="secondary"
|
||||
class="gl-w-auto! gl-mt-3 gl-text-center! gl-hover-text-white! gl-transition-medium! float-right"
|
||||
category="primary"
|
||||
variant="success"
|
||||
@click="onApply"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -27,6 +27,10 @@ export default {
|
|||
type: String,
|
||||
required: true,
|
||||
},
|
||||
defaultCommitMessage: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
suggestionsCount: {
|
||||
type: Number,
|
||||
required: false,
|
||||
|
|
@ -47,8 +51,8 @@ export default {
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
applySuggestion(callback) {
|
||||
this.$emit('apply', { suggestionId: this.suggestion.id, callback });
|
||||
applySuggestion(callback, message) {
|
||||
this.$emit('apply', { suggestionId: this.suggestion.id, callback, message });
|
||||
},
|
||||
applySuggestionBatch() {
|
||||
this.$emit('applyBatch');
|
||||
|
|
@ -74,6 +78,7 @@ export default {
|
|||
:is-applying-batch="suggestion.is_applying_batch"
|
||||
:batch-suggestions-count="batchSuggestionsCount"
|
||||
:help-page-path="helpPagePath"
|
||||
:default-commit-message="defaultCommitMessage"
|
||||
:inapplicable-reason="suggestion.inapplicable_reason"
|
||||
@apply="applySuggestion"
|
||||
@applyBatch="applySuggestionBatch"
|
||||
|
|
|
|||
|
|
@ -2,9 +2,10 @@
|
|||
import { GlButton, GlLoadingIcon, GlTooltipDirective, GlIcon } from '@gitlab/ui';
|
||||
import { __ } from '~/locale';
|
||||
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import ApplySuggestion from './apply_suggestion.vue';
|
||||
|
||||
export default {
|
||||
components: { GlIcon, GlButton, GlLoadingIcon },
|
||||
components: { GlIcon, GlButton, GlLoadingIcon, ApplySuggestion },
|
||||
directives: { 'gl-tooltip': GlTooltipDirective },
|
||||
mixins: [glFeatureFlagsMixin()],
|
||||
props: {
|
||||
|
|
@ -37,6 +38,10 @@ export default {
|
|||
type: String,
|
||||
required: true,
|
||||
},
|
||||
defaultCommitMessage: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
inapplicableReason: {
|
||||
type: String,
|
||||
required: false,
|
||||
|
|
@ -57,6 +62,9 @@ export default {
|
|||
canBeBatched() {
|
||||
return Boolean(this.glFeatures.batchSuggestions);
|
||||
},
|
||||
canAddCustomCommitMessage() {
|
||||
return this.glFeatures.suggestionsCustomCommit;
|
||||
},
|
||||
isApplying() {
|
||||
return this.isApplyingSingle || this.isApplyingBatch;
|
||||
},
|
||||
|
|
@ -77,10 +85,10 @@ export default {
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
applySuggestion() {
|
||||
applySuggestion(message) {
|
||||
if (!this.canApply) return;
|
||||
this.isApplyingSingle = true;
|
||||
this.$emit('apply', this.applySuggestionCallback);
|
||||
this.$emit('apply', this.applySuggestionCallback, message);
|
||||
},
|
||||
applySuggestionCallback() {
|
||||
this.isApplyingSingle = false;
|
||||
|
|
@ -142,7 +150,14 @@ export default {
|
|||
>
|
||||
{{ __('Add suggestion to batch') }}
|
||||
</gl-button>
|
||||
<span v-gl-tooltip.viewport="tooltipMessage" tabindex="0">
|
||||
<apply-suggestion
|
||||
v-if="canAddCustomCommitMessage"
|
||||
:disabled="isDisableButton"
|
||||
:default-commit-message="defaultCommitMessage"
|
||||
class="gl-ml-3"
|
||||
@apply="applySuggestion"
|
||||
/>
|
||||
<span v-else v-gl-tooltip.viewport="tooltipMessage" tabindex="0">
|
||||
<gl-button
|
||||
v-if="isLoggedIn"
|
||||
class="btn-inverted js-apply-btn btn-grouped"
|
||||
|
|
|
|||
|
|
@ -38,6 +38,10 @@ export default {
|
|||
type: String,
|
||||
required: true,
|
||||
},
|
||||
defaultCommitMessage: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
suggestionsCount: {
|
||||
type: Number,
|
||||
required: false,
|
||||
|
|
@ -82,16 +86,30 @@ export default {
|
|||
this.isRendered = true;
|
||||
},
|
||||
generateDiff(suggestionIndex) {
|
||||
const { suggestions, disabled, batchSuggestionsInfo, helpPagePath, suggestionsCount } = this;
|
||||
const {
|
||||
suggestions,
|
||||
disabled,
|
||||
batchSuggestionsInfo,
|
||||
helpPagePath,
|
||||
defaultCommitMessage,
|
||||
suggestionsCount,
|
||||
} = this;
|
||||
const suggestion =
|
||||
suggestions && suggestions[suggestionIndex] ? suggestions[suggestionIndex] : {};
|
||||
const SuggestionDiffComponent = Vue.extend(SuggestionDiff);
|
||||
const suggestionDiff = new SuggestionDiffComponent({
|
||||
propsData: { disabled, suggestion, batchSuggestionsInfo, helpPagePath, suggestionsCount },
|
||||
propsData: {
|
||||
disabled,
|
||||
suggestion,
|
||||
batchSuggestionsInfo,
|
||||
helpPagePath,
|
||||
defaultCommitMessage,
|
||||
suggestionsCount,
|
||||
},
|
||||
});
|
||||
|
||||
suggestionDiff.$on('apply', ({ suggestionId, callback }) => {
|
||||
this.$emit('apply', { suggestionId, callback, flashContainer: this.$el });
|
||||
suggestionDiff.$on('apply', ({ suggestionId, callback, message }) => {
|
||||
this.$emit('apply', { suggestionId, callback, flashContainer: this.$el, message });
|
||||
});
|
||||
|
||||
suggestionDiff.$on('applyBatch', () => {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
left: 0;
|
||||
width: 100%;
|
||||
background: $white;
|
||||
z-index: 300;
|
||||
z-index: $zindex-dropdown-menu;
|
||||
padding: 7px 0 6px; // to keep aligned with "collapse sidebar" button on the left sidebar
|
||||
border-top: 1px solid $border-color;
|
||||
padding-left: $contextual-sidebar-width;
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
top: 0;
|
||||
margin-top: 3px;
|
||||
padding: $gl-padding;
|
||||
z-index: 300;
|
||||
z-index: $zindex-dropdown-menu;
|
||||
width: $award-emoji-width;
|
||||
font-size: 14px;
|
||||
background-color: $white;
|
||||
|
|
|
|||
|
|
@ -471,7 +471,7 @@
|
|||
background-color: $black-transparent;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
z-index: 300;
|
||||
z-index: $zindex-dropdown-menu;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -216,7 +216,7 @@
|
|||
position: absolute;
|
||||
width: auto;
|
||||
top: 100%;
|
||||
z-index: 300;
|
||||
z-index: $zindex-dropdown-menu;
|
||||
min-width: 240px;
|
||||
max-width: 500px;
|
||||
margin-top: $dropdown-vertical-offset;
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@
|
|||
|
||||
.right-sidebar-expanded {
|
||||
padding-right: 0;
|
||||
z-index: 300;
|
||||
z-index: $zindex-dropdown-menu;
|
||||
|
||||
@include media-breakpoint-only(sm) {
|
||||
&:not(.wiki-sidebar):not(.build-sidebar):not(.issuable-bulk-update-sidebar) .content-wrapper {
|
||||
|
|
|
|||
|
|
@ -433,6 +433,7 @@ $browser-scrollbar-size: 10px;
|
|||
*/
|
||||
$header-height: 40px;
|
||||
$header-zindex: 1000;
|
||||
$zindex-dropdown-menu: 300;
|
||||
$suggestion-header-height: 46px;
|
||||
$ide-statusbar-height: 25px;
|
||||
$fixed-layout-width: 1280px;
|
||||
|
|
|
|||
|
|
@ -124,7 +124,7 @@ $mr-widget-min-height: 69px;
|
|||
padding: $gl-padding;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
padding-left: $gl-padding-8 * 7;
|
||||
margin-left: $gl-spacing-scale-7;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -221,7 +221,7 @@ $mr-widget-min-height: 69px;
|
|||
|
||||
.mr-widget-pipeline-graph {
|
||||
.dropdown-menu {
|
||||
z-index: 300;
|
||||
z-index: $zindex-dropdown-menu;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -396,10 +396,6 @@ $mr-widget-min-height: 69px;
|
|||
}
|
||||
}
|
||||
|
||||
.mr-widget-help {
|
||||
padding: 10px 16px 10px ($gl-padding-8 * 7);
|
||||
}
|
||||
|
||||
.ci-coverage {
|
||||
float: right;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@
|
|||
|
||||
// This utility is used to force the z-index to match that of dropdown menu's
|
||||
.gl-z-dropdown-menu\! {
|
||||
z-index: 300 !important;
|
||||
z-index: $zindex-dropdown-menu !important;
|
||||
}
|
||||
|
||||
.gl-flex-basis-quarter {
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
|
|||
push_frontend_feature_flag(:remove_resolve_note, @project, default_enabled: true)
|
||||
push_frontend_feature_flag(:diffs_gradual_load, @project, default_enabled: true)
|
||||
push_frontend_feature_flag(:codequality_mr_diff, @project)
|
||||
push_frontend_feature_flag(:suggestions_custom_commit, @project)
|
||||
|
||||
record_experiment_user(:invite_members_version_a)
|
||||
record_experiment_user(:invite_members_version_b)
|
||||
|
|
|
|||
|
|
@ -1,11 +1,34 @@
|
|||
#import "../fragments/linked_pipelines.fragment.graphql"
|
||||
fragment LinkedPipelineData on Pipeline {
|
||||
__typename
|
||||
id
|
||||
iid
|
||||
path
|
||||
status: detailedStatus {
|
||||
__typename
|
||||
group
|
||||
label
|
||||
icon
|
||||
}
|
||||
sourceJob {
|
||||
__typename
|
||||
name
|
||||
}
|
||||
project {
|
||||
__typename
|
||||
name
|
||||
fullPath
|
||||
}
|
||||
}
|
||||
|
||||
query getPipelineDetails($projectPath: ID!, $iid: ID!) {
|
||||
project(fullPath: $projectPath) {
|
||||
__typename
|
||||
pipeline(iid: $iid) {
|
||||
__typename
|
||||
id
|
||||
iid
|
||||
downstream {
|
||||
__typename
|
||||
nodes {
|
||||
...LinkedPipelineData
|
||||
}
|
||||
|
|
@ -14,18 +37,25 @@ query getPipelineDetails($projectPath: ID!, $iid: ID!) {
|
|||
...LinkedPipelineData
|
||||
}
|
||||
stages {
|
||||
__typename
|
||||
nodes {
|
||||
__typename
|
||||
name
|
||||
status: detailedStatus {
|
||||
__typename
|
||||
action {
|
||||
__typename
|
||||
icon
|
||||
path
|
||||
title
|
||||
}
|
||||
}
|
||||
groups {
|
||||
__typename
|
||||
nodes {
|
||||
__typename
|
||||
status: detailedStatus {
|
||||
__typename
|
||||
label
|
||||
group
|
||||
icon
|
||||
|
|
@ -33,21 +63,27 @@ query getPipelineDetails($projectPath: ID!, $iid: ID!) {
|
|||
name
|
||||
size
|
||||
jobs {
|
||||
__typename
|
||||
nodes {
|
||||
__typename
|
||||
name
|
||||
scheduledAt
|
||||
needs {
|
||||
__typename
|
||||
nodes {
|
||||
__typename
|
||||
name
|
||||
}
|
||||
}
|
||||
status: detailedStatus {
|
||||
__typename
|
||||
icon
|
||||
tooltip
|
||||
hasDetails
|
||||
detailsPath
|
||||
group
|
||||
action {
|
||||
__typename
|
||||
buttonTitle
|
||||
icon
|
||||
path
|
||||
|
|
@ -185,6 +185,10 @@ module MergeRequestsHelper
|
|||
|
||||
current_user.review_requested_open_merge_requests_count
|
||||
end
|
||||
|
||||
def default_suggestion_commit_message
|
||||
@project.suggestion_commit_message.presence || Gitlab::Suggestions::CommitMessage::DEFAULT_SUGGESTION_COMMIT_MESSAGE
|
||||
end
|
||||
end
|
||||
|
||||
MergeRequestsHelper.prepend_if_ee('EE::MergeRequestsHelper')
|
||||
|
|
|
|||
|
|
@ -1020,8 +1020,6 @@ module Ci
|
|||
end
|
||||
|
||||
def debug_mode?
|
||||
return false unless Feature.enabled?(:restrict_access_to_build_debug_mode, default_enabled: true)
|
||||
|
||||
# TODO: Have `debug_mode?` check against data on sent back from runner
|
||||
# to capture all the ways that variables can be set.
|
||||
# See (https://gitlab.com/gitlab-org/gitlab/-/issues/290955)
|
||||
|
|
|
|||
|
|
@ -83,6 +83,6 @@ module CacheableAttributes
|
|||
end
|
||||
|
||||
def cache!
|
||||
self.class.cache_backend.write(self.class.cache_key, self, expires_in: 1.minute)
|
||||
self.class.cache_backend.write(self.class.cache_key, self, expires_in: Gitlab.config.gitlab['application_settings_cache_seconds'] || 60)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -25,4 +25,4 @@
|
|||
= _("Explain the problem. If appropriate, provide a link to the relevant issue or comment.")
|
||||
|
||||
.form-actions
|
||||
= f.submit _("Send report"), class: "btn btn-success"
|
||||
= f.submit _("Send report"), class: "gl-button btn btn-success"
|
||||
|
|
|
|||
|
|
@ -33,5 +33,5 @@
|
|||
= visibility_level_icon(group.visibility_level)
|
||||
|
||||
.controls.gl-flex-shrink-0.gl-ml-5
|
||||
= link_to _('Edit'), admin_group_edit_path(group), id: "edit_#{dom_id(group)}", class: 'btn'
|
||||
= link_to _('Edit'), admin_group_edit_path(group), id: "edit_#{dom_id(group)}", class: 'gl-button btn'
|
||||
= link_to _('Delete'), [:admin, group], data: { confirm: _("Are you sure you want to remove %{group_name}?") % { group_name: group.name } }, method: :delete, class: 'gl-button btn btn-danger'
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@
|
|||
- @projects.each_with_index do |project|
|
||||
%li.project-row{ class: ('no-description' if project.description.blank?) }
|
||||
.controls
|
||||
= link_to 'Edit', edit_project_path(project), id: "edit_#{dom_id(project)}", class: "btn"
|
||||
%button.delete-project-button.btn.btn-danger{ data: { delete_project_url: admin_project_path(project), project_name: project.name } }
|
||||
= link_to 'Edit', edit_project_path(project), id: "edit_#{dom_id(project)}", class: "gl-button btn"
|
||||
%button.delete-project-button.gl-button.btn.btn-danger{ data: { delete_project_url: admin_project_path(project), project_name: project.name } }
|
||||
= s_('AdminProjects|Delete')
|
||||
|
||||
.stats
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@
|
|||
= image_tag avatar_icon_for_user(@user, 160), alt: '', class: 'avatar s160'
|
||||
%h5.gl-mt-0= s_("Profiles|Upload new avatar")
|
||||
.gl-mt-2.gl-mb-3
|
||||
%button.btn.js-choose-user-avatar-button{ type: 'button' }= s_("Profiles|Choose file...")
|
||||
%button.gl-button.btn.js-choose-user-avatar-button{ type: 'button' }= s_("Profiles|Choose file...")
|
||||
%span.avatar-file-name.gl-ml-3.js-avatar-filename= s_("Profiles|No file chosen")
|
||||
= f.file_field_without_bootstrap :avatar, class: 'js-user-avatar-input hidden', accept: 'image/*'
|
||||
.form-text.text-muted= s_("Profiles|The maximum file size allowed is 200KB.")
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@
|
|||
.js-project-permissions-form
|
||||
- if show_visibility_confirm_modal?(@project)
|
||||
= render "visibility_modal"
|
||||
= f.submit _('Save changes'), class: "btn btn-success #{('js-confirm-danger' if show_visibility_confirm_modal?(@project))}", data: { qa_selector: 'visibility_features_permissions_save_button', check_field_name: ("project[visibility_level]" if show_visibility_confirm_modal?(@project)), check_compare_value: @project.visibility_level }
|
||||
= f.submit _('Save changes'), class: "gl-button gl-button btn btn-success #{('js-confirm-danger' if show_visibility_confirm_modal?(@project))}", data: { qa_selector: 'visibility_features_permissions_save_button', check_field_name: ("project[visibility_level]" if show_visibility_confirm_modal?(@project)), check_compare_value: @project.visibility_level }
|
||||
|
||||
%section.qa-merge-request-settings.rspec-merge-request-settings.settings.merge-requests-feature.no-animate#js-merge-request-settings{ class: [('expanded' if expanded), ('hidden' if @project.project_feature.send(:merge_requests_access_level) == 0)] }
|
||||
.settings-header
|
||||
|
|
@ -39,7 +39,7 @@
|
|||
= form_for @project, remote: true, html: { multipart: true, class: "merge-request-settings-form js-mr-settings-form" }, authenticity_token: true do |f|
|
||||
%input{ name: 'update_section', type: 'hidden', value: 'js-merge-request-settings' }
|
||||
= render 'projects/merge_request_settings', form: f
|
||||
= f.submit _('Save changes'), class: "btn btn-succes qa-save-merge-request-changes rspec-save-merge-request-changes"
|
||||
= f.submit _('Save changes'), class: "gl-button btn btn-succes qa-save-merge-request-changes rspec-save-merge-request-changes"
|
||||
|
||||
= render_if_exists 'projects/merge_request_approvals_settings', expanded: expanded
|
||||
|
||||
|
|
@ -70,7 +70,7 @@
|
|||
%h4= _('Housekeeping')
|
||||
%p= _('Runs a number of housekeeping tasks within the current repository, such as compressing file revisions and removing unreachable objects.')
|
||||
= link_to _('Run housekeeping'), housekeeping_project_path(@project),
|
||||
method: :post, class: "btn btn-default"
|
||||
method: :post, class: "gl-button btn btn-default"
|
||||
|
||||
= render 'export', project: @project
|
||||
|
||||
|
|
@ -92,7 +92,7 @@
|
|||
%li= _('You will need to update your local repositories to point to the new location.')
|
||||
- if @project.deployment_platform.present?
|
||||
%li= _('Your deployment services will be broken, you will need to manually fix the services after renaming.')
|
||||
= f.submit _('Change path'), class: "btn btn-warning qa-change-path-button"
|
||||
= f.submit _('Change path'), class: "gl-button btn btn-warning qa-change-path-button"
|
||||
|
||||
= render 'transfer', project: @project
|
||||
|
||||
|
|
|
|||
|
|
@ -90,7 +90,8 @@
|
|||
dismiss_endpoint: user_callouts_path,
|
||||
show_suggest_popover: show_suggest_popover?.to_s,
|
||||
show_whitespace_default: @show_whitespace_default.to_s,
|
||||
file_by_file_default: @file_by_file_default.to_s }
|
||||
file_by_file_default: @file_by_file_default.to_s,
|
||||
default_suggestion_commit_message: default_suggestion_commit_message }
|
||||
|
||||
.mr-loading-status
|
||||
.loading.hide
|
||||
|
|
|
|||
|
|
@ -21,8 +21,8 @@
|
|||
|
||||
.form-actions
|
||||
- if @milestone.new_record?
|
||||
= f.submit _('Create milestone'), class: 'btn-success btn', data: { qa_selector: 'create_milestone_button' }
|
||||
= f.submit _('Create milestone'), class: 'gl-button btn-success btn', data: { qa_selector: 'create_milestone_button' }
|
||||
= link_to _('Cancel'), project_milestones_path(@project), class: 'gl-button btn btn-cancel'
|
||||
- else
|
||||
= f.submit _('Save changes'), class: 'btn-success btn'
|
||||
= f.submit _('Save changes'), class: 'gl-button btn-success btn'
|
||||
= link_to _('Cancel'), project_milestone_path(@project, @milestone), class: 'gl-button btn btn-cancel'
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@
|
|||
- add_page_specific_style 'page_bundles/reports'
|
||||
- add_page_specific_style 'page_bundles/ci_status'
|
||||
|
||||
- if Feature.enabled?(:graphql_pipeline_details, @project)
|
||||
- add_page_startup_graphql_call('pipelines/get_pipeline_details', { projectPath: @project.full_path, iid: @pipeline.iid })
|
||||
|
||||
.js-pipeline-container{ data: { controller_action: "#{controller.action_name}" } }
|
||||
#js-pipeline-header-vue.pipeline-header-container{ data: { full_path: @project.full_path, pipeline_iid: @pipeline.iid, pipeline_id: @pipeline.id, pipelines_path: project_pipelines_path(@project) } }
|
||||
- if @pipeline.commit.present?
|
||||
|
|
|
|||
|
|
@ -7,4 +7,4 @@
|
|||
.gl-alert-body
|
||||
= s_('AlertSettings|You can now set up alert endpoints for manually configured Prometheus instances in the Alerts section on the Operations settings page. Alert endpoint fields on this page have been deprecated.')
|
||||
.gl-alert-actions
|
||||
= link_to _('Visit settings page'), project_settings_operations_path(@project, anchor: 'js-alert-management-settings'), class: 'btn gl-alert-action btn-info gl-button'
|
||||
= link_to _('Visit settings page'), project_settings_operations_path(@project, anchor: 'js-alert-management-settings'), class: 'gl-button btn gl-alert-action btn-info'
|
||||
|
|
|
|||
|
|
@ -10,9 +10,9 @@
|
|||
%p= _("Unarchiving the project will restore people's ability to make changes to it. The repository can be committed to, and issues, comments, and other entities can be created. %{strong_start}Once active, this project shows up in the search and on the dashboard.%{strong_end}").html_safe % { strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe }
|
||||
= link_to _('Unarchive project'), unarchive_project_path(@project),
|
||||
data: { confirm: _("Are you sure that you want to unarchive this project?"), qa_selector: 'unarchive_project_link' },
|
||||
method: :post, class: "btn btn-success"
|
||||
method: :post, class: "gl-button btn btn-success"
|
||||
- else
|
||||
%p= _("Archiving the project will make it entirely read only. It is hidden from the dashboard and doesn't show up in searches. %{strong_start}The repository cannot be committed to, and no issues, comments, or other entities can be created.%{strong_end}").html_safe % { strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe }
|
||||
= link_to _('Archive project'), archive_project_path(@project),
|
||||
data: { confirm: _("Are you sure that you want to archive this project?"), qa_selector: 'archive_project_link' },
|
||||
method: :post, class: "btn btn-warning"
|
||||
method: :post, class: "gl-button btn btn-warning"
|
||||
|
|
|
|||
|
|
@ -40,4 +40,4 @@
|
|||
%hr
|
||||
= link_to _('Remove avatar'), project_avatar_path(@project), data: { confirm: _('Avatar will be removed. Are you sure?')}, method: :delete, class: 'btn btn-link'
|
||||
|
||||
= f.submit _('Save changes'), class: "btn btn-success mt-4 qa-save-naming-topics-avatar-button"
|
||||
= f.submit _('Save changes'), class: "gl-button btn btn-success gl-mt-6 qa-save-naming-topics-avatar-button"
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
.gl-alert-body
|
||||
= _('Webhooks have moved. They can now be found under the Settings menu.')
|
||||
.gl-alert-actions
|
||||
= link_to _('Go to Webhooks'), project_hooks_path(@project), class: 'btn gl-alert-action btn-info new-gl-button'
|
||||
= link_to _('Go to Webhooks'), project_hooks_path(@project), class: 'gl-button btn gl-alert-action btn-info'
|
||||
|
||||
%h4= _('Integrations')
|
||||
- integrations_link_start = '<a href="%{url}">'.html_safe % { url: help_page_url('user/project/integrations/overview') }
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
- classes = local_assigns.fetch(:classes, '')
|
||||
|
||||
%span.js-filepicker
|
||||
%button.btn.js-filepicker-button{ type: 'button', class: classes }= _("Choose file…")
|
||||
%button.gl-button.btn.js-filepicker-button{ type: 'button', class: classes }= _("Choose file…")
|
||||
%span.file_name.js-filepicker-filename= _("No file chosen")
|
||||
= f.file_field field, class: "js-filepicker-input hidden"
|
||||
- if help_text.present?
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@
|
|||
%button.js-unsubscribe-button.label-subscribe-button.btn.btn-default{ class: ('hidden' if status.unsubscribed?), data: { url: toggle_subscription_path, toggle: 'tooltip' }, title: tooltip_title }
|
||||
%span= _('Unsubscribe')
|
||||
.dropdown.dropdown-group-label{ class: ('hidden' unless status.unsubscribed?) }
|
||||
%button.label-subscribe-button.btn.btn-default{ data: { toggle: 'dropdown' } }
|
||||
%button.gl-button.label-subscribe-button.btn.btn-default{ data: { toggle: 'dropdown' } }
|
||||
%span
|
||||
= _('Subscribe')
|
||||
= sprite_icon('chevron-down')
|
||||
|
|
@ -59,7 +59,7 @@
|
|||
%button.js-subscribe-button.js-group-level.label-subscribe-button.btn.btn-default{ class: ('hidden' unless status.unsubscribed?), data: { status: status, url: toggle_subscription_group_label_path(label.group, label) } }
|
||||
%span= _('Subscribe at group level')
|
||||
- else
|
||||
%button.js-subscribe-button.label-subscribe-button.btn.btn-default{ data: { status: status, url: toggle_subscription_path, toggle: 'tooltip' }, title: tooltip_title }
|
||||
%button.gl-button.js-subscribe-button.label-subscribe-button.btn.btn-default{ data: { status: status, url: toggle_subscription_path, toggle: 'tooltip' }, title: tooltip_title }
|
||||
%span= label_subscription_toggle_button_text(label, @project)
|
||||
|
||||
= render 'shared/delete_label_modal', label: label
|
||||
|
|
|
|||
|
|
@ -12,9 +12,9 @@
|
|||
%p= current_user_empty_message_description
|
||||
|
||||
- if secondary_button_link.present?
|
||||
= link_to secondary_button_label, secondary_button_link, class: 'btn btn-success btn-inverted'
|
||||
= link_to secondary_button_label, secondary_button_link, class: 'gl-button btn btn-success btn-inverted'
|
||||
|
||||
- if primary_button_link.present?
|
||||
= link_to primary_button_label, primary_button_link, class: 'btn btn-success'
|
||||
= link_to primary_button_label, primary_button_link, class: 'gl-button btn btn-success'
|
||||
- else
|
||||
%h5= visitor_empty_message
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
.dropdown.gl-ml-3#js-add-list
|
||||
%button.btn.btn-success.btn-inverted.js-new-board-list{ type: "button", data: board_list_data }
|
||||
%button.gl-button.btn.btn-success.btn-inverted.js-new-board-list{ type: "button", data: board_list_data }
|
||||
Add list
|
||||
.dropdown-menu.dropdown-extended-height.dropdown-menu-paging.dropdown-menu-right.dropdown-menu-issues-board-new.dropdown-menu-selectable.js-tab-container-labels
|
||||
= render partial: "shared/issuable/label_page_default", locals: { show_footer: true, show_create: true, show_boards_content: true, title: "Add list" }
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@
|
|||
= form_tag [:bulk_update, @project, type], method: :post, class: "bulk-update" do
|
||||
.block.issuable-sidebar-header
|
||||
.filter-item.inline.update-issues-btn.float-left
|
||||
= button_tag _('Update all'), class: "btn update-selected-issues btn-info", disabled: true
|
||||
= button_tag _('Cancel'), class: "btn btn-default js-bulk-update-menu-hide float-right"
|
||||
= button_tag _('Update all'), class: "gl-button btn update-selected-issues btn-info", disabled: true
|
||||
= button_tag _('Cancel'), class: "gl-button btn btn-default js-bulk-update-menu-hide float-right"
|
||||
- if params[:state] != 'merged'
|
||||
.block
|
||||
.title
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
%input.js-add-list{ type: "checkbox", name: "add_list", checked: add_list }
|
||||
%span= _('Add list')
|
||||
.clearfix
|
||||
%button.btn.btn-primary.float-left.js-new-label-btn{ type: "button" }
|
||||
%button.gl-button.btn.btn-success.float-left.js-new-label-btn{ type: "button" }
|
||||
= _('Create')
|
||||
%button.btn.btn-default.float-right.js-cancel-label-btn{ type: "button" }
|
||||
%button.gl-button.btn.btn-default.float-right.js-cancel-label-btn{ type: "button" }
|
||||
= _('Cancel')
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add API command to remove pending member invitation
|
||||
merge_request: 51134
|
||||
author:
|
||||
type: added
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Migrate old button classes to our Pajamas style GitLab button in multiple areas
|
||||
merge_request: 51826
|
||||
author: Yogi (@yo)
|
||||
type: other
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
---
|
||||
name: restrict_access_to_build_debug_mode
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/48932
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/292661
|
||||
milestone: '13.7'
|
||||
type: development
|
||||
group: group::continuous integration
|
||||
default_enabled: true
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: suggestions_custom_commit
|
||||
introduced_by_url:
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/297404
|
||||
milestone: '13.9'
|
||||
type: development
|
||||
group: group::code review
|
||||
default_enabled: false
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
---
|
||||
name: upload_middleware_jwt_params_handler
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/33277
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/233895
|
||||
milestone: '13.4'
|
||||
type: development
|
||||
group: group::package
|
||||
default_enabled: true
|
||||
|
|
@ -165,6 +165,9 @@ production: &base
|
|||
## Disable jQuery and CSS animations
|
||||
# disable_animations: true
|
||||
|
||||
## Application settings cache expiry in seconds (default: 60)
|
||||
# application_settings_cache_seconds: 60
|
||||
|
||||
## Reply by email
|
||||
# Allow users to comment on issues and merge requests by replying to notification emails.
|
||||
# For documentation on how to set this up, see http://doc.gitlab.com/ce/administration/reply_by_email.html
|
||||
|
|
|
|||
|
|
@ -8,35 +8,6 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
|
||||
Uploads represent all user data that may be sent to GitLab as a single file. As an example, avatars and notes' attachments are uploads. Uploads are integral to GitLab functionality, and therefore cannot be disabled.
|
||||
|
||||
## Upload parameters
|
||||
|
||||
> - [Changed](https://gitlab.com/gitlab-org/gitlab/-/issues/214785) in GitLab 13.5.
|
||||
> - It's [deployed behind a feature flag](../user/feature_flags.md), enabled by default.
|
||||
> - It's enabled on GitLab.com.
|
||||
> - It's recommended for production use.
|
||||
> - For GitLab self-managed instances, GitLab administrators can opt to disable it. **(CORE ONLY)**
|
||||
|
||||
In 13.5 and later, upload parameters are passed [between Workhorse and GitLab Rails](../development/architecture.md#simplified-component-overview) differently than they
|
||||
were before.
|
||||
|
||||
This change is deployed behind a feature flag that is **enabled by default**.
|
||||
|
||||
If you experience any issues with upload,
|
||||
[GitLab administrators with access to the GitLab Rails console](feature_flags.md)
|
||||
can opt to disable it.
|
||||
|
||||
To enable it:
|
||||
|
||||
```ruby
|
||||
Feature.enable(:upload_middleware_jwt_params_handler)
|
||||
```
|
||||
|
||||
To disable it:
|
||||
|
||||
```ruby
|
||||
Feature.disable(:upload_middleware_jwt_params_handler)
|
||||
```
|
||||
|
||||
## Using local storage
|
||||
|
||||
This is the default configuration. To change the location where the uploads are
|
||||
|
|
|
|||
|
|
@ -26512,6 +26512,11 @@ type Vulnerability implements Noteable {
|
|||
"""
|
||||
description: String
|
||||
|
||||
"""
|
||||
Details of the vulnerability
|
||||
"""
|
||||
details: [VulnerabilityDetail!]!
|
||||
|
||||
"""
|
||||
Timestamp of when the vulnerability was first detected
|
||||
"""
|
||||
|
|
@ -26780,6 +26785,366 @@ type VulnerabilityConnection {
|
|||
pageInfo: PageInfo!
|
||||
}
|
||||
|
||||
"""
|
||||
Represents a vulnerability detail field. The fields with data will depend on the vulnerability detail type
|
||||
"""
|
||||
union VulnerabilityDetail = VulnerabilityDetailBase | VulnerabilityDetailBoolean | VulnerabilityDetailCode | VulnerabilityDetailCommit | VulnerabilityDetailDiff | VulnerabilityDetailFileLocation | VulnerabilityDetailInt | VulnerabilityDetailList | VulnerabilityDetailMarkdown | VulnerabilityDetailModuleLocation | VulnerabilityDetailTable | VulnerabilityDetailText | VulnerabilityDetailUrl
|
||||
|
||||
"""
|
||||
Represents the vulnerability details base
|
||||
"""
|
||||
type VulnerabilityDetailBase {
|
||||
"""
|
||||
Description of the field.
|
||||
"""
|
||||
description: String!
|
||||
|
||||
"""
|
||||
Name of the field.
|
||||
"""
|
||||
fieldName: String
|
||||
|
||||
"""
|
||||
Name of the field.
|
||||
"""
|
||||
name: String!
|
||||
}
|
||||
|
||||
"""
|
||||
Represents the vulnerability details boolean value
|
||||
"""
|
||||
type VulnerabilityDetailBoolean {
|
||||
"""
|
||||
Description of the field.
|
||||
"""
|
||||
description: String!
|
||||
|
||||
"""
|
||||
Name of the field.
|
||||
"""
|
||||
fieldName: String
|
||||
|
||||
"""
|
||||
Name of the field.
|
||||
"""
|
||||
name: String!
|
||||
|
||||
"""
|
||||
Value of the field.
|
||||
"""
|
||||
value: Boolean!
|
||||
}
|
||||
|
||||
"""
|
||||
Represents the vulnerability details code field
|
||||
"""
|
||||
type VulnerabilityDetailCode {
|
||||
"""
|
||||
Description of the field.
|
||||
"""
|
||||
description: String!
|
||||
|
||||
"""
|
||||
Name of the field.
|
||||
"""
|
||||
fieldName: String
|
||||
|
||||
"""
|
||||
Language of the code.
|
||||
"""
|
||||
lang: String
|
||||
|
||||
"""
|
||||
Name of the field.
|
||||
"""
|
||||
name: String!
|
||||
|
||||
"""
|
||||
Source code.
|
||||
"""
|
||||
value: String!
|
||||
}
|
||||
|
||||
"""
|
||||
Represents the vulnerability details commit field
|
||||
"""
|
||||
type VulnerabilityDetailCommit {
|
||||
"""
|
||||
Description of the field.
|
||||
"""
|
||||
description: String!
|
||||
|
||||
"""
|
||||
Name of the field.
|
||||
"""
|
||||
fieldName: String
|
||||
|
||||
"""
|
||||
Name of the field.
|
||||
"""
|
||||
name: String!
|
||||
|
||||
"""
|
||||
The commit SHA value.
|
||||
"""
|
||||
value: String!
|
||||
}
|
||||
|
||||
"""
|
||||
Represents the vulnerability details diff field
|
||||
"""
|
||||
type VulnerabilityDetailDiff {
|
||||
"""
|
||||
Value of the field after the change.
|
||||
"""
|
||||
after: String!
|
||||
|
||||
"""
|
||||
Value of the field before the change.
|
||||
"""
|
||||
before: String!
|
||||
|
||||
"""
|
||||
Description of the field.
|
||||
"""
|
||||
description: String!
|
||||
|
||||
"""
|
||||
Name of the field.
|
||||
"""
|
||||
fieldName: String
|
||||
|
||||
"""
|
||||
Name of the field.
|
||||
"""
|
||||
name: String!
|
||||
}
|
||||
|
||||
"""
|
||||
Represents the vulnerability details location within a file in the project
|
||||
"""
|
||||
type VulnerabilityDetailFileLocation {
|
||||
"""
|
||||
Description of the field.
|
||||
"""
|
||||
description: String!
|
||||
|
||||
"""
|
||||
Name of the field.
|
||||
"""
|
||||
fieldName: String
|
||||
|
||||
"""
|
||||
File name.
|
||||
"""
|
||||
fileName: String!
|
||||
|
||||
"""
|
||||
End line number of the file location.
|
||||
"""
|
||||
lineEnd: Int!
|
||||
|
||||
"""
|
||||
Start line number of the file location.
|
||||
"""
|
||||
lineStart: Int!
|
||||
|
||||
"""
|
||||
Name of the field.
|
||||
"""
|
||||
name: String!
|
||||
}
|
||||
|
||||
"""
|
||||
Represents the vulnerability details integer value
|
||||
"""
|
||||
type VulnerabilityDetailInt {
|
||||
"""
|
||||
Description of the field.
|
||||
"""
|
||||
description: String!
|
||||
|
||||
"""
|
||||
Name of the field.
|
||||
"""
|
||||
fieldName: String
|
||||
|
||||
"""
|
||||
Name of the field.
|
||||
"""
|
||||
name: String!
|
||||
|
||||
"""
|
||||
Value of the field.
|
||||
"""
|
||||
value: Int!
|
||||
}
|
||||
|
||||
"""
|
||||
Represents the vulnerability details list value
|
||||
"""
|
||||
type VulnerabilityDetailList {
|
||||
"""
|
||||
Description of the field.
|
||||
"""
|
||||
description: String!
|
||||
|
||||
"""
|
||||
Name of the field.
|
||||
"""
|
||||
fieldName: String
|
||||
|
||||
"""
|
||||
List of details.
|
||||
"""
|
||||
items: [VulnerabilityDetail!]!
|
||||
|
||||
"""
|
||||
Name of the field.
|
||||
"""
|
||||
name: String!
|
||||
}
|
||||
|
||||
"""
|
||||
Represents the vulnerability details Markdown field
|
||||
"""
|
||||
type VulnerabilityDetailMarkdown {
|
||||
"""
|
||||
Description of the field.
|
||||
"""
|
||||
description: String!
|
||||
|
||||
"""
|
||||
Name of the field.
|
||||
"""
|
||||
fieldName: String
|
||||
|
||||
"""
|
||||
Name of the field.
|
||||
"""
|
||||
name: String!
|
||||
|
||||
"""
|
||||
Value of the Markdown field.
|
||||
"""
|
||||
value: String!
|
||||
}
|
||||
|
||||
"""
|
||||
Represents the vulnerability details location within a file in the project
|
||||
"""
|
||||
type VulnerabilityDetailModuleLocation {
|
||||
"""
|
||||
Description of the field.
|
||||
"""
|
||||
description: String!
|
||||
|
||||
"""
|
||||
Name of the field.
|
||||
"""
|
||||
fieldName: String
|
||||
|
||||
"""
|
||||
Module name.
|
||||
"""
|
||||
moduleName: String!
|
||||
|
||||
"""
|
||||
Name of the field.
|
||||
"""
|
||||
name: String!
|
||||
|
||||
"""
|
||||
Offset of the module location.
|
||||
"""
|
||||
offset: Int!
|
||||
}
|
||||
|
||||
"""
|
||||
Represents the vulnerability details table value
|
||||
"""
|
||||
type VulnerabilityDetailTable {
|
||||
"""
|
||||
Description of the field.
|
||||
"""
|
||||
description: String!
|
||||
|
||||
"""
|
||||
Name of the field.
|
||||
"""
|
||||
fieldName: String
|
||||
|
||||
"""
|
||||
Table headers.
|
||||
"""
|
||||
headers: [VulnerabilityDetail!]!
|
||||
|
||||
"""
|
||||
Name of the field.
|
||||
"""
|
||||
name: String!
|
||||
|
||||
"""
|
||||
Table rows.
|
||||
"""
|
||||
rows: [VulnerabilityDetail!]!
|
||||
}
|
||||
|
||||
"""
|
||||
Represents the vulnerability details text field
|
||||
"""
|
||||
type VulnerabilityDetailText {
|
||||
"""
|
||||
Description of the field.
|
||||
"""
|
||||
description: String!
|
||||
|
||||
"""
|
||||
Name of the field.
|
||||
"""
|
||||
fieldName: String
|
||||
|
||||
"""
|
||||
Name of the field.
|
||||
"""
|
||||
name: String!
|
||||
|
||||
"""
|
||||
Value of the text field.
|
||||
"""
|
||||
value: String!
|
||||
}
|
||||
|
||||
"""
|
||||
Represents the vulnerability details URL field
|
||||
"""
|
||||
type VulnerabilityDetailUrl {
|
||||
"""
|
||||
Description of the field.
|
||||
"""
|
||||
description: String!
|
||||
|
||||
"""
|
||||
Name of the field.
|
||||
"""
|
||||
fieldName: String
|
||||
|
||||
"""
|
||||
Href of the URL.
|
||||
"""
|
||||
href: String!
|
||||
|
||||
"""
|
||||
Name of the field.
|
||||
"""
|
||||
name: String!
|
||||
|
||||
"""
|
||||
Text of the URL.
|
||||
"""
|
||||
text: String
|
||||
}
|
||||
|
||||
"""
|
||||
Autogenerated input type of VulnerabilityDismiss
|
||||
"""
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -3937,6 +3937,7 @@ Represents a vulnerability.
|
|||
| `confirmedAt` | Time | Timestamp of when the vulnerability state was changed to confirmed |
|
||||
| `confirmedBy` | User | The user that confirmed the vulnerability. |
|
||||
| `description` | String | Description of the vulnerability |
|
||||
| `details` | VulnerabilityDetail! => Array | Details of the vulnerability |
|
||||
| `detectedAt` | Time! | Timestamp of when the vulnerability was first detected |
|
||||
| `discussions` | DiscussionConnection! | All discussions on this noteable |
|
||||
| `dismissedAt` | Time | Timestamp of when the vulnerability state was changed to dismissed |
|
||||
|
|
@ -3973,6 +3974,155 @@ Autogenerated return type of VulnerabilityConfirm.
|
|||
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
|
||||
| `vulnerability` | Vulnerability | The vulnerability after state change. |
|
||||
|
||||
### VulnerabilityDetailBase
|
||||
|
||||
Represents the vulnerability details base.
|
||||
|
||||
| Field | Type | Description |
|
||||
| ----- | ---- | ----------- |
|
||||
| `description` | String! | Description of the field. |
|
||||
| `fieldName` | String | Name of the field. |
|
||||
| `name` | String! | Name of the field. |
|
||||
|
||||
### VulnerabilityDetailBoolean
|
||||
|
||||
Represents the vulnerability details boolean value.
|
||||
|
||||
| Field | Type | Description |
|
||||
| ----- | ---- | ----------- |
|
||||
| `description` | String! | Description of the field. |
|
||||
| `fieldName` | String | Name of the field. |
|
||||
| `name` | String! | Name of the field. |
|
||||
| `value` | Boolean! | Value of the field. |
|
||||
|
||||
### VulnerabilityDetailCode
|
||||
|
||||
Represents the vulnerability details code field.
|
||||
|
||||
| Field | Type | Description |
|
||||
| ----- | ---- | ----------- |
|
||||
| `description` | String! | Description of the field. |
|
||||
| `fieldName` | String | Name of the field. |
|
||||
| `lang` | String | Language of the code. |
|
||||
| `name` | String! | Name of the field. |
|
||||
| `value` | String! | Source code. |
|
||||
|
||||
### VulnerabilityDetailCommit
|
||||
|
||||
Represents the vulnerability details commit field.
|
||||
|
||||
| Field | Type | Description |
|
||||
| ----- | ---- | ----------- |
|
||||
| `description` | String! | Description of the field. |
|
||||
| `fieldName` | String | Name of the field. |
|
||||
| `name` | String! | Name of the field. |
|
||||
| `value` | String! | The commit SHA value. |
|
||||
|
||||
### VulnerabilityDetailDiff
|
||||
|
||||
Represents the vulnerability details diff field.
|
||||
|
||||
| Field | Type | Description |
|
||||
| ----- | ---- | ----------- |
|
||||
| `after` | String! | Value of the field after the change. |
|
||||
| `before` | String! | Value of the field before the change. |
|
||||
| `description` | String! | Description of the field. |
|
||||
| `fieldName` | String | Name of the field. |
|
||||
| `name` | String! | Name of the field. |
|
||||
|
||||
### VulnerabilityDetailFileLocation
|
||||
|
||||
Represents the vulnerability details location within a file in the project.
|
||||
|
||||
| Field | Type | Description |
|
||||
| ----- | ---- | ----------- |
|
||||
| `description` | String! | Description of the field. |
|
||||
| `fieldName` | String | Name of the field. |
|
||||
| `fileName` | String! | File name. |
|
||||
| `lineEnd` | Int! | End line number of the file location. |
|
||||
| `lineStart` | Int! | Start line number of the file location. |
|
||||
| `name` | String! | Name of the field. |
|
||||
|
||||
### VulnerabilityDetailInt
|
||||
|
||||
Represents the vulnerability details integer value.
|
||||
|
||||
| Field | Type | Description |
|
||||
| ----- | ---- | ----------- |
|
||||
| `description` | String! | Description of the field. |
|
||||
| `fieldName` | String | Name of the field. |
|
||||
| `name` | String! | Name of the field. |
|
||||
| `value` | Int! | Value of the field. |
|
||||
|
||||
### VulnerabilityDetailList
|
||||
|
||||
Represents the vulnerability details list value.
|
||||
|
||||
| Field | Type | Description |
|
||||
| ----- | ---- | ----------- |
|
||||
| `description` | String! | Description of the field. |
|
||||
| `fieldName` | String | Name of the field. |
|
||||
| `items` | VulnerabilityDetail! => Array | List of details. |
|
||||
| `name` | String! | Name of the field. |
|
||||
|
||||
### VulnerabilityDetailMarkdown
|
||||
|
||||
Represents the vulnerability details Markdown field.
|
||||
|
||||
| Field | Type | Description |
|
||||
| ----- | ---- | ----------- |
|
||||
| `description` | String! | Description of the field. |
|
||||
| `fieldName` | String | Name of the field. |
|
||||
| `name` | String! | Name of the field. |
|
||||
| `value` | String! | Value of the Markdown field. |
|
||||
|
||||
### VulnerabilityDetailModuleLocation
|
||||
|
||||
Represents the vulnerability details location within a file in the project.
|
||||
|
||||
| Field | Type | Description |
|
||||
| ----- | ---- | ----------- |
|
||||
| `description` | String! | Description of the field. |
|
||||
| `fieldName` | String | Name of the field. |
|
||||
| `moduleName` | String! | Module name. |
|
||||
| `name` | String! | Name of the field. |
|
||||
| `offset` | Int! | Offset of the module location. |
|
||||
|
||||
### VulnerabilityDetailTable
|
||||
|
||||
Represents the vulnerability details table value.
|
||||
|
||||
| Field | Type | Description |
|
||||
| ----- | ---- | ----------- |
|
||||
| `description` | String! | Description of the field. |
|
||||
| `fieldName` | String | Name of the field. |
|
||||
| `headers` | VulnerabilityDetail! => Array | Table headers. |
|
||||
| `name` | String! | Name of the field. |
|
||||
| `rows` | VulnerabilityDetail! => Array | Table rows. |
|
||||
|
||||
### VulnerabilityDetailText
|
||||
|
||||
Represents the vulnerability details text field.
|
||||
|
||||
| Field | Type | Description |
|
||||
| ----- | ---- | ----------- |
|
||||
| `description` | String! | Description of the field. |
|
||||
| `fieldName` | String | Name of the field. |
|
||||
| `name` | String! | Name of the field. |
|
||||
| `value` | String! | Value of the text field. |
|
||||
|
||||
### VulnerabilityDetailUrl
|
||||
|
||||
Represents the vulnerability details URL field.
|
||||
|
||||
| Field | Type | Description |
|
||||
| ----- | ---- | ----------- |
|
||||
| `description` | String! | Description of the field. |
|
||||
| `fieldName` | String | Name of the field. |
|
||||
| `href` | String! | Href of the URL. |
|
||||
| `name` | String! | Name of the field. |
|
||||
| `text` | String | Text of the URL. |
|
||||
|
||||
### VulnerabilityDismissPayload
|
||||
|
||||
Autogenerated return type of VulnerabilityDismiss.
|
||||
|
|
|
|||
|
|
@ -106,3 +106,27 @@ Example response:
|
|||
},
|
||||
]
|
||||
```
|
||||
|
||||
## Delete an invitation to a group or project
|
||||
|
||||
Deletes a pending invitation by email address.
|
||||
|
||||
```plaintext
|
||||
DELETE /groups/:id/invitations/:email
|
||||
DELETE /projects/:id/invitations/:email
|
||||
```
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the project or group](README.md#namespaced-path-encoding) owned by the authenticated user |
|
||||
| `email` | string | yes | The email address to which the invitation was previously sent |
|
||||
|
||||
```shell
|
||||
curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/55/invitations/email@example.org"
|
||||
curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/55/invitations/email@example.org"
|
||||
```
|
||||
|
||||
- Returns `204` and no content on success.
|
||||
- Returns `403` forbidden if unauthorized to delete the invitation.
|
||||
- Returns `404` not found if authorized and no invitation is found for that email address.
|
||||
- Returns `409` if the request was valid but the invitation could not be deleted.
|
||||
|
|
|
|||
|
|
@ -877,13 +877,7 @@ before making them visible again.
|
|||
### Restricted access to debug logging
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/213159) in GitLab 13.7.
|
||||
> - It's [deployed behind a feature flag](../../user/feature_flags.md), enabled by default.
|
||||
> - It's enabled on GitLab.com.
|
||||
> - It's recommended for production use.
|
||||
> - For GitLab self-managed instances, GitLab administrators can opt to [disable it](#enable-or-disable-restricted-access-to-debug-logging). **(CORE ONLY)**
|
||||
|
||||
WARNING:
|
||||
This feature might not be available to you. Check the **version history** note above for details.
|
||||
> - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/292661) in GitLab 13.8.
|
||||
|
||||
With restricted access to debug logging, only users with
|
||||
[developer or higher permissions](../../user/permissions.md#project-members-permissions)
|
||||
|
|
@ -897,25 +891,6 @@ If you add `CI_DEBUG_TRACE` as a local variable to your runners, debug logs are
|
|||
to all users with access to job logs. The permission levels are not checked by Runner,
|
||||
so you should make use of the variable in GitLab only.
|
||||
|
||||
#### Enable or disable Restricted access to debug logging **(CORE ONLY)**
|
||||
|
||||
Restricted Access to Debug logging is under development but ready for production use.
|
||||
It is deployed behind a feature flag that is **enabled by default**.
|
||||
[GitLab administrators with access to the GitLab Rails console](../../administration/feature_flags.md)
|
||||
can opt to disable it.
|
||||
|
||||
To enable it:
|
||||
|
||||
```ruby
|
||||
Feature.enable(:restrict_access_to_build_debug_mode)
|
||||
```
|
||||
|
||||
To disable it:
|
||||
|
||||
```ruby
|
||||
Feature.disable(:restrict_access_to_build_debug_mode)
|
||||
```
|
||||
|
||||
### Enable Debug logging
|
||||
|
||||
To enable debug logs (traces), set the `CI_DEBUG_TRACE` variable to `true`:
|
||||
|
|
|
|||
|
|
@ -48,6 +48,24 @@ module API
|
|||
|
||||
present_member_invitations invitations
|
||||
end
|
||||
|
||||
desc 'Removes an invitation from a group or project.'
|
||||
params do
|
||||
requires :email, type: String, desc: 'The email address of the invitation'
|
||||
end
|
||||
delete ":id/invitations/:email", requirements: { email: /[^\/]+/ } do
|
||||
source = find_source(source_type, params[:id])
|
||||
invite_email = params[:email]
|
||||
authorize_admin_source!(source_type, source)
|
||||
|
||||
invite = retrieve_member_invitations(source, invite_email).first
|
||||
not_found! unless invite
|
||||
|
||||
destroy_conditionally!(invite) do
|
||||
::Members::DestroyService.new(current_user, params).execute(invite)
|
||||
unprocessable_entity! unless invite.destroyed?
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -40,119 +40,6 @@ module Gitlab
|
|||
@open_files = []
|
||||
end
|
||||
|
||||
def with_open_files
|
||||
@rewritten_fields.each do |field, tmp_path|
|
||||
raise "invalid field: #{field.inspect}" unless valid_field_name?(field)
|
||||
|
||||
parsed_field = Rack::Utils.parse_nested_query(field)
|
||||
raise "unexpected field: #{field.inspect}" unless parsed_field.count == 1
|
||||
|
||||
key, value = parsed_field.first
|
||||
if value.nil? # we have a top level param, eg. field = 'foo' and not 'foo[bar]'
|
||||
raise "invalid field: #{field.inspect}" if field != key
|
||||
|
||||
value = open_file(@request.params, key, tmp_path.presence)
|
||||
@open_files << value
|
||||
else
|
||||
value = decorate_params_value(value, @request.params[key], tmp_path.presence)
|
||||
end
|
||||
|
||||
update_param(key, value)
|
||||
end
|
||||
|
||||
yield
|
||||
ensure
|
||||
@open_files.compact
|
||||
.each(&:close)
|
||||
end
|
||||
|
||||
# This function calls itself recursively
|
||||
def decorate_params_value(path_hash, value_hash, path_override = nil)
|
||||
unless path_hash.is_a?(Hash) && path_hash.count == 1
|
||||
raise "invalid path: #{path_hash.inspect}"
|
||||
end
|
||||
|
||||
path_key, path_value = path_hash.first
|
||||
|
||||
unless value_hash.is_a?(Hash) && value_hash[path_key]
|
||||
raise "invalid value hash: #{value_hash.inspect}"
|
||||
end
|
||||
|
||||
case path_value
|
||||
when nil
|
||||
value_hash[path_key] = open_file(value_hash.dig(path_key), '', path_override)
|
||||
@open_files << value_hash[path_key]
|
||||
value_hash
|
||||
when Hash
|
||||
decorate_params_value(path_value, value_hash[path_key], path_override)
|
||||
value_hash
|
||||
else
|
||||
raise "unexpected path value: #{path_value.inspect}"
|
||||
end
|
||||
end
|
||||
|
||||
def open_file(params, key, path_override = nil)
|
||||
::UploadedFile.from_params(params, key, allowed_paths, path_override)
|
||||
end
|
||||
|
||||
# update_params ensures that both rails controllers and rack middleware can find
|
||||
# workhorse accelerate files in the request
|
||||
def update_param(key, value)
|
||||
# we make sure we have key in POST otherwise update_params will add it in GET
|
||||
@request.POST[key] ||= value
|
||||
|
||||
# this will force Rack::Request to properly update env keys
|
||||
@request.update_param(key, value)
|
||||
|
||||
# ActionDispatch::Request is based on Rack::Request but it caches params
|
||||
# inside other env keys, here we ensure everything is updated correctly
|
||||
ActionDispatch::Request.new(@request.env).update_param(key, value)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def valid_field_name?(name)
|
||||
# length validation
|
||||
return false if name.size >= REWRITTEN_FIELD_NAME_MAX_LENGTH
|
||||
|
||||
# brackets validation
|
||||
return false if name.include?('[]') || name.start_with?('[', ']')
|
||||
return false unless ::Gitlab::Utils.valid_brackets?(name, allow_nested: false)
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
def package_allowed_paths
|
||||
packages_config = ::Gitlab.config.packages
|
||||
return [] unless allow_packages_storage_path?(packages_config)
|
||||
|
||||
[::Packages::PackageFileUploader.workhorse_upload_path]
|
||||
end
|
||||
|
||||
def allow_packages_storage_path?(packages_config)
|
||||
return false unless packages_config.enabled
|
||||
return false unless packages_config['storage_path']
|
||||
return false if packages_config.object_store.enabled && packages_config.object_store.direct_upload
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
def allowed_paths
|
||||
[
|
||||
Dir.tmpdir,
|
||||
::FileUploader.root,
|
||||
::Gitlab.config.uploads.storage_path,
|
||||
::JobArtifactUploader.workhorse_upload_path,
|
||||
::LfsObjectUploader.workhorse_upload_path,
|
||||
File.join(Rails.root, 'public/uploads/tmp')
|
||||
] + package_allowed_paths
|
||||
end
|
||||
end
|
||||
|
||||
# TODO this class is meant to replace Handler when the feature flag
|
||||
# upload_middleware_jwt_params_handler is removed
|
||||
# See https://gitlab.com/gitlab-org/gitlab/-/issues/233895#roll-out-steps
|
||||
class HandlerForJWTParams < Handler
|
||||
def with_open_files
|
||||
@rewritten_fields.keys.each do |field|
|
||||
raise "invalid field: #{field.inspect}" unless valid_field_name?(field)
|
||||
|
|
@ -205,7 +92,21 @@ module Gitlab
|
|||
end
|
||||
|
||||
def open_file(params)
|
||||
::UploadedFile.from_params_without_field(params, allowed_paths)
|
||||
::UploadedFile.from_params(params, allowed_paths)
|
||||
end
|
||||
|
||||
# update_params ensures that both rails controllers and rack middleware can find
|
||||
# workhorse accelerate files in the request
|
||||
def update_param(key, value)
|
||||
# we make sure we have key in POST otherwise update_params will add it in GET
|
||||
@request.POST[key] ||= value
|
||||
|
||||
# this will force Rack::Request to properly update env keys
|
||||
@request.update_param(key, value)
|
||||
|
||||
# ActionDispatch::Request is based on Rack::Request but it caches params
|
||||
# inside other env keys, here we ensure everything is updated correctly
|
||||
ActionDispatch::Request.new(@request.env).update_param(key, value)
|
||||
end
|
||||
|
||||
private
|
||||
|
|
@ -223,6 +124,43 @@ module Gitlab
|
|||
|
||||
upload_params
|
||||
end
|
||||
|
||||
def valid_field_name?(name)
|
||||
# length validation
|
||||
return false if name.size >= REWRITTEN_FIELD_NAME_MAX_LENGTH
|
||||
|
||||
# brackets validation
|
||||
return false if name.include?('[]') || name.start_with?('[', ']')
|
||||
return false unless ::Gitlab::Utils.valid_brackets?(name, allow_nested: false)
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
def package_allowed_paths
|
||||
packages_config = ::Gitlab.config.packages
|
||||
return [] unless allow_packages_storage_path?(packages_config)
|
||||
|
||||
[::Packages::PackageFileUploader.workhorse_upload_path]
|
||||
end
|
||||
|
||||
def allow_packages_storage_path?(packages_config)
|
||||
return false unless packages_config.enabled
|
||||
return false unless packages_config['storage_path']
|
||||
return false if packages_config.object_store.enabled && packages_config.object_store.direct_upload
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
def allowed_paths
|
||||
[
|
||||
Dir.tmpdir,
|
||||
::FileUploader.root,
|
||||
::Gitlab.config.uploads.storage_path,
|
||||
::JobArtifactUploader.workhorse_upload_path,
|
||||
::LfsObjectUploader.workhorse_upload_path,
|
||||
File.join(Rails.root, 'public/uploads/tmp')
|
||||
] + package_allowed_paths
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(app)
|
||||
|
|
@ -235,22 +173,12 @@ module Gitlab
|
|||
|
||||
message = ::Gitlab::Workhorse.decode_jwt(encoded_message)[0]
|
||||
|
||||
handler_class.new(env, message).with_open_files do
|
||||
::Gitlab::Middleware::Multipart::Handler.new(env, message).with_open_files do
|
||||
@app.call(env)
|
||||
end
|
||||
rescue UploadedFile::InvalidPathError => e
|
||||
[400, { 'Content-Type' => 'text/plain' }, e.message]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def handler_class
|
||||
if Feature.enabled?(:upload_middleware_jwt_params_handler, default_enabled: true)
|
||||
::Gitlab::Middleware::Multipart::HandlerForJWTParams
|
||||
else
|
||||
::Gitlab::Middleware::Multipart::Handler
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -42,10 +42,7 @@ class UploadedFile
|
|||
@remote_id = remote_id
|
||||
end
|
||||
|
||||
# TODO this function is meant to replace .from_params when the feature flag
|
||||
# upload_middleware_jwt_params_handler is removed
|
||||
# See https://gitlab.com/gitlab-org/gitlab/-/issues/233895#roll-out-steps
|
||||
def self.from_params_without_field(params, upload_paths)
|
||||
def self.from_params(params, upload_paths)
|
||||
path = params['path']
|
||||
remote_id = params['remote_id']
|
||||
return if path.blank? && remote_id.blank?
|
||||
|
|
@ -71,33 +68,6 @@ class UploadedFile
|
|||
)
|
||||
end
|
||||
|
||||
# Deprecated. Don't use it.
|
||||
# .from_params_without_field will replace this one
|
||||
# See .from_params_without_field and
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/issues/233895#roll-out-steps
|
||||
def self.from_params(params, field, upload_paths, path_override = nil)
|
||||
path = path_override || params["#{field}.path"]
|
||||
remote_id = params["#{field}.remote_id"]
|
||||
return if path.blank? && remote_id.blank?
|
||||
|
||||
if remote_id.present? # don't use file_path if remote_id is set
|
||||
file_path = nil
|
||||
elsif path.present?
|
||||
file_path = File.realpath(path)
|
||||
|
||||
unless self.allowed_path?(file_path, Array(upload_paths).compact)
|
||||
raise InvalidPathError, "insecure path used '#{file_path}'"
|
||||
end
|
||||
end
|
||||
|
||||
UploadedFile.new(file_path,
|
||||
filename: params["#{field}.name"],
|
||||
content_type: params["#{field}.type"] || 'application/octet-stream',
|
||||
sha256: params["#{field}.sha256"],
|
||||
remote_id: remote_id,
|
||||
size: params["#{field}.size"])
|
||||
end
|
||||
|
||||
def self.allowed_path?(file_path, paths)
|
||||
paths.any? do |path|
|
||||
File.exist?(path) && file_path.start_with?(File.realpath(path))
|
||||
|
|
|
|||
|
|
@ -3543,12 +3543,6 @@ msgstr ""
|
|||
msgid "Apply suggestion"
|
||||
msgstr ""
|
||||
|
||||
msgid "Apply suggestion commit message"
|
||||
msgstr ""
|
||||
|
||||
msgid "Apply suggestion on %{fileName}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Apply suggestions"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -18469,9 +18463,6 @@ msgstr ""
|
|||
msgid "ModalButton|Add projects"
|
||||
msgstr ""
|
||||
|
||||
msgid "Modal|Cancel"
|
||||
msgstr ""
|
||||
|
||||
msgid "Modal|Close"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -675,16 +675,6 @@ RSpec.describe Projects::JobsController, :clean_gitlab_redis_shared_state do
|
|||
|
||||
expect(response).to have_gitlab_http_status(:forbidden)
|
||||
end
|
||||
|
||||
context 'with restrict_access_to_build_debug_mode feature disabled' do
|
||||
before do
|
||||
stub_feature_flags(restrict_access_to_build_debug_mode: false)
|
||||
end
|
||||
|
||||
it 'returns response forbidden' do
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1139,18 +1129,6 @@ RSpec.describe Projects::JobsController, :clean_gitlab_redis_shared_state do
|
|||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
end
|
||||
|
||||
context 'with restrict_access_to_build_debug_mode feature disabled' do
|
||||
before do
|
||||
stub_feature_flags(restrict_access_to_build_debug_mode: false)
|
||||
end
|
||||
|
||||
it 'returns response ok' do
|
||||
response = subject
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'without proper permissions for debug logging on a project' do
|
||||
|
|
@ -1164,18 +1142,6 @@ RSpec.describe Projects::JobsController, :clean_gitlab_redis_shared_state do
|
|||
|
||||
expect(response).to have_gitlab_http_status(:forbidden)
|
||||
end
|
||||
|
||||
context 'with restrict_access_to_build_debug_mode feature disabled' do
|
||||
before do
|
||||
stub_feature_flags(restrict_access_to_build_debug_mode: false)
|
||||
end
|
||||
|
||||
it 'returns response ok' do
|
||||
response = subject
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -87,6 +87,7 @@ RSpec.describe 'User comments on a diff', :js do
|
|||
expect(page).not_to have_content('Applied')
|
||||
|
||||
click_button('Apply suggestion')
|
||||
click_button('Apply')
|
||||
wait_for_requests
|
||||
|
||||
expect(page).to have_content('Applied')
|
||||
|
|
@ -338,6 +339,7 @@ RSpec.describe 'User comments on a diff', :js do
|
|||
expect(page).not_to have_content('Applied')
|
||||
|
||||
click_button('Apply suggestion')
|
||||
click_button('Apply')
|
||||
wait_for_requests
|
||||
|
||||
expect(page).to have_content('Applied')
|
||||
|
|
@ -349,6 +351,7 @@ RSpec.describe 'User comments on a diff', :js do
|
|||
expect(page).not_to have_content('Unresolve thread')
|
||||
|
||||
click_button('Apply suggestion')
|
||||
click_button('Apply')
|
||||
wait_for_requests
|
||||
|
||||
expect(page).to have_content('Unresolve thread')
|
||||
|
|
|
|||
|
|
@ -179,34 +179,6 @@ RSpec.describe 'Project Jobs Permissions' do
|
|||
expect(status_code).to eq(expected_status_code)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when restrict_access_to_build_debug_mode feature not enabled' do
|
||||
where(:public_builds, :user_project_role, :ci_debug_trace, :expected_status_code) do
|
||||
true | 'developer' | true | 200
|
||||
true | 'guest' | true | 200
|
||||
true | 'developer' | false | 200
|
||||
true | 'guest' | false | 200
|
||||
false | 'developer' | true | 200
|
||||
false | 'guest' | true | 403
|
||||
false | 'developer' | false | 200
|
||||
false | 'guest' | false | 403
|
||||
end
|
||||
|
||||
with_them do
|
||||
before do
|
||||
stub_feature_flags(restrict_access_to_build_debug_mode: false)
|
||||
ci_instance_variable.update!(value: ci_debug_trace)
|
||||
project.update!(public_builds: public_builds)
|
||||
project.add_role(user, user_project_role)
|
||||
end
|
||||
|
||||
it 'renders trace to authorized users' do
|
||||
visit trace_project_job_path(project, job)
|
||||
|
||||
expect(status_code).to eq(expected_status_code)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'raw page' do
|
||||
|
|
@ -237,35 +209,6 @@ RSpec.describe 'Project Jobs Permissions' do
|
|||
expect(page).to have_content(expected_msg)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when restrict_access_to_build_debug_mode feature not enabled' do
|
||||
where(:public_builds, :user_project_role, :ci_debug_trace, :expected_status_code, :expected_msg) do
|
||||
true | 'developer' | true | 200 | nil
|
||||
true | 'guest' | true | 200 | nil
|
||||
true | 'developer' | false | 200 | nil
|
||||
true | 'guest' | false | 200 | nil
|
||||
false | 'developer' | true | 200 | nil
|
||||
false | 'guest' | true | 403 | 'The current user is not authorized to access the job log'
|
||||
false | 'developer' | false | 200 | nil
|
||||
false | 'guest' | false | 403 | 'The current user is not authorized to access the job log'
|
||||
end
|
||||
|
||||
with_them do
|
||||
before do
|
||||
stub_feature_flags(restrict_access_to_build_debug_mode: false)
|
||||
ci_instance_variable.update!(value: ci_debug_trace)
|
||||
project.update!(public_builds: public_builds)
|
||||
project.add_role(user, user_project_role)
|
||||
end
|
||||
|
||||
it 'renders raw trace to authorized users' do
|
||||
visit raw_project_job_path(project, job)
|
||||
|
||||
expect(status_code).to eq(expected_status_code)
|
||||
expect(page).to have_content(expected_msg)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@ import VueApollo from 'vue-apollo';
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import getPipelineDetails from 'shared_queries/pipelines/get_pipeline_details.query.graphql';
|
||||
import PipelineGraphWrapper from '~/pipelines/components/graph/graph_component_wrapper.vue';
|
||||
import PipelineGraph from '~/pipelines/components/graph/graph_component.vue';
|
||||
import getPipelineDetails from '~/pipelines/graphql/queries/get_pipeline_details.query.graphql';
|
||||
import { mockPipelineResponse } from './mock_data';
|
||||
|
||||
const defaultProvide = {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import VueApollo from 'vue-apollo';
|
||||
import { mount, shallowMount, createLocalVue } from '@vue/test-utils';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import getPipelineDetails from 'shared_queries/pipelines/get_pipeline_details.query.graphql';
|
||||
import PipelineGraph from '~/pipelines/components/graph/graph_component.vue';
|
||||
import LinkedPipelinesColumn from '~/pipelines/components/graph/linked_pipelines_column.vue';
|
||||
import LinkedPipeline from '~/pipelines/components/graph/linked_pipeline.vue';
|
||||
import getPipelineDetails from '~/pipelines/graphql/queries/get_pipeline_details.query.graphql';
|
||||
import { DOWNSTREAM, GRAPHQL } from '~/pipelines/components/graph/constants';
|
||||
import { LOAD_FAILURE } from '~/pipelines/constants';
|
||||
import {
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ export const mockPipelineResponse = {
|
|||
},
|
||||
},
|
||||
needs: {
|
||||
__typename: 'CiJobConnection',
|
||||
__typename: 'CiBuildNeedConnection',
|
||||
nodes: [],
|
||||
},
|
||||
},
|
||||
|
|
@ -96,7 +96,7 @@ export const mockPipelineResponse = {
|
|||
},
|
||||
},
|
||||
needs: {
|
||||
__typename: 'CiJobConnection',
|
||||
__typename: 'CiBuildNeedConnection',
|
||||
nodes: [],
|
||||
},
|
||||
},
|
||||
|
|
@ -136,7 +136,7 @@ export const mockPipelineResponse = {
|
|||
},
|
||||
},
|
||||
needs: {
|
||||
__typename: 'CiJobConnection',
|
||||
__typename: 'CiBuildNeedConnection',
|
||||
nodes: [],
|
||||
},
|
||||
},
|
||||
|
|
@ -176,7 +176,7 @@ export const mockPipelineResponse = {
|
|||
},
|
||||
},
|
||||
needs: {
|
||||
__typename: 'CiJobConnection',
|
||||
__typename: 'CiBuildNeedConnection',
|
||||
nodes: [],
|
||||
},
|
||||
},
|
||||
|
|
@ -200,7 +200,7 @@ export const mockPipelineResponse = {
|
|||
},
|
||||
},
|
||||
needs: {
|
||||
__typename: 'CiJobConnection',
|
||||
__typename: 'CiBuildNeedConnection',
|
||||
nodes: [],
|
||||
},
|
||||
},
|
||||
|
|
@ -224,7 +224,7 @@ export const mockPipelineResponse = {
|
|||
},
|
||||
},
|
||||
needs: {
|
||||
__typename: 'CiJobConnection',
|
||||
__typename: 'CiBuildNeedConnection',
|
||||
nodes: [],
|
||||
},
|
||||
},
|
||||
|
|
@ -277,18 +277,18 @@ export const mockPipelineResponse = {
|
|||
},
|
||||
},
|
||||
needs: {
|
||||
__typename: 'CiJobConnection',
|
||||
__typename: 'CiBuildNeedConnection',
|
||||
nodes: [
|
||||
{
|
||||
__typename: 'CiJob',
|
||||
__typename: 'CiBuildNeed',
|
||||
name: 'build_c',
|
||||
},
|
||||
{
|
||||
__typename: 'CiJob',
|
||||
__typename: 'CiBuildNeed',
|
||||
name: 'build_b',
|
||||
},
|
||||
{
|
||||
__typename: 'CiJob',
|
||||
__typename: 'CiBuildNeed',
|
||||
name:
|
||||
'build_a_nlfjkdnlvskfnksvjknlfdjvlvnjdkjdf_nvjkenjkrlngjeknjkl',
|
||||
},
|
||||
|
|
@ -331,26 +331,26 @@ export const mockPipelineResponse = {
|
|||
},
|
||||
},
|
||||
needs: {
|
||||
__typename: 'CiJobConnection',
|
||||
__typename: 'CiBuildNeedConnection',
|
||||
nodes: [
|
||||
{
|
||||
__typename: 'CiJob',
|
||||
__typename: 'CiBuildNeed',
|
||||
name: 'build_d 3/3',
|
||||
},
|
||||
{
|
||||
__typename: 'CiJob',
|
||||
__typename: 'CiBuildNeed',
|
||||
name: 'build_d 2/3',
|
||||
},
|
||||
{
|
||||
__typename: 'CiJob',
|
||||
__typename: 'CiBuildNeed',
|
||||
name: 'build_d 1/3',
|
||||
},
|
||||
{
|
||||
__typename: 'CiJob',
|
||||
__typename: 'CiBuildNeed',
|
||||
name: 'build_b',
|
||||
},
|
||||
{
|
||||
__typename: 'CiJob',
|
||||
__typename: 'CiBuildNeed',
|
||||
name:
|
||||
'build_a_nlfjkdnlvskfnksvjknlfdjvlvnjdkjdf_nvjkenjkrlngjeknjkl',
|
||||
},
|
||||
|
|
@ -377,26 +377,26 @@ export const mockPipelineResponse = {
|
|||
},
|
||||
},
|
||||
needs: {
|
||||
__typename: 'CiJobConnection',
|
||||
__typename: 'CiBuildNeedConnection',
|
||||
nodes: [
|
||||
{
|
||||
__typename: 'CiJob',
|
||||
__typename: 'CiBuildNeed',
|
||||
name: 'build_d 3/3',
|
||||
},
|
||||
{
|
||||
__typename: 'CiJob',
|
||||
__typename: 'CiBuildNeed',
|
||||
name: 'build_d 2/3',
|
||||
},
|
||||
{
|
||||
__typename: 'CiJob',
|
||||
__typename: 'CiBuildNeed',
|
||||
name: 'build_d 1/3',
|
||||
},
|
||||
{
|
||||
__typename: 'CiJob',
|
||||
__typename: 'CiBuildNeed',
|
||||
name: 'build_b',
|
||||
},
|
||||
{
|
||||
__typename: 'CiJob',
|
||||
__typename: 'CiBuildNeed',
|
||||
name:
|
||||
'build_a_nlfjkdnlvskfnksvjknlfdjvlvnjdkjdf_nvjkenjkrlngjeknjkl',
|
||||
},
|
||||
|
|
@ -433,18 +433,18 @@ export const mockPipelineResponse = {
|
|||
action: null,
|
||||
},
|
||||
needs: {
|
||||
__typename: 'CiJobConnection',
|
||||
__typename: 'CiBuildNeedConnection',
|
||||
nodes: [
|
||||
{
|
||||
__typename: 'CiJob',
|
||||
__typename: 'CiBuildNeed',
|
||||
name: 'build_c',
|
||||
},
|
||||
{
|
||||
__typename: 'CiJob',
|
||||
__typename: 'CiBuildNeed',
|
||||
name: 'build_b',
|
||||
},
|
||||
{
|
||||
__typename: 'CiJob',
|
||||
__typename: 'CiBuildNeed',
|
||||
name:
|
||||
'build_a_nlfjkdnlvskfnksvjknlfdjvlvnjdkjdf_nvjkenjkrlngjeknjkl',
|
||||
},
|
||||
|
|
@ -481,10 +481,10 @@ export const mockPipelineResponse = {
|
|||
action: null,
|
||||
},
|
||||
needs: {
|
||||
__typename: 'CiJobConnection',
|
||||
__typename: 'CiBuildNeedConnection',
|
||||
nodes: [
|
||||
{
|
||||
__typename: 'CiJob',
|
||||
__typename: 'CiBuildNeed',
|
||||
name: 'build_b',
|
||||
},
|
||||
],
|
||||
|
|
@ -578,41 +578,54 @@ export const upstream = {
|
|||
export const wrappedPipelineReturn = {
|
||||
data: {
|
||||
project: {
|
||||
__typename: 'Project',
|
||||
pipeline: {
|
||||
__typename: 'Pipeline',
|
||||
id: 'gid://gitlab/Ci::Pipeline/175',
|
||||
iid: '38',
|
||||
downstream: {
|
||||
__typename: 'PipelineConnection',
|
||||
nodes: [],
|
||||
},
|
||||
upstream: {
|
||||
id: 'gid://gitlab/Ci::Pipeline/174',
|
||||
iid: '37',
|
||||
path: '/root/elemenohpee/-/pipelines/174',
|
||||
__typename: 'Pipeline',
|
||||
status: {
|
||||
__typename: 'DetailedStatus',
|
||||
group: 'success',
|
||||
label: 'passed',
|
||||
icon: 'status_success',
|
||||
},
|
||||
sourceJob: {
|
||||
name: 'test_c',
|
||||
__typename: 'CiJob',
|
||||
},
|
||||
project: {
|
||||
id: 'gid://gitlab/Project/25',
|
||||
name: 'elemenohpee',
|
||||
fullPath: 'root/elemenohpee',
|
||||
__typename: 'Project',
|
||||
},
|
||||
},
|
||||
stages: {
|
||||
__typename: 'CiStageConnection',
|
||||
nodes: [
|
||||
{
|
||||
name: 'build',
|
||||
__typename: 'CiStage',
|
||||
status: {
|
||||
action: null,
|
||||
__typename: 'DetailedStatus',
|
||||
},
|
||||
groups: {
|
||||
__typename: 'CiGroupConnection',
|
||||
nodes: [
|
||||
{
|
||||
__typename: 'CiGroup',
|
||||
status: {
|
||||
__typename: 'DetailedStatus',
|
||||
label: 'passed',
|
||||
group: 'success',
|
||||
icon: 'status_success',
|
||||
|
|
@ -620,20 +633,25 @@ export const wrappedPipelineReturn = {
|
|||
name: 'build_n',
|
||||
size: 1,
|
||||
jobs: {
|
||||
__typename: 'CiJobConnection',
|
||||
nodes: [
|
||||
{
|
||||
__typename: 'CiJob',
|
||||
name: 'build_n',
|
||||
scheduledAt: null,
|
||||
needs: {
|
||||
__typename: 'CiBuildNeedConnection',
|
||||
nodes: [],
|
||||
},
|
||||
status: {
|
||||
__typename: 'DetailedStatus',
|
||||
icon: 'status_success',
|
||||
tooltip: 'passed',
|
||||
hasDetails: true,
|
||||
detailsPath: '/root/elemenohpee/-/jobs/1662',
|
||||
group: 'success',
|
||||
action: {
|
||||
__typename: 'StatusAction',
|
||||
buttonTitle: 'Retry this job',
|
||||
icon: 'retry',
|
||||
path: '/root/elemenohpee/-/jobs/1662/retry',
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import Vue from 'vue';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import Vue, { nextTick } from 'vue';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import mountComponent from 'helpers/vue_mount_component_helper';
|
||||
import Api from '~/api';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import mrWidgetOptions from '~/vue_merge_request_widget/mr_widget_options.vue';
|
||||
import MrWidgetOptions from '~/vue_merge_request_widget/mr_widget_options.vue';
|
||||
import eventHub from '~/vue_merge_request_widget/event_hub';
|
||||
import notify from '~/lib/utils/notify';
|
||||
import SmartInterval from '~/smart_interval';
|
||||
|
|
@ -24,51 +24,47 @@ const returnPromise = (data) =>
|
|||
});
|
||||
});
|
||||
|
||||
describe('mrWidgetOptions', () => {
|
||||
let vm;
|
||||
describe('MrWidgetOptions', () => {
|
||||
let wrapper;
|
||||
let mock;
|
||||
let MrWidgetOptions;
|
||||
|
||||
const COLLABORATION_MESSAGE = 'Allows commits from members who can merge to the target branch';
|
||||
|
||||
beforeEach(() => {
|
||||
// Prevent component mounting
|
||||
delete mrWidgetOptions.el;
|
||||
|
||||
gl.mrWidgetData = { ...mockData };
|
||||
gon.features = { asyncMrWidget: true };
|
||||
|
||||
mock = new MockAdapter(axios);
|
||||
mock.onGet(mockData.merge_request_widget_path).reply(() => [200, { ...mockData }]);
|
||||
mock.onGet(mockData.merge_request_cached_widget_path).reply(() => [200, { ...mockData }]);
|
||||
|
||||
MrWidgetOptions = Vue.extend(mrWidgetOptions);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore();
|
||||
vm.$destroy();
|
||||
vm = null;
|
||||
wrapper.destroy();
|
||||
wrapper = null;
|
||||
|
||||
gl.mrWidgetData = {};
|
||||
gon.features = {};
|
||||
});
|
||||
|
||||
const createComponent = (mrData = mockData) => {
|
||||
if (vm) {
|
||||
vm.$destroy();
|
||||
if (wrapper) {
|
||||
wrapper.destroy();
|
||||
}
|
||||
|
||||
vm = mountComponent(MrWidgetOptions, {
|
||||
mrData: { ...mrData },
|
||||
wrapper = mount(MrWidgetOptions, {
|
||||
propsData: {
|
||||
mrData: { ...mrData },
|
||||
},
|
||||
});
|
||||
|
||||
return axios.waitForAll();
|
||||
};
|
||||
|
||||
const findSuggestPipeline = () => vm.$el.querySelector('[data-testid="mr-suggest-pipeline"]');
|
||||
const findSuggestPipelineButton = () => findSuggestPipeline().querySelector('button');
|
||||
const findSecurityMrWidget = () => vm.$el.querySelector('[data-testid="security-mr-widget"]');
|
||||
const findSuggestPipeline = () => wrapper.find('[data-testid="mr-suggest-pipeline"]');
|
||||
const findSuggestPipelineButton = () => findSuggestPipeline().find('button');
|
||||
const findSecurityMrWidget = () => wrapper.find('[data-testid="security-mr-widget"]');
|
||||
|
||||
describe('default', () => {
|
||||
beforeEach(() => {
|
||||
|
|
@ -77,147 +73,147 @@ describe('mrWidgetOptions', () => {
|
|||
|
||||
describe('data', () => {
|
||||
it('should instantiate Store and Service', () => {
|
||||
expect(vm.mr).toBeDefined();
|
||||
expect(vm.service).toBeDefined();
|
||||
expect(wrapper.vm.mr).toBeDefined();
|
||||
expect(wrapper.vm.service).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('computed', () => {
|
||||
describe('componentName', () => {
|
||||
it('should return merged component', () => {
|
||||
expect(vm.componentName).toEqual('mr-widget-merged');
|
||||
expect(wrapper.vm.componentName).toEqual('mr-widget-merged');
|
||||
});
|
||||
|
||||
it('should return conflicts component', () => {
|
||||
vm.mr.state = 'conflicts';
|
||||
wrapper.vm.mr.state = 'conflicts';
|
||||
|
||||
expect(vm.componentName).toEqual('mr-widget-conflicts');
|
||||
expect(wrapper.vm.componentName).toEqual('mr-widget-conflicts');
|
||||
});
|
||||
});
|
||||
|
||||
describe('shouldRenderMergeHelp', () => {
|
||||
it('should return false for the initial merged state', () => {
|
||||
expect(vm.shouldRenderMergeHelp).toBeFalsy();
|
||||
expect(wrapper.vm.shouldRenderMergeHelp).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should return true for a state which requires help widget', () => {
|
||||
vm.mr.state = 'conflicts';
|
||||
wrapper.vm.mr.state = 'conflicts';
|
||||
|
||||
expect(vm.shouldRenderMergeHelp).toBeTruthy();
|
||||
expect(wrapper.vm.shouldRenderMergeHelp).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('shouldRenderPipelines', () => {
|
||||
it('should return true when hasCI is true', () => {
|
||||
vm.mr.hasCI = true;
|
||||
wrapper.vm.mr.hasCI = true;
|
||||
|
||||
expect(vm.shouldRenderPipelines).toBeTruthy();
|
||||
expect(wrapper.vm.shouldRenderPipelines).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should return false when hasCI is false', () => {
|
||||
vm.mr.hasCI = false;
|
||||
wrapper.vm.mr.hasCI = false;
|
||||
|
||||
expect(vm.shouldRenderPipelines).toBeFalsy();
|
||||
expect(wrapper.vm.shouldRenderPipelines).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('shouldRenderRelatedLinks', () => {
|
||||
it('should return false for the initial data', () => {
|
||||
expect(vm.shouldRenderRelatedLinks).toBeFalsy();
|
||||
expect(wrapper.vm.shouldRenderRelatedLinks).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should return true if there is relatedLinks in MR', () => {
|
||||
Vue.set(vm.mr, 'relatedLinks', {});
|
||||
Vue.set(wrapper.vm.mr, 'relatedLinks', {});
|
||||
|
||||
expect(vm.shouldRenderRelatedLinks).toBeTruthy();
|
||||
expect(wrapper.vm.shouldRenderRelatedLinks).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('shouldRenderSourceBranchRemovalStatus', () => {
|
||||
beforeEach(() => {
|
||||
vm.mr.state = 'readyToMerge';
|
||||
wrapper.vm.mr.state = 'readyToMerge';
|
||||
});
|
||||
|
||||
it('should return true when cannot remove source branch and branch will be removed', () => {
|
||||
vm.mr.canRemoveSourceBranch = false;
|
||||
vm.mr.shouldRemoveSourceBranch = true;
|
||||
wrapper.vm.mr.canRemoveSourceBranch = false;
|
||||
wrapper.vm.mr.shouldRemoveSourceBranch = true;
|
||||
|
||||
expect(vm.shouldRenderSourceBranchRemovalStatus).toEqual(true);
|
||||
expect(wrapper.vm.shouldRenderSourceBranchRemovalStatus).toEqual(true);
|
||||
});
|
||||
|
||||
it('should return false when can remove source branch and branch will be removed', () => {
|
||||
vm.mr.canRemoveSourceBranch = true;
|
||||
vm.mr.shouldRemoveSourceBranch = true;
|
||||
wrapper.vm.mr.canRemoveSourceBranch = true;
|
||||
wrapper.vm.mr.shouldRemoveSourceBranch = true;
|
||||
|
||||
expect(vm.shouldRenderSourceBranchRemovalStatus).toEqual(false);
|
||||
expect(wrapper.vm.shouldRenderSourceBranchRemovalStatus).toEqual(false);
|
||||
});
|
||||
|
||||
it('should return false when cannot remove source branch and branch will not be removed', () => {
|
||||
vm.mr.canRemoveSourceBranch = false;
|
||||
vm.mr.shouldRemoveSourceBranch = false;
|
||||
wrapper.vm.mr.canRemoveSourceBranch = false;
|
||||
wrapper.vm.mr.shouldRemoveSourceBranch = false;
|
||||
|
||||
expect(vm.shouldRenderSourceBranchRemovalStatus).toEqual(false);
|
||||
expect(wrapper.vm.shouldRenderSourceBranchRemovalStatus).toEqual(false);
|
||||
});
|
||||
|
||||
it('should return false when in merged state', () => {
|
||||
vm.mr.canRemoveSourceBranch = false;
|
||||
vm.mr.shouldRemoveSourceBranch = true;
|
||||
vm.mr.state = 'merged';
|
||||
wrapper.vm.mr.canRemoveSourceBranch = false;
|
||||
wrapper.vm.mr.shouldRemoveSourceBranch = true;
|
||||
wrapper.vm.mr.state = 'merged';
|
||||
|
||||
expect(vm.shouldRenderSourceBranchRemovalStatus).toEqual(false);
|
||||
expect(wrapper.vm.shouldRenderSourceBranchRemovalStatus).toEqual(false);
|
||||
});
|
||||
|
||||
it('should return false when in nothing to merge state', () => {
|
||||
vm.mr.canRemoveSourceBranch = false;
|
||||
vm.mr.shouldRemoveSourceBranch = true;
|
||||
vm.mr.state = 'nothingToMerge';
|
||||
wrapper.vm.mr.canRemoveSourceBranch = false;
|
||||
wrapper.vm.mr.shouldRemoveSourceBranch = true;
|
||||
wrapper.vm.mr.state = 'nothingToMerge';
|
||||
|
||||
expect(vm.shouldRenderSourceBranchRemovalStatus).toEqual(false);
|
||||
expect(wrapper.vm.shouldRenderSourceBranchRemovalStatus).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('shouldRenderCollaborationStatus', () => {
|
||||
describe('when collaboration is allowed', () => {
|
||||
beforeEach(() => {
|
||||
vm.mr.allowCollaboration = true;
|
||||
wrapper.vm.mr.allowCollaboration = true;
|
||||
});
|
||||
|
||||
describe('when merge request is opened', () => {
|
||||
beforeEach((done) => {
|
||||
vm.mr.isOpen = true;
|
||||
vm.$nextTick(done);
|
||||
wrapper.vm.mr.isOpen = true;
|
||||
nextTick(done);
|
||||
});
|
||||
|
||||
it('should render collaboration status', () => {
|
||||
expect(vm.$el.textContent).toContain(COLLABORATION_MESSAGE);
|
||||
expect(wrapper.text()).toContain(COLLABORATION_MESSAGE);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when merge request is not opened', () => {
|
||||
beforeEach((done) => {
|
||||
vm.mr.isOpen = false;
|
||||
vm.$nextTick(done);
|
||||
wrapper.vm.mr.isOpen = false;
|
||||
nextTick(done);
|
||||
});
|
||||
|
||||
it('should not render collaboration status', () => {
|
||||
expect(vm.$el.textContent).not.toContain(COLLABORATION_MESSAGE);
|
||||
expect(wrapper.text()).not.toContain(COLLABORATION_MESSAGE);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when collaboration is not allowed', () => {
|
||||
beforeEach(() => {
|
||||
vm.mr.allowCollaboration = false;
|
||||
wrapper.vm.mr.allowCollaboration = false;
|
||||
});
|
||||
|
||||
describe('when merge request is opened', () => {
|
||||
beforeEach((done) => {
|
||||
vm.mr.isOpen = true;
|
||||
vm.$nextTick(done);
|
||||
wrapper.vm.mr.isOpen = true;
|
||||
nextTick(done);
|
||||
});
|
||||
|
||||
it('should not render collaboration status', () => {
|
||||
expect(vm.$el.textContent).not.toContain(COLLABORATION_MESSAGE);
|
||||
expect(wrapper.text()).not.toContain(COLLABORATION_MESSAGE);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -226,55 +222,55 @@ describe('mrWidgetOptions', () => {
|
|||
describe('showMergePipelineForkWarning', () => {
|
||||
describe('when the source project and target project are the same', () => {
|
||||
beforeEach((done) => {
|
||||
Vue.set(vm.mr, 'mergePipelinesEnabled', true);
|
||||
Vue.set(vm.mr, 'sourceProjectId', 1);
|
||||
Vue.set(vm.mr, 'targetProjectId', 1);
|
||||
vm.$nextTick(done);
|
||||
Vue.set(wrapper.vm.mr, 'mergePipelinesEnabled', true);
|
||||
Vue.set(wrapper.vm.mr, 'sourceProjectId', 1);
|
||||
Vue.set(wrapper.vm.mr, 'targetProjectId', 1);
|
||||
nextTick(done);
|
||||
});
|
||||
|
||||
it('should be false', () => {
|
||||
expect(vm.showMergePipelineForkWarning).toEqual(false);
|
||||
expect(wrapper.vm.showMergePipelineForkWarning).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when merge pipelines are not enabled', () => {
|
||||
beforeEach((done) => {
|
||||
Vue.set(vm.mr, 'mergePipelinesEnabled', false);
|
||||
Vue.set(vm.mr, 'sourceProjectId', 1);
|
||||
Vue.set(vm.mr, 'targetProjectId', 2);
|
||||
vm.$nextTick(done);
|
||||
Vue.set(wrapper.vm.mr, 'mergePipelinesEnabled', false);
|
||||
Vue.set(wrapper.vm.mr, 'sourceProjectId', 1);
|
||||
Vue.set(wrapper.vm.mr, 'targetProjectId', 2);
|
||||
nextTick(done);
|
||||
});
|
||||
|
||||
it('should be false', () => {
|
||||
expect(vm.showMergePipelineForkWarning).toEqual(false);
|
||||
expect(wrapper.vm.showMergePipelineForkWarning).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when merge pipelines are enabled _and_ the source project and target project are different', () => {
|
||||
beforeEach((done) => {
|
||||
Vue.set(vm.mr, 'mergePipelinesEnabled', true);
|
||||
Vue.set(vm.mr, 'sourceProjectId', 1);
|
||||
Vue.set(vm.mr, 'targetProjectId', 2);
|
||||
vm.$nextTick(done);
|
||||
Vue.set(wrapper.vm.mr, 'mergePipelinesEnabled', true);
|
||||
Vue.set(wrapper.vm.mr, 'sourceProjectId', 1);
|
||||
Vue.set(wrapper.vm.mr, 'targetProjectId', 2);
|
||||
nextTick(done);
|
||||
});
|
||||
|
||||
it('should be true', () => {
|
||||
expect(vm.showMergePipelineForkWarning).toEqual(true);
|
||||
expect(wrapper.vm.showMergePipelineForkWarning).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('formattedHumanAccess', () => {
|
||||
it('when user is a tool admin but not a member of project', () => {
|
||||
vm.mr.humanAccess = null;
|
||||
wrapper.vm.mr.humanAccess = null;
|
||||
|
||||
expect(vm.formattedHumanAccess).toEqual('');
|
||||
expect(wrapper.vm.formattedHumanAccess).toEqual('');
|
||||
});
|
||||
|
||||
it('when user a member of the project', () => {
|
||||
vm.mr.humanAccess = 'Owner';
|
||||
wrapper.vm.mr.humanAccess = 'Owner';
|
||||
|
||||
expect(vm.formattedHumanAccess).toEqual('owner');
|
||||
expect(wrapper.vm.formattedHumanAccess).toEqual('owner');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -285,9 +281,9 @@ describe('mrWidgetOptions', () => {
|
|||
let isCbExecuted;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(vm.service, 'checkStatus').mockReturnValue(returnPromise(mockData));
|
||||
jest.spyOn(vm.mr, 'setData').mockImplementation(() => {});
|
||||
jest.spyOn(vm, 'handleNotification').mockImplementation(() => {});
|
||||
jest.spyOn(wrapper.vm.service, 'checkStatus').mockReturnValue(returnPromise(mockData));
|
||||
jest.spyOn(wrapper.vm.mr, 'setData').mockImplementation(() => {});
|
||||
jest.spyOn(wrapper.vm, 'handleNotification').mockImplementation(() => {});
|
||||
|
||||
isCbExecuted = false;
|
||||
cb = () => {
|
||||
|
|
@ -296,12 +292,12 @@ describe('mrWidgetOptions', () => {
|
|||
});
|
||||
|
||||
it('should tell service to check status if document is visible', () => {
|
||||
vm.checkStatus(cb);
|
||||
wrapper.vm.checkStatus(cb);
|
||||
|
||||
return vm.$nextTick().then(() => {
|
||||
expect(vm.service.checkStatus).toHaveBeenCalled();
|
||||
expect(vm.mr.setData).toHaveBeenCalled();
|
||||
expect(vm.handleNotification).toHaveBeenCalledWith(mockData);
|
||||
return nextTick().then(() => {
|
||||
expect(wrapper.vm.service.checkStatus).toHaveBeenCalled();
|
||||
expect(wrapper.vm.mr.setData).toHaveBeenCalled();
|
||||
expect(wrapper.vm.handleNotification).toHaveBeenCalledWith(mockData);
|
||||
expect(isCbExecuted).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
|
@ -309,11 +305,11 @@ describe('mrWidgetOptions', () => {
|
|||
|
||||
describe('initPolling', () => {
|
||||
it('should call SmartInterval', () => {
|
||||
vm.initPolling();
|
||||
wrapper.vm.initPolling();
|
||||
|
||||
expect(SmartInterval).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
callback: vm.checkStatus,
|
||||
callback: wrapper.vm.checkStatus,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
|
@ -321,11 +317,11 @@ describe('mrWidgetOptions', () => {
|
|||
|
||||
describe('initDeploymentsPolling', () => {
|
||||
it('should call SmartInterval', () => {
|
||||
vm.initDeploymentsPolling();
|
||||
wrapper.vm.initDeploymentsPolling();
|
||||
|
||||
expect(SmartInterval).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
callback: vm.fetchPreMergeDeployments,
|
||||
callback: wrapper.vm.fetchPreMergeDeployments,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
|
@ -334,15 +330,15 @@ describe('mrWidgetOptions', () => {
|
|||
describe('fetchDeployments', () => {
|
||||
it('should fetch deployments', () => {
|
||||
jest
|
||||
.spyOn(vm.service, 'fetchDeployments')
|
||||
.spyOn(wrapper.vm.service, 'fetchDeployments')
|
||||
.mockReturnValue(returnPromise([{ id: 1, status: SUCCESS }]));
|
||||
|
||||
vm.fetchPreMergeDeployments();
|
||||
wrapper.vm.fetchPreMergeDeployments();
|
||||
|
||||
return vm.$nextTick().then(() => {
|
||||
expect(vm.service.fetchDeployments).toHaveBeenCalled();
|
||||
expect(vm.mr.deployments.length).toEqual(1);
|
||||
expect(vm.mr.deployments[0].id).toBe(1);
|
||||
return nextTick().then(() => {
|
||||
expect(wrapper.vm.service.fetchDeployments).toHaveBeenCalled();
|
||||
expect(wrapper.vm.mr.deployments.length).toEqual(1);
|
||||
expect(wrapper.vm.mr.deployments[0].id).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -350,13 +346,13 @@ describe('mrWidgetOptions', () => {
|
|||
describe('fetchActionsContent', () => {
|
||||
it('should fetch content of Cherry Pick and Revert modals', () => {
|
||||
jest
|
||||
.spyOn(vm.service, 'fetchMergeActionsContent')
|
||||
.spyOn(wrapper.vm.service, 'fetchMergeActionsContent')
|
||||
.mockReturnValue(returnPromise('hello world'));
|
||||
|
||||
vm.fetchActionsContent();
|
||||
wrapper.vm.fetchActionsContent();
|
||||
|
||||
return vm.$nextTick().then(() => {
|
||||
expect(vm.service.fetchMergeActionsContent).toHaveBeenCalled();
|
||||
return nextTick().then(() => {
|
||||
expect(wrapper.vm.service.fetchMergeActionsContent).toHaveBeenCalled();
|
||||
expect(document.body.textContent).toContain('hello world');
|
||||
});
|
||||
});
|
||||
|
|
@ -371,40 +367,40 @@ describe('mrWidgetOptions', () => {
|
|||
${'EnablePolling'} | ${'resumePolling'} | ${() => []}
|
||||
${'DisablePolling'} | ${'stopPolling'} | ${() => []}
|
||||
`('should bind to $event', ({ event, method, methodArgs }) => {
|
||||
jest.spyOn(vm, method).mockImplementation();
|
||||
jest.spyOn(wrapper.vm, method).mockImplementation();
|
||||
|
||||
const eventArg = {};
|
||||
eventHub.$emit(event, eventArg);
|
||||
|
||||
expect(vm[method]).toHaveBeenCalledWith(...methodArgs(eventArg));
|
||||
expect(wrapper.vm[method]).toHaveBeenCalledWith(...methodArgs(eventArg));
|
||||
});
|
||||
|
||||
it('should bind to SetBranchRemoveFlag', () => {
|
||||
expect(vm.mr.isRemovingSourceBranch).toBe(false);
|
||||
expect(wrapper.vm.mr.isRemovingSourceBranch).toBe(false);
|
||||
|
||||
eventHub.$emit('SetBranchRemoveFlag', [true]);
|
||||
|
||||
expect(vm.mr.isRemovingSourceBranch).toBe(true);
|
||||
expect(wrapper.vm.mr.isRemovingSourceBranch).toBe(true);
|
||||
});
|
||||
|
||||
it('should bind to FailedToMerge', () => {
|
||||
vm.mr.state = '';
|
||||
vm.mr.mergeError = '';
|
||||
wrapper.vm.mr.state = '';
|
||||
wrapper.vm.mr.mergeError = '';
|
||||
|
||||
const mergeError = 'Something bad happened!';
|
||||
eventHub.$emit('FailedToMerge', mergeError);
|
||||
|
||||
expect(vm.mr.state).toBe('failedToMerge');
|
||||
expect(vm.mr.mergeError).toBe(mergeError);
|
||||
expect(wrapper.vm.mr.state).toBe('failedToMerge');
|
||||
expect(wrapper.vm.mr.mergeError).toBe(mergeError);
|
||||
});
|
||||
|
||||
it('should bind to UpdateWidgetData', () => {
|
||||
jest.spyOn(vm.mr, 'setData').mockImplementation();
|
||||
jest.spyOn(wrapper.vm.mr, 'setData').mockImplementation();
|
||||
|
||||
const data = { ...mockData };
|
||||
eventHub.$emit('UpdateWidgetData', data);
|
||||
|
||||
expect(vm.mr.setData).toHaveBeenCalledWith(data);
|
||||
expect(wrapper.vm.mr.setData).toHaveBeenCalledWith(data);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -425,16 +421,17 @@ describe('mrWidgetOptions', () => {
|
|||
});
|
||||
|
||||
it('should call setFavicon method', async () => {
|
||||
vm.mr.ciStatusFaviconPath = overlayDataUrl;
|
||||
wrapper.vm.mr.ciStatusFaviconPath = overlayDataUrl;
|
||||
|
||||
await vm.setFaviconHelper();
|
||||
await wrapper.vm.setFaviconHelper();
|
||||
|
||||
expect(setFaviconOverlay).toHaveBeenCalledWith(overlayDataUrl);
|
||||
});
|
||||
|
||||
it('should not call setFavicon when there is no ciStatusFaviconPath', (done) => {
|
||||
vm.mr.ciStatusFaviconPath = null;
|
||||
vm.setFaviconHelper()
|
||||
wrapper.vm.mr.ciStatusFaviconPath = null;
|
||||
wrapper.vm
|
||||
.setFaviconHelper()
|
||||
.then(() => {
|
||||
expect(faviconElement.getAttribute('href')).toEqual(null);
|
||||
done();
|
||||
|
|
@ -453,12 +450,12 @@ describe('mrWidgetOptions', () => {
|
|||
beforeEach(() => {
|
||||
jest.spyOn(notify, 'notifyMe').mockImplementation(() => {});
|
||||
|
||||
vm.mr.ciStatus = 'failed';
|
||||
vm.mr.gitlabLogo = 'logo.png';
|
||||
wrapper.vm.mr.ciStatus = 'failed';
|
||||
wrapper.vm.mr.gitlabLogo = 'logo.png';
|
||||
});
|
||||
|
||||
it('should call notifyMe', () => {
|
||||
vm.handleNotification(data);
|
||||
wrapper.vm.handleNotification(data);
|
||||
|
||||
expect(notify.notifyMe).toHaveBeenCalledWith(
|
||||
'Pipeline running-label',
|
||||
|
|
@ -468,15 +465,15 @@ describe('mrWidgetOptions', () => {
|
|||
});
|
||||
|
||||
it('should not call notifyMe if the status has not changed', () => {
|
||||
vm.mr.ciStatus = data.ci_status;
|
||||
wrapper.vm.mr.ciStatus = data.ci_status;
|
||||
|
||||
vm.handleNotification(data);
|
||||
wrapper.vm.handleNotification(data);
|
||||
|
||||
expect(notify.notifyMe).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not notify if no pipeline provided', () => {
|
||||
vm.handleNotification({
|
||||
wrapper.vm.handleNotification({
|
||||
...data,
|
||||
pipeline: undefined,
|
||||
});
|
||||
|
|
@ -487,47 +484,49 @@ describe('mrWidgetOptions', () => {
|
|||
|
||||
describe('resumePolling', () => {
|
||||
it('should call stopTimer on pollingInterval', () => {
|
||||
jest.spyOn(vm.pollingInterval, 'resume').mockImplementation(() => {});
|
||||
jest.spyOn(wrapper.vm.pollingInterval, 'resume').mockImplementation(() => {});
|
||||
|
||||
vm.resumePolling();
|
||||
wrapper.vm.resumePolling();
|
||||
|
||||
expect(vm.pollingInterval.resume).toHaveBeenCalled();
|
||||
expect(wrapper.vm.pollingInterval.resume).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('stopPolling', () => {
|
||||
it('should call stopTimer on pollingInterval', () => {
|
||||
jest.spyOn(vm.pollingInterval, 'stopTimer').mockImplementation(() => {});
|
||||
jest.spyOn(wrapper.vm.pollingInterval, 'stopTimer').mockImplementation(() => {});
|
||||
|
||||
vm.stopPolling();
|
||||
wrapper.vm.stopPolling();
|
||||
|
||||
expect(vm.pollingInterval.stopTimer).toHaveBeenCalled();
|
||||
expect(wrapper.vm.pollingInterval.stopTimer).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('rendering relatedLinks', () => {
|
||||
beforeEach((done) => {
|
||||
vm.mr.relatedLinks = {
|
||||
assignToMe: null,
|
||||
closing: `
|
||||
<a class="close-related-link" href="#">
|
||||
Close
|
||||
</a>
|
||||
`,
|
||||
mentioned: '',
|
||||
};
|
||||
Vue.nextTick(done);
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
...mockData,
|
||||
issues_links: {
|
||||
closing: `
|
||||
<a class="close-related-link" href="#">
|
||||
Close
|
||||
</a>
|
||||
`,
|
||||
},
|
||||
});
|
||||
|
||||
return nextTick();
|
||||
});
|
||||
|
||||
it('renders if there are relatedLinks', () => {
|
||||
expect(vm.$el.querySelector('.close-related-link')).toBeDefined();
|
||||
expect(wrapper.find('.close-related-link').exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('does not render if state is nothingToMerge', (done) => {
|
||||
vm.mr.state = stateKey.nothingToMerge;
|
||||
Vue.nextTick(() => {
|
||||
expect(vm.$el.querySelector('.close-related-link')).toBeNull();
|
||||
wrapper.vm.mr.state = stateKey.nothingToMerge;
|
||||
nextTick(() => {
|
||||
expect(wrapper.find('.close-related-link').exists()).toBe(false);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
|
@ -535,15 +534,15 @@ describe('mrWidgetOptions', () => {
|
|||
|
||||
describe('rendering source branch removal status', () => {
|
||||
it('renders when user cannot remove branch and branch should be removed', (done) => {
|
||||
vm.mr.canRemoveSourceBranch = false;
|
||||
vm.mr.shouldRemoveSourceBranch = true;
|
||||
vm.mr.state = 'readyToMerge';
|
||||
wrapper.vm.mr.canRemoveSourceBranch = false;
|
||||
wrapper.vm.mr.shouldRemoveSourceBranch = true;
|
||||
wrapper.vm.mr.state = 'readyToMerge';
|
||||
|
||||
vm.$nextTick(() => {
|
||||
const tooltip = vm.$el.querySelector('[data-testid="question-o-icon"]');
|
||||
nextTick(() => {
|
||||
const tooltip = wrapper.find('[data-testid="question-o-icon"]');
|
||||
|
||||
expect(vm.$el.textContent).toContain('Deletes source branch');
|
||||
expect(tooltip.getAttribute('title')).toBe(
|
||||
expect(wrapper.text()).toContain('Deletes source branch');
|
||||
expect(tooltip.attributes('title')).toBe(
|
||||
'A user with write access to the source branch selected this option',
|
||||
);
|
||||
|
||||
|
|
@ -552,13 +551,13 @@ describe('mrWidgetOptions', () => {
|
|||
});
|
||||
|
||||
it('does not render in merged state', (done) => {
|
||||
vm.mr.canRemoveSourceBranch = false;
|
||||
vm.mr.shouldRemoveSourceBranch = true;
|
||||
vm.mr.state = 'merged';
|
||||
wrapper.vm.mr.canRemoveSourceBranch = false;
|
||||
wrapper.vm.mr.shouldRemoveSourceBranch = true;
|
||||
wrapper.vm.mr.state = 'merged';
|
||||
|
||||
vm.$nextTick(() => {
|
||||
expect(vm.$el.textContent).toContain('The source branch has been deleted');
|
||||
expect(vm.$el.textContent).not.toContain('Deletes source branch');
|
||||
nextTick(() => {
|
||||
expect(wrapper.text()).toContain('The source branch has been deleted');
|
||||
expect(wrapper.text()).not.toContain('Deletes source branch');
|
||||
|
||||
done();
|
||||
});
|
||||
|
|
@ -596,7 +595,7 @@ describe('mrWidgetOptions', () => {
|
|||
};
|
||||
|
||||
beforeEach((done) => {
|
||||
vm.mr.deployments.push(
|
||||
wrapper.vm.mr.deployments.push(
|
||||
{
|
||||
...deploymentMockData,
|
||||
},
|
||||
|
|
@ -606,33 +605,32 @@ describe('mrWidgetOptions', () => {
|
|||
},
|
||||
);
|
||||
|
||||
vm.$nextTick(done);
|
||||
nextTick(done);
|
||||
});
|
||||
|
||||
it('renders multiple deployments', () => {
|
||||
expect(vm.$el.querySelectorAll('.deploy-heading').length).toBe(2);
|
||||
expect(wrapper.findAll('.deploy-heading').length).toBe(2);
|
||||
});
|
||||
|
||||
it('renders dropdpown with multiple file changes', () => {
|
||||
expect(
|
||||
vm.$el
|
||||
.querySelector('.js-mr-wigdet-deployment-dropdown')
|
||||
.querySelectorAll('.js-filtered-dropdown-result').length,
|
||||
wrapper.find('.js-mr-wigdet-deployment-dropdown').findAll('.js-filtered-dropdown-result')
|
||||
.length,
|
||||
).toEqual(changes.length);
|
||||
});
|
||||
});
|
||||
|
||||
describe('code quality widget', () => {
|
||||
it('renders the component', () => {
|
||||
expect(vm.$el.querySelector('.js-codequality-widget')).toExist();
|
||||
expect(wrapper.find('.js-codequality-widget').exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('pipeline for target branch after merge', () => {
|
||||
describe('with information for target branch pipeline', () => {
|
||||
beforeEach((done) => {
|
||||
vm.mr.state = 'merged';
|
||||
vm.mr.mergePipeline = {
|
||||
wrapper.vm.mr.state = 'merged';
|
||||
wrapper.vm.mr.mergePipeline = {
|
||||
id: 127,
|
||||
user: {
|
||||
id: 1,
|
||||
|
|
@ -738,16 +736,16 @@ describe('mrWidgetOptions', () => {
|
|||
},
|
||||
cancel_path: '/root/ci-web-terminal/pipelines/127/cancel',
|
||||
};
|
||||
vm.$nextTick(done);
|
||||
nextTick(done);
|
||||
});
|
||||
|
||||
it('renders pipeline block', () => {
|
||||
expect(vm.$el.querySelector('.js-post-merge-pipeline')).not.toBeNull();
|
||||
expect(wrapper.find('.js-post-merge-pipeline').exists()).toBe(true);
|
||||
});
|
||||
|
||||
describe('with post merge deployments', () => {
|
||||
beforeEach((done) => {
|
||||
vm.mr.postMergeDeployments = [
|
||||
wrapper.vm.mr.postMergeDeployments = [
|
||||
{
|
||||
id: 15,
|
||||
name: 'review/diplo',
|
||||
|
|
@ -779,46 +777,46 @@ describe('mrWidgetOptions', () => {
|
|||
},
|
||||
];
|
||||
|
||||
vm.$nextTick(done);
|
||||
nextTick(done);
|
||||
});
|
||||
|
||||
it('renders post deployment information', () => {
|
||||
expect(vm.$el.querySelector('.js-post-deployment')).not.toBeNull();
|
||||
expect(wrapper.find('.js-post-deployment').exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('without information for target branch pipeline', () => {
|
||||
beforeEach((done) => {
|
||||
vm.mr.state = 'merged';
|
||||
wrapper.vm.mr.state = 'merged';
|
||||
|
||||
vm.$nextTick(done);
|
||||
nextTick(done);
|
||||
});
|
||||
|
||||
it('does not render pipeline block', () => {
|
||||
expect(vm.$el.querySelector('.js-post-merge-pipeline')).toBeNull();
|
||||
expect(wrapper.find('.js-post-merge-pipeline').exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when state is not merged', () => {
|
||||
beforeEach((done) => {
|
||||
vm.mr.state = 'archived';
|
||||
wrapper.vm.mr.state = 'archived';
|
||||
|
||||
vm.$nextTick(done);
|
||||
nextTick(done);
|
||||
});
|
||||
|
||||
it('does not render pipeline block', () => {
|
||||
expect(vm.$el.querySelector('.js-post-merge-pipeline')).toBeNull();
|
||||
expect(wrapper.find('.js-post-merge-pipeline').exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('does not render post deployment information', () => {
|
||||
expect(vm.$el.querySelector('.js-post-deployment')).toBeNull();
|
||||
expect(wrapper.find('.js-post-deployment').exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should not suggest pipelines when feature flag is not present', () => {
|
||||
expect(findSuggestPipeline()).toBeNull();
|
||||
expect(findSuggestPipeline().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -847,11 +845,11 @@ describe('mrWidgetOptions', () => {
|
|||
|
||||
if (shouldRender) {
|
||||
it('renders', () => {
|
||||
expect(findSecurityMrWidget()).toEqual(expect.any(HTMLElement));
|
||||
expect(findSecurityMrWidget().exists()).toBe(true);
|
||||
});
|
||||
} else {
|
||||
it('does not render', () => {
|
||||
expect(findSecurityMrWidget()).toBeNull();
|
||||
expect(findSecurityMrWidget().exists()).toBe(false);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -860,21 +858,17 @@ describe('mrWidgetOptions', () => {
|
|||
describe('suggestPipeline', () => {
|
||||
beforeEach(() => {
|
||||
mock.onAny().reply(200);
|
||||
|
||||
// This is needed because some grandchildren Bootstrap components throw warnings
|
||||
// https://gitlab.com/gitlab-org/gitlab/issues/208458
|
||||
jest.spyOn(console, 'warn').mockImplementation();
|
||||
});
|
||||
|
||||
describe('given feature flag is enabled', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
|
||||
vm.mr.hasCI = false;
|
||||
wrapper.vm.mr.hasCI = false;
|
||||
});
|
||||
|
||||
it('should suggest pipelines when none exist', () => {
|
||||
expect(findSuggestPipeline()).toEqual(expect.any(Element));
|
||||
expect(findSuggestPipeline().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it.each([
|
||||
|
|
@ -882,19 +876,17 @@ describe('mrWidgetOptions', () => {
|
|||
{ mergeRequestAddCiConfigPath: null },
|
||||
{ hasCI: true },
|
||||
])('with %s, should not suggest pipeline', async (obj) => {
|
||||
Object.assign(vm.mr, obj);
|
||||
Object.assign(wrapper.vm.mr, obj);
|
||||
|
||||
await vm.$nextTick();
|
||||
await nextTick();
|
||||
|
||||
expect(findSuggestPipeline()).toBeNull();
|
||||
expect(findSuggestPipeline().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('should allow dismiss of the suggest pipeline message', async () => {
|
||||
findSuggestPipelineButton().click();
|
||||
await findSuggestPipelineButton().trigger('click');
|
||||
|
||||
await vm.$nextTick();
|
||||
|
||||
expect(findSuggestPipeline()).toBeNull();
|
||||
expect(findSuggestPipeline().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,258 +0,0 @@
|
|||
import Vue from 'vue';
|
||||
import mountComponent from 'helpers/vue_mount_component_helper';
|
||||
import DeprecatedModal2 from '~/vue_shared/components/deprecated_modal_2.vue';
|
||||
|
||||
const modalComponent = Vue.extend(DeprecatedModal2);
|
||||
|
||||
describe('DeprecatedModal2', () => {
|
||||
let vm;
|
||||
|
||||
afterEach(() => {
|
||||
vm.$destroy();
|
||||
});
|
||||
|
||||
describe('props', () => {
|
||||
describe('with id', () => {
|
||||
const props = {
|
||||
id: 'my-modal',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vm = mountComponent(modalComponent, props);
|
||||
});
|
||||
|
||||
it('assigns the id to the modal', () => {
|
||||
expect(vm.$el.id).toBe(props.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('without id', () => {
|
||||
beforeEach(() => {
|
||||
vm = mountComponent(modalComponent, {});
|
||||
});
|
||||
|
||||
it('does not add an id attribute to the modal', () => {
|
||||
expect(vm.$el.hasAttribute('id')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with headerTitleText', () => {
|
||||
const props = {
|
||||
headerTitleText: 'my title text',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vm = mountComponent(modalComponent, props);
|
||||
});
|
||||
|
||||
it('sets the modal title', () => {
|
||||
const modalTitle = vm.$el.querySelector('.modal-title');
|
||||
|
||||
expect(modalTitle.innerHTML.trim()).toBe(props.headerTitleText);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with footerPrimaryButtonVariant', () => {
|
||||
const props = {
|
||||
footerPrimaryButtonVariant: 'danger',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vm = mountComponent(modalComponent, props);
|
||||
});
|
||||
|
||||
it('sets the primary button class', () => {
|
||||
const primaryButton = vm.$el.querySelector('.modal-footer button:last-of-type');
|
||||
|
||||
expect(primaryButton).toHaveClass(`btn-${props.footerPrimaryButtonVariant}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with footerPrimaryButtonText', () => {
|
||||
const props = {
|
||||
footerPrimaryButtonText: 'my button text',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vm = mountComponent(modalComponent, props);
|
||||
});
|
||||
|
||||
it('sets the primary button text', () => {
|
||||
const primaryButton = vm.$el.querySelector('.js-modal-primary-action .gl-button-text');
|
||||
|
||||
expect(primaryButton.innerHTML.trim()).toBe(props.footerPrimaryButtonText);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('works with data-toggle="modal"', () => {
|
||||
setFixtures(`
|
||||
<button id="modal-button" data-toggle="modal" data-target="#my-modal"></button>
|
||||
<div id="modal-container"></div>
|
||||
`);
|
||||
|
||||
const modalContainer = document.getElementById('modal-container');
|
||||
const modalButton = document.getElementById('modal-button');
|
||||
vm = mountComponent(
|
||||
modalComponent,
|
||||
{
|
||||
id: 'my-modal',
|
||||
},
|
||||
modalContainer,
|
||||
);
|
||||
const modalElement = document.getElementById('my-modal');
|
||||
|
||||
modalButton.click();
|
||||
|
||||
expect(modalElement).not.toHaveClass('show');
|
||||
|
||||
// let the modal fade in
|
||||
jest.runOnlyPendingTimers();
|
||||
|
||||
expect(modalElement).toHaveClass('show');
|
||||
});
|
||||
|
||||
describe('methods', () => {
|
||||
const dummyEvent = 'not really an event';
|
||||
|
||||
beforeEach(() => {
|
||||
vm = mountComponent(modalComponent, {});
|
||||
jest.spyOn(vm, '$emit').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
describe('emitCancel', () => {
|
||||
it('emits a cancel event', () => {
|
||||
vm.emitCancel(dummyEvent);
|
||||
|
||||
expect(vm.$emit).toHaveBeenCalledWith('cancel', dummyEvent);
|
||||
});
|
||||
});
|
||||
|
||||
describe('emitSubmit', () => {
|
||||
it('emits a submit event', () => {
|
||||
vm.emitSubmit(dummyEvent);
|
||||
|
||||
expect(vm.$emit).toHaveBeenCalledWith('submit', dummyEvent);
|
||||
});
|
||||
});
|
||||
|
||||
describe('opened', () => {
|
||||
it('emits a open event', () => {
|
||||
vm.opened();
|
||||
|
||||
expect(vm.$emit).toHaveBeenCalledWith('open');
|
||||
});
|
||||
});
|
||||
|
||||
describe('closed', () => {
|
||||
it('emits a closed event', () => {
|
||||
vm.closed();
|
||||
|
||||
expect(vm.$emit).toHaveBeenCalledWith('closed');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('slots', () => {
|
||||
const slotContent = 'this should go into the slot';
|
||||
|
||||
const modalWithSlot = (slot) => {
|
||||
return Vue.extend({
|
||||
components: {
|
||||
DeprecatedModal2,
|
||||
},
|
||||
render: (h) =>
|
||||
h('deprecated-modal-2', [slot ? h('template', { slot }, slotContent) : slotContent]),
|
||||
});
|
||||
};
|
||||
|
||||
describe('default slot', () => {
|
||||
beforeEach(() => {
|
||||
vm = mountComponent(modalWithSlot());
|
||||
});
|
||||
|
||||
it('sets the modal body', () => {
|
||||
const modalBody = vm.$el.querySelector('.modal-body');
|
||||
|
||||
expect(modalBody.innerHTML).toBe(slotContent);
|
||||
});
|
||||
});
|
||||
|
||||
describe('header slot', () => {
|
||||
beforeEach(() => {
|
||||
vm = mountComponent(modalWithSlot('header'));
|
||||
});
|
||||
|
||||
it('sets the modal header', () => {
|
||||
const modalHeader = vm.$el.querySelector('.modal-header');
|
||||
|
||||
expect(modalHeader.innerHTML).toBe(slotContent);
|
||||
});
|
||||
});
|
||||
|
||||
describe('title slot', () => {
|
||||
beforeEach(() => {
|
||||
vm = mountComponent(modalWithSlot('title'));
|
||||
});
|
||||
|
||||
it('sets the modal title', () => {
|
||||
const modalTitle = vm.$el.querySelector('.modal-title');
|
||||
|
||||
expect(modalTitle.innerHTML).toBe(slotContent);
|
||||
});
|
||||
});
|
||||
|
||||
describe('footer slot', () => {
|
||||
beforeEach(() => {
|
||||
vm = mountComponent(modalWithSlot('footer'));
|
||||
});
|
||||
|
||||
it('sets the modal footer', () => {
|
||||
const modalFooter = vm.$el.querySelector('.modal-footer');
|
||||
|
||||
expect(modalFooter.innerHTML).toBe(slotContent);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('handling sizes', () => {
|
||||
it('should render modal-sm', () => {
|
||||
vm = mountComponent(modalComponent, {
|
||||
modalSize: 'sm',
|
||||
});
|
||||
|
||||
expect(vm.$el.querySelector('.modal-dialog').classList.contains('modal-sm')).toEqual(true);
|
||||
});
|
||||
|
||||
it('should render modal-lg', () => {
|
||||
vm = mountComponent(modalComponent, {
|
||||
modalSize: 'lg',
|
||||
});
|
||||
|
||||
expect(vm.$el.querySelector('.modal-dialog').classList.contains('modal-lg')).toEqual(true);
|
||||
});
|
||||
|
||||
it('should render modal-xl', () => {
|
||||
vm = mountComponent(modalComponent, {
|
||||
modalSize: 'xl',
|
||||
});
|
||||
|
||||
expect(vm.$el.querySelector('.modal-dialog').classList.contains('modal-xl')).toEqual(true);
|
||||
});
|
||||
|
||||
it('should not add modal size classes when md size is passed', () => {
|
||||
vm = mountComponent(modalComponent, {
|
||||
modalSize: 'md',
|
||||
});
|
||||
|
||||
expect(vm.$el.querySelector('.modal-dialog').classList.contains('modal-md')).toEqual(false);
|
||||
});
|
||||
|
||||
it('should not add modal size classes by default', () => {
|
||||
vm = mountComponent(modalComponent, {});
|
||||
|
||||
expect(vm.$el.querySelector('.modal-dialog').classList.contains('modal-sm')).toEqual(false);
|
||||
expect(vm.$el.querySelector('.modal-dialog').classList.contains('modal-lg')).toEqual(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -7,6 +7,7 @@ exports[`Suggestion Diff component matches snapshot 1`] = `
|
|||
<suggestion-diff-header-stub
|
||||
batchsuggestionscount="1"
|
||||
class="qa-suggestion-diff-header js-suggestion-diff-header"
|
||||
defaultcommitmessage="Apply suggestion"
|
||||
helppagepath="path_to_docs"
|
||||
isapplyingbatch="true"
|
||||
isbatched="true"
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { GlDropdown, GlFormTextarea, GlButton } from '@gitlab/ui';
|
|||
import ApplySuggestionComponent from '~/vue_shared/components/markdown/apply_suggestion.vue';
|
||||
|
||||
describe('Apply Suggestion component', () => {
|
||||
const propsData = { fileName: 'test.js', disabled: false };
|
||||
const propsData = { defaultCommitMessage: 'Apply suggestion', disabled: false };
|
||||
let wrapper;
|
||||
|
||||
const createWrapper = (props) => {
|
||||
|
|
@ -27,7 +27,6 @@ describe('Apply Suggestion component', () => {
|
|||
|
||||
expect(dropdown.exists()).toBe(true);
|
||||
expect(dropdown.props('text')).toBe('Apply suggestion');
|
||||
expect(dropdown.props('headerText')).toBe('Apply suggestion commit message');
|
||||
expect(dropdown.props('disabled')).toBe(false);
|
||||
});
|
||||
|
||||
|
|
@ -35,7 +34,7 @@ describe('Apply Suggestion component', () => {
|
|||
const textArea = findTextArea();
|
||||
|
||||
expect(textArea.exists()).toBe(true);
|
||||
expect(textArea.attributes('placeholder')).toBe('Apply suggestion on test.js');
|
||||
expect(textArea.attributes('placeholder')).toBe('Apply suggestion');
|
||||
});
|
||||
|
||||
it('renders an apply button', () => {
|
||||
|
|
@ -55,11 +54,11 @@ describe('Apply Suggestion component', () => {
|
|||
});
|
||||
|
||||
describe('apply suggestion', () => {
|
||||
it('emits an apply event with a default message if no message was added', () => {
|
||||
it('emits an apply event with no message if no message was added', () => {
|
||||
findTextArea().vm.$emit('input', null);
|
||||
findApplyButton().vm.$emit('click');
|
||||
|
||||
expect(wrapper.emitted('apply')).toEqual([['Apply suggestion on test.js']]);
|
||||
expect(wrapper.emitted('apply')).toEqual([[null]]);
|
||||
});
|
||||
|
||||
it('emits an apply event with a user-defined message', () => {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ const DEFAULT_PROPS = {
|
|||
isBatched: false,
|
||||
isApplyingBatch: false,
|
||||
helpPagePath: 'path_to_docs',
|
||||
defaultCommitMessage: 'Apply suggestion',
|
||||
};
|
||||
|
||||
describe('Suggestion Diff component', () => {
|
||||
|
|
@ -91,7 +92,7 @@ describe('Suggestion Diff component', () => {
|
|||
});
|
||||
|
||||
it('emits apply', () => {
|
||||
expect(wrapper.emitted().apply).toEqual([[expect.any(Function)]]);
|
||||
expect(wrapper.emitted().apply).toEqual([[expect.any(Function), undefined]]);
|
||||
});
|
||||
|
||||
it('does not render apply suggestion and add to batch buttons', () => {
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ const MOCK_DATA = {
|
|||
is_applying_batch: true,
|
||||
},
|
||||
helpPagePath: 'path_to_docs',
|
||||
defaultCommitMessage: 'Apply suggestion',
|
||||
batchSuggestionsInfo: [{ suggestionId }],
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ const MOCK_DATA = {
|
|||
`,
|
||||
isApplied: false,
|
||||
helpPagePath: 'path_to_docs',
|
||||
defaultCommitMessage: 'Apply suggestion',
|
||||
};
|
||||
|
||||
describe('Suggestion component', () => {
|
||||
|
|
|
|||
|
|
@ -1,53 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::Middleware::Multipart::HandlerForJWTParams do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
let_it_be(:env) { Rack::MockRequest.env_for('/', method: 'post', params: {}) }
|
||||
let_it_be(:message) { { 'rewritten_fields' => {} } }
|
||||
|
||||
describe '#allowed_paths' do
|
||||
let_it_be(:expected_allowed_paths) do
|
||||
[
|
||||
Dir.tmpdir,
|
||||
::FileUploader.root,
|
||||
::Gitlab.config.uploads.storage_path,
|
||||
::JobArtifactUploader.workhorse_upload_path,
|
||||
::LfsObjectUploader.workhorse_upload_path,
|
||||
File.join(Rails.root, 'public/uploads/tmp')
|
||||
]
|
||||
end
|
||||
|
||||
let_it_be(:expected_with_packages_path) { expected_allowed_paths + [::Packages::PackageFileUploader.workhorse_upload_path] }
|
||||
|
||||
subject { described_class.new(env, message).send(:allowed_paths) }
|
||||
|
||||
where(:package_features_enabled, :object_storage_enabled, :direct_upload_enabled, :expected_paths) do
|
||||
false | false | true | :expected_allowed_paths
|
||||
false | false | false | :expected_allowed_paths
|
||||
false | true | true | :expected_allowed_paths
|
||||
false | true | false | :expected_allowed_paths
|
||||
true | false | true | :expected_with_packages_path
|
||||
true | false | false | :expected_with_packages_path
|
||||
true | true | true | :expected_allowed_paths
|
||||
true | true | false | :expected_with_packages_path
|
||||
end
|
||||
|
||||
with_them do
|
||||
before do
|
||||
stub_config(packages: {
|
||||
enabled: package_features_enabled,
|
||||
object_store: {
|
||||
enabled: object_storage_enabled,
|
||||
direct_upload: direct_upload_enabled
|
||||
},
|
||||
storage_path: '/any/dir'
|
||||
})
|
||||
end
|
||||
|
||||
it { is_expected.to eq(send(expected_paths)) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -21,10 +21,6 @@ RSpec.describe Gitlab::Middleware::Multipart do
|
|||
middleware.call(env)
|
||||
end
|
||||
|
||||
before do
|
||||
stub_feature_flags(upload_middleware_jwt_params_handler: true)
|
||||
end
|
||||
|
||||
context 'remote file mode' do
|
||||
let(:mode) { :remote }
|
||||
|
||||
|
|
@ -34,7 +30,7 @@ RSpec.describe Gitlab::Middleware::Multipart do
|
|||
include_context 'with one temporary file for multipart'
|
||||
|
||||
let(:rewritten_fields) { rewritten_fields_hash('file' => uploaded_filepath) }
|
||||
let(:params) { upload_parameters_for(key: 'file', filename: filename, remote_id: remote_id).merge('file.path' => '/should/not/be/read') }
|
||||
let(:params) { upload_parameters_for(key: 'file', mode: mode, filename: filename, remote_id: remote_id).merge('file.path' => '/should/not/be/read') }
|
||||
|
||||
it 'builds an UploadedFile' do
|
||||
expect_uploaded_files(original_filename: filename, remote_id: remote_id, size: uploaded_file.size, params_path: %w(file))
|
||||
|
|
@ -55,14 +51,14 @@ RSpec.describe Gitlab::Middleware::Multipart do
|
|||
let(:allowed_paths) { [Dir.tmpdir] }
|
||||
|
||||
before do
|
||||
expect_next_instance_of(::Gitlab::Middleware::Multipart::HandlerForJWTParams) do |handler|
|
||||
expect_next_instance_of(::Gitlab::Middleware::Multipart::Handler) do |handler|
|
||||
expect(handler).to receive(:allowed_paths).and_return(allowed_paths)
|
||||
end
|
||||
end
|
||||
|
||||
context 'in allowed paths' do
|
||||
let(:rewritten_fields) { rewritten_fields_hash('file' => uploaded_filepath) }
|
||||
let(:params) { upload_parameters_for(filepath: uploaded_filepath, key: 'file', filename: filename) }
|
||||
let(:params) { upload_parameters_for(filepath: uploaded_filepath, key: 'file', mode: mode, filename: filename) }
|
||||
|
||||
it 'builds an UploadedFile' do
|
||||
expect_uploaded_files(filepath: uploaded_filepath, original_filename: filename, size: uploaded_file.size, params_path: %w(file))
|
||||
|
|
@ -75,7 +71,7 @@ RSpec.describe Gitlab::Middleware::Multipart do
|
|||
let(:allowed_paths) { [] }
|
||||
|
||||
let(:rewritten_fields) { rewritten_fields_hash('file' => uploaded_filepath) }
|
||||
let(:params) { upload_parameters_for(filepath: uploaded_filepath, key: 'file') }
|
||||
let(:params) { upload_parameters_for(filepath: uploaded_filepath, key: 'file', mode: mode) }
|
||||
|
||||
it 'returns an error' do
|
||||
result = subject
|
||||
|
|
@ -89,7 +85,7 @@ RSpec.describe Gitlab::Middleware::Multipart do
|
|||
|
||||
context 'with dummy params in remote mode' do
|
||||
let(:rewritten_fields) { { 'file' => 'should/not/be/read' } }
|
||||
let(:params) { upload_parameters_for(key: 'file') }
|
||||
let(:params) { upload_parameters_for(key: 'file', mode: mode) }
|
||||
let(:mode) { :remote }
|
||||
|
||||
context 'with an invalid secret' do
|
||||
|
|
@ -128,7 +124,7 @@ RSpec.describe Gitlab::Middleware::Multipart do
|
|||
|
||||
RSpec.shared_examples 'rejecting the invalid key' do |key_in_header:, key_in_upload_params:, error_message:|
|
||||
let(:rewritten_fields) { rewritten_fields_hash(key_in_header => uploaded_filepath) }
|
||||
let(:params) { upload_parameters_for(filepath: uploaded_filepath, key: key_in_upload_params, filename: filename, remote_id: remote_id) }
|
||||
let(:params) { upload_parameters_for(filepath: uploaded_filepath, key: key_in_upload_params, mode: mode, filename: filename, remote_id: remote_id) }
|
||||
|
||||
it 'raises an error' do
|
||||
expect { subject }.to raise_error(RuntimeError, error_message)
|
||||
|
|
@ -171,7 +167,7 @@ RSpec.describe Gitlab::Middleware::Multipart do
|
|||
let(:rewritten_fields) { rewritten_fields_hash('file' => uploaded_filepath) }
|
||||
let(:crafted_payload) { Base64.urlsafe_encode64({ 'path' => 'test' }.to_json) }
|
||||
let(:params) do
|
||||
upload_parameters_for(filepath: uploaded_filepath, key: 'file', filename: filename, remote_id: remote_id).tap do |params|
|
||||
upload_parameters_for(filepath: uploaded_filepath, key: 'file', mode: mode, filename: filename, remote_id: remote_id).tap do |params|
|
||||
header, _, sig = params['file.gitlab-workhorse-upload'].split('.')
|
||||
params['file.gitlab-workhorse-upload'] = [header, crafted_payload, sig].join('.')
|
||||
end
|
||||
|
|
@ -187,7 +183,7 @@ RSpec.describe Gitlab::Middleware::Multipart do
|
|||
|
||||
let(:rewritten_fields) { rewritten_fields_hash('file' => uploaded_filepath) }
|
||||
let(:params) do
|
||||
upload_parameters_for(filepath: uploaded_filepath, key: 'file', filename: filename, remote_id: remote_id).tap do |params|
|
||||
upload_parameters_for(filepath: uploaded_filepath, key: 'file', mode: mode, filename: filename, remote_id: remote_id).tap do |params|
|
||||
header, payload, sig = params['file.gitlab-workhorse-upload'].split('.')
|
||||
params['file.gitlab-workhorse-upload'] = [header, payload, "#{sig}modified"].join('.')
|
||||
end
|
||||
|
|
@ -1,196 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::Middleware::Multipart do
|
||||
include MultipartHelpers
|
||||
|
||||
describe '#call' do
|
||||
let(:app) { double(:app) }
|
||||
let(:middleware) { described_class.new(app) }
|
||||
let(:secret) { Gitlab::Workhorse.secret }
|
||||
let(:issuer) { 'gitlab-workhorse' }
|
||||
|
||||
subject do
|
||||
env = post_env(
|
||||
rewritten_fields: rewritten_fields,
|
||||
params: params,
|
||||
secret: secret,
|
||||
issuer: issuer
|
||||
)
|
||||
middleware.call(env)
|
||||
end
|
||||
|
||||
before do
|
||||
stub_feature_flags(upload_middleware_jwt_params_handler: false)
|
||||
end
|
||||
|
||||
context 'remote file mode' do
|
||||
let(:mode) { :remote }
|
||||
|
||||
it_behaves_like 'handling all upload parameters conditions'
|
||||
|
||||
context 'and a path set' do
|
||||
include_context 'with one temporary file for multipart'
|
||||
|
||||
let(:rewritten_fields) { rewritten_fields_hash('file' => uploaded_filepath) }
|
||||
let(:params) { upload_parameters_for(key: 'file', filename: filename, remote_id: remote_id).merge('file.path' => '/should/not/be/read') }
|
||||
|
||||
it 'builds an UploadedFile' do
|
||||
expect_uploaded_files(original_filename: filename, remote_id: remote_id, size: uploaded_file.size, params_path: %w(file))
|
||||
|
||||
subject
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'local file mode' do
|
||||
let(:mode) { :local }
|
||||
|
||||
it_behaves_like 'handling all upload parameters conditions'
|
||||
|
||||
context 'when file is' do
|
||||
include_context 'with one temporary file for multipart'
|
||||
|
||||
let(:allowed_paths) { [Dir.tmpdir] }
|
||||
|
||||
before do
|
||||
expect_next_instance_of(::Gitlab::Middleware::Multipart::Handler) do |handler|
|
||||
expect(handler).to receive(:allowed_paths).and_return(allowed_paths)
|
||||
end
|
||||
end
|
||||
|
||||
context 'in allowed paths' do
|
||||
let(:rewritten_fields) { rewritten_fields_hash('file' => uploaded_filepath) }
|
||||
let(:params) { upload_parameters_for(filepath: uploaded_filepath, key: 'file', filename: filename) }
|
||||
|
||||
it 'builds an UploadedFile' do
|
||||
expect_uploaded_files(filepath: uploaded_filepath, original_filename: filename, size: uploaded_file.size, params_path: %w(file))
|
||||
|
||||
subject
|
||||
end
|
||||
end
|
||||
|
||||
context 'not in allowed paths' do
|
||||
let(:allowed_paths) { [] }
|
||||
|
||||
let(:rewritten_fields) { rewritten_fields_hash('file' => uploaded_filepath) }
|
||||
let(:params) { upload_parameters_for(filepath: uploaded_filepath, key: 'file') }
|
||||
|
||||
it 'returns an error' do
|
||||
result = subject
|
||||
|
||||
expect(result[0]).to eq(400)
|
||||
expect(result[2]).to include('insecure path used')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with dummy params in remote mode' do
|
||||
let(:rewritten_fields) { { 'file' => 'should/not/be/read' } }
|
||||
let(:params) { upload_parameters_for(key: 'file') }
|
||||
let(:mode) { :remote }
|
||||
|
||||
context 'with an invalid secret' do
|
||||
let(:secret) { 'INVALID_SECRET' }
|
||||
|
||||
it { expect { subject }.to raise_error(JWT::VerificationError) }
|
||||
end
|
||||
|
||||
context 'with an invalid issuer' do
|
||||
let(:issuer) { 'INVALID_ISSUER' }
|
||||
|
||||
it { expect { subject }.to raise_error(JWT::InvalidIssuerError) }
|
||||
end
|
||||
|
||||
context 'with invalid rewritten field key' do
|
||||
invalid_keys = [
|
||||
'[file]',
|
||||
';file',
|
||||
'file]',
|
||||
';file]',
|
||||
'file]]',
|
||||
'file;;'
|
||||
]
|
||||
|
||||
invalid_keys.each do |invalid_key|
|
||||
context invalid_key do
|
||||
let(:rewritten_fields) { { invalid_key => 'should/not/be/read' } }
|
||||
|
||||
it { expect { subject }.to raise_error(RuntimeError, "invalid field: \"#{invalid_key}\"") }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with invalid key in parameters' do
|
||||
include_context 'with one temporary file for multipart'
|
||||
|
||||
let(:rewritten_fields) { rewritten_fields_hash('file' => uploaded_filepath) }
|
||||
let(:params) { upload_parameters_for(filepath: uploaded_filepath, key: 'wrong_key', filename: filename, remote_id: remote_id) }
|
||||
|
||||
it 'builds no UploadedFile' do
|
||||
expect(app).to receive(:call) do |env|
|
||||
received_params = get_params(env)
|
||||
expect(received_params['file']).to be_nil
|
||||
expect(received_params['wrong_key']).to be_nil
|
||||
end
|
||||
|
||||
subject
|
||||
end
|
||||
end
|
||||
|
||||
context 'with invalid key in header' do
|
||||
include_context 'with one temporary file for multipart'
|
||||
|
||||
RSpec.shared_examples 'rejecting the invalid key' do |key_in_header:, key_in_upload_params:, error_message:|
|
||||
let(:rewritten_fields) { rewritten_fields_hash(key_in_header => uploaded_filepath) }
|
||||
let(:params) { upload_parameters_for(filepath: uploaded_filepath, key: key_in_upload_params, filename: filename, remote_id: remote_id) }
|
||||
|
||||
it 'raises an error' do
|
||||
expect { subject }.to raise_error(RuntimeError, error_message)
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'rejecting the invalid key',
|
||||
key_in_header: 'user[avatar',
|
||||
key_in_upload_params: 'user[avatar]',
|
||||
error_message: 'invalid field: "user[avatar"'
|
||||
it_behaves_like 'rejecting the invalid key',
|
||||
key_in_header: '[user]avatar',
|
||||
key_in_upload_params: 'user[avatar]',
|
||||
error_message: 'invalid field: "[user]avatar"'
|
||||
it_behaves_like 'rejecting the invalid key',
|
||||
key_in_header: 'user[]avatar',
|
||||
key_in_upload_params: 'user[avatar]',
|
||||
error_message: 'invalid field: "user[]avatar"'
|
||||
it_behaves_like 'rejecting the invalid key',
|
||||
key_in_header: 'user[avatar[image[url]]]',
|
||||
key_in_upload_params: 'user[avatar]',
|
||||
error_message: 'invalid field: "user[avatar[image[url]]]"'
|
||||
it_behaves_like 'rejecting the invalid key',
|
||||
key_in_header: '[]',
|
||||
key_in_upload_params: 'user[avatar]',
|
||||
error_message: 'invalid field: "[]"'
|
||||
it_behaves_like 'rejecting the invalid key',
|
||||
key_in_header: 'x' * 11000,
|
||||
key_in_upload_params: 'user[avatar]',
|
||||
error_message: "invalid field: \"#{'x' * 11000}\""
|
||||
end
|
||||
|
||||
context 'with key with unbalanced brackets in header' do
|
||||
include_context 'with one temporary file for multipart'
|
||||
|
||||
let(:invalid_key) { 'user[avatar' }
|
||||
let(:rewritten_fields) { rewritten_fields_hash( invalid_key => uploaded_filepath) }
|
||||
let(:params) { upload_parameters_for(filepath: uploaded_filepath, key: 'user[avatar]', filename: filename, remote_id: remote_id) }
|
||||
|
||||
it 'builds no UploadedFile' do
|
||||
expect(app).not_to receive(:call)
|
||||
|
||||
expect { subject }.to raise_error(RuntimeError, "invalid field: \"#{invalid_key}\"")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -27,12 +27,12 @@ RSpec.describe UploadedFile do
|
|||
end
|
||||
|
||||
it 'handles a blank path' do
|
||||
params['file.path'] = ''
|
||||
params['path'] = ''
|
||||
|
||||
# Not a real file, so can't determine size itself
|
||||
params['file.size'] = 1.byte
|
||||
params['size'] = 1.byte
|
||||
|
||||
expect { described_class.from_params(params, :file, upload_path) }
|
||||
expect { described_class.from_params(params, upload_path) }
|
||||
.not_to raise_error
|
||||
end
|
||||
end
|
||||
|
|
@ -50,7 +50,7 @@ RSpec.describe UploadedFile do
|
|||
end
|
||||
end
|
||||
|
||||
describe '.from_params_without_field' do
|
||||
describe '.from_params' do
|
||||
let(:upload_path) { nil }
|
||||
|
||||
after do
|
||||
|
|
@ -58,7 +58,7 @@ RSpec.describe UploadedFile do
|
|||
end
|
||||
|
||||
subject do
|
||||
described_class.from_params_without_field(params, [upload_path, Dir.tmpdir])
|
||||
described_class.from_params(params, [upload_path, Dir.tmpdir])
|
||||
end
|
||||
|
||||
context 'when valid file is specified' do
|
||||
|
|
@ -170,190 +170,6 @@ RSpec.describe UploadedFile do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.from_params' do
|
||||
let(:upload_path) { nil }
|
||||
let(:file_path_override) { nil }
|
||||
|
||||
after do
|
||||
FileUtils.rm_r(upload_path) if upload_path
|
||||
end
|
||||
|
||||
subject do
|
||||
described_class.from_params(params, :file, [upload_path, Dir.tmpdir], file_path_override)
|
||||
end
|
||||
|
||||
RSpec.shared_context 'filepath override' do
|
||||
let(:temp_file_override) { Tempfile.new(%w[override override], temp_dir) }
|
||||
let(:file_path_override) { temp_file_override.path }
|
||||
|
||||
before do
|
||||
FileUtils.touch(temp_file_override)
|
||||
end
|
||||
|
||||
after do
|
||||
FileUtils.rm_f(temp_file_override)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when valid file is specified' do
|
||||
context 'only local path is specified' do
|
||||
let(:params) do
|
||||
{ 'file.path' => temp_file.path }
|
||||
end
|
||||
|
||||
it { is_expected.not_to be_nil }
|
||||
|
||||
it "generates filename from path" do
|
||||
expect(subject.original_filename).to eq(::File.basename(temp_file.path))
|
||||
end
|
||||
end
|
||||
|
||||
context 'all parameters are specified' do
|
||||
context 'with a filepath' do
|
||||
let(:params) do
|
||||
{ 'file.path' => temp_file.path,
|
||||
'file.name' => 'dir/my file&.txt',
|
||||
'file.type' => 'my/type',
|
||||
'file.sha256' => 'sha256' }
|
||||
end
|
||||
|
||||
it_behaves_like 'using the file path',
|
||||
filename: 'my_file_.txt',
|
||||
content_type: 'my/type',
|
||||
sha256: 'sha256',
|
||||
path_suffix: 'test'
|
||||
end
|
||||
|
||||
context 'with a filepath override' do
|
||||
include_context 'filepath override'
|
||||
|
||||
let(:params) do
|
||||
{ 'file.path' => temp_file.path,
|
||||
'file.name' => 'dir/my file&.txt',
|
||||
'file.type' => 'my/type',
|
||||
'file.sha256' => 'sha256' }
|
||||
end
|
||||
|
||||
it_behaves_like 'using the file path',
|
||||
filename: 'my_file_.txt',
|
||||
content_type: 'my/type',
|
||||
sha256: 'sha256',
|
||||
path_suffix: 'override'
|
||||
end
|
||||
|
||||
context 'with a remote id' do
|
||||
let(:params) do
|
||||
{
|
||||
'file.name' => 'dir/my file&.txt',
|
||||
'file.sha256' => 'sha256',
|
||||
'file.remote_url' => 'http://localhost/file',
|
||||
'file.remote_id' => '1234567890',
|
||||
'file.etag' => 'etag1234567890',
|
||||
'file.size' => '123456'
|
||||
}
|
||||
end
|
||||
|
||||
it_behaves_like 'using the remote id',
|
||||
filename: 'my_file_.txt',
|
||||
content_type: 'application/octet-stream',
|
||||
sha256: 'sha256',
|
||||
size: 123456,
|
||||
remote_id: '1234567890'
|
||||
end
|
||||
|
||||
context 'with a path and a remote id' do
|
||||
let(:params) do
|
||||
{
|
||||
'file.path' => temp_file.path,
|
||||
'file.name' => 'dir/my file&.txt',
|
||||
'file.sha256' => 'sha256',
|
||||
'file.remote_url' => 'http://localhost/file',
|
||||
'file.remote_id' => '1234567890',
|
||||
'file.etag' => 'etag1234567890',
|
||||
'file.size' => '123456'
|
||||
}
|
||||
end
|
||||
|
||||
it_behaves_like 'using the remote id',
|
||||
filename: 'my_file_.txt',
|
||||
content_type: 'application/octet-stream',
|
||||
sha256: 'sha256',
|
||||
size: 123456,
|
||||
remote_id: '1234567890'
|
||||
end
|
||||
|
||||
context 'with a path override and a remote id' do
|
||||
include_context 'filepath override'
|
||||
|
||||
let(:params) do
|
||||
{
|
||||
'file.name' => 'dir/my file&.txt',
|
||||
'file.sha256' => 'sha256',
|
||||
'file.remote_url' => 'http://localhost/file',
|
||||
'file.remote_id' => '1234567890',
|
||||
'file.etag' => 'etag1234567890',
|
||||
'file.size' => '123456'
|
||||
}
|
||||
end
|
||||
|
||||
it_behaves_like 'using the remote id',
|
||||
filename: 'my_file_.txt',
|
||||
content_type: 'application/octet-stream',
|
||||
sha256: 'sha256',
|
||||
size: 123456,
|
||||
remote_id: '1234567890'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when no params are specified' do
|
||||
let(:params) do
|
||||
{}
|
||||
end
|
||||
|
||||
it "does not return an object" do
|
||||
is_expected.to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when verifying allowed paths' do
|
||||
let(:params) do
|
||||
{ 'file.path' => temp_file.path }
|
||||
end
|
||||
|
||||
context 'when file is stored in system temporary folder' do
|
||||
let(:temp_dir) { Dir.tmpdir }
|
||||
|
||||
it "succeeds" do
|
||||
is_expected.not_to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when file is stored in user provided upload path' do
|
||||
let(:upload_path) { Dir.mktmpdir }
|
||||
let(:temp_dir) { upload_path }
|
||||
|
||||
it "succeeds" do
|
||||
is_expected.not_to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when file is stored outside of user provided upload path' do
|
||||
let!(:generated_dir) { Dir.mktmpdir }
|
||||
let!(:temp_dir) { Dir.mktmpdir }
|
||||
|
||||
before do
|
||||
# We overwrite default temporary path
|
||||
allow(Dir).to receive(:tmpdir).and_return(generated_dir)
|
||||
end
|
||||
|
||||
it "raises an error" do
|
||||
expect { subject }.to raise_error(UploadedFile::InvalidPathError, /insecure path used/)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.initialize' do
|
||||
|
|
|
|||
|
|
@ -4775,22 +4775,6 @@ RSpec.describe Ci::Build do
|
|||
describe '#debug_mode?' do
|
||||
subject { build.debug_mode? }
|
||||
|
||||
context 'when feature is disabled' do
|
||||
before do
|
||||
stub_feature_flags(restrict_access_to_build_debug_mode: false)
|
||||
end
|
||||
|
||||
it { is_expected.to eq false }
|
||||
|
||||
context 'when in variables' do
|
||||
before do
|
||||
create(:ci_instance_variable, key: 'CI_DEBUG_TRACE', value: 'true')
|
||||
end
|
||||
|
||||
it { is_expected.to eq false }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when CI_DEBUG_TRACE=true is in variables' do
|
||||
context 'when in instance variables' do
|
||||
before do
|
||||
|
|
|
|||
|
|
@ -30,6 +30,10 @@ RSpec.describe API::Invitations do
|
|||
api("/#{source.model_name.plural}/#{source.id}/invitations", user)
|
||||
end
|
||||
|
||||
def invite_member_by_email(source, source_type, email, created_by)
|
||||
create(:"#{source_type}_member", invite_token: '123', invite_email: email, source: source, user: nil, created_by: created_by)
|
||||
end
|
||||
|
||||
shared_examples 'POST /:source_type/:id/invitations' do |source_type|
|
||||
context "with :source_type == #{source_type.pluralize}" do
|
||||
it_behaves_like 'a 404 response when source is private' do
|
||||
|
|
@ -280,10 +284,6 @@ RSpec.describe API::Invitations do
|
|||
expect(json_response.first['created_by_name']).to eq(developer.name)
|
||||
expect(json_response.first['user_name']).to eq(nil)
|
||||
end
|
||||
|
||||
def invite_member_by_email(source, source_type, email, created_by)
|
||||
create(:"#{source_type}_member", invite_token: '123', invite_email: email, source: source, user: nil, created_by: created_by)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -298,4 +298,80 @@ RSpec.describe API::Invitations do
|
|||
let(:source) { group }
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'DELETE /:source_type/:id/invitations/:email' do |source_type|
|
||||
def invite_api(source, user, email)
|
||||
api("/#{source.model_name.plural}/#{source.id}/invitations/#{email}", user)
|
||||
end
|
||||
|
||||
context "with :source_type == #{source_type.pluralize}" do
|
||||
let!(:invite) { invite_member_by_email(source, source_type, developer.email, developer) }
|
||||
|
||||
it_behaves_like 'a 404 response when source is private' do
|
||||
let(:route) { delete api("/#{source_type.pluralize}/#{source.id}/invitations/#{invite.invite_email}", stranger) }
|
||||
end
|
||||
|
||||
context 'when authenticated as a non-member or member with insufficient rights' do
|
||||
%i[access_requester stranger].each do |type|
|
||||
context "as a #{type}" do
|
||||
it 'returns 403' do
|
||||
user = public_send(type)
|
||||
|
||||
delete invite_api(source, user, invite.invite_email)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:forbidden)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when authenticated as a member and deleting themself' do
|
||||
it 'does not delete the member' do
|
||||
expect do
|
||||
delete invite_api(source, developer, invite.invite_email)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:forbidden)
|
||||
end.not_to change { source.members.count }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when authenticated as a maintainer/owner' do
|
||||
it 'deletes the member and returns 204 with no content' do
|
||||
expect do
|
||||
delete invite_api(source, maintainer, invite.invite_email)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:no_content)
|
||||
end.to change { source.members.count }.by(-1)
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns 404 if member does not exist' do
|
||||
delete invite_api(source, maintainer, non_existing_record_id)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
|
||||
it 'returns 422 for a valid request if the resource was not destroyed' do
|
||||
allow_next_instance_of(::Members::DestroyService) do |instance|
|
||||
allow(instance).to receive(:execute).with(invite).and_return(invite)
|
||||
end
|
||||
|
||||
delete invite_api(source, maintainer, invite.invite_email)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:unprocessable_entity)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'DELETE /projects/:id/inviations/:email' do
|
||||
it_behaves_like 'DELETE /:source_type/:id/invitations/:email', 'project' do
|
||||
let(:source) { project }
|
||||
end
|
||||
end
|
||||
|
||||
describe 'DELETE /groups/:id/inviations/:email' do
|
||||
it_behaves_like 'DELETE /:source_type/:id/invitations/:email', 'group' do
|
||||
let(:source) { group }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -827,32 +827,6 @@ RSpec.describe API::Jobs do
|
|||
expect(response).to have_gitlab_http_status(expected_status)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with restrict_access_to_build_debug_mode feature disabled' do
|
||||
before do
|
||||
stub_feature_flags(restrict_access_to_build_debug_mode: false)
|
||||
end
|
||||
|
||||
where(:public_builds, :user_project_role, :expected_status) do
|
||||
true | 'developer' | :ok
|
||||
true | 'guest' | :ok
|
||||
false | 'developer' | :ok
|
||||
false | 'guest' | :forbidden
|
||||
end
|
||||
|
||||
with_them do
|
||||
before do
|
||||
project.update!(public_builds: public_builds)
|
||||
project.add_role(user, user_project_role)
|
||||
|
||||
get api("/projects/#{project.id}/jobs/#{job.id}/trace", api_user)
|
||||
end
|
||||
|
||||
it 'renders trace to authorized users' do
|
||||
expect(response).to have_gitlab_http_status(expected_status)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -13,29 +13,23 @@ module MultipartHelpers
|
|||
)
|
||||
end
|
||||
|
||||
# This function assumes a `mode` variable to be set
|
||||
def upload_parameters_for(filepath: nil, key: nil, filename: 'filename', remote_id: 'remote_id')
|
||||
def upload_parameters_for(filepath: nil, key: nil, mode: nil, filename: 'filename', remote_id: 'remote_id')
|
||||
result = {
|
||||
"#{key}.name" => filename,
|
||||
"#{key}.type" => "application/octet-stream",
|
||||
"#{key}.sha256" => "1234567890"
|
||||
"name" => filename,
|
||||
"type" => "application/octet-stream",
|
||||
"sha256" => "1234567890"
|
||||
}
|
||||
|
||||
case mode
|
||||
when :local
|
||||
result["#{key}.path"] = filepath
|
||||
result["path"] = filepath
|
||||
when :remote
|
||||
result["#{key}.remote_id"] = remote_id
|
||||
result["#{key}.size"] = 3.megabytes
|
||||
result["remote_id"] = remote_id
|
||||
result["size"] = 3.megabytes
|
||||
else
|
||||
raise ArgumentError, "can't handle #{mode} mode"
|
||||
end
|
||||
|
||||
return result if ::Feature.disabled?(:upload_middleware_jwt_params_handler, default_enabled: true)
|
||||
|
||||
# the HandlerForJWTParams expects a jwt token with the upload parameters
|
||||
# *without* the "#{key}." prefix
|
||||
result.deep_transform_keys! { |k| k.remove("#{key}.") }
|
||||
{
|
||||
"#{key}.gitlab-workhorse-upload" => jwt_token(data: { 'upload' => result })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,28 +2,6 @@
|
|||
|
||||
RSpec.shared_examples 'handling file uploads' do |shared_examples_name|
|
||||
context 'with object storage disabled' do
|
||||
context 'with upload_middleware_jwt_params_handler disabled' do
|
||||
before do
|
||||
stub_feature_flags(upload_middleware_jwt_params_handler: false)
|
||||
|
||||
expect_next_instance_of(Gitlab::Middleware::Multipart::Handler) do |handler|
|
||||
expect(handler).to receive(:with_open_files).and_call_original
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like shared_examples_name
|
||||
end
|
||||
|
||||
context 'with upload_middleware_jwt_params_handler enabled' do
|
||||
before do
|
||||
stub_feature_flags(upload_middleware_jwt_params_handler: true)
|
||||
|
||||
expect_next_instance_of(Gitlab::Middleware::Multipart::HandlerForJWTParams) do |handler|
|
||||
expect(handler).to receive(:with_open_files).and_call_original
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like shared_examples_name
|
||||
end
|
||||
it_behaves_like shared_examples_name
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ RSpec.shared_examples 'handling all upload parameters conditions' do
|
|||
include_context 'with one temporary file for multipart'
|
||||
|
||||
let(:rewritten_fields) { rewritten_fields_hash('file' => uploaded_filepath) }
|
||||
let(:params) { upload_parameters_for(filepath: uploaded_filepath, key: 'file', filename: filename, remote_id: remote_id) }
|
||||
let(:params) { upload_parameters_for(filepath: uploaded_filepath, key: 'file', mode: mode, filename: filename, remote_id: remote_id) }
|
||||
|
||||
it 'builds an UploadedFile' do
|
||||
expect_uploaded_files(filepath: uploaded_filepath, original_filename: filename, remote_id: remote_id, size: uploaded_file.size, params_path: %w(file))
|
||||
|
|
@ -19,8 +19,8 @@ RSpec.shared_examples 'handling all upload parameters conditions' do
|
|||
|
||||
let(:rewritten_fields) { rewritten_fields_hash('file1' => uploaded_filepath, 'file2' => uploaded_filepath2) }
|
||||
let(:params) do
|
||||
upload_parameters_for(filepath: uploaded_filepath, key: 'file1', filename: filename, remote_id: remote_id).merge(
|
||||
upload_parameters_for(filepath: uploaded_filepath2, key: 'file2', filename: filename2, remote_id: remote_id2)
|
||||
upload_parameters_for(filepath: uploaded_filepath, key: 'file1', mode: mode, filename: filename, remote_id: remote_id).merge(
|
||||
upload_parameters_for(filepath: uploaded_filepath2, key: 'file2', mode: mode, filename: filename2, remote_id: remote_id2)
|
||||
)
|
||||
end
|
||||
|
||||
|
|
@ -38,7 +38,7 @@ RSpec.shared_examples 'handling all upload parameters conditions' do
|
|||
include_context 'with one temporary file for multipart'
|
||||
|
||||
let(:rewritten_fields) { rewritten_fields_hash('user[avatar]' => uploaded_filepath) }
|
||||
let(:params) { { 'user' => { 'avatar' => upload_parameters_for(filepath: uploaded_filepath, filename: filename, remote_id: remote_id) } } }
|
||||
let(:params) { { 'user' => { 'avatar' => upload_parameters_for(filepath: uploaded_filepath, mode: mode, filename: filename, remote_id: remote_id) } } }
|
||||
|
||||
it 'builds an UploadedFile' do
|
||||
expect_uploaded_files(filepath: uploaded_filepath, original_filename: filename, remote_id: remote_id, size: uploaded_file.size, params_path: %w(user avatar))
|
||||
|
|
@ -54,8 +54,8 @@ RSpec.shared_examples 'handling all upload parameters conditions' do
|
|||
let(:params) do
|
||||
{
|
||||
'user' => {
|
||||
'avatar' => upload_parameters_for(filepath: uploaded_filepath, filename: filename, remote_id: remote_id),
|
||||
'screenshot' => upload_parameters_for(filepath: uploaded_filepath2, filename: filename2, remote_id: remote_id2)
|
||||
'avatar' => upload_parameters_for(filepath: uploaded_filepath, mode: mode, filename: filename, remote_id: remote_id),
|
||||
'screenshot' => upload_parameters_for(filepath: uploaded_filepath2, mode: mode, filename: filename2, remote_id: remote_id2)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
|
@ -74,7 +74,7 @@ RSpec.shared_examples 'handling all upload parameters conditions' do
|
|||
include_context 'with one temporary file for multipart'
|
||||
|
||||
let(:rewritten_fields) { rewritten_fields_hash('user[avatar][bananas]' => uploaded_filepath) }
|
||||
let(:params) { { 'user' => { 'avatar' => { 'bananas' => upload_parameters_for(filepath: uploaded_filepath, filename: filename, remote_id: remote_id) } } } }
|
||||
let(:params) { { 'user' => { 'avatar' => { 'bananas' => upload_parameters_for(filepath: uploaded_filepath, mode: mode, filename: filename, remote_id: remote_id) } } } }
|
||||
|
||||
it 'builds an UploadedFile' do
|
||||
expect_uploaded_files(filepath: uploaded_file, original_filename: filename, remote_id: remote_id, size: uploaded_file.size, params_path: %w(user avatar bananas))
|
||||
|
|
@ -91,10 +91,10 @@ RSpec.shared_examples 'handling all upload parameters conditions' do
|
|||
{
|
||||
'user' => {
|
||||
'avatar' => {
|
||||
'bananas' => upload_parameters_for(filepath: uploaded_filepath, filename: filename, remote_id: remote_id)
|
||||
'bananas' => upload_parameters_for(filepath: uploaded_filepath, mode: mode, filename: filename, remote_id: remote_id)
|
||||
},
|
||||
'friend' => {
|
||||
'ananas' => upload_parameters_for(filepath: uploaded_filepath2, filename: filename2, remote_id: remote_id2)
|
||||
'ananas' => upload_parameters_for(filepath: uploaded_filepath2, mode: mode, filename: filename2, remote_id: remote_id2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -122,11 +122,11 @@ RSpec.shared_examples 'handling all upload parameters conditions' do
|
|||
end
|
||||
|
||||
let(:params) do
|
||||
upload_parameters_for(filepath: uploaded_filepath, filename: filename, key: 'file', remote_id: remote_id).merge(
|
||||
upload_parameters_for(filepath: uploaded_filepath, filename: filename, key: 'file', mode: mode, remote_id: remote_id).merge(
|
||||
'user' => {
|
||||
'avatar' => upload_parameters_for(filepath: uploaded_filepath2, filename: filename2, remote_id: remote_id2),
|
||||
'avatar' => upload_parameters_for(filepath: uploaded_filepath2, mode: mode, filename: filename2, remote_id: remote_id2),
|
||||
'friend' => {
|
||||
'avatar' => upload_parameters_for(filepath: uploaded_filepath3, filename: filename3, remote_id: remote_id3)
|
||||
'avatar' => upload_parameters_for(filepath: uploaded_filepath3, mode: mode, filename: filename3, remote_id: remote_id3)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,10 @@
|
|||
*
|
||||
!*/
|
||||
!.gitignore
|
||||
!.gitkeep
|
||||
|
||||
# explicitly list ignored sub directories to speed up performance
|
||||
/cache/
|
||||
/capybara/
|
||||
/letter_opener/
|
||||
/rubocop_cache/
|
||||
/tests/
|
||||
|
|
|
|||
Loading…
Reference in New Issue