Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-09-01 15:12:07 +00:00
parent a53033814d
commit 7b197a72aa
58 changed files with 995 additions and 688 deletions

View File

@ -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/**/*"

View File

@ -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"

View File

@ -13,7 +13,8 @@ export default {
props: {
issuePath: {
type: String,
required: true,
required: false,
default: '',
},
issueType: {
type: String,

View File

@ -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>

View File

@ -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>

View File

@ -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"

View File

@ -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>

View File

@ -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>

View File

@ -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" />

View File

@ -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') }}

View File

@ -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>

View File

@ -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" />

View File

@ -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 }}

View File

@ -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"

View File

@ -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" />

View File

@ -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"

View File

@ -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!"
>

View File

@ -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>

View File

@ -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

View File

@ -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;
}
}

View File

@ -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>

View File

@ -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"

View File

@ -26,6 +26,8 @@ query workItemLinksQuery($id: WorkItemID!) {
}
title
state
createdAt
closedAt
}
}
}

View File

@ -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)

View File

@ -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)

View File

@ -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,

View File

@ -13,7 +13,6 @@
- application_performance
- attack_emulation
- audit_events
- audit_reports
- authentication_and_authorization
- auto_devops
- backup_restore

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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(#.+)?\))'

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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`

View File

@ -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

View File

@ -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

View File

@ -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).

View File

@ -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

View File

@ -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 ""

View File

@ -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",

View File

@ -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');
});
});
});

View File

@ -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>
`;

View File

@ -6,7 +6,7 @@ describe('MRWidgetArchived', () => {
let wrapper;
beforeEach(() => {
wrapper = shallowMount(archivedComponent);
wrapper = shallowMount(archivedComponent, { propsData: { mr: {} } });
});
afterEach(() => {

View File

@ -6,7 +6,7 @@ describe('MRWidgetChecking', () => {
let wrapper;
beforeEach(() => {
wrapper = shallowMount(CheckingComponent);
wrapper = shallowMount(CheckingComponent, { propsData: { mr: {} } });
});
afterEach(() => {

View File

@ -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]]);
});
});
});

View File

@ -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();

View File

@ -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: {

View File

@ -706,7 +706,7 @@ ProtectedEnvironment:
- name
- created_at
- updated_at
ProtectedEnvironment::DeployAccessLevel:
ProtectedEnvironments::DeployAccessLevel:
- id
- protected_environment_id
- access_level

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
)

View File

@ -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=