Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
a53033814d
commit
7b197a72aa
|
@ -336,7 +336,7 @@
|
|||
|
||||
.qa-patterns: &qa-patterns
|
||||
- ".dockerignore"
|
||||
- "qa/**/*"
|
||||
- "{,jh/}qa/**/*"
|
||||
|
||||
# Code patterns + .ci-patterns
|
||||
.code-patterns: &code-patterns
|
||||
|
@ -414,7 +414,7 @@
|
|||
- ".gitlab/ci/**/*"
|
||||
# QA changes
|
||||
- ".dockerignore"
|
||||
- "qa/**/*"
|
||||
- "{,jh/}qa/**/*"
|
||||
# Mapped patterns (see tests.yml)
|
||||
- "data/whats_new/*.yml"
|
||||
|
||||
|
@ -448,7 +448,7 @@
|
|||
- "{,spec/}tooling/**/*"
|
||||
# QA changes
|
||||
- ".dockerignore"
|
||||
- "qa/**/*"
|
||||
- "{,jh/}qa/**/*"
|
||||
# Mapped patterns (see tests.yml)
|
||||
- "data/whats_new/*.yml"
|
||||
|
||||
|
@ -483,7 +483,7 @@
|
|||
- "{,spec/}tooling/**/*"
|
||||
# QA changes
|
||||
- ".dockerignore"
|
||||
- "qa/**/*"
|
||||
- "{,jh/}qa/**/*"
|
||||
# Workhorse changes
|
||||
- "GITLAB_WORKHORSE_VERSION"
|
||||
- "workhorse/**/*"
|
||||
|
|
|
@ -51,20 +51,11 @@ export default {
|
|||
required: true,
|
||||
type: Boolean,
|
||||
},
|
||||
canDestroy: {
|
||||
required: true,
|
||||
type: Boolean,
|
||||
},
|
||||
showInlineEditButton: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
showDeleteButton: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
enableAutocomplete: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
|
@ -494,14 +485,12 @@ export default {
|
|||
:endpoint="endpoint"
|
||||
:form-state="formState"
|
||||
:initial-description-text="initialDescriptionText"
|
||||
:can-destroy="canDestroy"
|
||||
:issuable-templates="issuableTemplates"
|
||||
:markdown-docs-path="markdownDocsPath"
|
||||
:markdown-preview-path="markdownPreviewPath"
|
||||
:project-path="projectPath"
|
||||
:project-id="projectId"
|
||||
:project-namespace="projectNamespace"
|
||||
:show-delete-button="showDeleteButton"
|
||||
:can-attach-file="canAttachFile"
|
||||
:enable-autocomplete="enableAutocomplete"
|
||||
:issuable-type="issuableType"
|
||||
|
|
|
@ -13,7 +13,8 @@ export default {
|
|||
props: {
|
||||
issuePath: {
|
||||
type: String,
|
||||
required: true,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
issueType: {
|
||||
type: String,
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
<script>
|
||||
import { GlButton, GlModalDirective } from '@gitlab/ui';
|
||||
import { uniqueId } from 'lodash';
|
||||
import { __, sprintf } from '~/locale';
|
||||
import { GlButton } from '@gitlab/ui';
|
||||
import { __ } from '~/locale';
|
||||
import Tracking from '~/tracking';
|
||||
import eventHub from '../event_hub';
|
||||
import updateMixin from '../mixins/update';
|
||||
import getIssueStateQuery from '../queries/get_issue_state.query.graphql';
|
||||
import DeleteIssueModal from './delete_issue_modal.vue';
|
||||
|
||||
const issuableTypes = {
|
||||
issue: __('Issue'),
|
||||
|
@ -18,18 +16,10 @@ const trackingMixin = Tracking.mixin({ label: 'delete_issue' });
|
|||
|
||||
export default {
|
||||
components: {
|
||||
DeleteIssueModal,
|
||||
GlButton,
|
||||
},
|
||||
directives: {
|
||||
GlModal: GlModalDirective,
|
||||
},
|
||||
mixins: [trackingMixin, updateMixin],
|
||||
props: {
|
||||
canDestroy: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
endpoint: {
|
||||
required: true,
|
||||
type: String,
|
||||
|
@ -38,11 +28,6 @@ export default {
|
|||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
showDeleteButton: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
issuableType: {
|
||||
type: String,
|
||||
required: true,
|
||||
|
@ -53,7 +38,6 @@ export default {
|
|||
deleteLoading: false,
|
||||
skipApollo: false,
|
||||
issueState: {},
|
||||
modalId: uniqueId('delete-issuable-modal-'),
|
||||
};
|
||||
},
|
||||
apollo: {
|
||||
|
@ -68,17 +52,9 @@ export default {
|
|||
},
|
||||
},
|
||||
computed: {
|
||||
deleteIssuableButtonText() {
|
||||
return sprintf(__('Delete %{issuableType}'), {
|
||||
issuableType: this.typeToShow.toLowerCase(),
|
||||
});
|
||||
},
|
||||
isSubmitEnabled() {
|
||||
return this.formState.title.trim() !== '';
|
||||
},
|
||||
shouldShowDeleteButton() {
|
||||
return this.canDestroy && this.showDeleteButton && this.typeToShow;
|
||||
},
|
||||
typeToShow() {
|
||||
const { issueState, issuableType } = this;
|
||||
const type = issueState.issueType ?? issuableType;
|
||||
|
@ -89,52 +65,26 @@ export default {
|
|||
closeForm() {
|
||||
eventHub.$emit('close.form');
|
||||
},
|
||||
deleteIssuable() {
|
||||
this.deleteLoading = true;
|
||||
eventHub.$emit('delete.issuable');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="gl-mt-3 gl-mb-3 gl-display-flex gl-justify-content-space-between">
|
||||
<div>
|
||||
<gl-button
|
||||
:loading="formState.updateLoading"
|
||||
:disabled="formState.updateLoading || !isSubmitEnabled"
|
||||
category="primary"
|
||||
variant="confirm"
|
||||
class="gl-mr-3"
|
||||
data-testid="issuable-save-button"
|
||||
type="submit"
|
||||
@click.prevent="updateIssuable"
|
||||
>
|
||||
{{ __('Save changes') }}
|
||||
</gl-button>
|
||||
<gl-button data-testid="issuable-cancel-button" @click="closeForm">
|
||||
{{ __('Cancel') }}
|
||||
</gl-button>
|
||||
</div>
|
||||
<div v-if="shouldShowDeleteButton">
|
||||
<gl-button
|
||||
v-gl-modal="modalId"
|
||||
:loading="deleteLoading"
|
||||
:disabled="deleteLoading"
|
||||
category="secondary"
|
||||
variant="danger"
|
||||
data-testid="issuable-delete-button"
|
||||
@click="track('click_button')"
|
||||
>
|
||||
{{ deleteIssuableButtonText }}
|
||||
</gl-button>
|
||||
<delete-issue-modal
|
||||
:issue-path="endpoint"
|
||||
:issue-type="typeToShow"
|
||||
:modal-id="modalId"
|
||||
:title="deleteIssuableButtonText"
|
||||
@delete="deleteIssuable"
|
||||
/>
|
||||
</div>
|
||||
<div class="gl-mt-3 gl-mb-3 gl-display-flex">
|
||||
<gl-button
|
||||
:loading="formState.updateLoading"
|
||||
:disabled="formState.updateLoading || !isSubmitEnabled"
|
||||
category="primary"
|
||||
variant="confirm"
|
||||
class="gl-mr-3"
|
||||
data-testid="issuable-save-button"
|
||||
type="submit"
|
||||
@click.prevent="updateIssuable"
|
||||
>
|
||||
{{ __('Save changes') }}
|
||||
</gl-button>
|
||||
<gl-button data-testid="issuable-cancel-button" @click="closeForm">
|
||||
{{ __('Cancel') }}
|
||||
</gl-button>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -22,10 +22,6 @@ export default {
|
|||
LockedWarning,
|
||||
},
|
||||
props: {
|
||||
canDestroy: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
endpoint: {
|
||||
type: String,
|
||||
required: true,
|
||||
|
@ -63,11 +59,6 @@ export default {
|
|||
type: String,
|
||||
required: true,
|
||||
},
|
||||
showDeleteButton: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
canAttachFile: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
|
@ -231,12 +222,6 @@ export default {
|
|||
:enable-autocomplete="enableAutocomplete"
|
||||
/>
|
||||
|
||||
<edit-actions
|
||||
:endpoint="endpoint"
|
||||
:form-state="formState"
|
||||
:can-destroy="canDestroy"
|
||||
:show-delete-button="showDeleteButton"
|
||||
:issuable-type="issuableType"
|
||||
/>
|
||||
<edit-actions :endpoint="endpoint" :form-state="formState" :issuable-type="issuableType" />
|
||||
</form>
|
||||
</template>
|
||||
|
|
|
@ -168,7 +168,7 @@ export default {
|
|||
</p>
|
||||
</template>
|
||||
<template v-else-if="!hasPipeline">
|
||||
<gl-loading-icon size="lg" />
|
||||
<gl-loading-icon size="md" />
|
||||
<p
|
||||
class="gl-flex-grow-1 gl-display-flex gl-ml-3 gl-mb-0"
|
||||
data-testid="monitoring-pipeline-message"
|
||||
|
|
|
@ -1,13 +1,23 @@
|
|||
<script>
|
||||
import { GlButton, GlTooltipDirective } from '@gitlab/ui';
|
||||
import { __ } from '~/locale';
|
||||
import StatusIcon from './mr_widget_status_icon.vue';
|
||||
import Actions from './action_buttons.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlButton,
|
||||
StatusIcon,
|
||||
Actions,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
props: {
|
||||
mr: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
|
@ -24,6 +34,10 @@ export default {
|
|||
default: () => [],
|
||||
},
|
||||
},
|
||||
i18n: {
|
||||
expandDetailsTooltip: __('Expand merge details'),
|
||||
collapseDetailsTooltip: __('Collapse merge details'),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
@ -36,18 +50,37 @@ export default {
|
|||
<slot name="icon">
|
||||
<status-icon :status="status" />
|
||||
</slot>
|
||||
<div
|
||||
:class="{ 'gl-display-flex': actions.length, 'gl-md-display-flex': !actions.length }"
|
||||
class="media-body"
|
||||
>
|
||||
<slot></slot>
|
||||
<div class="gl-display-flex gl-w-full">
|
||||
<div
|
||||
:class="{ 'gl-flex-direction-column-reverse': !actions.length }"
|
||||
class="gl-display-flex gl-md-display-block gl-font-size-0 gl-ml-auto"
|
||||
:class="{ 'gl-display-flex': actions.length, 'gl-md-display-flex': !actions.length }"
|
||||
class="media-body"
|
||||
>
|
||||
<slot name="actions">
|
||||
<actions v-if="actions.length" :tertiary-buttons="actions" />
|
||||
</slot>
|
||||
<slot></slot>
|
||||
<div
|
||||
:class="{ 'gl-flex-direction-column-reverse': !actions.length }"
|
||||
class="gl-display-flex gl-md-display-block gl-font-size-0 gl-ml-auto"
|
||||
>
|
||||
<slot name="actions">
|
||||
<actions v-if="actions.length" :tertiary-buttons="actions" />
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="gl-md-display-none gl-border-l-1 gl-border-l-solid gl-border-gray-100 gl-ml-3 gl-pl-3 gl-h-6 gl-mt-1"
|
||||
>
|
||||
<gl-button
|
||||
v-gl-tooltip
|
||||
:title="
|
||||
mr.mergeDetailsCollapsed
|
||||
? $options.i18n.expandDetailsTooltip
|
||||
: $options.i18n.collapseDetailsTooltip
|
||||
"
|
||||
:icon="mr.mergeDetailsCollapsed ? 'chevron-lg-down' : 'chevron-lg-up'"
|
||||
category="tertiary"
|
||||
size="small"
|
||||
class="gl-vertical-align-top"
|
||||
@click="() => mr.toggleMergeDetails()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -6,11 +6,17 @@ export default {
|
|||
components: {
|
||||
StateContainer,
|
||||
},
|
||||
props: {
|
||||
mr: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<state-container status="failed">
|
||||
<state-container :mr="mr" status="failed">
|
||||
<span class="gl-font-weight-bold">
|
||||
{{ s__('mrWidget|Merge unavailable: merge requests are read-only on archived projects.') }}
|
||||
</span>
|
||||
|
|
|
@ -150,7 +150,7 @@ export default {
|
|||
};
|
||||
</script>
|
||||
<template>
|
||||
<state-container status="scheduled" :is-loading="loading" :actions="actions">
|
||||
<state-container :mr="mr" status="scheduled" :is-loading="loading" :actions="actions">
|
||||
<template #loading>
|
||||
<gl-skeleton-loader :width="334" :height="30">
|
||||
<rect x="0" y="3" width="24" height="24" rx="4" />
|
||||
|
|
|
@ -58,7 +58,7 @@ export default {
|
|||
};
|
||||
</script>
|
||||
<template>
|
||||
<state-container status="failed" :actions="actions">
|
||||
<state-container :mr="mr" status="failed" :actions="actions">
|
||||
<span class="gl-font-weight-bold">
|
||||
<template v-if="mergeError">{{ mergeError }}</template>
|
||||
{{ s__('mrWidget|This merge request failed to be merged automatically') }}
|
||||
|
|
|
@ -6,10 +6,16 @@ export default {
|
|||
components: {
|
||||
StateContainer,
|
||||
},
|
||||
props: {
|
||||
mr: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<state-container status="loading">
|
||||
<state-container :mr="mr" status="loading">
|
||||
<span class="gl-font-weight-bold">
|
||||
{{ s__('mrWidget|Checking if merge request can be merged…') }}
|
||||
</span>
|
||||
|
|
|
@ -86,7 +86,7 @@ export default {
|
|||
};
|
||||
</script>
|
||||
<template>
|
||||
<state-container status="failed" :is-loading="isLoading">
|
||||
<state-container :mr="mr" status="failed" :is-loading="isLoading">
|
||||
<template #loading>
|
||||
<gl-skeleton-loader :width="334" :height="30">
|
||||
<rect x="0" y="7" width="150" height="16" rx="4" />
|
||||
|
|
|
@ -95,12 +95,12 @@ export default {
|
|||
};
|
||||
</script>
|
||||
<template>
|
||||
<state-container v-if="isRefreshing" status="loading">
|
||||
<state-container v-if="isRefreshing" :mr="mr" status="loading">
|
||||
<span class="gl-font-weight-bold">
|
||||
{{ s__('mrWidget|Refreshing now') }}
|
||||
</span>
|
||||
</state-container>
|
||||
<state-container v-else status="failed" :actions="actions">
|
||||
<state-container v-else :mr="mr" status="failed" :actions="actions">
|
||||
<span class="gl-font-weight-bold">
|
||||
<span v-if="mr.mergeError" class="has-error-message" data-testid="merge-error">
|
||||
{{ mergeError }}
|
||||
|
|
|
@ -150,7 +150,7 @@ export default {
|
|||
};
|
||||
</script>
|
||||
<template>
|
||||
<state-container :actions="actions" status="merged">
|
||||
<state-container :mr="mr" :actions="actions" status="merged">
|
||||
<mr-widget-author-time
|
||||
:action-text="s__('mrWidget|Merged by')"
|
||||
:author="mr.metrics.mergedBy"
|
||||
|
|
|
@ -152,7 +152,7 @@ export default {
|
|||
};
|
||||
</script>
|
||||
<template>
|
||||
<state-container :status="status" :is-loading="isLoading">
|
||||
<state-container :mr="mr" :status="status" :is-loading="isLoading">
|
||||
<template #loading>
|
||||
<gl-skeleton-loader :width="334" :height="30">
|
||||
<rect x="0" y="3" width="24" height="24" rx="4" />
|
||||
|
|
|
@ -22,7 +22,7 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<state-container status="failed">
|
||||
<state-container :mr="mr" status="failed">
|
||||
<span
|
||||
class="gl-font-weight-bold gl-md-mr-3 gl-flex-grow-1 gl-ml-0! gl-text-body!"
|
||||
data-qa-selector="head_mismatch_content"
|
||||
|
|
|
@ -24,7 +24,7 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<state-container status="failed">
|
||||
<state-container :mr="mr" status="failed">
|
||||
<span
|
||||
class="gl-ml-3 gl-font-weight-bold gl-w-100 gl-flex-grow-1 gl-md-mr-3 gl-ml-0! gl-text-body!"
|
||||
>
|
||||
|
|
|
@ -163,7 +163,7 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<state-container status="failed">
|
||||
<state-container :mr="mr" status="failed">
|
||||
<span class="gl-font-weight-bold gl-ml-0! gl-text-body! gl-flex-grow-1">
|
||||
{{ __("Merge blocked: merge request must be marked as ready. It's still marked as draft.") }}
|
||||
</span>
|
||||
|
|
|
@ -230,6 +230,11 @@ export default {
|
|||
shouldShowCodeQualityExtension() {
|
||||
return window.gon?.features?.refactorCodeQualityExtension;
|
||||
},
|
||||
shouldShowMergeDetails() {
|
||||
if (this.mr.state === 'readyToMerge') return true;
|
||||
|
||||
return !this.mr.mergeDetailsCollapsed;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
'mr.machineValue': {
|
||||
|
@ -318,6 +323,12 @@ export default {
|
|||
this.initPolling();
|
||||
this.bindEventHubListeners();
|
||||
eventHub.$on('mr.discussion.updated', this.checkStatus);
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
if (window.innerWidth >= 768) {
|
||||
this.mr.toggleMergeDetails(false);
|
||||
}
|
||||
});
|
||||
},
|
||||
getServiceEndpoints(store) {
|
||||
return {
|
||||
|
@ -621,7 +632,12 @@ export default {
|
|||
|
||||
<div class="mr-widget-section" data-qa-selector="mr_widget_content">
|
||||
<component :is="componentName" :mr="mr" :service="service" />
|
||||
<ready-to-merge v-if="mr.commitsCount" :mr="mr" :service="service" />
|
||||
<ready-to-merge
|
||||
v-if="mr.commitsCount"
|
||||
v-show="shouldShowMergeDetails"
|
||||
:mr="mr"
|
||||
:service="service"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<mr-widget-pipeline-container
|
||||
|
|
|
@ -28,6 +28,7 @@ export default class MergeRequestStore {
|
|||
|
||||
this.stateMachine = machine(STATE_MACHINE.definition);
|
||||
this.machineValue = this.stateMachine.value;
|
||||
this.mergeDetailsCollapsed = window.innerWidth < 768;
|
||||
|
||||
this.setPaths(data);
|
||||
|
||||
|
@ -405,4 +406,8 @@ export default class MergeRequestStore {
|
|||
|
||||
this.transitionStateMachine(transitionOptions);
|
||||
}
|
||||
|
||||
toggleMergeDetails(val = !this.mergeDetailsCollapsed) {
|
||||
this.mergeDetailsCollapsed = val;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,109 @@
|
|||
<script>
|
||||
import { GlButton, GlIcon, GlTooltipDirective } from '@gitlab/ui';
|
||||
|
||||
import { __ } from '~/locale';
|
||||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
import RichTimestampTooltip from '~/vue_shared/components/rich_timestamp_tooltip.vue';
|
||||
|
||||
import { STATE_OPEN } from '../../constants';
|
||||
import WorkItemLinksMenu from './work_item_links_menu.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlButton,
|
||||
GlIcon,
|
||||
RichTimestampTooltip,
|
||||
WorkItemLinksMenu,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
props: {
|
||||
projectPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
canUpdate: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
issuableGid: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
childItem: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
isItemOpen() {
|
||||
return this.childItem.state === STATE_OPEN;
|
||||
},
|
||||
iconClass() {
|
||||
return this.isItemOpen ? 'gl-text-green-500' : 'gl-text-blue-500';
|
||||
},
|
||||
iconName() {
|
||||
return this.isItemOpen ? 'issue-open-m' : 'issue-close';
|
||||
},
|
||||
stateTimestamp() {
|
||||
return this.isItemOpen ? this.childItem.createdAt : this.childItem.closedAt;
|
||||
},
|
||||
stateTimestampTypeText() {
|
||||
return this.isItemOpen ? __('Created') : __('Closed');
|
||||
},
|
||||
childPath() {
|
||||
return `/${this.projectPath}/-/work_items/${getIdFromGraphQLId(this.childItem.id)}`;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="gl-relative gl-display-flex gl-overflow-break-word gl-min-w-0 gl-bg-white gl-mb-3 gl-py-3 gl-px-4 gl-border gl-border-gray-100 gl-rounded-base gl-line-height-32"
|
||||
data-testid="links-child"
|
||||
>
|
||||
<div class="gl-overflow-hidden gl-display-flex gl-align-items-center gl-flex-grow-1">
|
||||
<span :id="`stateIcon-${childItem.id}`" class="gl-mr-3" data-testid="item-status-icon">
|
||||
<gl-icon :name="iconName" :class="iconClass" :aria-label="stateTimestampTypeText" />
|
||||
</span>
|
||||
<rich-timestamp-tooltip
|
||||
:target="`stateIcon-${childItem.id}`"
|
||||
:raw-timestamp="stateTimestamp"
|
||||
:timestamp-type-text="stateTimestampTypeText"
|
||||
/>
|
||||
<gl-icon
|
||||
v-if="childItem.confidential"
|
||||
v-gl-tooltip.top
|
||||
name="eye-slash"
|
||||
class="gl-mr-2 gl-text-orange-500"
|
||||
data-testid="confidential-icon"
|
||||
:aria-label="__('Confidential')"
|
||||
:title="__('Confidential')"
|
||||
/>
|
||||
<gl-button
|
||||
:href="childPath"
|
||||
category="tertiary"
|
||||
variant="link"
|
||||
class="gl-text-truncate gl-max-w-80 gl-text-black-normal!"
|
||||
@click="$emit('click', childItem.id, $event)"
|
||||
@mouseover="$emit('mouseover', childItem.id, $event)"
|
||||
@mouseout="$emit('mouseout', childItem.id, $event)"
|
||||
>
|
||||
{{ childItem.title }}
|
||||
</gl-button>
|
||||
</div>
|
||||
<div
|
||||
v-if="canUpdate"
|
||||
class="gl-ml-0 gl-sm-ml-auto! gl-display-inline-flex gl-align-items-center"
|
||||
>
|
||||
<work-item-links-menu
|
||||
:work-item-id="childItem.id"
|
||||
:parent-work-item-id="issuableGid"
|
||||
data-testid="links-menu"
|
||||
@removeChild="$emit('remove', childItem.id)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -9,18 +9,13 @@ import issueConfidentialQuery from '~/sidebar/queries/issue_confidential.query.g
|
|||
import { isMetaKey } from '~/lib/utils/common_utils';
|
||||
import { setUrlParams, updateHistory } from '~/lib/utils/url_utility';
|
||||
|
||||
import {
|
||||
STATE_OPEN,
|
||||
WIDGET_ICONS,
|
||||
WORK_ITEM_STATUS_TEXT,
|
||||
WIDGET_TYPE_HIERARCHY,
|
||||
} from '../../constants';
|
||||
import { WIDGET_ICONS, WORK_ITEM_STATUS_TEXT, WIDGET_TYPE_HIERARCHY } from '../../constants';
|
||||
import getWorkItemLinksQuery from '../../graphql/work_item_links.query.graphql';
|
||||
import updateWorkItemMutation from '../../graphql/update_work_item.mutation.graphql';
|
||||
import workItemQuery from '../../graphql/work_item.query.graphql';
|
||||
import WorkItemDetailModal from '../work_item_detail_modal.vue';
|
||||
import WorkItemLinkChild from './work_item_link_child.vue';
|
||||
import WorkItemLinksForm from './work_item_links_form.vue';
|
||||
import WorkItemLinksMenu from './work_item_links_menu.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -28,8 +23,8 @@ export default {
|
|||
GlIcon,
|
||||
GlAlert,
|
||||
GlLoadingIcon,
|
||||
WorkItemLinkChild,
|
||||
WorkItemLinksForm,
|
||||
WorkItemLinksMenu,
|
||||
WorkItemDetailModal,
|
||||
},
|
||||
directives: {
|
||||
|
@ -124,12 +119,6 @@ export default {
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
iconClass(state) {
|
||||
return state === STATE_OPEN ? 'gl-text-green-500' : 'gl-text-blue-500';
|
||||
},
|
||||
iconName(state) {
|
||||
return state === STATE_OPEN ? 'issue-open-m' : 'issue-close';
|
||||
},
|
||||
toggle() {
|
||||
this.isOpen = !this.isOpen;
|
||||
},
|
||||
|
@ -171,9 +160,6 @@ export default {
|
|||
replace: true,
|
||||
});
|
||||
},
|
||||
childPath(childItemId) {
|
||||
return `/${this.projectPath}/-/work_items/${getIdFromGraphQLId(childItemId)}`;
|
||||
},
|
||||
toggleChildFromCache(workItem, childId, store) {
|
||||
const sourceData = store.readQuery({
|
||||
query: getWorkItemLinksQuery,
|
||||
|
@ -322,48 +308,18 @@ export default {
|
|||
@cancel="hideAddForm"
|
||||
@addWorkItemChild="addChild"
|
||||
/>
|
||||
<div
|
||||
<work-item-link-child
|
||||
v-for="child in children"
|
||||
:key="child.id"
|
||||
class="gl-relative gl-display-flex gl-overflow-break-word gl-min-w-0 gl-bg-white gl-mb-3 gl-py-3 gl-px-4 gl-border gl-border-gray-100 gl-rounded-base gl-line-height-32"
|
||||
data-testid="links-child"
|
||||
>
|
||||
<div class="gl-overflow-hidden gl-display-flex gl-align-items-center gl-flex-grow-1">
|
||||
<gl-icon
|
||||
:name="iconName(child.state)"
|
||||
class="gl-mr-3"
|
||||
:class="iconClass(child.state)"
|
||||
/>
|
||||
<gl-icon
|
||||
v-if="child.confidential"
|
||||
v-gl-tooltip.top
|
||||
name="eye-slash"
|
||||
class="gl-mr-2 gl-text-orange-500"
|
||||
data-testid="confidential-icon"
|
||||
:title="__('Confidential')"
|
||||
/>
|
||||
<gl-button
|
||||
:href="childPath(child.id)"
|
||||
category="tertiary"
|
||||
variant="link"
|
||||
class="gl-text-truncate gl-max-w-80 gl-text-black-normal!"
|
||||
@click="openChild(child.id, $event)"
|
||||
@mouseover="prefetchWorkItem(child.id)"
|
||||
@mouseout="clearPrefetching"
|
||||
>
|
||||
{{ child.title }}
|
||||
</gl-button>
|
||||
</div>
|
||||
<div class="gl-ml-0 gl-sm-ml-auto! gl-display-inline-flex gl-align-items-center">
|
||||
<work-item-links-menu
|
||||
v-if="canUpdate"
|
||||
:work-item-id="child.id"
|
||||
:parent-work-item-id="issuableGid"
|
||||
data-testid="links-menu"
|
||||
@removeChild="removeChild(child.id)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
:project-path="projectPath"
|
||||
:can-update="canUpdate"
|
||||
:issuable-gid="issuableGid"
|
||||
:child-item="child"
|
||||
@click="openChild"
|
||||
@mouseover="prefetchWorkItem"
|
||||
@mouseout="clearPrefetching"
|
||||
@remove="removeChild"
|
||||
/>
|
||||
<work-item-detail-modal
|
||||
ref="modal"
|
||||
:work-item-id="activeChildId"
|
||||
|
|
|
@ -26,6 +26,8 @@ query workItemLinksQuery($id: WorkItemID!) {
|
|||
}
|
||||
title
|
||||
state
|
||||
createdAt
|
||||
closedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -62,9 +62,7 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
|
|||
options[:allow_tree_conflicts]
|
||||
]
|
||||
|
||||
if Feature.enabled?(:etag_merge_request_diff_batches, @merge_request.project)
|
||||
return unless stale?(etag: [cache_context + diff_options_hash.fetch(:paths, []), diffs])
|
||||
end
|
||||
return unless stale?(etag: [cache_context + diff_options_hash.fetch(:paths, []), diffs])
|
||||
|
||||
if diff_options_hash[:paths].blank?
|
||||
if Feature.enabled?(:remove_caching_diff_batches, @merge_request.project)
|
||||
|
|
|
@ -19,7 +19,11 @@ module Ci
|
|||
def builds_for_group_runner
|
||||
return new_builds.none if runner.namespace_ids.empty?
|
||||
|
||||
new_builds.where('ci_pending_builds.namespace_traversal_ids && ARRAY[?]::int[]', runner.namespace_ids)
|
||||
new_builds_relation = new_builds.where('ci_pending_builds.namespace_traversal_ids && ARRAY[?]::int[]', runner.namespace_ids)
|
||||
|
||||
return order(new_builds_relation) if ::Feature.enabled?(:order_builds_for_group_runner)
|
||||
|
||||
new_builds_relation
|
||||
end
|
||||
|
||||
def builds_matching_tag_ids(relation, ids)
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
= f.text_area :outbound_local_requests_allowlist_raw, placeholder: "example.com, 192.168.1.1, xn--itlab-j1a.com", class: 'form-control gl-form-input', rows: 8
|
||||
%span.form-text.text-muted
|
||||
= s_('OutboundRequests|Requests to these domains and IP addresses are accessible to both system hooks and web hooks even when local requests are not allowed. IP ranges such as 1:0:0:0:0:0:0:0/124 and 127.0.0.0/28 are supported. Domain wildcards are not supported. To separate entries use commas, semicolons, or newlines. The allowlist can hold a maximum of 1000 entries. Domains must be IDNA encoded.')
|
||||
= link_to _('Learn more.'), help_page_path('security/webhooks.md', anchor: 'allowlist-for-local-requests'), target: '_blank', rel: 'noopener noreferrer'
|
||||
= link_to _('Learn more.'), help_page_path('security/webhooks.md', anchor: 'create-an-allowlist-for-local-requests'), target: '_blank', rel: 'noopener noreferrer'
|
||||
|
||||
.form-group
|
||||
= f.gitlab_ui_checkbox_component :dns_rebinding_protection_enabled,
|
||||
|
|
|
@ -13,7 +13,6 @@
|
|||
- application_performance
|
||||
- attack_emulation
|
||||
- audit_events
|
||||
- audit_reports
|
||||
- authentication_and_authorization
|
||||
- auto_devops
|
||||
- backup_restore
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
---
|
||||
name: etag_merge_request_diff_batches
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/93953
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/369488
|
||||
milestone: '15.3'
|
||||
name: ci_cost_factors_narrow_os_contribution_by_plan
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/96273
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/372263
|
||||
milestone: '15.4'
|
||||
type: development
|
||||
group: group::code review
|
||||
group: group::pipeline execution
|
||||
default_enabled: false
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: order_builds_for_group_runner
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/94815
|
||||
rollout_issue_url:
|
||||
milestone: '15.4'
|
||||
type: development
|
||||
group: group::pipeline execution
|
||||
default_enabled: true
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
table_name: protected_environment_deploy_access_levels
|
||||
classes:
|
||||
- ProtectedEnvironment::DeployAccessLevel
|
||||
- ProtectedEnvironments::DeployAccessLevel
|
||||
feature_categories:
|
||||
- continuous_delivery
|
||||
description: https://docs.gitlab.com/ee/ci/environments/protected_environments.html
|
||||
|
|
|
@ -5,9 +5,9 @@
|
|||
#
|
||||
# For a list of all options, see https://errata-ai.gitbook.io/vale/getting-started/styles
|
||||
extends: existence
|
||||
message: 'Link "%s" must use the .md file extension.'
|
||||
message: 'Link "%s" must link directly to a file and use the .md file extension.'
|
||||
link: https://docs.gitlab.com/ee/development/documentation/styleguide/index.html#links-to-internal-documentation
|
||||
level: error
|
||||
scope: raw
|
||||
raw:
|
||||
- '\[.+\]\([\w\/\.-]+\.html[^)]*\)'
|
||||
- '\[[^\]]+\]\([^:\)]+(\/(#[^\)]+)?\)|\.html(#.+)?\))'
|
||||
|
|
|
@ -15,7 +15,7 @@ Read more about [group-level protected environments](../ci/environments/protecte
|
|||
|
||||
## Valid access levels
|
||||
|
||||
The access levels are defined in the `ProtectedEnvironment::DeployAccessLevel::ALLOWED_ACCESS_LEVELS` method.
|
||||
The access levels are defined in the `ProtectedEnvironments::DeployAccessLevel::ALLOWED_ACCESS_LEVELS` method.
|
||||
Currently, these levels are recognized:
|
||||
|
||||
```plaintext
|
||||
|
|
|
@ -11,7 +11,7 @@ type: concepts, howto
|
|||
|
||||
## Valid access levels
|
||||
|
||||
The access levels are defined in the `ProtectedEnvironment::DeployAccessLevel::ALLOWED_ACCESS_LEVELS` method.
|
||||
The access levels are defined in the `ProtectedEnvironments::DeployAccessLevel::ALLOWED_ACCESS_LEVELS` method.
|
||||
Currently, these levels are recognized:
|
||||
|
||||
```plaintext
|
||||
|
|
|
@ -16,7 +16,7 @@ GitLab CI/CD supports [OpenID Connect (OIDC)](https://openid.net/connect/faq/) t
|
|||
- Account on GitLab.
|
||||
- Access to a cloud provider that supports OIDC to configure authorization and create roles.
|
||||
|
||||
The original implementation of `CI_JOB_JWT` supports [HashiCorp Vault integration](../examples/authenticating-with-hashicorp-vault/). The updated implementation of `CI_JOB_JWT_V2` supports additional cloud providers with OIDC including AWS, Azure, GCP, and Vault.
|
||||
The original implementation of `CI_JOB_JWT` supports [HashiCorp Vault integration](../examples/authenticating-with-hashicorp-vault/index.md). The updated implementation of `CI_JOB_JWT_V2` supports additional cloud providers with OIDC including AWS, Azure, GCP, and Vault.
|
||||
|
||||
NOTE:
|
||||
Configuring OIDC enables JWT token access to the target environments for all pipelines.
|
||||
|
|
|
@ -6,7 +6,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
|
||||
# Deployment safety **(FREE)**
|
||||
|
||||
[Deployment jobs](../jobs/#deployment-jobs) are a specific kind of CI/CD
|
||||
[Deployment jobs](../jobs/index.md#deployment-jobs) are a specific kind of CI/CD
|
||||
job. They can be more sensitive than other jobs in a pipeline,
|
||||
and might need to be treated with extra care. GitLab has several features
|
||||
that help maintain deployment security and stability.
|
||||
|
@ -66,7 +66,7 @@ For more information, see [Resource Group documentation](../resource_groups/inde
|
|||
## Skip outdated deployment jobs
|
||||
|
||||
The effective execution order of pipeline jobs can vary from run to run, which
|
||||
could cause undesired behavior. For example, a [deployment job](../jobs/#deployment-jobs)
|
||||
could cause undesired behavior. For example, a [deployment job](../jobs/index.md#deployment-jobs)
|
||||
in a newer pipeline could finish before a deployment job in an older pipeline.
|
||||
This creates a race condition where the older deployment finishes later,
|
||||
overwriting the "newer" deployment.
|
||||
|
|
|
@ -156,7 +156,7 @@ If parsing JUnit report XML results in an error, an indicator is shown next to t
|
|||
|
||||
![Test Reports With Errors](img/pipelines_junit_test_report_with_errors_v13_10.png)
|
||||
|
||||
For test case parsing limits, see [Max test cases per unit test report](../../user/gitlab_com/#gitlab-cicd).
|
||||
For test case parsing limits, see [Max test cases per unit test report](../../user/gitlab_com/index.md#gitlab-cicd).
|
||||
|
||||
GitLab does not parse very [large nodes](https://nokogiri.org/tutorials/parsing_an_html_xml_document.html#parse-options) of JUnit reports. There is [an issue](https://gitlab.com/gitlab-org/gitlab/-/issues/268035) open to make this optional.
|
||||
|
||||
|
|
|
@ -169,7 +169,7 @@ GitLab can display the results of one or more reports in:
|
|||
- The pipeline [**Security** tab](../../user/application_security/vulnerability_report/pipeline.md#view-vulnerabilities-in-a-pipeline).
|
||||
- The [security dashboard](../../user/application_security/security_dashboard/index.md).
|
||||
- The [Project Vulnerability report](../../user/application_security/vulnerability_report/index.md).
|
||||
- The [dependency list](../../user/application_security/dependency_list/).
|
||||
- The [dependency list](../../user/application_security/dependency_list/index.md).
|
||||
|
||||
## `artifacts:reports:dotenv`
|
||||
|
||||
|
|
|
@ -177,7 +177,7 @@ If you get this error message while configuring GitLab, the following are possib
|
|||
- GitLab is unable to reach your Jenkins instance at the address. If your GitLab instance is self-managed, try pinging the
|
||||
Jenkins instance at the domain provided on the GitLab instance.
|
||||
- The Jenkins instance is at a local address and is not included in the
|
||||
[GitLab installation's allowlist](../security/webhooks.md#allowlist-for-local-requests).
|
||||
[GitLab installation's allowlist](../security/webhooks.md#create-an-allowlist-for-local-requests).
|
||||
- The credentials for the Jenkins instance do not have sufficient access or are invalid.
|
||||
- The **Enable authentication for `/project` end-point** checkbox is not selected in your [Jenkin's plugin configuration](#configure-the-jenkins-server).
|
||||
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 16 KiB |
|
@ -7,78 +7,80 @@ type: concepts, reference, howto
|
|||
|
||||
# Webhooks and insecure internal web services **(FREE SELF)**
|
||||
|
||||
Users with at least the Maintainer role can set up [Webhooks](../user/project/integrations/webhooks.md) that are
|
||||
Users with at least the Maintainer role can set up [webhooks](../user/project/integrations/webhooks.md) that are
|
||||
triggered when specific changes occur in a project. When triggered, a `POST` HTTP request is sent to a URL. A webhook is
|
||||
usually configured to send data to a specific external web service, which
|
||||
processes the data in an appropriate way.
|
||||
usually configured to send data to a specific external web service, which processes the data in an appropriate way.
|
||||
|
||||
However, a Webhook can be configured with a URL for an internal web service instead of an external web service.
|
||||
When the Webhook is triggered,
|
||||
non-GitLab web services running on your GitLab server or in its local network could be exploited.
|
||||
However, a webhook can be configured with a URL for an internal web service instead of an external web service.
|
||||
When the webhook is triggered, non-GitLab web services running on your GitLab server or in its local network could be
|
||||
exploited.
|
||||
|
||||
Webhook requests are made by the GitLab server itself and use a single
|
||||
(optional) secret token per hook for authorization (instead of a user or
|
||||
repository-specific token). As a result, these requests may have broader access than
|
||||
intended, including access to everything running on the server hosting the webhook. This
|
||||
may include the GitLab server or API itself (for example, `http://localhost:123`).
|
||||
Depending on the called webhook, this may also result in network access
|
||||
to other servers within that webhook server's local network (for example,
|
||||
`http://192.168.1.12:345`), even if these services are otherwise protected
|
||||
and inaccessible from the outside world.
|
||||
Webhook requests are made by the GitLab server itself and use a single optional secret token per hook for authorization
|
||||
instead of:
|
||||
|
||||
If a web service does not require authentication, Webhooks can be used to
|
||||
trigger destructive commands by getting the GitLab server to make POST requests
|
||||
to endpoints like `http://localhost:123/some-resource/delete`.
|
||||
- A user token.
|
||||
- A repository-specific token.
|
||||
|
||||
## Allow requests to local network
|
||||
As a result, these requests can have broader access than intended, including access to everything running on the server
|
||||
that hosts the webhook including:
|
||||
|
||||
To prevent this type of exploitation from happening, starting with GitLab 10.6,
|
||||
all Webhook requests to the current GitLab instance server address and/or in a
|
||||
private network are forbidden by default. That means that all requests made
|
||||
to `127.0.0.1`, `::1` and `0.0.0.0`, as well as IPv4 `10.0.0.0/8`, `172.16.0.0/12`,
|
||||
`192.168.0.0/16` and IPv6 site-local (`ffc0::/10`) addresses aren't allowed.
|
||||
- The GitLab server.
|
||||
- The API itself.
|
||||
- For some webhooks, network access to other servers in that webhook server's local network, even if these services
|
||||
are otherwise protected and inaccessible from the outside world.
|
||||
|
||||
This behavior can be overridden:
|
||||
Webhooks can be used to trigger destructive commands using web services that don't require authentication. These webhooks
|
||||
can get the GitLab server to make `POST` HTTP requests to endpoints that delete resources.
|
||||
|
||||
## Allow webhook and service requests to local network
|
||||
|
||||
To prevent exploitation of insecure internal web services, all webhook requests to the following local network addresses are not allowed:
|
||||
|
||||
- The current GitLab instance server address.
|
||||
- Private network addresses, including `127.0.0.1`, `::1`, `0.0.0.0`, `10.0.0.0/8`, `172.16.0.0/12`,
|
||||
`192.168.0.0/16`, and IPv6 site-local (`ffc0::/10`) addresses.
|
||||
|
||||
To allow access to these addresses:
|
||||
|
||||
1. On the top bar, select **Menu > Admin**.
|
||||
1. On the left sidebar, select **Settings > Network**.
|
||||
1. Expand the **Outbound requests** section.
|
||||
1. Select **Allow requests to the local network from web hooks and services**.
|
||||
1. Expand **Outbound requests**.
|
||||
1. Select the **Allow requests to the local network from web hooks and services** checkbox.
|
||||
|
||||
NOTE:
|
||||
*System hooks* are enabled to make requests to local network by default since they are
|
||||
set up by administrators. However, you can turn this off by disabling the
|
||||
**Allow requests to the local network from system hooks** option.
|
||||
## Prevent system hook requests to local network
|
||||
|
||||
## Allowlist for local requests
|
||||
[System hooks](../administration/system_hooks.md) are permitted to make requests to local network by default because
|
||||
they are set up by administrators. To prevent system hook requests to the local network:
|
||||
|
||||
1. On the top bar, select **Menu > Admin**.
|
||||
1. On the left sidebar, select **Settings > Network**.
|
||||
1. Expand **Outbound requests**.
|
||||
1. Clear the **Allow requests to the local network from system hooks** checkbox.
|
||||
|
||||
## Create an allowlist for local requests
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/44496) in GitLab 12.2
|
||||
|
||||
You can allow certain domains and IP addresses to be accessible to both *system hooks*
|
||||
and *webhooks* even when local requests are not allowed by adding them to the
|
||||
allowlist:
|
||||
You can allow certain domains and IP addresses to be accessible to both system hooks and webhooks, even when local
|
||||
requests are forbidden. To add these domains to the allowlist:
|
||||
|
||||
1. On the top bar, select **Menu > Admin**.
|
||||
1. On the left sidebar, select **Settings > Network** (`/admin/application_settings/network`)
|
||||
and expand **Outbound requests**:
|
||||
1. On the left sidebar, select **Settings > Network**.
|
||||
1. Expand **Outbound requests** and add entries.
|
||||
|
||||
![Outbound local requests allowlist](img/allowlist_v13_0.png)
|
||||
The entries can:
|
||||
|
||||
The allowed entries can be separated by semicolons, commas or whitespaces
|
||||
(including newlines) and be in different formats like hostnames, IP addresses and/or
|
||||
IP ranges. IPv6 is supported. Hostnames that contain Unicode characters should
|
||||
use [Internationalized Domain Names in Applications](https://www.icann.org/en/icann-acronyms-and-terms/internationalized-domain-names-in-applications-en)
|
||||
(IDNA) encoding.
|
||||
- Be separated by semicolons, commas, or whitespaces (including newlines).
|
||||
- Be in different formats like hostnames, IP addresses, IP address ranges. IPv6 is supported. Hostnames that contain
|
||||
Unicode characters should use [Internationalized Domain Names in Applications](https://www.icann.org/en/icann-acronyms-and-terms/internationalized-domain-names-in-applications-en)
|
||||
(IDNA) encoding.
|
||||
- Include ports. For example, `127.0.0.1:8080` only allows connections to port 8080 on `127.0.0.1`. If no port is specified,
|
||||
all ports on that IP address or domain are allowed. An IP address range allows all ports on all IP addresses in that
|
||||
range.
|
||||
- Number no more than 1000 entries of no more than 255 characters for each entry.
|
||||
- Not contain wildcards (for example, `*.example.com`).
|
||||
|
||||
The allowlist can hold a maximum of 1000 entries. Each entry can be a maximum of
|
||||
255 characters.
|
||||
|
||||
You can allow a particular port by specifying it in the allowlist entry.
|
||||
For example `127.0.0.1:8080` only allows connections to port 8080 on `127.0.0.1`.
|
||||
If no port is mentioned, all ports on that IP/domain are allowed. An IP range
|
||||
allows all ports on all IPs in that range.
|
||||
|
||||
Example:
|
||||
For example:
|
||||
|
||||
```plaintext
|
||||
example.com;gitlab.example.com
|
||||
|
@ -89,9 +91,6 @@ example.com;gitlab.example.com
|
|||
example.com:8080
|
||||
```
|
||||
|
||||
NOTE:
|
||||
Wildcards (`*.example.com`) are not currently supported.
|
||||
|
||||
<!-- ## Troubleshooting
|
||||
|
||||
Include any troubleshooting steps that you can foresee. If you know beforehand what issues
|
||||
|
|
|
@ -26,7 +26,7 @@ If you are importing from GitHub Enterprise to a self-managed GitLab instance:
|
|||
|
||||
- You must first enable [GitHub integration](../../../integration/github.md).
|
||||
- To import projects from GitHub Enterprise to GitLab.com, use the [Import API](../../../api/import.md).
|
||||
- If GitLab is behind a HTTP/HTTPS proxy, you must populate the [allowlist for local requests](../../../security/webhooks.md#allowlist-for-local-requests)
|
||||
- If GitLab is behind a HTTP/HTTPS proxy, you must populate the [allowlist for local requests](../../../security/webhooks.md#create-an-allowlist-for-local-requests)
|
||||
with `github.com` and `api.github.com` to solve the hostname. For more information, read the issue
|
||||
[Importing a GitHub project requires DNS resolution even when behind a proxy](https://gitlab.com/gitlab-org/gitlab/-/issues/37941).
|
||||
|
||||
|
|
|
@ -2,11 +2,22 @@
|
|||
module API
|
||||
class RpmProjectPackages < ::API::Base
|
||||
helpers ::API::Helpers::PackagesHelpers
|
||||
helpers ::API::Helpers::Packages::BasicAuthHelpers
|
||||
include ::API::Helpers::Authentication
|
||||
|
||||
feature_category :package_registry
|
||||
|
||||
before do
|
||||
require_packages_enabled!
|
||||
not_found! unless Feature.enabled?(:rpm_packages)
|
||||
|
||||
not_found! unless ::Feature.enabled?(:rpm_packages, authorized_user_project)
|
||||
|
||||
authorize_read_package!(authorized_user_project)
|
||||
end
|
||||
|
||||
authenticate_with do |accept|
|
||||
accept.token_types(:personal_access_token_with_username, :deploy_token_with_username, :job_token_with_username)
|
||||
.sent_through(:http_basic_auth)
|
||||
end
|
||||
|
||||
params do
|
||||
|
@ -18,7 +29,7 @@ module API
|
|||
params do
|
||||
requires :file_name, type: String, desc: 'Repository metadata file name'
|
||||
end
|
||||
get 'repodata/*file_name' do
|
||||
get 'repodata/*file_name', requirements: { file_name: API::NO_SLASH_URL_PART_REGEX } do
|
||||
not_found!
|
||||
end
|
||||
|
||||
|
@ -27,12 +38,13 @@ module API
|
|||
requires :package_file_id, type: Integer, desc: 'RPM package file id'
|
||||
requires :file_name, type: String, desc: 'RPM package file name'
|
||||
end
|
||||
get '*package_file_id/*file_name' do
|
||||
get '*package_file_id/*file_name', requirements: { file_name: API::NO_SLASH_URL_PART_REGEX } do
|
||||
not_found!
|
||||
end
|
||||
|
||||
desc 'Upload a RPM package'
|
||||
post do
|
||||
authorize_create_package!(authorized_user_project)
|
||||
not_found!
|
||||
end
|
||||
|
||||
|
|
|
@ -9391,6 +9391,9 @@ msgstr ""
|
|||
msgid "Collapse jobs"
|
||||
msgstr ""
|
||||
|
||||
msgid "Collapse merge details"
|
||||
msgstr ""
|
||||
|
||||
msgid "Collapse milestones"
|
||||
msgstr ""
|
||||
|
||||
|
@ -12502,6 +12505,9 @@ msgstr ""
|
|||
msgid "Delete deploy key"
|
||||
msgstr ""
|
||||
|
||||
msgid "Delete epic"
|
||||
msgstr ""
|
||||
|
||||
msgid "Delete file"
|
||||
msgstr ""
|
||||
|
||||
|
@ -14986,6 +14992,9 @@ msgstr ""
|
|||
msgid "Epic Boards"
|
||||
msgstr ""
|
||||
|
||||
msgid "Epic actions"
|
||||
msgstr ""
|
||||
|
||||
msgid "Epic cannot be found."
|
||||
msgstr ""
|
||||
|
||||
|
@ -15687,6 +15696,9 @@ msgstr ""
|
|||
msgid "Expand jobs"
|
||||
msgstr ""
|
||||
|
||||
msgid "Expand merge details"
|
||||
msgstr ""
|
||||
|
||||
msgid "Expand milestones"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
"jest": "jest --config jest.config.js",
|
||||
"jest-debug": "node --inspect-brk node_modules/.bin/jest --runInBand",
|
||||
"jest:ci": "jest --config jest.config.js --ci --coverage --testSequencer ./scripts/frontend/parallel_ci_sequencer.js",
|
||||
"jest:ci:minimal": "jest --config jest.config.js --ci --coverage --findRelatedTests $(cat $RSPEC_MATCHING_TESTS_PATH) --passWithNoTests --testSequencer ./scripts/frontend/parallel_ci_sequencer.js",
|
||||
"jest:ci:minimal": "jest --config jest.config.js --ci --coverage --findRelatedTests $(cat $RSPEC_CHANGED_FILES_PATH) --passWithNoTests --testSequencer ./scripts/frontend/parallel_ci_sequencer.js",
|
||||
"jest:integration": "jest --config jest.config.integration.js",
|
||||
"lint:eslint": "node scripts/frontend/eslint.js",
|
||||
"lint:eslint:fix": "node scripts/frontend/eslint.js --fix",
|
||||
|
|
|
@ -2,16 +2,9 @@ import { GlButton } from '@gitlab/ui';
|
|||
import Vue from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import { mockTracking } from 'helpers/tracking_helper';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import IssuableEditActions from '~/issues/show/components/edit_actions.vue';
|
||||
import DeleteIssueModal from '~/issues/show/components/delete_issue_modal.vue';
|
||||
import eventHub from '~/issues/show/event_hub';
|
||||
import {
|
||||
getIssueStateQueryResponse,
|
||||
updateIssueStateQueryResponse,
|
||||
} from '../mock_data/apollo_mock';
|
||||
|
||||
describe('Edit Actions component', () => {
|
||||
let wrapper;
|
||||
|
@ -31,8 +24,6 @@ describe('Edit Actions component', () => {
|
|||
},
|
||||
};
|
||||
|
||||
const modalId = 'delete-issuable-modal-1';
|
||||
|
||||
const createComponent = ({ props, data } = {}) => {
|
||||
fakeApollo = createMockApollo([], mockResolvers);
|
||||
|
||||
|
@ -50,16 +41,13 @@ describe('Edit Actions component', () => {
|
|||
data() {
|
||||
return {
|
||||
issueState: {},
|
||||
modalId,
|
||||
...data,
|
||||
};
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const findModal = () => wrapper.findComponent(DeleteIssueModal);
|
||||
const findEditButtons = () => wrapper.findAllComponents(GlButton);
|
||||
const findDeleteButton = () => wrapper.findByTestId('issuable-delete-button');
|
||||
const findSaveButton = () => wrapper.findByTestId('issuable-save-button');
|
||||
const findCancelButton = () => wrapper.findByTestId('issuable-cancel-button');
|
||||
|
||||
|
@ -79,23 +67,12 @@ describe('Edit Actions component', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('does not render the delete button if canDestroy is false', () => {
|
||||
createComponent({ props: { canDestroy: false } });
|
||||
expect(findDeleteButton().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('disables save button when title is blank', () => {
|
||||
createComponent({ props: { formState: { title: '', issue_type: '' } } });
|
||||
|
||||
expect(findSaveButton().attributes('disabled')).toBe('true');
|
||||
});
|
||||
|
||||
it('does not render the delete button if showDeleteButton is false', () => {
|
||||
createComponent({ props: { showDeleteButton: false } });
|
||||
|
||||
expect(findDeleteButton().exists()).toBe(false);
|
||||
});
|
||||
|
||||
describe('updateIssuable', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
|
||||
|
@ -119,63 +96,4 @@ describe('Edit Actions component', () => {
|
|||
expect(eventHub.$emit).toHaveBeenCalledWith('close.form');
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete issue button', () => {
|
||||
let trackingSpy;
|
||||
|
||||
beforeEach(() => {
|
||||
trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
|
||||
});
|
||||
|
||||
it('tracks clicking on button', () => {
|
||||
findDeleteButton().vm.$emit('click');
|
||||
|
||||
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_button', {
|
||||
label: 'delete_issue',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete issue modal', () => {
|
||||
it('renders', () => {
|
||||
expect(findModal().props()).toEqual({
|
||||
issuePath: 'gitlab-org/gitlab-test/-/issues/1',
|
||||
issueType: 'Issue',
|
||||
modalId,
|
||||
title: 'Delete issue',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteIssuable', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
it('does not send the `delete.issuable` event when clicking delete button', () => {
|
||||
findDeleteButton().vm.$emit('click');
|
||||
expect(eventHub.$emit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('sends the `delete.issuable` event when clicking the delete confirm button', async () => {
|
||||
expect(eventHub.$emit).toHaveBeenCalledTimes(0);
|
||||
findModal().vm.$emit('delete');
|
||||
expect(eventHub.$emit).toHaveBeenCalledWith('delete.issuable');
|
||||
expect(eventHub.$emit).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with Apollo cache mock', () => {
|
||||
it('renders the right delete button text per apollo cache type', async () => {
|
||||
mockIssueStateData.mockResolvedValue(getIssueStateQueryResponse);
|
||||
await waitForPromises();
|
||||
expect(findDeleteButton().text()).toBe('Delete issue');
|
||||
});
|
||||
|
||||
it('should not change the delete button text per apollo cache mutation', async () => {
|
||||
mockIssueStateData.mockResolvedValue(updateIssueStateQueryResponse);
|
||||
await waitForPromises();
|
||||
expect(findDeleteButton().text()).toBe('Delete issue');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -41,106 +41,135 @@ exports[`MRWidgetAutoMergeEnabled when graphql is disabled template should have
|
|||
</div>
|
||||
|
||||
<div
|
||||
class="media-body gl-display-flex"
|
||||
class="gl-display-flex gl-w-full"
|
||||
>
|
||||
|
||||
<h4
|
||||
class="gl-mr-3"
|
||||
data-testid="statusText"
|
||||
>
|
||||
Set by
|
||||
<a
|
||||
class="author-link inline"
|
||||
>
|
||||
<img
|
||||
class="avatar avatar-inline s16"
|
||||
src="no_avatar.png"
|
||||
/>
|
||||
|
||||
<span
|
||||
class="author"
|
||||
>
|
||||
|
||||
</span>
|
||||
</a>
|
||||
to be merged automatically when the pipeline succeeds
|
||||
</h4>
|
||||
|
||||
<div
|
||||
class="gl-display-flex gl-md-display-block gl-font-size-0 gl-ml-auto"
|
||||
class="media-body gl-display-flex"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="dropdown b-dropdown gl-new-dropdown gl-display-block gl-md-display-none! btn-group"
|
||||
lazy=""
|
||||
no-caret=""
|
||||
|
||||
<h4
|
||||
class="gl-mr-3"
|
||||
data-testid="statusText"
|
||||
>
|
||||
Set by
|
||||
<a
|
||||
class="author-link inline"
|
||||
>
|
||||
<!---->
|
||||
<img
|
||||
class="avatar avatar-inline s16"
|
||||
src="no_avatar.png"
|
||||
/>
|
||||
|
||||
<span
|
||||
class="author"
|
||||
>
|
||||
|
||||
</span>
|
||||
</a>
|
||||
to be merged automatically when the pipeline succeeds
|
||||
</h4>
|
||||
|
||||
<div
|
||||
class="gl-display-flex gl-md-display-block gl-font-size-0 gl-ml-auto"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="dropdown b-dropdown gl-new-dropdown gl-display-block gl-md-display-none! btn-group"
|
||||
lazy=""
|
||||
no-caret=""
|
||||
>
|
||||
<!---->
|
||||
<button
|
||||
aria-expanded="false"
|
||||
aria-haspopup="true"
|
||||
class="btn dropdown-toggle btn-default btn-sm gl-p-2! gl-button gl-dropdown-toggle btn-default-tertiary dropdown-icon-only dropdown-toggle-no-caret"
|
||||
type="button"
|
||||
>
|
||||
<!---->
|
||||
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="dropdown-icon gl-icon s16"
|
||||
data-testid="ellipsis_v-icon"
|
||||
role="img"
|
||||
>
|
||||
<use
|
||||
href="#ellipsis_v"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<span
|
||||
class="gl-new-dropdown-button-text gl-sr-only"
|
||||
>
|
||||
|
||||
</span>
|
||||
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="gl-button-icon dropdown-chevron gl-icon s16"
|
||||
data-testid="chevron-down-icon"
|
||||
role="img"
|
||||
>
|
||||
<use
|
||||
href="#chevron-down"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<ul
|
||||
class="dropdown-menu dropdown-menu-right"
|
||||
role="menu"
|
||||
tabindex="-1"
|
||||
>
|
||||
<!---->
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<button
|
||||
aria-expanded="false"
|
||||
aria-haspopup="true"
|
||||
class="btn dropdown-toggle btn-default btn-sm gl-p-2! gl-button gl-dropdown-toggle btn-default-tertiary dropdown-icon-only dropdown-toggle-no-caret"
|
||||
class="btn gl-display-none gl-md-display-block gl-float-left btn-confirm btn-sm gl-button btn-confirm-tertiary js-cancel-auto-merge"
|
||||
data-qa-selector="cancel_auto_merge_button"
|
||||
data-testid="cancelAutomaticMergeButton"
|
||||
type="button"
|
||||
>
|
||||
<!---->
|
||||
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="dropdown-icon gl-icon s16"
|
||||
data-testid="ellipsis_v-icon"
|
||||
role="img"
|
||||
>
|
||||
<use
|
||||
href="#ellipsis_v"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<!---->
|
||||
|
||||
<span
|
||||
class="gl-new-dropdown-button-text gl-sr-only"
|
||||
class="gl-button-text"
|
||||
>
|
||||
|
||||
</span>
|
||||
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="gl-button-icon dropdown-chevron gl-icon s16"
|
||||
data-testid="chevron-down-icon"
|
||||
role="img"
|
||||
>
|
||||
<use
|
||||
href="#chevron-down"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<ul
|
||||
class="dropdown-menu dropdown-menu-right"
|
||||
role="menu"
|
||||
tabindex="-1"
|
||||
>
|
||||
<!---->
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="btn gl-display-none gl-md-display-block gl-float-left btn-confirm btn-sm gl-button btn-confirm-tertiary js-cancel-auto-merge"
|
||||
data-qa-selector="cancel_auto_merge_button"
|
||||
data-testid="cancelAutomaticMergeButton"
|
||||
type="button"
|
||||
>
|
||||
<!---->
|
||||
|
||||
<!---->
|
||||
|
||||
<span
|
||||
class="gl-button-text"
|
||||
>
|
||||
|
||||
Cancel auto-merge
|
||||
|
||||
</span>
|
||||
</button>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="gl-md-display-none gl-border-l-1 gl-border-l-solid gl-border-gray-100 gl-ml-3 gl-pl-3 gl-h-6 gl-mt-1"
|
||||
>
|
||||
<button
|
||||
class="btn gl-vertical-align-top btn-default btn-sm gl-button btn-default-tertiary btn-icon"
|
||||
title="Collapse merge details"
|
||||
type="button"
|
||||
>
|
||||
<!---->
|
||||
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="gl-button-icon gl-icon s16"
|
||||
data-testid="chevron-lg-up-icon"
|
||||
role="img"
|
||||
>
|
||||
<use
|
||||
href="#chevron-lg-up"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<!---->
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
@ -186,106 +215,135 @@ exports[`MRWidgetAutoMergeEnabled when graphql is enabled template should have c
|
|||
</div>
|
||||
|
||||
<div
|
||||
class="media-body gl-display-flex"
|
||||
class="gl-display-flex gl-w-full"
|
||||
>
|
||||
|
||||
<h4
|
||||
class="gl-mr-3"
|
||||
data-testid="statusText"
|
||||
>
|
||||
Set by
|
||||
<a
|
||||
class="author-link inline"
|
||||
>
|
||||
<img
|
||||
class="avatar avatar-inline s16"
|
||||
src="no_avatar.png"
|
||||
/>
|
||||
|
||||
<span
|
||||
class="author"
|
||||
>
|
||||
|
||||
</span>
|
||||
</a>
|
||||
to be merged automatically when the pipeline succeeds
|
||||
</h4>
|
||||
|
||||
<div
|
||||
class="gl-display-flex gl-md-display-block gl-font-size-0 gl-ml-auto"
|
||||
class="media-body gl-display-flex"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="dropdown b-dropdown gl-new-dropdown gl-display-block gl-md-display-none! btn-group"
|
||||
lazy=""
|
||||
no-caret=""
|
||||
|
||||
<h4
|
||||
class="gl-mr-3"
|
||||
data-testid="statusText"
|
||||
>
|
||||
Set by
|
||||
<a
|
||||
class="author-link inline"
|
||||
>
|
||||
<!---->
|
||||
<img
|
||||
class="avatar avatar-inline s16"
|
||||
src="no_avatar.png"
|
||||
/>
|
||||
|
||||
<span
|
||||
class="author"
|
||||
>
|
||||
|
||||
</span>
|
||||
</a>
|
||||
to be merged automatically when the pipeline succeeds
|
||||
</h4>
|
||||
|
||||
<div
|
||||
class="gl-display-flex gl-md-display-block gl-font-size-0 gl-ml-auto"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="dropdown b-dropdown gl-new-dropdown gl-display-block gl-md-display-none! btn-group"
|
||||
lazy=""
|
||||
no-caret=""
|
||||
>
|
||||
<!---->
|
||||
<button
|
||||
aria-expanded="false"
|
||||
aria-haspopup="true"
|
||||
class="btn dropdown-toggle btn-default btn-sm gl-p-2! gl-button gl-dropdown-toggle btn-default-tertiary dropdown-icon-only dropdown-toggle-no-caret"
|
||||
type="button"
|
||||
>
|
||||
<!---->
|
||||
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="dropdown-icon gl-icon s16"
|
||||
data-testid="ellipsis_v-icon"
|
||||
role="img"
|
||||
>
|
||||
<use
|
||||
href="#ellipsis_v"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<span
|
||||
class="gl-new-dropdown-button-text gl-sr-only"
|
||||
>
|
||||
|
||||
</span>
|
||||
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="gl-button-icon dropdown-chevron gl-icon s16"
|
||||
data-testid="chevron-down-icon"
|
||||
role="img"
|
||||
>
|
||||
<use
|
||||
href="#chevron-down"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<ul
|
||||
class="dropdown-menu dropdown-menu-right"
|
||||
role="menu"
|
||||
tabindex="-1"
|
||||
>
|
||||
<!---->
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<button
|
||||
aria-expanded="false"
|
||||
aria-haspopup="true"
|
||||
class="btn dropdown-toggle btn-default btn-sm gl-p-2! gl-button gl-dropdown-toggle btn-default-tertiary dropdown-icon-only dropdown-toggle-no-caret"
|
||||
class="btn gl-display-none gl-md-display-block gl-float-left btn-confirm btn-sm gl-button btn-confirm-tertiary js-cancel-auto-merge"
|
||||
data-qa-selector="cancel_auto_merge_button"
|
||||
data-testid="cancelAutomaticMergeButton"
|
||||
type="button"
|
||||
>
|
||||
<!---->
|
||||
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="dropdown-icon gl-icon s16"
|
||||
data-testid="ellipsis_v-icon"
|
||||
role="img"
|
||||
>
|
||||
<use
|
||||
href="#ellipsis_v"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<!---->
|
||||
|
||||
<span
|
||||
class="gl-new-dropdown-button-text gl-sr-only"
|
||||
class="gl-button-text"
|
||||
>
|
||||
|
||||
</span>
|
||||
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="gl-button-icon dropdown-chevron gl-icon s16"
|
||||
data-testid="chevron-down-icon"
|
||||
role="img"
|
||||
>
|
||||
<use
|
||||
href="#chevron-down"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<ul
|
||||
class="dropdown-menu dropdown-menu-right"
|
||||
role="menu"
|
||||
tabindex="-1"
|
||||
>
|
||||
<!---->
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="btn gl-display-none gl-md-display-block gl-float-left btn-confirm btn-sm gl-button btn-confirm-tertiary js-cancel-auto-merge"
|
||||
data-qa-selector="cancel_auto_merge_button"
|
||||
data-testid="cancelAutomaticMergeButton"
|
||||
type="button"
|
||||
>
|
||||
<!---->
|
||||
|
||||
<!---->
|
||||
|
||||
<span
|
||||
class="gl-button-text"
|
||||
>
|
||||
|
||||
Cancel auto-merge
|
||||
|
||||
</span>
|
||||
</button>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="gl-md-display-none gl-border-l-1 gl-border-l-solid gl-border-gray-100 gl-ml-3 gl-pl-3 gl-h-6 gl-mt-1"
|
||||
>
|
||||
<button
|
||||
class="btn gl-vertical-align-top btn-default btn-sm gl-button btn-default-tertiary btn-icon"
|
||||
title="Collapse merge details"
|
||||
type="button"
|
||||
>
|
||||
<!---->
|
||||
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="gl-button-icon gl-icon s16"
|
||||
data-testid="chevron-lg-up-icon"
|
||||
role="img"
|
||||
>
|
||||
<use
|
||||
href="#chevron-lg-up"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<!---->
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
|
@ -6,7 +6,7 @@ describe('MRWidgetArchived', () => {
|
|||
let wrapper;
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = shallowMount(archivedComponent);
|
||||
wrapper = shallowMount(archivedComponent, { propsData: { mr: {} } });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
|
|
@ -6,7 +6,7 @@ describe('MRWidgetChecking', () => {
|
|||
let wrapper;
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = shallowMount(CheckingComponent);
|
||||
wrapper = shallowMount(CheckingComponent, { propsData: { mr: {} } });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
|
|
@ -0,0 +1,122 @@
|
|||
import { GlButton, GlIcon } from '@gitlab/ui';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
|
||||
import RichTimestampTooltip from '~/vue_shared/components/rich_timestamp_tooltip.vue';
|
||||
|
||||
import WorkItemLinkChild from '~/work_items/components/work_item_links/work_item_link_child.vue';
|
||||
import WorkItemLinksMenu from '~/work_items/components/work_item_links/work_item_links_menu.vue';
|
||||
|
||||
import { workItemTask, confidentialWorkItemTask, closedWorkItemTask } from '../../mock_data';
|
||||
|
||||
describe('WorkItemLinkChild', () => {
|
||||
const WORK_ITEM_ID = 'gid://gitlab/WorkItem/2';
|
||||
let wrapper;
|
||||
|
||||
const createComponent = ({
|
||||
projectPath = 'gitlab-org/gitlab-test',
|
||||
canUpdate = true,
|
||||
issuableGid = WORK_ITEM_ID,
|
||||
childItem = workItemTask,
|
||||
} = {}) => {
|
||||
wrapper = shallowMountExtended(WorkItemLinkChild, {
|
||||
propsData: {
|
||||
projectPath,
|
||||
canUpdate,
|
||||
issuableGid,
|
||||
childItem,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
it.each`
|
||||
status | childItem | statusIconName | statusIconColorClass | rawTimestamp | tooltipContents
|
||||
${'open'} | ${workItemTask} | ${'issue-open-m'} | ${'gl-text-green-500'} | ${workItemTask.createdAt} | ${'Created'}
|
||||
${'closed'} | ${closedWorkItemTask} | ${'issue-close'} | ${'gl-text-blue-500'} | ${closedWorkItemTask.closedAt} | ${'Closed'}
|
||||
`(
|
||||
'renders item status icon and tooltip when item status is `$status`',
|
||||
({ childItem, statusIconName, statusIconColorClass, rawTimestamp, tooltipContents }) => {
|
||||
createComponent({ childItem });
|
||||
|
||||
const statusIcon = wrapper.findByTestId('item-status-icon').findComponent(GlIcon);
|
||||
const statusTooltip = wrapper.findComponent(RichTimestampTooltip);
|
||||
|
||||
expect(statusIcon.props('name')).toBe(statusIconName);
|
||||
expect(statusIcon.classes()).toContain(statusIconColorClass);
|
||||
expect(statusTooltip.props('rawTimestamp')).toBe(rawTimestamp);
|
||||
expect(statusTooltip.props('timestampTypeText')).toContain(tooltipContents);
|
||||
},
|
||||
);
|
||||
|
||||
it('renders confidential icon when item is confidential', () => {
|
||||
createComponent({ childItem: confidentialWorkItemTask });
|
||||
|
||||
const confidentialIcon = wrapper.findByTestId('confidential-icon');
|
||||
|
||||
expect(confidentialIcon.props('name')).toBe('eye-slash');
|
||||
expect(confidentialIcon.attributes('title')).toBe('Confidential');
|
||||
});
|
||||
|
||||
describe('item title', () => {
|
||||
let titleEl;
|
||||
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
|
||||
titleEl = wrapper.findComponent(GlButton);
|
||||
});
|
||||
|
||||
it('renders item title', () => {
|
||||
expect(titleEl.attributes('href')).toBe('/gitlab-org/gitlab-test/-/work_items/4');
|
||||
expect(titleEl.text()).toBe(workItemTask.title);
|
||||
});
|
||||
|
||||
it.each`
|
||||
action | event | emittedEvent
|
||||
${'clicking'} | ${'click'} | ${'click'}
|
||||
${'doing mouseover on'} | ${'mouseover'} | ${'mouseover'}
|
||||
${'doing mouseout on'} | ${'mouseout'} | ${'mouseout'}
|
||||
`('$action item title emit `$emittedEvent` event', ({ event, emittedEvent }) => {
|
||||
const eventObj = {
|
||||
preventDefault: jest.fn(),
|
||||
};
|
||||
titleEl.vm.$emit(event, eventObj);
|
||||
|
||||
expect(wrapper.emitted(emittedEvent)).toEqual([[workItemTask.id, eventObj]]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('item menu', () => {
|
||||
let itemMenuEl;
|
||||
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
|
||||
itemMenuEl = wrapper.findComponent(WorkItemLinksMenu);
|
||||
});
|
||||
|
||||
it('renders work-item-links-menu', () => {
|
||||
expect(itemMenuEl.exists()).toBe(true);
|
||||
|
||||
expect(itemMenuEl.attributes()).toMatchObject({
|
||||
'work-item-id': workItemTask.id,
|
||||
'parent-work-item-id': WORK_ITEM_ID,
|
||||
});
|
||||
});
|
||||
|
||||
it('does not render work-item-links-menu when canUpdate is false', () => {
|
||||
createComponent({ canUpdate: false });
|
||||
|
||||
expect(wrapper.findComponent(WorkItemLinksMenu).exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('removeChild event on menu triggers `click-remove-child` event', () => {
|
||||
itemMenuEl.vm.$emit('removeChild');
|
||||
|
||||
expect(wrapper.emitted('remove')).toEqual([[workItemTask.id]]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,5 +1,5 @@
|
|||
import Vue, { nextTick } from 'vue';
|
||||
import { GlButton, GlIcon, GlAlert } from '@gitlab/ui';
|
||||
import { GlAlert } from '@gitlab/ui';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
|
@ -7,6 +7,7 @@ import waitForPromises from 'helpers/wait_for_promises';
|
|||
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
|
||||
import issueConfidentialQuery from '~/sidebar/queries/issue_confidential.query.graphql';
|
||||
import WorkItemLinks from '~/work_items/components/work_item_links/work_item_links.vue';
|
||||
import WorkItemLinkChild from '~/work_items/components/work_item_links/work_item_link_child.vue';
|
||||
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
|
||||
import changeWorkItemParentMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
|
||||
import getWorkItemLinksQuery from '~/work_items/graphql/work_item_links.query.graphql';
|
||||
|
@ -92,10 +93,10 @@ describe('WorkItemLinks', () => {
|
|||
const findLinksBody = () => wrapper.findByTestId('links-body');
|
||||
const findEmptyState = () => wrapper.findByTestId('links-empty');
|
||||
const findToggleAddFormButton = () => wrapper.findByTestId('toggle-add-form');
|
||||
const findWorkItemLinkChildItems = () => wrapper.findAllComponents(WorkItemLinkChild);
|
||||
const findFirstWorkItemLinkChild = () => findWorkItemLinkChildItems().at(0);
|
||||
const findAddLinksForm = () => wrapper.findByTestId('add-links-form');
|
||||
const findFirstLinksMenu = () => wrapper.findByTestId('links-menu');
|
||||
const findChildrenCount = () => wrapper.findByTestId('children-count');
|
||||
const findChildren = () => wrapper.findAllByTestId('links-child');
|
||||
|
||||
beforeEach(async () => {
|
||||
await createComponent();
|
||||
|
@ -148,8 +149,7 @@ describe('WorkItemLinks', () => {
|
|||
it('renders all hierarchy widget children', () => {
|
||||
expect(findLinksBody().exists()).toBe(true);
|
||||
|
||||
expect(findChildren()).toHaveLength(4);
|
||||
expect(findFirstLinksMenu().exists()).toBe(true);
|
||||
expect(findWorkItemLinkChildItems()).toHaveLength(4);
|
||||
});
|
||||
|
||||
it('shows alert when list loading fails', async () => {
|
||||
|
@ -164,19 +164,6 @@ describe('WorkItemLinks', () => {
|
|||
expect(findAlert().text()).toBe(errorMessage);
|
||||
});
|
||||
|
||||
it('renders widget child icon and tooltip', () => {
|
||||
expect(findChildren().at(0).findComponent(GlIcon).props('name')).toBe('issue-open-m');
|
||||
expect(findChildren().at(1).findComponent(GlIcon).props('name')).toBe('issue-close');
|
||||
});
|
||||
|
||||
it('renders confidentiality icon when child item is confidential', () => {
|
||||
const children = wrapper.findAll('[data-testid="links-child"]');
|
||||
const confidentialIcon = children.at(0).find('[data-testid="confidential-icon"]');
|
||||
|
||||
expect(confidentialIcon.exists()).toBe(true);
|
||||
expect(confidentialIcon.props('name')).toBe('eye-slash');
|
||||
});
|
||||
|
||||
it('displays number if children', () => {
|
||||
expect(findChildrenCount().exists()).toBe(true);
|
||||
|
||||
|
@ -195,17 +182,21 @@ describe('WorkItemLinks', () => {
|
|||
});
|
||||
|
||||
it('does not display link menu on children', () => {
|
||||
expect(findFirstLinksMenu().exists()).toBe(false);
|
||||
expect(findWorkItemLinkChildItems().at(0).props('canUpdate')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('remove child', () => {
|
||||
let firstChild;
|
||||
|
||||
beforeEach(async () => {
|
||||
await createComponent({ mutationHandler: mutationChangeParentHandler });
|
||||
|
||||
firstChild = findFirstWorkItemLinkChild();
|
||||
});
|
||||
|
||||
it('calls correct mutation with correct variables', async () => {
|
||||
findFirstLinksMenu().vm.$emit('removeChild');
|
||||
firstChild.vm.$emit('remove', firstChild.vm.childItem.id);
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
|
@ -220,7 +211,7 @@ describe('WorkItemLinks', () => {
|
|||
});
|
||||
|
||||
it('shows toast when mutation succeeds', async () => {
|
||||
findFirstLinksMenu().vm.$emit('removeChild');
|
||||
firstChild.vm.$emit('remove', firstChild.vm.childItem.id);
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
|
@ -230,28 +221,30 @@ describe('WorkItemLinks', () => {
|
|||
});
|
||||
|
||||
it('renders correct number of children after removal', async () => {
|
||||
expect(findChildren()).toHaveLength(4);
|
||||
expect(findWorkItemLinkChildItems()).toHaveLength(4);
|
||||
|
||||
findFirstLinksMenu().vm.$emit('removeChild');
|
||||
firstChild.vm.$emit('remove', firstChild.vm.childItem.id);
|
||||
await waitForPromises();
|
||||
|
||||
expect(findChildren()).toHaveLength(3);
|
||||
expect(findWorkItemLinkChildItems()).toHaveLength(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('prefetching child items', () => {
|
||||
let firstChild;
|
||||
|
||||
beforeEach(async () => {
|
||||
await createComponent();
|
||||
});
|
||||
|
||||
const findChildLink = () => findChildren().at(0).findComponent(GlButton);
|
||||
firstChild = findFirstWorkItemLinkChild();
|
||||
});
|
||||
|
||||
it('does not fetch the child work item before hovering work item links', () => {
|
||||
expect(childWorkItemQueryHandler).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('fetches the child work item if link is hovered for 250+ ms', async () => {
|
||||
findChildLink().vm.$emit('mouseover');
|
||||
firstChild.vm.$emit('mouseover', firstChild.vm.childItem.id);
|
||||
jest.advanceTimersByTime(DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
|
||||
await waitForPromises();
|
||||
|
||||
|
@ -261,9 +254,9 @@ describe('WorkItemLinks', () => {
|
|||
});
|
||||
|
||||
it('does not fetch the child work item if link is hovered for less than 250 ms', async () => {
|
||||
findChildLink().vm.$emit('mouseover');
|
||||
firstChild.vm.$emit('mouseover', firstChild.vm.childItem.id);
|
||||
jest.advanceTimersByTime(200);
|
||||
findChildLink().vm.$emit('mouseout');
|
||||
firstChild.vm.$emit('mouseout', firstChild.vm.childItem.id);
|
||||
await waitForPromises();
|
||||
|
||||
expect(childWorkItemQueryHandler).not.toHaveBeenCalled();
|
||||
|
|
|
@ -515,6 +515,48 @@ export const workItemHierarchyNoUpdatePermissionResponse = {
|
|||
},
|
||||
};
|
||||
|
||||
export const workItemTask = {
|
||||
id: 'gid://gitlab/WorkItem/4',
|
||||
workItemType: {
|
||||
id: 'gid://gitlab/WorkItems::Type/5',
|
||||
__typename: 'WorkItemType',
|
||||
},
|
||||
title: 'bar',
|
||||
state: 'OPEN',
|
||||
confidential: false,
|
||||
createdAt: '2022-08-03T12:41:54Z',
|
||||
closedAt: null,
|
||||
__typename: 'WorkItem',
|
||||
};
|
||||
|
||||
export const confidentialWorkItemTask = {
|
||||
id: 'gid://gitlab/WorkItem/2',
|
||||
workItemType: {
|
||||
id: 'gid://gitlab/WorkItems::Type/5',
|
||||
__typename: 'WorkItemType',
|
||||
},
|
||||
title: 'xyz',
|
||||
state: 'OPEN',
|
||||
confidential: true,
|
||||
createdAt: '2022-08-03T12:41:54Z',
|
||||
closedAt: null,
|
||||
__typename: 'WorkItem',
|
||||
};
|
||||
|
||||
export const closedWorkItemTask = {
|
||||
id: 'gid://gitlab/WorkItem/3',
|
||||
workItemType: {
|
||||
id: 'gid://gitlab/WorkItems::Type/5',
|
||||
__typename: 'WorkItemType',
|
||||
},
|
||||
title: 'abc',
|
||||
state: 'CLOSED',
|
||||
confidential: false,
|
||||
createdAt: '2022-08-03T12:41:54Z',
|
||||
closedAt: '2022-08-12T13:07:52Z',
|
||||
__typename: 'WorkItem',
|
||||
};
|
||||
|
||||
export const workItemHierarchyResponse = {
|
||||
data: {
|
||||
workItem: {
|
||||
|
@ -544,45 +586,9 @@ export const workItemHierarchyResponse = {
|
|||
parent: null,
|
||||
children: {
|
||||
nodes: [
|
||||
{
|
||||
id: 'gid://gitlab/WorkItem/2',
|
||||
workItemType: {
|
||||
id: 'gid://gitlab/WorkItems::Type/5',
|
||||
__typename: 'WorkItemType',
|
||||
},
|
||||
title: 'xyz',
|
||||
state: 'OPEN',
|
||||
confidential: true,
|
||||
createdAt: '2022-08-03T12:41:54Z',
|
||||
closedAt: null,
|
||||
__typename: 'WorkItem',
|
||||
},
|
||||
{
|
||||
id: 'gid://gitlab/WorkItem/3',
|
||||
workItemType: {
|
||||
id: 'gid://gitlab/WorkItems::Type/5',
|
||||
__typename: 'WorkItemType',
|
||||
},
|
||||
title: 'abc',
|
||||
state: 'CLOSED',
|
||||
confidential: false,
|
||||
createdAt: '2022-08-03T12:41:54Z',
|
||||
closedAt: '2022-08-12T13:07:52Z',
|
||||
__typename: 'WorkItem',
|
||||
},
|
||||
{
|
||||
id: 'gid://gitlab/WorkItem/4',
|
||||
workItemType: {
|
||||
id: 'gid://gitlab/WorkItems::Type/5',
|
||||
__typename: 'WorkItemType',
|
||||
},
|
||||
title: 'bar',
|
||||
state: 'OPEN',
|
||||
confidential: false,
|
||||
createdAt: '2022-08-03T12:41:54Z',
|
||||
closedAt: null,
|
||||
__typename: 'WorkItem',
|
||||
},
|
||||
confidentialWorkItemTask,
|
||||
closedWorkItemTask,
|
||||
workItemTask,
|
||||
{
|
||||
id: 'gid://gitlab/WorkItem/5',
|
||||
workItemType: {
|
||||
|
|
|
@ -706,7 +706,7 @@ ProtectedEnvironment:
|
|||
- name
|
||||
- created_at
|
||||
- updated_at
|
||||
ProtectedEnvironment::DeployAccessLevel:
|
||||
ProtectedEnvironments::DeployAccessLevel:
|
||||
- id
|
||||
- protected_environment_id
|
||||
- access_level
|
||||
|
|
|
@ -2,13 +2,194 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe API::RpmProjectPackages do
|
||||
include PackagesManagerApiSpecHelpers
|
||||
let(:project) { create(:project) }
|
||||
include HttpBasicAuthHelpers
|
||||
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
let_it_be_with_reload(:project) { create(:project, :public) }
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:personal_access_token) { create(:personal_access_token, user: user) }
|
||||
let_it_be(:deploy_token) { create(:deploy_token, read_package_registry: true, write_package_registry: true) }
|
||||
let_it_be(:project_deploy_token) { create(:project_deploy_token, deploy_token: deploy_token, project: project) }
|
||||
let_it_be(:job) { create(:ci_build, :running, user: user, project: project) }
|
||||
|
||||
let(:headers) { {} }
|
||||
let(:package_name) { 'rpm-package.0-1.x86_64.rpm' }
|
||||
let(:package_file_id) { 1 }
|
||||
|
||||
shared_examples 'an unimplemented route' do
|
||||
shared_examples 'rejects rpm packages access' do |status|
|
||||
it_behaves_like 'returning response status', status
|
||||
|
||||
if status == :unauthorized
|
||||
it 'has the correct response header' do
|
||||
subject
|
||||
|
||||
expect(response.headers['WWW-Authenticate']).to eq 'Basic realm="GitLab Packages Registry"'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'process rpm packages upload/download' do |status|
|
||||
it_behaves_like 'returning response status', status
|
||||
end
|
||||
|
||||
shared_examples 'a deploy token for RPM requests' do
|
||||
context 'with deploy token headers' do
|
||||
before do
|
||||
project.update_column(:visibility_level, Gitlab::VisibilityLevel::PRIVATE)
|
||||
end
|
||||
|
||||
let(:headers) { basic_auth_header(deploy_token.username, deploy_token.token) }
|
||||
|
||||
context 'when token is valid' do
|
||||
it_behaves_like 'returning response status', :not_found
|
||||
end
|
||||
|
||||
context 'when token is invalid' do
|
||||
let(:headers) { basic_auth_header(deploy_token.username, 'bar') }
|
||||
|
||||
it_behaves_like 'returning response status', :unauthorized
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'a job token for RPM requests' do
|
||||
context 'with job token headers' do
|
||||
let(:headers) { basic_auth_header(::Gitlab::Auth::CI_JOB_USER, job.token) }
|
||||
|
||||
before do
|
||||
project.update_column(:visibility_level, Gitlab::VisibilityLevel::PRIVATE)
|
||||
project.add_developer(user)
|
||||
end
|
||||
|
||||
context 'with valid token' do
|
||||
it_behaves_like 'returning response status', :not_found
|
||||
end
|
||||
|
||||
context 'with invalid token' do
|
||||
let(:headers) { basic_auth_header(::Gitlab::Auth::CI_JOB_USER, 'bar') }
|
||||
|
||||
it_behaves_like 'returning response status', :unauthorized
|
||||
end
|
||||
|
||||
context 'with invalid user' do
|
||||
let(:headers) { basic_auth_header('foo', job.token) }
|
||||
|
||||
it_behaves_like 'returning response status', :unauthorized
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'a user token for RPM requests' do
|
||||
context 'with valid project' do
|
||||
where(:visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do
|
||||
'PUBLIC' | :developer | true | true | 'process rpm packages upload/download' | :not_found
|
||||
'PUBLIC' | :guest | true | true | 'process rpm packages upload/download' | :forbidden
|
||||
'PUBLIC' | :developer | true | false | 'rejects rpm packages access' | :unauthorized
|
||||
'PUBLIC' | :guest | true | false | 'rejects rpm packages access' | :unauthorized
|
||||
'PUBLIC' | :developer | false | true | 'process rpm packages upload/download' | :not_found
|
||||
'PUBLIC' | :guest | false | true | 'process rpm packages upload/download' | :not_found
|
||||
'PUBLIC' | :developer | false | false | 'rejects rpm packages access' | :unauthorized
|
||||
'PUBLIC' | :guest | false | false | 'rejects rpm packages access' | :unauthorized
|
||||
'PUBLIC' | :anonymous | false | true | 'process rpm packages upload/download' | :unauthorized
|
||||
'PRIVATE' | :developer | true | true | 'process rpm packages upload/download' | :not_found
|
||||
'PRIVATE' | :guest | true | true | 'rejects rpm packages access' | :forbidden
|
||||
'PRIVATE' | :developer | true | false | 'rejects rpm packages access' | :unauthorized
|
||||
'PRIVATE' | :guest | true | false | 'rejects rpm packages access' | :unauthorized
|
||||
'PRIVATE' | :developer | false | true | 'rejects rpm packages access' | :not_found
|
||||
'PRIVATE' | :guest | false | true | 'rejects rpm packages access' | :not_found
|
||||
'PRIVATE' | :developer | false | false | 'rejects rpm packages access' | :unauthorized
|
||||
'PRIVATE' | :guest | false | false | 'rejects rpm packages access' | :unauthorized
|
||||
'PRIVATE' | :anonymous | false | true | 'rejects rpm packages access' | :unauthorized
|
||||
end
|
||||
|
||||
with_them do
|
||||
let(:token) { user_token ? personal_access_token.token : 'wrong' }
|
||||
let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) }
|
||||
|
||||
subject { get api(url), headers: headers }
|
||||
|
||||
before do
|
||||
project.update_column(:visibility_level, Gitlab::VisibilityLevel.level_value(visibility_level))
|
||||
project.send("add_#{user_role}", user) if member && user_role != :anonymous
|
||||
end
|
||||
|
||||
it_behaves_like params[:shared_examples_name], params[:expected_status]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /api/v4/projects/:project_id/packages/rpm/repodata/:filename' do
|
||||
let(:url) { "/projects/#{project.id}/packages/rpm/repodata/#{package_name}" }
|
||||
|
||||
subject { get api(url), headers: headers }
|
||||
|
||||
it_behaves_like 'a job token for RPM requests'
|
||||
it_behaves_like 'a deploy token for RPM requests'
|
||||
it_behaves_like 'a user token for RPM requests'
|
||||
end
|
||||
|
||||
describe 'GET /api/v4/projects/:id/packages/rpm/:package_file_id/:filename' do
|
||||
let(:url) { "/projects/#{project.id}/packages/rpm/#{package_file_id}/#{package_name}" }
|
||||
|
||||
subject { get api(url), headers: headers }
|
||||
|
||||
it_behaves_like 'a job token for RPM requests'
|
||||
it_behaves_like 'a deploy token for RPM requests'
|
||||
it_behaves_like 'a user token for RPM requests'
|
||||
end
|
||||
|
||||
describe 'POST /api/v4/projects/:project_id/packages/rpm' do
|
||||
let(:url) { "/projects/#{project.id}/packages/rpm" }
|
||||
|
||||
subject { post api(url), headers: headers }
|
||||
|
||||
context 'with user token' do
|
||||
context 'with valid project' do
|
||||
where(:visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do
|
||||
'PUBLIC' | :developer | true | true | 'process rpm packages upload/download' | :not_found
|
||||
'PUBLIC' | :guest | true | true | 'rejects rpm packages access' | :forbidden
|
||||
'PUBLIC' | :developer | true | false | 'rejects rpm packages access' | :unauthorized
|
||||
'PUBLIC' | :guest | true | false | 'rejects rpm packages access' | :unauthorized
|
||||
'PUBLIC' | :developer | false | true | 'rejects rpm packages access' | :not_found
|
||||
'PUBLIC' | :guest | false | true | 'rejects rpm packages access' | :not_found
|
||||
'PUBLIC' | :developer | false | false | 'rejects rpm packages access' | :unauthorized
|
||||
'PUBLIC' | :guest | false | false | 'rejects rpm packages access' | :unauthorized
|
||||
'PUBLIC' | :anonymous | false | true | 'rejects rpm packages access' | :unauthorized
|
||||
'PRIVATE' | :developer | true | true | 'process rpm packages upload/download' | :not_found
|
||||
'PRIVATE' | :guest | true | true | 'rejects rpm packages access' | :forbidden
|
||||
'PRIVATE' | :developer | true | false | 'rejects rpm packages access' | :unauthorized
|
||||
'PRIVATE' | :guest | true | false | 'rejects rpm packages access' | :unauthorized
|
||||
'PRIVATE' | :developer | false | true | 'rejects rpm packages access' | :not_found
|
||||
'PRIVATE' | :guest | false | true | 'rejects rpm packages access' | :not_found
|
||||
'PRIVATE' | :developer | false | false | 'rejects rpm packages access' | :unauthorized
|
||||
'PRIVATE' | :guest | false | false | 'rejects rpm packages access' | :unauthorized
|
||||
'PRIVATE' | :anonymous | false | true | 'rejects rpm packages access' | :unauthorized
|
||||
end
|
||||
|
||||
with_them do
|
||||
let(:token) { user_token ? personal_access_token.token : 'wrong' }
|
||||
let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) }
|
||||
|
||||
before do
|
||||
project.update_column(:visibility_level, Gitlab::VisibilityLevel.level_value(visibility_level))
|
||||
project.send("add_#{user_role}", user) if member && user_role != :anonymous
|
||||
end
|
||||
|
||||
it_behaves_like params[:shared_examples_name], params[:expected_status]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'a deploy token for RPM requests'
|
||||
it_behaves_like 'a job token for RPM requests'
|
||||
end
|
||||
|
||||
describe 'POST /api/v4/projects/:project_id/packages/rpm/authorize' do
|
||||
let(:url) { api("/projects/#{project.id}/packages/rpm/authorize") }
|
||||
|
||||
subject { post(url, headers: headers) }
|
||||
|
||||
it_behaves_like 'returning response status', :not_found
|
||||
|
||||
context 'when feature flag is disabled' do
|
||||
|
@ -27,36 +208,4 @@ RSpec.describe API::RpmProjectPackages do
|
|||
it_behaves_like 'returning response status', :not_found
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /api/v4/projects/:project_id/packages/rpm/repodata/:filename' do
|
||||
let(:url) { api("/projects/#{project.id}/packages/rpm/repodata/#{package_name}") }
|
||||
|
||||
subject { get(url, headers: headers) }
|
||||
|
||||
it_behaves_like 'an unimplemented route'
|
||||
end
|
||||
|
||||
describe 'GET /api/v4/projects/:project_id/packages/rpm/:package_file_id/:filename' do
|
||||
let(:url) { api("/projects/#{project.id}/packages/rpm/#{package_file_id}/#{package_name}") }
|
||||
|
||||
subject { get(url, headers: headers) }
|
||||
|
||||
it_behaves_like 'an unimplemented route'
|
||||
end
|
||||
|
||||
describe 'POST /api/v4/projects/:project_id/packages/rpm/authorize' do
|
||||
let(:url) { api("/projects/#{project.id}/packages/rpm/authorize") }
|
||||
|
||||
subject { post(url, headers: headers) }
|
||||
|
||||
it_behaves_like 'an unimplemented route'
|
||||
end
|
||||
|
||||
describe 'POST /api/v4/projects/:project_id/packages/rpm' do
|
||||
let(:url) { api("/projects/#{project.id}/packages/rpm") }
|
||||
|
||||
subject { post(url, headers: headers) }
|
||||
|
||||
it_behaves_like 'an unimplemented route'
|
||||
end
|
||||
end
|
||||
|
|
|
@ -98,30 +98,12 @@ RSpec.describe 'Merge Requests Context Commit Diffs' do
|
|||
end
|
||||
|
||||
context 'when using ETags' do
|
||||
context 'when etag_merge_request_diff_batches is true' do
|
||||
it 'does not serialize diffs' do
|
||||
expect(PaginatedDiffSerializer).not_to receive(:new)
|
||||
it 'does not serialize diffs' do
|
||||
expect(PaginatedDiffSerializer).not_to receive(:new)
|
||||
|
||||
go(headers: { 'If-None-Match' => response.etag }, page: 0, per_page: 5)
|
||||
go(headers: { 'If-None-Match' => response.etag }, page: 0, per_page: 5)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_modified)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when etag_merge_request_diff_batches is false' do
|
||||
before do
|
||||
stub_feature_flags(etag_merge_request_diff_batches: false)
|
||||
end
|
||||
|
||||
it 'does not serialize diffs' do
|
||||
expect_next_instance_of(PaginatedDiffSerializer) do |instance|
|
||||
expect(instance).not_to receive(:represent)
|
||||
end
|
||||
|
||||
go(headers: { 'If-None-Match' => response.etag }, page: 0, per_page: 5)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:success)
|
||||
end
|
||||
expect(response).to have_gitlab_http_status(:not_modified)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -97,34 +97,14 @@ RSpec.describe 'Merge Requests Diffs' do
|
|||
end
|
||||
|
||||
context 'when using ETags' do
|
||||
context 'when etag_merge_request_diff_batches is true' do
|
||||
let(:headers) { { 'If-None-Match' => response.etag } }
|
||||
let(:headers) { { 'If-None-Match' => response.etag } }
|
||||
|
||||
it 'does not serialize diffs' do
|
||||
expect(PaginatedDiffSerializer).not_to receive(:new)
|
||||
it 'does not serialize diffs' do
|
||||
expect(PaginatedDiffSerializer).not_to receive(:new)
|
||||
|
||||
go(headers: headers, page: 0, per_page: 5)
|
||||
go(headers: headers, page: 0, per_page: 5)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_modified)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when etag_merge_request_diff_batches is false' do
|
||||
let(:headers) { { 'If-None-Match' => response.etag } }
|
||||
|
||||
before do
|
||||
stub_feature_flags(etag_merge_request_diff_batches: false)
|
||||
end
|
||||
|
||||
it 'does not serialize diffs' do
|
||||
expect_next_instance_of(PaginatedDiffSerializer) do |instance|
|
||||
expect(instance).not_to receive(:represent)
|
||||
end
|
||||
|
||||
subject
|
||||
|
||||
expect(response).to have_gitlab_http_status(:success)
|
||||
end
|
||||
expect(response).to have_gitlab_http_status(:not_modified)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -279,28 +259,12 @@ RSpec.describe 'Merge Requests Diffs' do
|
|||
context 'when using ETag caching' do
|
||||
let(:headers) { { 'If-None-Match' => response.etag } }
|
||||
|
||||
context 'when etag_merge_request_diff_batches is true' do
|
||||
it 'does not serialize diffs' do
|
||||
expect(PaginatedDiffSerializer).not_to receive(:new)
|
||||
it 'does not serialize diffs' do
|
||||
expect(PaginatedDiffSerializer).not_to receive(:new)
|
||||
|
||||
subject
|
||||
subject
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_modified)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when etag_merge_request_diff_batches is false' do
|
||||
before do
|
||||
stub_feature_flags(etag_merge_request_diff_batches: false)
|
||||
end
|
||||
|
||||
it 'does not use cache' do
|
||||
expect(Rails.cache).not_to receive(:fetch).with(/cache:gitlab:PaginatedDiffSerializer/).and_call_original
|
||||
|
||||
subject
|
||||
|
||||
expect(response).to have_gitlab_http_status(:success)
|
||||
end
|
||||
expect(response).to have_gitlab_http_status(:not_modified)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Ci::Queue::PendingBuildsStrategy do
|
||||
let_it_be(:group) { create(:group) }
|
||||
let_it_be(:group_runner) { create(:ci_runner, :group, groups: [group]) }
|
||||
let_it_be(:project) { create(:project, group: group) }
|
||||
let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
|
||||
|
||||
let!(:build_1) { create(:ci_build, :created, pipeline: pipeline) }
|
||||
let!(:build_2) { create(:ci_build, :created, pipeline: pipeline) }
|
||||
let!(:build_3) { create(:ci_build, :created, pipeline: pipeline) }
|
||||
let!(:pending_build_1) { create(:ci_pending_build, build: build_2, project: project) }
|
||||
let!(:pending_build_2) { create(:ci_pending_build, build: build_3, project: project) }
|
||||
let!(:pending_build_3) { create(:ci_pending_build, build: build_1, project: project) }
|
||||
|
||||
describe 'builds_for_group_runner' do
|
||||
it 'returns builds ordered by build ID' do
|
||||
strategy = described_class.new(group_runner)
|
||||
expect(strategy.builds_for_group_runner).to eq([pending_build_3, pending_build_1, pending_build_2])
|
||||
end
|
||||
end
|
||||
end
|
|
@ -34,7 +34,7 @@ require (
|
|||
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616
|
||||
golang.org/x/net v0.0.0-20220531201128-c960675eff93
|
||||
golang.org/x/tools v0.1.11
|
||||
google.golang.org/grpc v1.48.0
|
||||
google.golang.org/grpc v1.49.0
|
||||
google.golang.org/protobuf v1.28.1
|
||||
honnef.co/go/tools v0.3.3
|
||||
)
|
||||
|
|
|
@ -1780,8 +1780,9 @@ google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9K
|
|||
google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
|
||||
google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
|
||||
google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ=
|
||||
google.golang.org/grpc v1.48.0 h1:rQOsyJ/8+ufEDJd/Gdsz7HG220Mh9HAhFHRGnIjda0w=
|
||||
google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
|
||||
google.golang.org/grpc v1.49.0 h1:WTLtQzmQori5FUH25Pq4WT22oCsv8USpQ+F6rqtsmxw=
|
||||
google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI=
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
|
|
Loading…
Reference in New Issue