Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-10-16 12:11:07 +00:00
parent dab4155f21
commit b1c7c08640
33 changed files with 837 additions and 87 deletions

View File

@ -189,11 +189,57 @@ async function fetchTraces(tracingUrl, { filters = {}, pageToken, pageSize } = {
}
}
export function buildClient({ provisioningUrl, tracingUrl }) {
async function fetchServices(servicesUrl) {
try {
const { data } = await axios.get(servicesUrl, {
withCredentials: true,
});
if (!Array.isArray(data.services)) {
throw new Error('failed to fetch services. invalid response'); // eslint-disable-line @gitlab/require-i18n-strings
}
return data.services;
} catch (e) {
return reportErrorAndThrow(e);
}
}
async function fetchOperations(operationsUrl, serviceName) {
try {
if (!serviceName) {
throw new Error('fetchOperations() - serviceName is required.');
}
if (!operationsUrl.includes('$SERVICE_NAME$')) {
throw new Error('fetchOperations() - operationsUrl must contain $SERVICE_NAME$');
}
const url = operationsUrl.replace('$SERVICE_NAME$', serviceName);
const { data } = await axios.get(url, {
withCredentials: true,
});
if (!Array.isArray(data.operations)) {
throw new Error('failed to fetch operations. invalid response'); // eslint-disable-line @gitlab/require-i18n-strings
}
return data.operations;
} catch (e) {
return reportErrorAndThrow(e);
}
}
export function buildClient({ provisioningUrl, tracingUrl, servicesUrl, operationsUrl } = {}) {
if (!provisioningUrl || !tracingUrl || !servicesUrl || !operationsUrl) {
throw new Error(
'missing required params. provisioningUrl, tracingUrl, servicesUrl, operationsUrl are required',
);
}
return {
enableTraces: () => enableTraces(provisioningUrl),
isTracingEnabled: () => isTracingEnabled(provisioningUrl),
fetchTraces: (filters) => fetchTraces(tracingUrl, filters),
fetchTrace: (traceId) => fetchTrace(tracingUrl, traceId),
fetchServices: () => fetchServices(servicesUrl),
fetchOperations: (serviceName) => fetchOperations(operationsUrl, serviceName),
};
}

View File

@ -13,11 +13,19 @@ export default {
type: String,
required: true,
},
provisioningUrl: {
type: String,
required: true,
},
tracingUrl: {
type: String,
required: true,
},
provisioningUrl: {
servicesUrl: {
type: String,
required: true,
},
operationsUrl: {
type: String,
required: true,
},
@ -58,6 +66,8 @@ export default {
this.observabilityClient = buildClient({
provisioningUrl: this.provisioningUrl,
tracingUrl: this.tracingUrl,
servicesUrl: this.servicesUrl,
operationsUrl: this.operationsUrl,
});
this.$refs.observabilitySkeleton?.onContentLoaded();
} else if (status === 'error') {

View File

@ -1,5 +1,6 @@
<!-- eslint-disable vue/multi-word-component-names -->
<script>
import { GlButton } from '@gitlab/ui';
// NOTE! For the first iteration, we are simply copying the implementation of Assignees
// It will soon be overhauled in Issue https://gitlab.com/gitlab-org/gitlab/-/issues/233736
import { TYPE_ISSUE } from '~/issues/constants';
@ -11,6 +12,7 @@ export default {
// eslint-disable-next-line @gitlab/require-i18n-strings
name: 'Reviewers',
components: {
GlButton,
CollapsedReviewerList,
UncollapsedReviewerList,
},
@ -64,15 +66,16 @@ export default {
{{ __('None') }}
<template v-if="editable">
-
<button
type="button"
class="gl-button btn-link gl-reset-color!"
<gl-button
category="tertiary"
variant="link"
class="gl-ml-2"
data-testid="assign-yourself"
data-qa-selector="assign_yourself_button"
@click="assignSelf"
>
{{ __('assign yourself') }}
</button>
<span class="gl-text-gray-500 gl-hover-text-blue-800">{{ __('assign yourself') }}</span>
</gl-button>
</template>
</span>

View File

@ -150,7 +150,7 @@ export default {
:title="setTooltip(btn)"
:href="btn.href"
:target="btn.target"
:class="[{ 'gl-mr-1': index !== tertiaryButtons.length - 1 }, btn.class]"
:class="[{ 'gl-mr-3': index !== tertiaryButtons.length - 1 }, btn.class]"
:data-clipboard-text="btn.dataClipboardText"
:data-method="btn.dataMethod"
:icon="btn.icon"

View File

@ -24,10 +24,6 @@ export default {
GlSprintf,
},
mixins: [approvalsMixin, glFeatureFlagsMixin()],
provide: {
expandDetailsTooltip: __('Expand eligible approvers'),
collapseDetailsTooltip: __('Collapse eligible approvers'),
},
props: {
mr: {
type: Object,
@ -248,6 +244,8 @@ export default {
is-collapsible
collapse-on-desktop
:collapsed="collapsed"
:expand-details-tooltip="__('Expand eligible approvers')"
:collapse-details-tooltip="__('Collapse eligible approvers')"
@toggle="() => $emit('toggle')"
>
<template v-if="$apollo.queries.approvals.loading">{{ $options.FETCH_LOADING }}</template>

View File

@ -0,0 +1,77 @@
<script>
import { __ } from '~/locale';
import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables';
import conflictsStateQuery from '../../queries/states/conflicts.query.graphql';
import ActionButtons from '../action_buttons.vue';
import MergeChecksMessage from './message.vue';
export default {
name: 'MergeChecksConflicts',
components: {
MergeChecksMessage,
ActionButtons,
},
mixins: [mergeRequestQueryVariablesMixin],
apollo: {
state: {
query: conflictsStateQuery,
variables() {
return this.mergeRequestQueryVariables;
},
update: (data) => data?.project?.mergeRequest,
},
},
props: {
check: {
type: Object,
required: true,
},
mr: {
type: Object,
required: false,
default: () => ({}),
},
},
data() {
return {
state: {},
};
},
computed: {
isLoading() {
return this.$apollo.queries.state.loading;
},
userPermissions() {
return this.state.userPermissions;
},
showResolveButton() {
return (
this.mr.conflictResolutionPath &&
this.userPermissions.pushToSourceBranch &&
!this.state.sourceBranchProtected
);
},
tertiaryActionsButtons() {
if (this.state.shouldBeRebased) return [];
return [
{
text: __('Resolve locally'),
class: 'js-check-out-modal-trigger',
},
this.showResolveButton && {
text: __('Resolve conflicts'),
category: 'default',
href: this.mr.conflictResolutionPath,
},
].filter((b) => b);
},
},
};
</script>
<template>
<merge-checks-message :check="check">
<action-buttons v-if="!isLoading" :tertiary-buttons="tertiaryActionsButtons" />
</merge-checks-message>
</template>

View File

@ -0,0 +1,44 @@
<script>
import StatusIcon from '../widget/status_icon.vue';
const ICON_NAMES = {
failed: 'failed',
allowed_to_fail: 'neutral',
passed: 'success',
};
export default {
name: 'MergeChecksMessage',
components: {
StatusIcon,
},
props: {
check: {
type: Object,
required: true,
},
mr: {
type: Object,
required: false,
default: () => ({}),
},
},
computed: {
iconName() {
return ICON_NAMES[this.check.result];
},
},
};
</script>
<template>
<div class="gl-py-3 gl-pl-7">
<div class="gl-display-flex">
<status-icon :icon-name="iconName" :level="2" />
<div class="gl-w-full gl-min-w-0">
<div class="gl-display-flex">{{ check.failureReason }}</div>
</div>
<slot></slot>
</div>
</div>
</template>

View File

@ -0,0 +1,91 @@
import createMockApollo from 'helpers/mock_apollo_helper';
import mergeChecksQuery from '../queries/merge_checks.query.graphql';
import conflictsStateQuery from '../queries/states/conflicts.query.graphql';
import MergeChecks from './merge_checks.vue';
const stylesheetsRequireCtx = require.context(
'../../../stylesheets',
true,
/(page_bundles\/merge_requests)\.scss$/,
);
stylesheetsRequireCtx('./page_bundles/merge_requests.scss');
const defaultRender = (apolloProvider) => ({
components: { MergeChecks },
apolloProvider,
data() {
return { mr: { conflictResolutionPath: 'https://gitlab.com' } };
},
template: '<merge-checks :mr="mr" />',
});
const Template = ({ canMerge, failed, pushToSourceBranch }) => {
const requestHandlers = [
[
mergeChecksQuery,
() =>
Promise.resolve({
data: {
project: {
id: 1,
mergeRequest: {
id: 1,
userPermissions: { canMerge },
mergeChecks: [
{
failureReason: 'Unresolved discussions',
identifier: 'unresolved_discussions',
result: failed ? 'failed' : 'passed',
},
{
failureReason: 'Resolve conflicts',
identifier: 'conflicts',
result: failed ? 'failed' : 'passed',
},
],
},
},
},
}),
],
[
conflictsStateQuery,
() =>
Promise.resolve({
data: {
project: {
id: 1,
mergeRequest: {
id: 1,
shouldBeRebased: false,
sourceBranchProtected: false,
userPermissions: { pushToSourceBranch },
},
},
},
}),
],
];
const apolloProvider = createMockApollo(requestHandlers);
return defaultRender(apolloProvider);
};
const LoadingTemplate = () => {
const requestHandlers = [[mergeChecksQuery, () => new Promise(() => {})]];
const apolloProvider = createMockApollo(requestHandlers);
return defaultRender(apolloProvider);
};
export const Default = Template.bind({});
Default.args = { canMerge: true, failed: true, pushToSourceBranch: true };
export const Loading = LoadingTemplate.bind({});
Loading.args = {};
export default {
title: 'vue_merge_request_widget/merge_checks',
component: MergeChecks,
};

View File

@ -0,0 +1,129 @@
<script>
import { GlSkeletonLoader } from '@gitlab/ui';
import { n__, __, sprintf } from '~/locale';
import mergeRequestQueryVariablesMixin from '../mixins/merge_request_query_variables';
import mergeChecksQuery from '../queries/merge_checks.query.graphql';
import StateContainer from './state_container.vue';
import BoldText from './bold_text.vue';
const COMPONENTS = {
conflicts: () => import('./checks/conflicts.vue'),
default: () => import('./checks/message.vue'),
};
export default {
apollo: {
state: {
query: mergeChecksQuery,
skip() {
return !this.mr;
},
variables() {
return this.mergeRequestQueryVariables;
},
update: (data) => data?.project?.mergeRequest,
},
},
components: {
GlSkeletonLoader,
StateContainer,
BoldText,
},
mixins: [mergeRequestQueryVariablesMixin],
props: {
mr: {
type: Object,
required: true,
},
},
data() {
return {
collapsed: true,
state: {},
};
},
computed: {
isLoading() {
return this.$apollo.queries.state.loading;
},
statusIcon() {
return this.failedChecks.length ? 'failed' : 'success';
},
summaryText() {
if (!this.failedChecks.length) {
return this.state?.userPermissions?.canMerge
? __('%{boldStart}Ready to merge!%{boldEnd}')
: __(
'%{boldStart}Ready to merge by members who can write to the target branch.%{boldEnd}',
);
}
return sprintf(
n__(
'%{boldStart}Merge blocked:%{boldEnd} %{count} check failed',
'%{boldStart}Merge blocked:%{boldEnd} %{count} checks failed',
this.failedChecks.length,
),
{ count: this.failedChecks.length },
);
},
checks() {
return this.state.mergeChecks || [];
},
failedChecks() {
return this.checks.filter((c) => c.result === 'failed');
},
},
methods: {
toggleCollapsed() {
this.collapsed = !this.collapsed;
},
checkComponent(check) {
return COMPONENTS[check.identifier] || COMPONENTS.default;
},
},
};
</script>
<template>
<div>
<state-container
:is-loading="isLoading"
:status="statusIcon"
is-collapsible
collapse-on-desktop
:collapsed="collapsed"
:expand-details-tooltip="__('Expand merge checks')"
:collapse-details-tooltip="__('Collapse merge checks')"
@toggle="toggleCollapsed"
>
<template v-if="isLoading" #loading>
<gl-skeleton-loader :width="334" :height="24">
<rect x="0" y="0" width="24" height="24" rx="4" />
<rect x="32" y="2" width="302" height="20" rx="4" />
</gl-skeleton-loader>
</template>
<template v-else>
<bold-text :message="summaryText" />
</template>
</state-container>
<div
v-if="!collapsed"
class="gl-border-t-1 gl-border-t-solid gl-border-gray-100 gl-relative gl-bg-gray-10"
data-testid="merge-checks-full"
>
<div class="gl-px-5">
<component
:is="checkComponent(check)"
v-for="(check, index) in checks"
:key="index"
:class="{
'gl-border-b-solid gl-border-b-1 gl-border-gray-100': index !== checks.length - 1,
}"
:check="check"
:mr="mr"
/>
</div>
</div>
</div>
</template>

View File

@ -1,5 +1,6 @@
<script>
import { GlButton, GlTooltipDirective } from '@gitlab/ui';
import { __ } from '~/locale';
import { STATUS_CLOSED, STATUS_MERGED } from '~/issues/constants';
import StatusIcon from './mr_widget_status_icon.vue';
import Actions from './action_buttons.vue';
@ -13,14 +14,6 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
inject: {
expandDetailsTooltip: {
default: '',
},
collapseDetailsTooltip: {
default: '',
},
},
props: {
isCollapsible: {
type: Boolean,
@ -57,6 +50,16 @@ export default {
required: false,
default: () => [],
},
expandDetailsTooltip: {
required: false,
type: String,
default: __('Expand merge details'),
},
collapseDetailsTooltip: {
required: false,
type: String,
default: __('Collapse merge details'),
},
},
computed: {
wrapperClasses() {
@ -120,6 +123,7 @@ export default {
<gl-button
v-gl-tooltip
:title="collapsed ? expandDetailsTooltip : collapseDetailsTooltip"
:aria-label="collapsed ? expandDetailsTooltip : collapseDetailsTooltip"
:icon="collapsed ? 'chevron-lg-down' : 'chevron-lg-up'"
category="tertiary"
size="small"

View File

@ -153,10 +153,6 @@ export default {
},
},
mixins: [mergeRequestQueryVariablesMixin],
provide: {
expandDetailsTooltip: __('Expand merge details'),
collapseDetailsTooltip: __('Collapse merge details'),
},
props: {
mrData: {
type: Object,

View File

@ -0,0 +1,12 @@
query mergeChecks($projectPath: ID!, $iid: String!) {
project(fullPath: $projectPath) {
id
mergeRequest(iid: $iid) {
id
userPermissions {
canMerge
}
mergeChecks @client
}
}
}

View File

@ -5,6 +5,9 @@ query workInProgress($projectPath: ID!, $iid: String!) {
id
shouldBeRebased
sourceBranchProtected
userPermissions {
pushToSourceBranch
}
}
}
}

View File

@ -29,7 +29,9 @@ export default {
<div class="content-editor-switcher gl-display-inline-flex gl-align-items-center">
<gl-button
:id="$options.richTextEditorButtonId"
class="btn btn-default btn-sm gl-button btn-default-tertiary gl-font-sm! gl-text-secondary! gl-px-4!"
size="small"
category="tertiary"
class="gl-font-sm! gl-text-secondary! gl-px-4!"
data-testid="editing-mode-switcher"
@click="$emit('switch')"
>{{ text }}</gl-button

View File

@ -24,7 +24,6 @@ module Clusters
has_one :cluster_project, -> { order(id: :desc) }, class_name: 'Clusters::Project'
has_many :deployment_clusters
has_many :deployments, inverse_of: :cluster, through: :deployment_clusters
has_many :successful_deployments, -> { success }, class_name: 'Deployment'
has_many :environments, -> { distinct }, through: :deployments
has_many :cluster_groups, class_name: 'Clusters::Group'

View File

@ -2000,15 +2000,11 @@ class MergeRequest < ApplicationRecord
end
def base_pipeline
@base_pipeline ||= project.ci_pipelines
.order(id: :desc)
.find_by(sha: diff_base_sha, ref: target_branch)
@base_pipeline ||= base_pipelines.last
end
def merge_base_pipeline
@merge_base_pipeline ||= project.ci_pipelines
.order(id: :desc)
.find_by(sha: actual_head_pipeline.target_sha, ref: target_branch)
@merge_base_pipeline ||= merge_base_pipelines.last
end
def discussions_rendered_on_frontend?
@ -2176,6 +2172,19 @@ class MergeRequest < ApplicationRecord
attr_accessor :skip_fetch_ref
def merge_base_pipelines
target_branch_pipelines_for(sha: actual_head_pipeline.target_sha)
end
def base_pipelines
target_branch_pipelines_for(sha: diff_base_sha)
end
def target_branch_pipelines_for(sha:)
project.ci_pipelines
.where(sha: sha, ref: target_branch)
end
def set_draft_status
self.draft = draft?
end

View File

@ -37,3 +37,5 @@ module Branches
end
end
end
Branches::DeleteService.prepend_mod

View File

@ -156,7 +156,7 @@ module Git
return if branch_to_sync.nil? && commits_to_sync.empty?
if commits_to_sync.any? && Feature.enabled?(:batch_delay_jira_branch_sync_worker, project)
if commits_to_sync.any?
commits_to_sync.each_slice(JIRA_SYNC_BATCH_SIZE).with_index do |commits, i|
JiraConnect::SyncBranchWorker.perform_in(
JIRA_SYNC_BATCH_DELAY * i,

View File

@ -20,10 +20,13 @@ module Todos
private
def delete_todos
authorized_users = User.from_union([
group.project_users_with_descendants.select(:id),
group.members_with_parents.select(:user_id)
], remove_duplicates: false)
authorized_users = Member.from_union(
[
group.descendant_project_members_with_inactive.select(:user_id),
group.members_with_parents.select(:user_id)
],
remove_duplicates: false
).select(:user_id)
todos.not_in_users(authorized_users).delete_all
end

View File

@ -1,8 +0,0 @@
---
name: batch_delay_jira_branch_sync_worker
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/120866
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/411865
milestone: '16.1'
type: development
group: group::source code
default_enabled: true

View File

@ -51,13 +51,14 @@ depends on where you mention the Jira issue ID in GitLab.
| GitLab: where you mention the Jira issue ID | Jira development panel: what information is displayed |
|------------------------------------------------|-------------------------------------------------------|
| Merge request title or description | Link to the merge request<br>Link to the deployment<br>Link to the pipeline through merge request title<br>Link to the pipeline through merge request description <sup>1</sup><br>Link to the branch <sup>2</sup> |
| Merge request title or description | Link to the merge request<br>Link to the deployment<br>Link to the pipeline through merge request title<br>Link to the pipeline through merge request description <sup>1</sup><br>Link to the branch <sup>2</sup><br>Reviewer information and approval status <sup>3</sup> |
| Branch name | Link to the branch<br>Link to the deployment |
| Commit message | Link to the commit<br>Link to the deployment from up to 5,000 commits after the last successful deployment to the environment <sup>3</sup> <sup>4</sup> |
| Commit message | Link to the commit<br>Link to the deployment from up to 5,000 commits after the last successful deployment to the environment <sup>4</sup> <sup>5</sup> |
| [Jira Smart Commit](#jira-smart-commits) | Custom comment, logged time, or workflow transition |
1. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/390888) in GitLab 15.10.
1. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/354373) in GitLab 15.11.
1. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/364273) in GitLab 16.5.
1. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/300031) in GitLab 16.2 [with a flag](../../administration/feature_flags.md) named `jira_deployment_issue_keys`. Enabled by default.
1. [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/415025) in GitLab 16.3. Feature flag `jira_deployment_issue_keys` removed.

View File

@ -169,11 +169,12 @@ separation of duties is:
### Export a report of merge request compliance violations on projects in a group
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/356791) in GitLab 16.4 [with a flag](../../../administration/feature_flags.md) named `compliance_violation_csv_export`. Disabled by default.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/356791) in GitLab 16.4 [with a flag](../../../administration/feature_flags.md) named `compliance_violation_csv_export`. Disabled by default.
> - [Enabled on GitLab.com and self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/424447) in GitLab 16.5.
FLAG:
On self-managed GitLab, by default this feature is not available. To make it available, ask an administrator to [enable the feature flag](../../../administration/feature_flags.md) named
`compliance_violation_csv_export`. On GitLab.com, this feature is not available. The feature is not ready for production use.
On self-managed GitLab, by default this feature is available. To hide the feature, an administrator can [disable the feature flag](../../../administration/feature_flags.md) named
`compliance_violation_csv_export`. On GitLab.com, this feature is available.
Export a report of merge request compliance violations on merge requests belonging to projects in a group. Reports:

View File

@ -190,7 +190,7 @@ prevent a project from being shared with other groups:
1. Select **Projects in `<group_name>` cannot be shared with other groups**.
1. Select **Save changes**.
This setting applies to all subgroups unless overridden by a group Owner. Groups already
This setting, when enabled, applies to all subgroups unless overridden by a group Owner. Groups already
added to a project lose access when the setting is enabled.
## Prevent users from requesting access to a group

View File

@ -30,13 +30,18 @@ module Gitlab
def validate_regex!
return unless spec.key?(:regex)
return if actual_value.match?(spec[:regex])
safe_regex = ::Gitlab::UntrustedRegexp.new(spec[:regex])
return if safe_regex.match?(actual_value)
if value.nil?
error('default value does not match required RegEx pattern')
else
error('provided value does not match required RegEx pattern')
end
rescue RegexpError
error('invalid regular expression')
end
end
end

View File

@ -548,6 +548,17 @@ msgstr ""
msgid "%{board_target} not found"
msgstr ""
msgid "%{boldStart}Merge blocked:%{boldEnd} %{count} check failed"
msgid_plural "%{boldStart}Merge blocked:%{boldEnd} %{count} checks failed"
msgstr[0] ""
msgstr[1] ""
msgid "%{boldStart}Ready to merge by members who can write to the target branch.%{boldEnd}"
msgstr ""
msgid "%{boldStart}Ready to merge!%{boldEnd}"
msgstr ""
msgid "%{bold_start}%{count}%{bold_end} issue"
msgid_plural "%{bold_start}%{count}%{bold_end} issues"
msgstr[0] ""
@ -11817,6 +11828,9 @@ msgstr ""
msgid "Collapse jobs"
msgstr ""
msgid "Collapse merge checks"
msgstr ""
msgid "Collapse merge details"
msgstr ""
@ -16018,6 +16032,9 @@ msgstr ""
msgid "Deleting a project places it into a read-only state until %{date}, at which point the project will be permanently deleted. Are you ABSOLUTELY sure?"
msgstr ""
msgid "Deleting protected branches is blocked by security policies"
msgstr ""
msgid "Deleting the project will delete its repository and all related resources, including issues and merge requests."
msgstr ""
@ -19549,6 +19566,9 @@ msgstr ""
msgid "Expand jobs"
msgstr ""
msgid "Expand merge checks"
msgstr ""
msgid "Expand merge details"
msgstr ""

View File

@ -12,7 +12,8 @@ describe('buildClient', () => {
const tracingUrl = 'https://example.com/tracing';
const provisioningUrl = 'https://example.com/provisioning';
const servicesUrl = 'https://example.com/services';
const operationsUrl = 'https://example.com/services/$SERVICE_NAME$/operations';
const FETCHING_TRACES_ERROR = 'traces are missing/invalid in the response';
beforeEach(() => {
@ -22,6 +23,8 @@ describe('buildClient', () => {
client = buildClient({
tracingUrl,
provisioningUrl,
servicesUrl,
operationsUrl,
});
});
@ -29,6 +32,27 @@ describe('buildClient', () => {
axiosMock.restore();
});
describe('buildClient', () => {
it('rejects if params are missing', () => {
const e = new Error(
'missing required params. provisioningUrl, tracingUrl, servicesUrl, operationsUrl are required',
);
expect(() =>
buildClient({ tracingUrl: 'test', servicesUrl: 'test', operationsUrl: 'test' }),
).toThrow(e);
expect(() =>
buildClient({ provisioningUrl: 'test', servicesUrl: 'test', operationsUrl: 'test' }),
).toThrow(e);
expect(() =>
buildClient({ provisioningUrl: 'test', tracingUrl: 'test', operationsUrl: 'test' }),
).toThrow(e);
expect(() =>
buildClient({ provisioningUrl: 'test', tracingUrl: 'test', servicesUrl: 'test' }),
).toThrow(e);
expect(() => buildClient({})).toThrow(e);
});
});
describe('isTracingEnabled', () => {
it('returns true if requests succeedes', async () => {
axiosMock.onGet(provisioningUrl).reply(200, {
@ -301,4 +325,77 @@ describe('buildClient', () => {
});
});
});
describe('fetchServices', () => {
it('fetches services from the services URL', async () => {
const mockResponse = {
services: [{ name: 'service-1' }, { name: 'service-2' }],
};
axiosMock.onGet(servicesUrl).reply(200, mockResponse);
const result = await client.fetchServices();
expect(axios.get).toHaveBeenCalledTimes(1);
expect(axios.get).toHaveBeenCalledWith(servicesUrl, {
withCredentials: true,
});
expect(result).toEqual(mockResponse.services);
});
it('rejects if services are missing', async () => {
axiosMock.onGet(servicesUrl).reply(200, {});
const e = 'failed to fetch services. invalid response';
await expect(client.fetchServices()).rejects.toThrow(e);
expect(Sentry.captureException).toHaveBeenCalledWith(new Error(e));
});
});
describe('fetchOperations', () => {
const serviceName = 'test-service';
const parsedOperationsUrl = `https://example.com/services/${serviceName}/operations`;
it('fetches operations from the operations URL', async () => {
const mockResponse = {
operations: [{ name: 'operation-1' }, { name: 'operation-2' }],
};
axiosMock.onGet(parsedOperationsUrl).reply(200, mockResponse);
const result = await client.fetchOperations(serviceName);
expect(axios.get).toHaveBeenCalledTimes(1);
expect(axios.get).toHaveBeenCalledWith(parsedOperationsUrl, {
withCredentials: true,
});
expect(result).toEqual(mockResponse.operations);
});
it('rejects if serviceName is missing', async () => {
const e = 'fetchOperations() - serviceName is required.';
await expect(client.fetchOperations()).rejects.toThrow(e);
expect(Sentry.captureException).toHaveBeenCalledWith(new Error(e));
});
it('rejects if operationUrl does not contain $SERVICE_NAME$', async () => {
client = buildClient({
tracingUrl,
provisioningUrl,
servicesUrl,
operationsUrl: 'something',
});
const e = 'fetchOperations() - operationsUrl must contain $SERVICE_NAME$';
await expect(client.fetchOperations(serviceName)).rejects.toThrow(e);
expect(Sentry.captureException).toHaveBeenCalledWith(new Error(e));
});
it('rejects if operations are missing', async () => {
axiosMock.onGet(parsedOperationsUrl).reply(200, {});
const e = 'failed to fetch operations. invalid response';
await expect(client.fetchOperations(serviceName)).rejects.toThrow(e);
expect(Sentry.captureException).toHaveBeenCalledWith(new Error(e));
});
});
});

View File

@ -16,6 +16,8 @@ describe('ObservabilityContainer', () => {
const OAUTH_URL = 'https://example.com/oauth';
const TRACING_URL = 'https://example.com/tracing';
const PROVISIONING_URL = 'https://example.com/provisioning';
const SERVICES_URL = 'https://example.com/services';
const OPERATIONS_URL = 'https://example.com/operations';
beforeEach(() => {
jest.spyOn(console, 'error').mockImplementation();
@ -27,6 +29,8 @@ describe('ObservabilityContainer', () => {
oauthUrl: OAUTH_URL,
tracingUrl: TRACING_URL,
provisioningUrl: PROVISIONING_URL,
servicesUrl: SERVICES_URL,
operationsUrl: OPERATIONS_URL,
},
stubs: {
ObservabilitySkeleton: stubComponent(ObservabilitySkeleton, {
@ -93,6 +97,8 @@ describe('ObservabilityContainer', () => {
expect(buildClient).toHaveBeenCalledWith({
provisioningUrl: PROVISIONING_URL,
tracingUrl: TRACING_URL,
servicesUrl: SERVICES_URL,
operationsUrl: OPERATIONS_URL,
});
expect(findIframe().exists()).toBe(false);
});

View File

@ -0,0 +1,90 @@
import VueApollo from 'vue-apollo';
import Vue from 'vue';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import ConflictsComponent from '~/vue_merge_request_widget/components/checks/conflicts.vue';
import conflictsStateQuery from '~/vue_merge_request_widget/queries/states/conflicts.query.graphql';
Vue.use(VueApollo);
let wrapper;
let apolloProvider;
function factory({
result = 'passed',
canMerge = true,
pushToSourceBranch = true,
shouldBeRebased = false,
sourceBranchProtected = false,
mr = {},
} = {}) {
apolloProvider = createMockApollo([
[
conflictsStateQuery,
jest.fn().mockResolvedValue({
data: {
project: {
id: 1,
mergeRequest: {
id: 1,
shouldBeRebased,
sourceBranchProtected,
userPermissions: { canMerge, pushToSourceBranch },
},
},
},
}),
],
]);
wrapper = mountExtended(ConflictsComponent, {
apolloProvider,
propsData: {
mr,
check: { result, failureReason: 'Conflicts message' },
},
});
}
describe('Merge request merge checks conflicts component', () => {
afterEach(() => {
apolloProvider = null;
});
it('renders failure reason text', () => {
factory();
expect(wrapper.text()).toEqual('Conflicts message');
});
it.each`
conflictResolutionPath | pushToSourceBranch | sourceBranchProtected | rendersConflictButton | rendersConflictButtonText
${'https://gitlab.com'} | ${true} | ${false} | ${true} | ${'renders'}
${undefined} | ${true} | ${false} | ${false} | ${'does not render'}
${'https://gitlab.com'} | ${false} | ${false} | ${false} | ${'does not render'}
${'https://gitlab.com'} | ${true} | ${true} | ${false} | ${'does not render'}
${'https://gitlab.com'} | ${false} | ${false} | ${false} | ${'does not render'}
${undefined} | ${false} | ${false} | ${false} | ${'does not render'}
`(
'$rendersConflictButtonText the conflict button for $conflictResolutionPath $pushToSourceBranch $sourceBranchProtected $rendersConflictButton',
async ({
conflictResolutionPath,
pushToSourceBranch,
sourceBranchProtected,
rendersConflictButton,
}) => {
factory({ mr: { conflictResolutionPath }, pushToSourceBranch, sourceBranchProtected });
await waitForPromises();
expect(wrapper.findAllByTestId('extension-actions-button').length).toBe(
rendersConflictButton ? 2 : 1,
);
expect(wrapper.findAllByTestId('extension-actions-button').at(-1).text()).toBe(
rendersConflictButton ? 'Resolve conflicts' : 'Resolve locally',
);
},
);
});

View File

@ -0,0 +1,30 @@
import { mountExtended } from 'helpers/vue_test_utils_helper';
import MessageComponent from '~/vue_merge_request_widget/components/checks/message.vue';
import StatusIcon from '~/vue_merge_request_widget/components/widget/status_icon.vue';
let wrapper;
function factory(propsData = {}) {
wrapper = mountExtended(MessageComponent, {
propsData,
});
}
describe('Merge request merge checks message component', () => {
it('renders failure reason text', () => {
factory({ check: { result: 'passed', failureReason: 'Failed message' } });
expect(wrapper.text()).toEqual('Failed message');
});
it.each`
result | icon
${'passed'} | ${'success'}
${'failed'} | ${'failed'}
${'allowed_to_fail'} | ${'neutral'}
`('renders $icon icon for $result result', ({ result, icon }) => {
factory({ check: { result, failureReason: 'Failed message' } });
expect(wrapper.findComponent(StatusIcon).props('iconName')).toBe(icon);
});
});

View File

@ -0,0 +1,92 @@
import VueApollo from 'vue-apollo';
import Vue from 'vue';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import MergeChecksComponent from '~/vue_merge_request_widget/components/merge_checks.vue';
import mergeChecksQuery from '~/vue_merge_request_widget/queries/merge_checks.query.graphql';
import StatusIcon from '~/vue_merge_request_widget/components/extensions/status_icon.vue';
Vue.use(VueApollo);
let wrapper;
let apolloProvider;
function factory({ canMerge = true, mergeChecks = [] } = {}) {
apolloProvider = createMockApollo([
[
mergeChecksQuery,
jest.fn().mockResolvedValue({
data: {
project: {
id: 1,
mergeRequest: { id: 1, userPermissions: { canMerge }, mergeChecks },
},
},
}),
],
]);
wrapper = mountExtended(MergeChecksComponent, {
apolloProvider,
propsData: {
mr: {},
},
});
}
describe('Merge request merge checks component', () => {
afterEach(() => {
apolloProvider = null;
});
it('renders ready to merge text if user can merge', async () => {
factory({ canMerge: true });
await waitForPromises();
expect(wrapper.text()).toBe('Ready to merge!');
});
it('renders ready to merge by members text if user can not merge', async () => {
factory({ canMerge: false });
await waitForPromises();
expect(wrapper.text()).toBe('Ready to merge by members who can write to the target branch.');
});
it.each`
mergeChecks | text
${[{ identifier: 'discussions', result: 'failed' }]} | ${'Merge blocked: 1 check failed'}
${[{ identifier: 'discussions', result: 'failed' }, { identifier: 'rebase', result: 'failed' }]} | ${'Merge blocked: 2 checks failed'}
`('renders $text for $mergeChecks', async ({ mergeChecks, text }) => {
factory({ mergeChecks });
await waitForPromises();
expect(wrapper.text()).toBe(text);
});
it.each`
result | statusIcon
${'failed'} | ${'failed'}
${'passed'} | ${'success'}
`('renders $statusIcon for $result result', async ({ result, statusIcon }) => {
factory({ mergeChecks: [{ result, identifier: 'discussions' }] });
await waitForPromises();
expect(wrapper.findComponent(StatusIcon).props('iconName')).toBe(statusIcon);
});
it('expands collapsed area', async () => {
factory();
await waitForPromises();
await wrapper.findByTestId('widget-toggle').trigger('click');
expect(wrapper.findByTestId('merge-checks-full').exists()).toBe(true);
});
});

View File

@ -294,5 +294,17 @@ RSpec.describe Gitlab::Ci::Config::Interpolation::Inputs, feature_category: :pip
)
end
end
context 'when the pattern is unsafe' do
let(:specs) { { test_input: { regex: 'a++' } } }
let(:args) { { test_input: 'aaaaaaaaaaaaaaaaaaaaa' } }
it 'is invalid' do
expect(inputs).not_to be_valid
expect(inputs.errors).to contain_exactly(
'`test_input` input: invalid regular expression'
)
end
end
end
end

View File

@ -26,7 +26,6 @@ RSpec.describe Clusters::Cluster, :use_clean_rails_memory_store_caching,
it { is_expected.to have_many(:kubernetes_namespaces) }
it { is_expected.to have_one(:cluster_project) }
it { is_expected.to have_many(:deployment_clusters) }
it { is_expected.to have_many(:successful_deployments) }
it { is_expected.to have_many(:environments).through(:deployments) }
it { is_expected.to delegate_method(:status).to(:provider) }

View File

@ -685,44 +685,21 @@ RSpec.describe Git::BranchPushService, :use_clean_rails_redis_caching, services:
let(:commits_to_sync) { [] }
shared_examples 'enqueues Jira sync worker' do
context "batch_delay_jira_branch_sync_worker feature flag is enabled" do
before do
stub_feature_flags(batch_delay_jira_branch_sync_worker: true)
end
specify :aggregate_failures do
Sidekiq::Testing.fake! do
if commits_to_sync.any?
expect(JiraConnect::SyncBranchWorker)
.to receive(:perform_in)
.with(kind_of(Numeric), project.id, branch_to_sync, commits_to_sync, kind_of(Numeric))
.and_call_original
else
expect(JiraConnect::SyncBranchWorker)
.to receive(:perform_async)
.with(project.id, branch_to_sync, commits_to_sync, kind_of(Numeric))
.and_call_original
end
expect { subject }.to change(JiraConnect::SyncBranchWorker.jobs, :size).by(1)
end
end
end
context "batch_delay_jira_branch_sync_worker feature flag is disabled" do
before do
stub_feature_flags(batch_delay_jira_branch_sync_worker: false)
end
specify :aggregate_failures do
Sidekiq::Testing.fake! do
specify :aggregate_failures do
Sidekiq::Testing.fake! do
if commits_to_sync.any?
expect(JiraConnect::SyncBranchWorker)
.to receive(:perform_in)
.with(kind_of(Numeric), project.id, branch_to_sync, commits_to_sync, kind_of(Numeric))
.and_call_original
else
expect(JiraConnect::SyncBranchWorker)
.to receive(:perform_async)
.with(project.id, branch_to_sync, commits_to_sync, kind_of(Numeric))
.and_call_original
expect { subject }.to change(JiraConnect::SyncBranchWorker.jobs, :size).by(1)
end
expect { subject }.to change(JiraConnect::SyncBranchWorker.jobs, :size).by(1)
end
end
end