Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
efbd20fd1e
commit
a2fd863d3b
|
@ -210,6 +210,7 @@ RSpec/VerifiedDoubles:
|
|||
- ee/spec/views/layouts/header/_ee_subscribable_banner.html.haml_spec.rb
|
||||
- ee/spec/workers/ci/sync_reports_to_report_approval_rules_worker_spec.rb
|
||||
- ee/spec/workers/geo/container_repository_sync_worker_spec.rb
|
||||
- ee/spec/workers/compliance_management/chain_of_custody_report_worker_spec.rb
|
||||
- ee/spec/workers/geo/design_repository_sync_worker_spec.rb
|
||||
- ee/spec/workers/geo/destroy_worker_spec.rb
|
||||
- ee/spec/workers/geo/event_worker_spec.rb
|
||||
|
|
|
@ -6,6 +6,9 @@ import {
|
|||
GlLink,
|
||||
GlSprintf,
|
||||
GlFormCheckboxGroup,
|
||||
GlButton,
|
||||
GlCollapse,
|
||||
GlIcon,
|
||||
} from '@gitlab/ui';
|
||||
import { partition, isString, uniqueId, isEmpty } from 'lodash';
|
||||
import InviteModalBase from 'ee_else_ce/invite_members/components/invite_modal_base.vue';
|
||||
|
@ -13,7 +16,7 @@ import Api from '~/api';
|
|||
import ExperimentTracking from '~/experimentation/experiment_tracking';
|
||||
import { BV_SHOW_MODAL, BV_HIDE_MODAL } from '~/lib/utils/constants';
|
||||
import { getParameterValues } from '~/lib/utils/url_utility';
|
||||
import { n__ } from '~/locale';
|
||||
import { n__, sprintf } from '~/locale';
|
||||
import {
|
||||
CLOSE_TO_LIMIT_COUNT,
|
||||
USERS_FILTER_ALL,
|
||||
|
@ -38,6 +41,9 @@ export default {
|
|||
GlDropdownItem,
|
||||
GlSprintf,
|
||||
GlFormCheckboxGroup,
|
||||
GlButton,
|
||||
GlCollapse,
|
||||
GlIcon,
|
||||
InviteModalBase,
|
||||
MembersTokenSelect,
|
||||
ModalConfetti,
|
||||
|
@ -110,6 +116,8 @@ export default {
|
|||
mode: 'default',
|
||||
// Kept in sync with "base"
|
||||
selectedAccessLevel: undefined,
|
||||
errorsLimit: 2,
|
||||
isErrorsSectionExpanded: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
@ -135,7 +143,7 @@ export default {
|
|||
return n__(
|
||||
"InviteMembersModal|The following member couldn't be invited",
|
||||
"InviteMembersModal|The following %d members couldn't be invited",
|
||||
Object.keys(this.invalidMembers).length,
|
||||
this.errorList.length,
|
||||
);
|
||||
},
|
||||
tasksToBeDoneEnabled() {
|
||||
|
@ -187,6 +195,29 @@ export default {
|
|||
? this.$options.labels.placeHolderDisabled
|
||||
: this.$options.labels.placeHolder;
|
||||
},
|
||||
errorList() {
|
||||
return Object.entries(this.invalidMembers).map(([member, error]) => {
|
||||
return { member, displayedMemberName: this.tokenName(member), message: error };
|
||||
});
|
||||
},
|
||||
errorsLimited() {
|
||||
return this.errorList.slice(0, this.errorsLimit);
|
||||
},
|
||||
errorsExpanded() {
|
||||
return this.errorList.slice(this.errorsLimit);
|
||||
},
|
||||
shouldErrorsSectionExpand() {
|
||||
return Boolean(this.errorsExpanded.length);
|
||||
},
|
||||
errorCollapseText() {
|
||||
if (this.isErrorsSectionExpanded) {
|
||||
return this.$options.labels.expandedErrors;
|
||||
}
|
||||
|
||||
return sprintf(this.$options.labels.collapsedErrors, {
|
||||
count: this.errorsExpanded.length,
|
||||
});
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
eventHub.$on('openModal', (options) => {
|
||||
|
@ -311,6 +342,9 @@ export default {
|
|||
delete this.invalidMembers[memberName(token)];
|
||||
this.invalidMembers = { ...this.invalidMembers };
|
||||
},
|
||||
toggleErrorExpansion() {
|
||||
this.isErrorsSectionExpanded = !this.isErrorsSectionExpanded;
|
||||
},
|
||||
},
|
||||
labels: MEMBER_MODAL_LABELS,
|
||||
};
|
||||
|
@ -357,10 +391,36 @@ export default {
|
|||
>
|
||||
{{ $options.labels.memberErrorListText }}
|
||||
<ul class="gl-pl-5 gl-mb-0">
|
||||
<li v-for="(error, member) in invalidMembers" :key="member">
|
||||
<strong>{{ tokenName(member) }}:</strong> {{ error }}
|
||||
<li v-for="error in errorsLimited" :key="error.member" data-testid="errors-limited-item">
|
||||
<strong>{{ error.displayedMemberName }}:</strong> {{ error.message }}
|
||||
</li>
|
||||
</ul>
|
||||
<template v-if="shouldErrorsSectionExpand">
|
||||
<gl-collapse v-model="isErrorsSectionExpanded">
|
||||
<ul class="gl-pl-5 gl-mb-0">
|
||||
<li
|
||||
v-for="error in errorsExpanded"
|
||||
:key="error.member"
|
||||
data-testid="errors-expanded-item"
|
||||
>
|
||||
<strong>{{ error.displayedMemberName }}:</strong> {{ error.message }}
|
||||
</li>
|
||||
</ul>
|
||||
</gl-collapse>
|
||||
<gl-button
|
||||
class="gl-text-decoration-none! gl-shadow-none! gl-mt-3"
|
||||
data-testid="accordion-button"
|
||||
variant="link"
|
||||
@click="toggleErrorExpansion"
|
||||
>
|
||||
{{ errorCollapseText }}
|
||||
<gl-icon
|
||||
name="chevron-down"
|
||||
class="gl-transition-medium"
|
||||
:class="{ 'gl-rotate-180': isErrorsSectionExpanded }"
|
||||
/>
|
||||
</gl-button>
|
||||
</template>
|
||||
</gl-alert>
|
||||
<user-limit-notification
|
||||
v-else
|
||||
|
|
|
@ -79,6 +79,8 @@ export const HEADER_CLOSE_LABEL = s__('InviteMembersModal|Close invite team memb
|
|||
export const MEMBER_ERROR_LIST_TEXT = s__(
|
||||
'InviteMembersModal|Review the invite errors and try again:',
|
||||
);
|
||||
export const COLLAPSED_ERRORS = s__('InviteMembersModal|Show more (%{count})');
|
||||
export const EXPANDED_ERRORS = s__('InviteMembersModal|Show less');
|
||||
|
||||
export const MEMBER_MODAL_LABELS = {
|
||||
modal: {
|
||||
|
@ -115,6 +117,8 @@ export const MEMBER_MODAL_LABELS = {
|
|||
},
|
||||
toastMessageSuccessful: TOAST_MESSAGE_SUCCESSFUL,
|
||||
memberErrorListText: MEMBER_ERROR_LIST_TEXT,
|
||||
collapsedErrors: COLLAPSED_ERRORS,
|
||||
expandedErrors: EXPANDED_ERRORS,
|
||||
};
|
||||
|
||||
export const GROUP_MODAL_LABELS = {
|
||||
|
|
|
@ -51,9 +51,6 @@ export default {
|
|||
isLoading() {
|
||||
return this.glFeatures.mergeRequestWidgetGraphql && this.$apollo.queries.state.loading;
|
||||
},
|
||||
showRebaseWithoutCi() {
|
||||
return this.glFeatures?.rebaseWithoutCiUi;
|
||||
},
|
||||
rebaseInProgress() {
|
||||
if (this.glFeatures.mergeRequestWidgetGraphql) {
|
||||
return this.state.rebaseInProgress;
|
||||
|
@ -195,7 +192,6 @@ export default {
|
|||
</template>
|
||||
<template v-if="!isLoading" #actions>
|
||||
<gl-button
|
||||
v-if="showRebaseWithoutCi"
|
||||
:loading="isMakingRequest"
|
||||
variant="confirm"
|
||||
size="small"
|
||||
|
|
|
@ -3,11 +3,15 @@ import * as Sentry from '@sentry/browser';
|
|||
import { normalizeHeaders } from '~/lib/utils/common_utils';
|
||||
import { __ } from '~/locale';
|
||||
import Poll from '~/lib/utils/poll';
|
||||
import StatusIcon from '../extensions/status_icon.vue';
|
||||
import { EXTENSION_ICON_NAMES } from '../../constants';
|
||||
|
||||
const FETCH_TYPE_COLLAPSED = 'collapsed';
|
||||
// const FETCH_TYPE_EXPANDED = 'expanded';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
StatusIcon,
|
||||
},
|
||||
props: {
|
||||
/**
|
||||
* @param {value.collapsed} Object
|
||||
|
@ -19,7 +23,8 @@ export default {
|
|||
},
|
||||
loadingText: {
|
||||
type: String,
|
||||
required: true,
|
||||
required: false,
|
||||
default: __('Loading'),
|
||||
},
|
||||
errorText: {
|
||||
type: String,
|
||||
|
@ -52,16 +57,30 @@ export default {
|
|||
required: false,
|
||||
default: false,
|
||||
},
|
||||
statusIconName: {
|
||||
type: String,
|
||||
default: 'neutral',
|
||||
required: false,
|
||||
validator: (value) => Object.keys(EXTENSION_ICON_NAMES).indexOf(value) > -1,
|
||||
},
|
||||
widgetName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
statusIcon: null,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
isLoading(newValue) {
|
||||
this.$emit('is-loading', newValue);
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
this.loading = true;
|
||||
this.isLoading = true;
|
||||
|
||||
try {
|
||||
await this.fetch(this.fetchCollapsedData, FETCH_TYPE_COLLAPSED);
|
||||
|
@ -69,20 +88,14 @@ export default {
|
|||
this.error = this.errorText;
|
||||
}
|
||||
|
||||
this.loading = false;
|
||||
this.isLoading = false;
|
||||
},
|
||||
methods: {
|
||||
fetch(handler, dataType) {
|
||||
const requests = this.multiPolling ? handler() : [handler];
|
||||
const allData = [];
|
||||
|
||||
const promises = requests.map((request) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const setData = (data) => {
|
||||
this.$emit('input', { ...this.value, [dataType]: data });
|
||||
resolve(data);
|
||||
};
|
||||
|
||||
const poll = new Poll({
|
||||
resource: {
|
||||
fetchData: () => request(),
|
||||
|
@ -95,17 +108,7 @@ export default {
|
|||
return;
|
||||
}
|
||||
|
||||
if (this.multiPolling) {
|
||||
allData.push(response.data);
|
||||
|
||||
if (allData.length === requests.length) {
|
||||
setData(allData);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setData(response.data);
|
||||
resolve(response.data);
|
||||
},
|
||||
errorCallback: (e) => {
|
||||
Sentry.captureException(e);
|
||||
|
@ -117,7 +120,9 @@ export default {
|
|||
});
|
||||
});
|
||||
|
||||
return Promise.all(promises);
|
||||
return Promise.all(promises).then((data) => {
|
||||
this.$emit('input', { ...this.value, [dataType]: this.multiPolling ? data : data[0] });
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -126,13 +131,18 @@ export default {
|
|||
<template>
|
||||
<section class="media-section" data-testid="widget-extension">
|
||||
<div class="media gl-p-5">
|
||||
<!-- status icon will go here -->
|
||||
<status-icon
|
||||
:level="1"
|
||||
:name="widgetName"
|
||||
:is-loading="isLoading"
|
||||
:icon-name="statusIconName"
|
||||
/>
|
||||
<div
|
||||
class="media-body gl-display-flex gl-flex-direction-row! gl-align-self-center"
|
||||
data-testid="widget-extension-top-level"
|
||||
>
|
||||
<div class="gl-flex-grow-1" data-testid="widget-extension-top-level-summary">
|
||||
<slot name="summary">{{ summary }}</slot>
|
||||
<slot name="summary">{{ isLoading ? loadingText : summary }}</slot>
|
||||
</div>
|
||||
<!-- actions will go here -->
|
||||
<!-- toggle button will go here -->
|
||||
|
|
|
@ -37,7 +37,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
|
|||
push_frontend_feature_flag(:refactor_mr_widgets_extensions, project)
|
||||
push_frontend_feature_flag(:refactor_code_quality_extension, project)
|
||||
push_frontend_feature_flag(:refactor_mr_widget_test_summary, project)
|
||||
push_frontend_feature_flag(:rebase_without_ci_ui, project)
|
||||
push_frontend_feature_flag(:issue_assignees_widget, @project)
|
||||
push_frontend_feature_flag(:realtime_labels, project)
|
||||
push_frontend_feature_flag(:refactor_security_extension, @project)
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Groups
|
||||
class GroupDeletedEvent < ::Gitlab::EventStore::Event
|
||||
def schema
|
||||
{
|
||||
'type' => 'object',
|
||||
'properties' => {
|
||||
'group_id' => { 'type' => 'integer' },
|
||||
'root_namespace_id' => { 'type' => 'integer' }
|
||||
},
|
||||
'required' => %w[group_id root_namespace_id]
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
|
@ -45,6 +45,8 @@ module Groups
|
|||
.execute(blocking: true)
|
||||
end
|
||||
|
||||
publish_event
|
||||
|
||||
group
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
@ -91,6 +93,17 @@ module Groups
|
|||
end
|
||||
end
|
||||
# rubocop:enable CodeReuse/ActiveRecord
|
||||
|
||||
def publish_event
|
||||
event = Groups::GroupDeletedEvent.new(
|
||||
data: {
|
||||
group_id: group.id,
|
||||
root_namespace_id: group.root_ancestor.id
|
||||
}
|
||||
)
|
||||
|
||||
Gitlab::EventStore.publish(event)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -5,4 +5,4 @@ rollout_issue_url:
|
|||
milestone: '15.1'
|
||||
type: development
|
||||
group: group::code review
|
||||
default_enabled: false
|
||||
default_enabled: true
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
---
|
||||
name: rebase_without_ci_ui
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/78194
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/350262
|
||||
milestone: '14.7'
|
||||
type: development
|
||||
group: group::pipeline execution
|
||||
name: skip_rugged_auto_detect
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/95330
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/370708
|
||||
milestone: '15.3'
|
||||
type: ops
|
||||
group: group::gitaly
|
||||
default_enabled: false
|
|
@ -0,0 +1,25 @@
|
|||
---
|
||||
key_path: redis_hll_counters.ci_templates.p_ci_templates_security_fortify_fod_sast_monthly
|
||||
description: Count of pipelines using the Fortify FoD SAST template
|
||||
product_section: sec
|
||||
product_stage: secure
|
||||
product_group: static_analysis
|
||||
product_category: SAST
|
||||
value_type: number
|
||||
status: active
|
||||
milestone: "15.3"
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/91956
|
||||
time_frame: 28d
|
||||
data_source: redis_hll
|
||||
data_category: optional
|
||||
instrumentation_class: RedisHLLMetric
|
||||
distribution:
|
||||
- ce
|
||||
- ee
|
||||
tier:
|
||||
- free
|
||||
- premium
|
||||
- ultimate
|
||||
options:
|
||||
events:
|
||||
- p_ci_templates_security_fortify_fod_sast
|
|
@ -0,0 +1,25 @@
|
|||
---
|
||||
key_path: redis_hll_counters.ci_templates.p_ci_templates_implicit_security_fortify_fod_sast_monthly
|
||||
description: Count of pipelines with implicit runs using the Fortify FoD SAST template
|
||||
product_section: sec
|
||||
product_stage: secure
|
||||
product_group: static_analysis
|
||||
product_category: SAST
|
||||
value_type: number
|
||||
status: active
|
||||
milestone: "15.3"
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/91956
|
||||
time_frame: 28d
|
||||
data_source: redis_hll
|
||||
data_category: optional
|
||||
instrumentation_class: RedisHLLMetric
|
||||
distribution:
|
||||
- ce
|
||||
- ee
|
||||
tier:
|
||||
- free
|
||||
- premium
|
||||
- ultimate
|
||||
options:
|
||||
events:
|
||||
- p_ci_templates_implicit_security_fortify_fod_sast
|
|
@ -0,0 +1,25 @@
|
|||
---
|
||||
key_path: redis_hll_counters.ci_templates.p_ci_templates_security_fortify_fod_sast_weekly
|
||||
description: Count of pipelines using the Fortify FoD SAST template
|
||||
product_section: sec
|
||||
product_stage: secure
|
||||
product_group: static_analysis
|
||||
product_category: SAST
|
||||
value_type: number
|
||||
status: active
|
||||
milestone: "15.3"
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/91956
|
||||
time_frame: 7d
|
||||
data_source: redis_hll
|
||||
data_category: optional
|
||||
instrumentation_class: RedisHLLMetric
|
||||
distribution:
|
||||
- ce
|
||||
- ee
|
||||
tier:
|
||||
- free
|
||||
- premium
|
||||
- ultimate
|
||||
options:
|
||||
events:
|
||||
- p_ci_templates_security_fortify_fod_sast
|
|
@ -0,0 +1,25 @@
|
|||
---
|
||||
key_path: redis_hll_counters.ci_templates.p_ci_templates_implicit_security_fortify_fod_sast_weekly
|
||||
description: Count of pipelines with implicit runs using the Fortify FoD SAST template
|
||||
product_section: sec
|
||||
product_stage: secure
|
||||
product_group: static_analysis
|
||||
product_category: SAST
|
||||
value_type: number
|
||||
status: active
|
||||
milestone: "15.3"
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/91956
|
||||
time_frame: 7d
|
||||
data_source: redis_hll
|
||||
data_category: optional
|
||||
instrumentation_class: RedisHLLMetric
|
||||
distribution:
|
||||
- ce
|
||||
- ee
|
||||
tier:
|
||||
- free
|
||||
- premium
|
||||
- ultimate
|
||||
options:
|
||||
events:
|
||||
- p_ci_templates_implicit_security_fortify_fod_sast
|
|
@ -99,6 +99,8 @@
|
|||
- 1
|
||||
- - cluster_agent
|
||||
- 1
|
||||
- - compliance_management_chain_of_custody_report
|
||||
- 1
|
||||
- - compliance_management_merge_requests_compliance_violations
|
||||
- 1
|
||||
- - container_repository
|
||||
|
|
|
@ -92,19 +92,28 @@ Our criteria for the separation of duties is as follows:
|
|||
|
||||
## Chain of Custody report
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/213364) in GitLab 13.3.
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/213364) in GitLab 13.3.
|
||||
> - Chain of Custody reports sent using email [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/342594) in GitLab 15.3 with a flag named `async_chain_of_custody_report`. Disabled by default.
|
||||
|
||||
FLAG:
|
||||
On self-managed GitLab, by default sending Chain of Custody reports through email is not available. To make it available, ask an administrator to [enable the feature flag](../../../administration/feature_flags.md) named `async_chain_of_custody_report`. On GitLab.com, this feature is not available.
|
||||
|
||||
The Chain of Custody report allows customers to export a list of merge commits within the group.
|
||||
The data provides a comprehensive view with respect to merge commits. It includes the merge commit SHA,
|
||||
merge request author, merge request ID, merge user, date merged, pipeline ID, group name, project name, and merge request approvers.
|
||||
Depending on the merge strategy, the merge commit SHA can be a merge commit, squash commit, or a diff head commit.
|
||||
|
||||
To download the Chain of Custody report:
|
||||
To generate the Chain of Custody report:
|
||||
|
||||
1. On the top bar, select **Menu > Groups** and find your group.
|
||||
1. On the left sidebar, select **Security & Compliance > Compliance report**.
|
||||
1. Select **List of all merge commits**.
|
||||
|
||||
The Chain of Custody report is either:
|
||||
|
||||
- Available for download.
|
||||
- Sent through email. Requires GitLab 15.3 and later with `async_chain_of_custody_report` feature flag enabled.
|
||||
|
||||
### Commit-specific Chain of Custody report
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/267629) in GitLab 13.6.
|
||||
|
@ -119,6 +128,11 @@ Authenticated group owners can generate a commit-specific Chain of Custody repor
|
|||
1. Enter the merge commit SHA, and then select **Export commit custody report**.
|
||||
SHA and then select **Export commit custody report**.
|
||||
|
||||
The Chain of Custody report is either:
|
||||
|
||||
- Available for download.
|
||||
- Sent through email. Requires GitLab 15.3 and later with `async_chain_of_custody_report` feature flag enabled.
|
||||
|
||||
- Using a direct link: `https://gitlab.com/groups/<group-name>/-/security/merge_commit_reports.csv?commit_sha={optional_commit_sha}`, passing in an optional value to the
|
||||
`commit_sha` query parameter.
|
||||
|
||||
|
|
|
@ -150,12 +150,7 @@ considered equivalent to rebasing.
|
|||
|
||||
### Rebase without CI/CD pipeline
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/118825) in GitLab 14.7 [with a flag](../../../../administration/feature_flags.md) named `rebase_without_ci_ui`. Disabled by default.
|
||||
|
||||
FLAG:
|
||||
On GitLab.com and 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 `rebase_without_ci_ui`.
|
||||
The feature is not ready for production use.
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/118825) in GitLab 14.7.
|
||||
|
||||
To rebase a merge request's branch without triggering a CI/CD pipeline, select
|
||||
**Rebase without pipeline** from the merge request reports section.
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
# This template is provided and maintained by Fortify, an official Technology Partner with GitLab.
|
||||
# You can copy and paste this template into a new `.gitlab-ci.yml` file.
|
||||
# You should not add this template to an existing `.gitlab-ci.yml` file by using the `include:` keyword.
|
||||
|
||||
################################################################################################################################################
|
||||
# Fortify lets you build secure software fast with an appsec platform that automates testing throughout the DevSecOps pipeline. Fortify static,#
|
||||
# dynamic, interactive, and runtime security testing is available on premises or as a service. To learn more about Fortify, start a free trial #
|
||||
# or contact our sales team, visit microfocus.com/appsecurity. #
|
||||
# #
|
||||
# Use this pipeline template as a basis for integrating Fortify on Demand Static Application Security Testing(SAST) into your GitLab pipelines.#
|
||||
# This template demonstrates the steps to prepare the code+dependencies and initiate a scan. As an option, it also supports waiting for the #
|
||||
# SAST scan to complete and optinally failing the job. Software Composition Analysis can be also be performed in conjunection with the SAST #
|
||||
# scan if that service has been purchased. Users should review inputs and environment variables below to configure scanning for an existing #
|
||||
# application in your Fortify on Demand tenant. Additional information is available in the comments throughout the template and the Fortify on #
|
||||
# Demand, FoD Uploader and ScanCentral Client product documentation. If you need additional assistance with configuration, feel free to create #
|
||||
# a help ticket in the Fortify on Demand portal. #
|
||||
################################################################################################################################################
|
||||
|
||||
fortify_fod_sast:
|
||||
image: fortifydocker/fortify-ci-tools:3-jdk-8
|
||||
variables:
|
||||
# Update/override PACKAGE_OPTS based on the ScanCentral Client documentation for your project's included tech stack(s). Helpful hints:
|
||||
# ScanCentral Client will download dependencies for maven (-bt mvn) and gradle (-bt gradle).
|
||||
# The current fortify-ci-tools image is Linux only at this time. Msbuild integration is not currently supported.
|
||||
# ScanCentral has additional options that should be set for PHP and Python projects.
|
||||
# For other build tools (-bt none), add your build commands to download necessary dependencies and prepare according to Fortify on Demand Packaging documentation.
|
||||
# ScanCentral Client documentation is located at https://www.microfocus.com/documentation/fortify-software-security-center/
|
||||
PACKAGE_OPTS: "-bt mvn"
|
||||
|
||||
# Update/override the FoDUploader environment variables as needed. For more information on FoDUploader commands, see https://github.com/fod-dev/fod-uploader-java. Helpful hints:
|
||||
# Credentials (FOD_USERNAME, FOD_PAT, FOD_TENANT) are expected as GitLab CICD Variables in the template (masking recommended).
|
||||
# Static scan settings should be configured in Fortify on Demand portal (Automated Audit preference strongly recommended).
|
||||
# FOD_RELEASE_ID is expected as a GitLab CICD Variable.
|
||||
# FOD_UPLOADER_OPTS can be adjusted to wait for scan completion/pull results (-I 1) and control whether to fail the job (-apf).
|
||||
FOD_URL: "https://ams.fortify.com"
|
||||
FOD_API_URL: "https://api.ams.fortify.com/"
|
||||
FOD_UPLOADER_OPTS: "-ep 2 -pp 0"
|
||||
FOD_NOTES: "Triggered by Gitlab Pipeline IID $CI_PIPELINE_IID: $CI_PIPELINE_URL"
|
||||
|
||||
script:
|
||||
# Package source code and dependencies using Fortify ScanCentral client
|
||||
- 'scancentral package $PACKAGE_OPTS -o package.zip'
|
||||
# Start Fortify on Demand SAST scan
|
||||
- 'FoDUpload -z package.zip -aurl $FOD_API_URL -purl $FOD_URL -rid "$FOD_RELEASE" -tc "$FOD_TENANT" -uc "$FOD_USERNAME" "$FOD_PAT" $FOD_UPLOADER_OPTS -I 1 -n "$FOD_NOTES"'
|
||||
# Generate GitLab reports
|
||||
- 'FortifyVulnerabilityExporter FoDToGitLabSAST --fod.baseUrl=$FOD_URL --fod.tenant="$FOD_TENANT" --fod.userName="$FOD_USERNAME" --fod.password="$FOD_PAT" --fod.release.id=$FOD_RELEASE'
|
||||
# Change to false to fail the entire pipeline if the scan fails and/or the result of a scan causes security policy failure (see "-apf" option in FoDUploader documentation)
|
||||
allow_failure: true
|
||||
# Report SAST vulnerabilities back to GitLab
|
||||
artifacts:
|
||||
reports:
|
||||
sast: gl-fortify-sast.json
|
|
@ -10,6 +10,7 @@ module Gitlab
|
|||
# Disable Rugged auto-detect(can_use_disk?) when Puma threads>1
|
||||
# https://gitlab.com/gitlab-org/gitlab/issues/119326
|
||||
return false if running_puma_with_multiple_threads?
|
||||
return false if Feature.enabled?(:skip_rugged_auto_detect, type: :ops)
|
||||
|
||||
Gitlab::GitalyClient.can_use_disk?(repo.storage)
|
||||
end
|
||||
|
|
|
@ -126,15 +126,18 @@ module Gitlab
|
|||
end
|
||||
end
|
||||
|
||||
# When an assignee did not exist in the members mapper, the importer is
|
||||
# When an assignee (or any other listed association) did not exist in the members mapper, the importer is
|
||||
# assigned. We only need to assign each user once.
|
||||
def remove_duplicate_assignees
|
||||
if @relation_hash['issue_assignees']
|
||||
@relation_hash['issue_assignees'].uniq!(&:user_id)
|
||||
end
|
||||
associations = %w[issue_assignees merge_request_assignees merge_request_reviewers]
|
||||
|
||||
if @relation_hash['merge_request_assignees']
|
||||
@relation_hash['merge_request_assignees'].uniq!(&:user_id)
|
||||
associations.each do |association|
|
||||
next unless @relation_hash.key?(association)
|
||||
next unless @relation_hash[association].is_a?(Array)
|
||||
next if @relation_hash[association].empty?
|
||||
|
||||
@relation_hash[association].select! { |record| record.respond_to?(:user_id) }
|
||||
@relation_hash[association].uniq!(&:user_id)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -56,6 +56,7 @@ tree:
|
|||
- :metrics
|
||||
- :award_emoji
|
||||
- :merge_request_assignees
|
||||
- :merge_request_reviewers
|
||||
- notes:
|
||||
- :author
|
||||
- :award_emoji
|
||||
|
@ -594,6 +595,10 @@ included_attributes:
|
|||
- :user_id
|
||||
- :created_at
|
||||
- :state
|
||||
merge_request_reviewers:
|
||||
- :user_id
|
||||
- :created_at
|
||||
- :state
|
||||
sentry_issue:
|
||||
- :sentry_issue_identifier
|
||||
zoom_meetings:
|
||||
|
|
|
@ -147,6 +147,10 @@
|
|||
category: ci_templates
|
||||
redis_slot: ci_templates
|
||||
aggregation: weekly
|
||||
- name: p_ci_templates_security_fortify_fod_sast
|
||||
category: ci_templates
|
||||
redis_slot: ci_templates
|
||||
aggregation: weekly
|
||||
- name: p_ci_templates_security_sast_iac_latest
|
||||
category: ci_templates
|
||||
redis_slot: ci_templates
|
||||
|
@ -639,6 +643,10 @@
|
|||
category: ci_templates
|
||||
redis_slot: ci_templates
|
||||
aggregation: weekly
|
||||
- name: p_ci_templates_implicit_security_fortify_fod_sast
|
||||
category: ci_templates
|
||||
redis_slot: ci_templates
|
||||
aggregation: weekly
|
||||
- name: p_ci_templates_implicit_security_sast_iac_latest
|
||||
category: ci_templates
|
||||
redis_slot: ci_templates
|
||||
|
|
|
@ -3977,6 +3977,9 @@ msgstr ""
|
|||
msgid "An email notification was recently sent from the admin panel. Please wait %{wait_time_in_words} before attempting to send another message."
|
||||
msgstr ""
|
||||
|
||||
msgid "An email will be sent with the report attached after it is generated."
|
||||
msgstr ""
|
||||
|
||||
msgid "An empty GitLab User field will add the FogBugz user's full name (e.g. \"By John Smith\") in the description of all issues and comments. It will also associate and/or assign these issues and comments with the project creator."
|
||||
msgstr ""
|
||||
|
||||
|
@ -7111,21 +7114,12 @@ msgstr ""
|
|||
msgid "CICDAnalytics|Releases"
|
||||
msgstr ""
|
||||
|
||||
msgid "CICDAnalytics|Shared Runners Usage"
|
||||
msgstr ""
|
||||
|
||||
msgid "CICDAnalytics|Shared runner duration is the total runtime of all jobs that ran on shared runners"
|
||||
msgstr ""
|
||||
|
||||
msgid "CICDAnalytics|Shared runner pipeline minute duration by month"
|
||||
msgstr ""
|
||||
|
||||
msgid "CICDAnalytics|Shared runner usage"
|
||||
msgstr ""
|
||||
|
||||
msgid "CICDAnalytics|Shared runner usage is the total runtime of all jobs that ran on shared runners"
|
||||
msgstr ""
|
||||
|
||||
msgid "CICDAnalytics|Something went wrong while fetching release statistics"
|
||||
msgstr ""
|
||||
|
||||
|
@ -7135,9 +7129,6 @@ msgstr ""
|
|||
msgid "CICDAnalytics|What is shared runner duration?"
|
||||
msgstr ""
|
||||
|
||||
msgid "CICDAnalytics|What is shared runner usage?"
|
||||
msgstr ""
|
||||
|
||||
msgid "CICD|Add a %{base_domain_link_start}base domain%{link_end} to your %{kubernetes_cluster_link_start}Kubernetes cluster%{link_end} for your deployment strategy to work."
|
||||
msgstr ""
|
||||
|
||||
|
@ -21574,6 +21565,12 @@ msgstr ""
|
|||
msgid "InviteMembersModal|Select members or type email addresses"
|
||||
msgstr ""
|
||||
|
||||
msgid "InviteMembersModal|Show less"
|
||||
msgstr ""
|
||||
|
||||
msgid "InviteMembersModal|Show more (%{count})"
|
||||
msgstr ""
|
||||
|
||||
msgid "InviteMembersModal|Something went wrong"
|
||||
msgstr ""
|
||||
|
||||
|
@ -45242,6 +45239,12 @@ msgstr ""
|
|||
msgid "Your CSV import for project"
|
||||
msgstr ""
|
||||
|
||||
msgid "Your Chain of Custody CSV export for the group %{group_link} has been added to this email as an attachment."
|
||||
msgstr ""
|
||||
|
||||
msgid "Your Chain of Custody CSV export for the group %{group_name} has been added to this email as an attachment."
|
||||
msgstr ""
|
||||
|
||||
msgid "Your DevOps Reports give an overview of how you are using GitLab from a feature perspective. Use them to view how you compare with other organizations, and how your teams compare against each other."
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -98,7 +98,7 @@ module QA
|
|||
#
|
||||
# @return [void]
|
||||
def setup_logger!
|
||||
Knapsack.logger = QA::Runtime::Logger.logger
|
||||
Knapsack.logger = logger
|
||||
end
|
||||
|
||||
# Set knapsack environment variables
|
||||
|
@ -112,9 +112,9 @@ module QA
|
|||
|
||||
# Logger instance
|
||||
#
|
||||
# @return [Logger]
|
||||
# @return [ActiveSupport::Logger]
|
||||
def logger
|
||||
@logger ||= Knapsack.logger
|
||||
QA::Runtime::Logger.logger
|
||||
end
|
||||
|
||||
# GCS client
|
||||
|
|
|
@ -5,7 +5,7 @@ module QA
|
|||
# Helper utility to fetch parallel job names in a given pipelines stage
|
||||
#
|
||||
class ParallelPipelineJobs
|
||||
include Support::API
|
||||
include API
|
||||
|
||||
PARALLEL_JOB_NAME_PATTERN = %r{^\S+ \d+/\d+$}.freeze
|
||||
|
||||
|
@ -60,6 +60,8 @@ module QA
|
|||
# @return [Hash, Array]
|
||||
def api_get(path)
|
||||
response = get("#{api_url}/#{path}", { headers: { "PRIVATE-TOKEN" => access_token } })
|
||||
raise "Failed to fetch pipeline jobs: '#{response.body}'" unless response.code == API::HTTP_STATUS_OK
|
||||
|
||||
parse_body(response)
|
||||
end
|
||||
|
||||
|
|
|
@ -3171,6 +3171,28 @@
|
|||
"created_at": "2020-01-10T11:21:21.235Z",
|
||||
"state": "unreviewed"
|
||||
}
|
||||
],
|
||||
"merge_request_reviewers": [
|
||||
{
|
||||
"user_id": 1,
|
||||
"created_at": "2020-01-07T11:21:21.235Z",
|
||||
"state": "unreviewed"
|
||||
},
|
||||
{
|
||||
"user_id": 15,
|
||||
"created_at": "2020-01-08T11:21:21.235Z",
|
||||
"state": "reviewed"
|
||||
},
|
||||
{
|
||||
"user_id": 16,
|
||||
"created_at": "2020-01-09T11:21:21.235Z",
|
||||
"state": "attention_requested"
|
||||
},
|
||||
{
|
||||
"user_id": 6,
|
||||
"created_at": "2020-01-10T11:21:21.235Z",
|
||||
"state": "unreviewed"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
|
@ -3439,7 +3461,8 @@
|
|||
"author_id": 1
|
||||
}
|
||||
],
|
||||
"merge_request_assignees": []
|
||||
"merge_request_assignees": [],
|
||||
"merge_request_reviewers": []
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -2,7 +2,7 @@ import Ajv from 'ajv';
|
|||
import AjvFormats from 'ajv-formats';
|
||||
import CiSchema from '~/editor/schema/ci.json';
|
||||
|
||||
// JSON POSITIVE TESTS
|
||||
// JSON POSITIVE TESTS (LEGACY)
|
||||
import AllowFailureJson from './json_tests/positive_tests/allow_failure.json';
|
||||
import EnvironmentJson from './json_tests/positive_tests/environment.json';
|
||||
import GitlabCiDependenciesJson from './json_tests/positive_tests/gitlab-ci-dependencies.json';
|
||||
|
@ -14,7 +14,7 @@ import TerraformReportJson from './json_tests/positive_tests/terraform_report.js
|
|||
import VariablesMixStringAndUserInputJson from './json_tests/positive_tests/variables_mix_string_and_user_input.json';
|
||||
import VariablesJson from './json_tests/positive_tests/variables.json';
|
||||
|
||||
// JSON NEGATIVE TESTS
|
||||
// JSON NEGATIVE TESTS (LEGACY)
|
||||
import DefaultNoAdditionalPropertiesJson from './json_tests/negative_tests/default_no_additional_properties.json';
|
||||
import InheritDefaultNoAdditionalPropertiesJson from './json_tests/negative_tests/inherit_default_no_additional_properties.json';
|
||||
import JobVariablesMustNotContainObjectsJson from './json_tests/negative_tests/job_variables_must_not_contain_objects.json';
|
||||
|
@ -34,6 +34,7 @@ import RulesYaml from './yaml_tests/positive_tests/rules.yml';
|
|||
import ArtifactsNegativeYaml from './yaml_tests/negative_tests/artifacts.yml';
|
||||
import CacheNegativeYaml from './yaml_tests/negative_tests/cache.yml';
|
||||
import IncludeNegativeYaml from './yaml_tests/negative_tests/include.yml';
|
||||
import RulesNegativeYaml from './yaml_tests/negative_tests/rules.yml';
|
||||
|
||||
const ajv = new Ajv({
|
||||
strictTypes: false,
|
||||
|
@ -61,11 +62,11 @@ describe('positive tests', () => {
|
|||
VariablesJson,
|
||||
|
||||
// YAML
|
||||
ArtifactsYaml,
|
||||
CacheYaml,
|
||||
FilterYaml,
|
||||
IncludeYaml,
|
||||
RulesYaml,
|
||||
ArtifactsYaml,
|
||||
}),
|
||||
)('schema validates %s', (_, input) => {
|
||||
expect(input).toValidateJsonSchema(schema);
|
||||
|
@ -85,9 +86,10 @@ describe('negative tests', () => {
|
|||
RetryUnknownWhenJson,
|
||||
|
||||
// YAML
|
||||
ArtifactsNegativeYaml,
|
||||
CacheNegativeYaml,
|
||||
IncludeNegativeYaml,
|
||||
ArtifactsNegativeYaml,
|
||||
RulesNegativeYaml,
|
||||
}),
|
||||
)('schema validates %s', (_, input) => {
|
||||
expect(input).not.toValidateJsonSchema(schema);
|
||||
|
|
|
@ -1,15 +1,13 @@
|
|||
# Covers https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70779
|
||||
stages:
|
||||
- prepare
|
||||
|
||||
# invalid cache:when value
|
||||
job1:
|
||||
# invalid cache:when values
|
||||
when no integer:
|
||||
stage: prepare
|
||||
cache:
|
||||
when: 0
|
||||
|
||||
# invalid cache:when value
|
||||
job2:
|
||||
when must be a reserved word:
|
||||
stage: prepare
|
||||
cache:
|
||||
when: 'never'
|
||||
|
|
|
@ -1,16 +1,14 @@
|
|||
# Covers https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70779
|
||||
stages:
|
||||
- prepare
|
||||
|
||||
# missing file property
|
||||
childPipeline:
|
||||
# invalid trigger:include
|
||||
trigger missing file property:
|
||||
stage: prepare
|
||||
trigger:
|
||||
include:
|
||||
- project: 'my-group/my-pipeline-library'
|
||||
|
||||
# missing project property
|
||||
childPipeline2:
|
||||
trigger missing project property:
|
||||
stage: prepare
|
||||
trigger:
|
||||
include:
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
# unnecessary ref declaration
|
||||
rules:
|
||||
- changes:
|
||||
paths:
|
||||
- README.md
|
||||
compare_to: { ref: 'main' }
|
||||
# invalid rules:changes
|
||||
unnecessary ref declaration:
|
||||
script: exit 0
|
||||
rules:
|
||||
- changes:
|
||||
paths:
|
||||
- README.md
|
||||
compare_to: { ref: 'main' }
|
||||
|
||||
# wrong path declaration
|
||||
rules:
|
||||
- changes:
|
||||
paths: { file: 'DOCKER' }
|
||||
wrong path declaration:
|
||||
script: exit 0
|
||||
rules:
|
||||
- changes:
|
||||
paths: { file: 'DOCKER' }
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
# Covers https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70779
|
||||
stages:
|
||||
- prepare
|
||||
|
||||
# test for cache:when values
|
||||
# valid cache:when values
|
||||
job1:
|
||||
stage: prepare
|
||||
script:
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# Covers https://gitlab.com/gitlab-org/gitlab/-/merge_requests/79335
|
||||
deploy-template:
|
||||
# valid only/except values
|
||||
only and except as array of strings:
|
||||
script:
|
||||
- echo "hello world"
|
||||
only:
|
||||
|
@ -7,12 +7,10 @@ deploy-template:
|
|||
except:
|
||||
- bar
|
||||
|
||||
# null value allowed
|
||||
deploy-without-only:
|
||||
only as null value:
|
||||
extends: deploy-template
|
||||
only:
|
||||
|
||||
# null value allowed
|
||||
deploy-without-except:
|
||||
except as null value:
|
||||
extends: deploy-template
|
||||
except:
|
||||
|
|
|
@ -1,17 +1,15 @@
|
|||
# Covers https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70779
|
||||
stages:
|
||||
- prepare
|
||||
|
||||
# test for include:rules
|
||||
# valid include:rules
|
||||
include:
|
||||
- local: builds.yml
|
||||
rules:
|
||||
- if: '$INCLUDE_BUILDS == "true"'
|
||||
when: always
|
||||
|
||||
stages:
|
||||
- prepare
|
||||
|
||||
# test for trigger:include
|
||||
childPipeline:
|
||||
# valid trigger:include
|
||||
trigger:include accepts project and file properties:
|
||||
stage: prepare
|
||||
script:
|
||||
- echo 'creating pipeline...'
|
||||
|
@ -20,8 +18,7 @@ childPipeline:
|
|||
- project: 'my-group/my-pipeline-library'
|
||||
file: '.gitlab-ci.yml'
|
||||
|
||||
# accepts optional ref property
|
||||
childPipeline2:
|
||||
trigger:include accepts optional ref property:
|
||||
stage: prepare
|
||||
script:
|
||||
- echo 'creating pipeline...'
|
||||
|
|
|
@ -1,9 +1,5 @@
|
|||
# tests for:
|
||||
# workflow:rules:changes
|
||||
# workflow:rules:exists
|
||||
# rules:changes:path
|
||||
|
||||
job_name1:
|
||||
# valid workflow:rules:changes
|
||||
rules:changes with paths and compare_to properties:
|
||||
script: exit 0
|
||||
rules:
|
||||
- changes:
|
||||
|
@ -11,12 +7,14 @@ job_name1:
|
|||
- README.md
|
||||
compare_to: main
|
||||
|
||||
job_name2:
|
||||
rules:changes as array of strings:
|
||||
script: exit 0
|
||||
rules:
|
||||
- changes:
|
||||
- README.md
|
||||
|
||||
# valid workflow:rules:exists
|
||||
# valid rules:changes:path
|
||||
workflow:
|
||||
rules:
|
||||
- changes:
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { GlLink, GlModal, GlSprintf, GlFormGroup } from '@gitlab/ui';
|
||||
import { GlLink, GlModal, GlSprintf, GlFormGroup, GlCollapse, GlIcon } from '@gitlab/ui';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { nextTick } from 'vue';
|
||||
import { stubComponent } from 'helpers/stub_component';
|
||||
|
@ -18,6 +18,7 @@ import {
|
|||
MEMBERS_PLACEHOLDER_DISABLED,
|
||||
MEMBERS_TO_PROJECT_CELEBRATE_INTRO_TEXT,
|
||||
LEARN_GITLAB,
|
||||
EXPANDED_ERRORS,
|
||||
} from '~/invite_members/constants';
|
||||
import eventHub from '~/invite_members/event_hub';
|
||||
import ContentTransition from '~/vue_shared/components/content_transition.vue';
|
||||
|
@ -36,6 +37,7 @@ import {
|
|||
user3,
|
||||
user4,
|
||||
user5,
|
||||
user6,
|
||||
GlEmoji,
|
||||
} from '../mock_data/member_modal';
|
||||
|
||||
|
@ -95,9 +97,12 @@ describe('InviteMembersModal', () => {
|
|||
const findBase = () => wrapper.findComponent(InviteModalBase);
|
||||
const findIntroText = () => wrapper.findByTestId('modal-base-intro-text').text();
|
||||
const findMemberErrorAlert = () => wrapper.findByTestId('alert-member-error');
|
||||
const findMoreInviteErrorsButton = () => wrapper.findByTestId('accordion-button');
|
||||
const findAccordion = () => wrapper.findComponent(GlCollapse);
|
||||
const findErrorsIcon = () => wrapper.findComponent(GlIcon);
|
||||
const findMemberErrorMessage = (element) =>
|
||||
`${Object.keys(invitationsApiResponse.MULTIPLE_RESTRICTED.message)[element]}: ${
|
||||
Object.values(invitationsApiResponse.MULTIPLE_RESTRICTED.message)[element]
|
||||
`${Object.keys(invitationsApiResponse.EXPANDED_RESTRICTED.message)[element]}: ${
|
||||
Object.values(invitationsApiResponse.EXPANDED_RESTRICTED.message)[element]
|
||||
}`;
|
||||
const emitEventFromModal = (eventName) => () =>
|
||||
findModal().vm.$emit(eventName, { preventDefault: jest.fn() });
|
||||
|
@ -666,8 +671,8 @@ describe('InviteMembersModal', () => {
|
|||
it('displays errors for multiple and allows clearing', async () => {
|
||||
createInviteMembersToGroupWrapper();
|
||||
|
||||
await triggerMembersTokenSelect([user3, user4, user5]);
|
||||
mockInvitationsApi(httpStatus.CREATED, invitationsApiResponse.MULTIPLE_RESTRICTED);
|
||||
await triggerMembersTokenSelect([user3, user4, user5, user6]);
|
||||
mockInvitationsApi(httpStatus.CREATED, invitationsApiResponse.EXPANDED_RESTRICTED);
|
||||
|
||||
clickInviteButton();
|
||||
|
||||
|
@ -675,18 +680,43 @@ describe('InviteMembersModal', () => {
|
|||
|
||||
expect(findMemberErrorAlert().exists()).toBe(true);
|
||||
expect(findMemberErrorAlert().props('title')).toContain(
|
||||
"The following 3 members couldn't be invited",
|
||||
"The following 4 members couldn't be invited",
|
||||
);
|
||||
expect(findMemberErrorAlert().text()).toContain(findMemberErrorMessage(0));
|
||||
expect(findMemberErrorAlert().text()).toContain(findMemberErrorMessage(1));
|
||||
expect(findMemberErrorAlert().text()).toContain(findMemberErrorMessage(2));
|
||||
expect(findMemberErrorAlert().text()).toContain(findMemberErrorMessage(3));
|
||||
expect(findAccordion().exists()).toBe(true);
|
||||
expect(findMoreInviteErrorsButton().text()).toContain('Show more (2)');
|
||||
expect(findErrorsIcon().attributes('class')).not.toContain('gl-rotate-180');
|
||||
expect(findAccordion().attributes('visible')).toBeUndefined();
|
||||
|
||||
await findMoreInviteErrorsButton().vm.$emit('click');
|
||||
|
||||
expect(findMoreInviteErrorsButton().text()).toContain(EXPANDED_ERRORS);
|
||||
expect(findErrorsIcon().attributes('class')).toContain('gl-rotate-180');
|
||||
expect(findAccordion().attributes('visible')).toBeDefined();
|
||||
|
||||
await findMoreInviteErrorsButton().vm.$emit('click');
|
||||
|
||||
expect(findMoreInviteErrorsButton().text()).toContain('Show more (2)');
|
||||
expect(findAccordion().attributes('visible')).toBeUndefined();
|
||||
|
||||
await removeMembersToken(user3);
|
||||
|
||||
expect(findMoreInviteErrorsButton().text()).toContain('Show more (1)');
|
||||
expect(findMemberErrorAlert().props('title')).toContain(
|
||||
"The following 3 members couldn't be invited",
|
||||
);
|
||||
expect(findMemberErrorAlert().text()).not.toContain(findMemberErrorMessage(0));
|
||||
|
||||
await removeMembersToken(user6);
|
||||
|
||||
expect(findMoreInviteErrorsButton().exists()).toBe(false);
|
||||
expect(findMemberErrorAlert().props('title')).toContain(
|
||||
"The following 2 members couldn't be invited",
|
||||
);
|
||||
expect(findMemberErrorAlert().text()).not.toContain(findMemberErrorMessage(0));
|
||||
expect(findMemberErrorAlert().text()).not.toContain(findMemberErrorMessage(2));
|
||||
|
||||
await removeMembersToken(user4);
|
||||
|
||||
|
|
|
@ -26,6 +26,20 @@ const MULTIPLE_RESTRICTED = {
|
|||
status: 'error',
|
||||
};
|
||||
|
||||
const EXPANDED_RESTRICTED = {
|
||||
message: {
|
||||
'email@example.com':
|
||||
"The member's email address is not allowed for this project. Go to the Admin area > Sign-up restrictions, and check Allowed domains for sign-ups.",
|
||||
'email4@example.com':
|
||||
"The member's email address is not allowed for this project. Go to the Admin area > Sign-up restrictions, and check the Domain denylist.",
|
||||
'email5@example.com':
|
||||
"The member's email address is not allowed for this project. Go to the Admin area > Sign-up restrictions, and check the Domain denylist.",
|
||||
root:
|
||||
"The member's email address is not allowed for this project. Go to the Admin area > Sign-up restrictions, and check Allowed domains for sign-ups.",
|
||||
},
|
||||
status: 'error',
|
||||
};
|
||||
|
||||
const EMAIL_TAKEN = {
|
||||
message: {
|
||||
'email@example.org': "The member's email address has already been taken",
|
||||
|
@ -41,4 +55,5 @@ export const invitationsApiResponse = {
|
|||
EMAIL_RESTRICTED,
|
||||
MULTIPLE_RESTRICTED,
|
||||
EMAIL_TAKEN,
|
||||
EXPANDED_RESTRICTED,
|
||||
};
|
||||
|
|
|
@ -39,5 +39,10 @@ export const user5 = {
|
|||
name: 'root',
|
||||
avatar_url: '',
|
||||
};
|
||||
export const user6 = {
|
||||
id: 'user-defined-token3',
|
||||
name: 'email5@example.com',
|
||||
avatar_url: '',
|
||||
};
|
||||
|
||||
export const GlEmoji = { template: '<img/>' };
|
||||
|
|
|
@ -17,8 +17,8 @@ describe('AccessRequestActionButtons', () => {
|
|||
});
|
||||
};
|
||||
|
||||
const findRemoveMemberButton = () => wrapper.find(RemoveMemberButton);
|
||||
const findApproveButton = () => wrapper.find(ApproveAccessRequestButton);
|
||||
const findRemoveMemberButton = () => wrapper.findComponent(RemoveMemberButton);
|
||||
const findApproveButton = () => wrapper.findComponent(ApproveAccessRequestButton);
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
|
|
|
@ -43,8 +43,8 @@ describe('ApproveAccessRequestButton', () => {
|
|||
});
|
||||
};
|
||||
|
||||
const findForm = () => wrapper.find(GlForm);
|
||||
const findButton = () => findForm().find(GlButton);
|
||||
const findForm = () => wrapper.findComponent(GlForm);
|
||||
const findButton = () => findForm().findComponent(GlButton);
|
||||
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
|
|
|
@ -16,8 +16,8 @@ describe('InviteActionButtons', () => {
|
|||
});
|
||||
};
|
||||
|
||||
const findRemoveMemberButton = () => wrapper.find(RemoveMemberButton);
|
||||
const findResendInviteButton = () => wrapper.find(ResendInviteButton);
|
||||
const findRemoveMemberButton = () => wrapper.findComponent(RemoveMemberButton);
|
||||
const findResendInviteButton = () => wrapper.findComponent(ResendInviteButton);
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
|
|
|
@ -22,7 +22,7 @@ describe('LeaveButton', () => {
|
|||
});
|
||||
};
|
||||
|
||||
const findButton = () => wrapper.find(GlButton);
|
||||
const findButton = () => wrapper.findComponent(GlButton);
|
||||
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
|
@ -44,7 +44,7 @@ describe('LeaveButton', () => {
|
|||
});
|
||||
|
||||
it('renders leave modal', () => {
|
||||
const leaveModal = wrapper.find(LeaveModal);
|
||||
const leaveModal = wrapper.findComponent(LeaveModal);
|
||||
|
||||
expect(leaveModal.exists()).toBe(true);
|
||||
expect(leaveModal.props('member')).toEqual(member);
|
||||
|
|
|
@ -42,7 +42,7 @@ describe('RemoveGroupLinkButton', () => {
|
|||
});
|
||||
};
|
||||
|
||||
const findButton = () => wrapper.find(GlButton);
|
||||
const findButton = () => wrapper.findComponent(GlButton);
|
||||
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
|
|
|
@ -44,7 +44,7 @@ describe('ResendInviteButton', () => {
|
|||
};
|
||||
|
||||
const findForm = () => wrapper.find('form');
|
||||
const findButton = () => findForm().find(GlButton);
|
||||
const findButton = () => findForm().findComponent(GlButton);
|
||||
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
|
|
|
@ -19,7 +19,7 @@ describe('UserActionButtons', () => {
|
|||
});
|
||||
};
|
||||
|
||||
const findRemoveMemberButton = () => wrapper.find(RemoveMemberButton);
|
||||
const findRemoveMemberButton = () => wrapper.findComponent(RemoveMemberButton);
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
|
@ -80,7 +80,7 @@ describe('UserActionButtons', () => {
|
|||
},
|
||||
});
|
||||
|
||||
expect(wrapper.find(LeaveButton).exists()).toBe(true);
|
||||
expect(wrapper.findComponent(LeaveButton).exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -41,8 +41,8 @@ describe('MembersApp', () => {
|
|||
});
|
||||
};
|
||||
|
||||
const findAlert = () => wrapper.find(GlAlert);
|
||||
const findFilterSortContainer = () => wrapper.find(FilterSortContainer);
|
||||
const findAlert = () => wrapper.findComponent(GlAlert);
|
||||
const findFilterSortContainer = () => wrapper.findComponent(FilterSortContainer);
|
||||
|
||||
beforeEach(() => {
|
||||
commonUtils.scrollToElement = jest.fn();
|
||||
|
|
|
@ -30,7 +30,7 @@ describe('MemberList', () => {
|
|||
});
|
||||
|
||||
it('renders link to group', () => {
|
||||
const link = wrapper.find(GlAvatarLink);
|
||||
const link = wrapper.findComponent(GlAvatarLink);
|
||||
|
||||
expect(link.exists()).toBe(true);
|
||||
expect(link.attributes('href')).toBe(group.webUrl);
|
||||
|
|
|
@ -33,7 +33,7 @@ describe('UserAvatar', () => {
|
|||
it("renders link to user's profile", () => {
|
||||
createComponent();
|
||||
|
||||
const link = wrapper.find(GlAvatarLink);
|
||||
const link = wrapper.findComponent(GlAvatarLink);
|
||||
|
||||
expect(link.exists()).toBe(true);
|
||||
expect(link.attributes()).toMatchObject({
|
||||
|
@ -77,7 +77,7 @@ describe('UserAvatar', () => {
|
|||
`('renders the "$badgeText" badge', ({ member, badgeText }) => {
|
||||
createComponent({ member });
|
||||
|
||||
expect(wrapper.find(GlBadge).text()).toBe(badgeText);
|
||||
expect(wrapper.findComponent(GlBadge).text()).toBe(badgeText);
|
||||
});
|
||||
|
||||
it('renders the "It\'s you" badge when member is current user', () => {
|
||||
|
|
|
@ -60,7 +60,7 @@ describe('FilterSortContainer', () => {
|
|||
},
|
||||
});
|
||||
|
||||
expect(wrapper.find(MembersFilteredSearchBar).exists()).toBe(true);
|
||||
expect(wrapper.findComponent(MembersFilteredSearchBar).exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -70,7 +70,7 @@ describe('FilterSortContainer', () => {
|
|||
tableSortableFields: ['account'],
|
||||
});
|
||||
|
||||
expect(wrapper.find(SortDropdown).exists()).toBe(true);
|
||||
expect(wrapper.findComponent(SortDropdown).exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -56,7 +56,7 @@ describe('MembersFilteredSearchBar', () => {
|
|||
});
|
||||
};
|
||||
|
||||
const findFilteredSearchBar = () => wrapper.find(FilteredSearchBar);
|
||||
const findFilteredSearchBar = () => wrapper.findComponent(FilteredSearchBar);
|
||||
|
||||
it('passes correct props to `FilteredSearchBar` component', () => {
|
||||
createComponent();
|
||||
|
|
|
@ -43,13 +43,13 @@ describe('SortDropdown', () => {
|
|||
});
|
||||
};
|
||||
|
||||
const findSortingComponent = () => wrapper.find(GlSorting);
|
||||
const findSortingComponent = () => wrapper.findComponent(GlSorting);
|
||||
const findSortDirectionToggle = () =>
|
||||
findSortingComponent().find('button[title="Sort direction"]');
|
||||
const findDropdownToggle = () => wrapper.find('button[aria-haspopup="true"]');
|
||||
const findDropdownItemByText = (text) =>
|
||||
wrapper
|
||||
.findAll(GlSortingItem)
|
||||
.findAllComponents(GlSortingItem)
|
||||
.wrappers.find((dropdownItemWrapper) => dropdownItemWrapper.text() === text);
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
|
@ -47,8 +47,8 @@ describe('RemoveGroupLinkModal', () => {
|
|||
});
|
||||
};
|
||||
|
||||
const findModal = () => wrapper.find(GlModal);
|
||||
const findForm = () => findModal().find(GlForm);
|
||||
const findModal = () => wrapper.findComponent(GlModal);
|
||||
const findForm = () => findModal().findComponent(GlForm);
|
||||
const getByText = (text, options) =>
|
||||
createWrapper(within(findModal().element).getByText(text, options));
|
||||
|
||||
|
|
|
@ -46,7 +46,7 @@ describe('RemoveMemberModal', () => {
|
|||
});
|
||||
};
|
||||
|
||||
const findForm = () => wrapper.find({ ref: 'form' });
|
||||
const findForm = () => wrapper.findComponent({ ref: 'form' });
|
||||
const findGlModal = () => wrapper.findComponent(GlModal);
|
||||
const findUserDeletionObstaclesList = () => wrapper.findComponent(UserDeletionObstaclesList);
|
||||
|
||||
|
|
|
@ -39,7 +39,7 @@ describe('CreatedAt', () => {
|
|||
});
|
||||
|
||||
it('uses `TimeAgoTooltip` component to display tooltip', () => {
|
||||
expect(wrapper.find(TimeAgoTooltip).exists()).toBe(true);
|
||||
expect(wrapper.findComponent(TimeAgoTooltip).exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -56,7 +56,7 @@ describe('ExpirationDatepicker', () => {
|
|||
};
|
||||
|
||||
const findInput = () => wrapper.find('input');
|
||||
const findDatepicker = () => wrapper.find(GlDatepicker);
|
||||
const findDatepicker = () => wrapper.findComponent(GlDatepicker);
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
|
|
|
@ -38,7 +38,7 @@ describe('MemberActionButtons', () => {
|
|||
({ memberType, member, expectedComponent }) => {
|
||||
createComponent({ memberType, member });
|
||||
|
||||
expect(wrapper.find(expectedComponent).exists()).toBe(true);
|
||||
expect(wrapper.findComponent(expectedComponent).exists()).toBe(true);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
|
@ -33,7 +33,7 @@ describe('MemberList', () => {
|
|||
({ memberType, member, expectedComponent }) => {
|
||||
createComponent({ memberType, member });
|
||||
|
||||
expect(wrapper.find(expectedComponent).exists()).toBe(true);
|
||||
expect(wrapper.findComponent(expectedComponent).exists()).toBe(true);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
|
@ -69,7 +69,7 @@ describe('MembersTableCell', () => {
|
|||
});
|
||||
};
|
||||
|
||||
const findWrappedComponent = () => wrapper.find(WrappedComponent);
|
||||
const findWrappedComponent = () => wrapper.findComponent(WrappedComponent);
|
||||
|
||||
const memberCurrentUser = {
|
||||
...memberMock,
|
||||
|
|
|
@ -81,13 +81,13 @@ describe('MembersTable', () => {
|
|||
|
||||
const url = 'https://localhost/foo-bar/-/project_members?tab=invited';
|
||||
|
||||
const findTable = () => wrapper.find(GlTable);
|
||||
const findTable = () => wrapper.findComponent(GlTable);
|
||||
const findTableCellByMemberId = (tableCellLabel, memberId) =>
|
||||
wrapper
|
||||
.findByTestId(`members-table-row-${memberId}`)
|
||||
.find(`[data-label="${tableCellLabel}"][role="cell"]`);
|
||||
|
||||
const findPagination = () => extendedWrapper(wrapper.find(GlPagination));
|
||||
const findPagination = () => extendedWrapper(wrapper.findComponent(GlPagination));
|
||||
|
||||
const expectCorrectLinkToPage2 = () => {
|
||||
expect(findPagination().findByText('2', { selector: 'a' }).attributes('href')).toBe(
|
||||
|
@ -126,7 +126,10 @@ describe('MembersTable', () => {
|
|||
|
||||
if (expectedComponent) {
|
||||
expect(
|
||||
wrapper.find(`[data-label="${label}"][role="cell"]`).find(expectedComponent).exists(),
|
||||
wrapper
|
||||
.find(`[data-label="${label}"][role="cell"]`)
|
||||
.findComponent(expectedComponent)
|
||||
.exists(),
|
||||
).toBe(true);
|
||||
}
|
||||
});
|
||||
|
@ -179,7 +182,10 @@ describe('MembersTable', () => {
|
|||
expect(actionField.exists()).toBe(true);
|
||||
expect(actionField.classes('gl-sr-only')).toBe(true);
|
||||
expect(
|
||||
wrapper.find(`[data-label="Actions"][role="cell"]`).find(MemberActionButtons).exists(),
|
||||
wrapper
|
||||
.find(`[data-label="Actions"][role="cell"]`)
|
||||
.findComponent(MemberActionButtons)
|
||||
.exists(),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
|
@ -250,9 +256,9 @@ describe('MembersTable', () => {
|
|||
it('renders badge in "Max role" field', () => {
|
||||
createComponent({ members: [memberMock], tableFields: ['maxRole'] });
|
||||
|
||||
expect(wrapper.find(`[data-label="Max role"][role="cell"]`).find(GlBadge).text()).toBe(
|
||||
memberMock.accessLevel.stringValue,
|
||||
);
|
||||
expect(
|
||||
wrapper.find(`[data-label="Max role"][role="cell"]`).findComponent(GlBadge).text(),
|
||||
).toBe(memberMock.accessLevel.stringValue);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -57,11 +57,11 @@ describe('RoleDropdown', () => {
|
|||
);
|
||||
const getCheckedDropdownItem = () =>
|
||||
wrapper
|
||||
.findAll(GlDropdownItem)
|
||||
.findAllComponents(GlDropdownItem)
|
||||
.wrappers.find((dropdownItemWrapper) => dropdownItemWrapper.props('isChecked'));
|
||||
|
||||
const findDropdownToggle = () => wrapper.find('button[aria-haspopup="true"]');
|
||||
const findDropdown = () => wrapper.find(GlDropdown);
|
||||
const findDropdown = () => wrapper.findComponent(GlDropdown);
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
|
|
|
@ -39,7 +39,7 @@ describe('initMembersApp', () => {
|
|||
it('renders `MembersTabs`', () => {
|
||||
setup();
|
||||
|
||||
expect(wrapper.find(MembersTabs).exists()).toBe(true);
|
||||
expect(wrapper.findComponent(MembersTabs).exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('parses and sets `members` in Vuex store', () => {
|
||||
|
|
|
@ -8,7 +8,7 @@ jest.mock('~/vue_shared/plugins/global_toast');
|
|||
|
||||
let wrapper;
|
||||
|
||||
function createWrapper(propsData, mergeRequestWidgetGraphql, rebaseWithoutCiUi) {
|
||||
function createWrapper(propsData, mergeRequestWidgetGraphql) {
|
||||
wrapper = mount(WidgetRebase, {
|
||||
propsData,
|
||||
data() {
|
||||
|
@ -22,7 +22,7 @@ function createWrapper(propsData, mergeRequestWidgetGraphql, rebaseWithoutCiUi)
|
|||
},
|
||||
};
|
||||
},
|
||||
provide: { glFeatures: { mergeRequestWidgetGraphql, rebaseWithoutCiUi } },
|
||||
provide: { glFeatures: { mergeRequestWidgetGraphql } },
|
||||
mocks: {
|
||||
$apollo: {
|
||||
queries: {
|
||||
|
@ -110,7 +110,7 @@ describe('Merge request widget rebase component', () => {
|
|||
expect(findRebaseMessageText()).toContain('Something went wrong!');
|
||||
});
|
||||
|
||||
describe('Rebase buttons with flag rebaseWithoutCiUi', () => {
|
||||
describe('Rebase buttons with', () => {
|
||||
beforeEach(() => {
|
||||
createWrapper(
|
||||
{
|
||||
|
@ -124,7 +124,6 @@ describe('Merge request widget rebase component', () => {
|
|||
},
|
||||
},
|
||||
mergeRequestWidgetGraphql,
|
||||
{ rebaseWithoutCiUi: true },
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -149,35 +148,6 @@ describe('Merge request widget rebase component', () => {
|
|||
expect(rebaseMock).toHaveBeenCalledWith({ skipCi: true });
|
||||
});
|
||||
});
|
||||
|
||||
describe('Rebase button with rebaseWithoutCiUI flag disabled', () => {
|
||||
beforeEach(() => {
|
||||
createWrapper(
|
||||
{
|
||||
mr: {
|
||||
rebaseInProgress: false,
|
||||
canPushToSourceBranch: true,
|
||||
},
|
||||
service: {
|
||||
rebase: rebaseMock,
|
||||
poll: pollMock,
|
||||
},
|
||||
},
|
||||
mergeRequestWidgetGraphql,
|
||||
);
|
||||
});
|
||||
|
||||
it('standard rebase button is rendered', () => {
|
||||
expect(findStandardRebaseButton().exists()).toBe(true);
|
||||
expect(findRebaseWithoutCiButton().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('calls rebase method with skip_ci false', () => {
|
||||
findStandardRebaseButton().vm.$emit('click');
|
||||
|
||||
expect(rebaseMock).toHaveBeenCalledWith({ skipCi: false });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('without permissions', () => {
|
||||
|
@ -216,7 +186,7 @@ describe('Merge request widget rebase component', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('does render the "Rebase without pipeline" button with rebaseWithoutCiUI flag enabled', () => {
|
||||
it('does render the "Rebase without pipeline" button', () => {
|
||||
createWrapper(
|
||||
{
|
||||
mr: {
|
||||
|
@ -227,7 +197,6 @@ describe('Merge request widget rebase component', () => {
|
|||
service: {},
|
||||
},
|
||||
mergeRequestWidgetGraphql,
|
||||
true,
|
||||
);
|
||||
|
||||
expect(findRebaseWithoutCiButton().exists()).toBe(true);
|
||||
|
|
|
@ -1,15 +1,20 @@
|
|||
import { nextTick } from 'vue';
|
||||
import * as Sentry from '@sentry/browser';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import StatusIcon from '~/vue_merge_request_widget/components/extensions/status_icon.vue';
|
||||
import Widget from '~/vue_merge_request_widget/components/widget/widget.vue';
|
||||
|
||||
describe('MR Widget', () => {
|
||||
let wrapper;
|
||||
|
||||
const findStatusIcon = () => wrapper.findComponent(StatusIcon);
|
||||
|
||||
const createComponent = ({ propsData, slots } = {}) => {
|
||||
wrapper = shallowMountExtended(Widget, {
|
||||
propsData: {
|
||||
loadingText: 'Loading widget',
|
||||
widgetName: 'MyWidget',
|
||||
value: {
|
||||
collapsed: null,
|
||||
expanded: null,
|
||||
|
@ -42,6 +47,33 @@ describe('MR Widget', () => {
|
|||
await waitForPromises();
|
||||
expect(wrapper.vm.error).toBe('Failed to load');
|
||||
});
|
||||
|
||||
it('displays loading icon until request is made and then displays status icon when the request is complete', async () => {
|
||||
const fetchCollapsedData = jest
|
||||
.fn()
|
||||
.mockReturnValue(Promise.resolve({ headers: {}, status: 200, data: {} }));
|
||||
|
||||
createComponent({ propsData: { fetchCollapsedData, statusIconName: 'warning' } });
|
||||
|
||||
// Let on mount be called
|
||||
await nextTick();
|
||||
|
||||
expect(findStatusIcon().props('isLoading')).toBe(true);
|
||||
|
||||
// Wait until `fetchCollapsedData` is resolved
|
||||
await waitForPromises();
|
||||
|
||||
expect(findStatusIcon().props('isLoading')).toBe(false);
|
||||
expect(findStatusIcon().props('iconName')).toBe('warning');
|
||||
});
|
||||
|
||||
it('displays the loading text', async () => {
|
||||
const fetchCollapsedData = jest.fn().mockReturnValue(() => Promise.reject());
|
||||
createComponent({ propsData: { fetchCollapsedData, statusIconName: 'warning' } });
|
||||
expect(wrapper.text()).not.toContain('Loading');
|
||||
await nextTick();
|
||||
expect(wrapper.text()).toContain('Loading');
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetch', () => {
|
||||
|
@ -65,8 +97,9 @@ describe('MR Widget', () => {
|
|||
],
|
||||
},
|
||||
});
|
||||
|
||||
await waitForPromises();
|
||||
await waitForPromises();
|
||||
|
||||
expect(wrapper.emitted('input')[0][0]).toEqual({
|
||||
collapsed: [mockData1.data, mockData2.data],
|
||||
expanded: null,
|
||||
|
|
|
@ -58,35 +58,55 @@ RSpec.describe Gitlab::Git::RuggedImpl::UseRugged, :seed_helper do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when not running puma with multiple threads' do
|
||||
context 'when skip_rugged_auto_detect feature flag is enabled' do
|
||||
context 'when not running puma with multiple threads' do
|
||||
before do
|
||||
allow(subject).to receive(:running_puma_with_multiple_threads?).and_return(false)
|
||||
stub_feature_flags(feature_flag_name => nil)
|
||||
stub_feature_flags(skip_rugged_auto_detect: true)
|
||||
end
|
||||
|
||||
it 'returns false' do
|
||||
expect(subject.use_rugged?(repository, feature_flag_name)).to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when skip_rugged_auto_detect feature flag is disabled' do
|
||||
before do
|
||||
allow(subject).to receive(:running_puma_with_multiple_threads?).and_return(false)
|
||||
stub_feature_flags(skip_rugged_auto_detect: false)
|
||||
end
|
||||
|
||||
it 'returns true when gitaly matches disk' do
|
||||
expect(subject.use_rugged?(repository, feature_flag_name)).to be true
|
||||
end
|
||||
context 'when not running puma with multiple threads' do
|
||||
before do
|
||||
allow(subject).to receive(:running_puma_with_multiple_threads?).and_return(false)
|
||||
end
|
||||
|
||||
it 'returns false when disk access fails' do
|
||||
allow(Gitlab::GitalyClient).to receive(:storage_metadata_file_path).and_return("/fake/path/doesnt/exist")
|
||||
it 'returns true when gitaly matches disk' do
|
||||
expect(subject.use_rugged?(repository, feature_flag_name)).to be true
|
||||
end
|
||||
|
||||
expect(subject.use_rugged?(repository, feature_flag_name)).to be false
|
||||
end
|
||||
it 'returns false when disk access fails' do
|
||||
allow(Gitlab::GitalyClient).to receive(:storage_metadata_file_path).and_return("/fake/path/doesnt/exist")
|
||||
|
||||
it "returns false when gitaly doesn't match disk" do
|
||||
allow(Gitlab::GitalyClient).to receive(:storage_metadata_file_path).and_return(temp_gitaly_metadata_file)
|
||||
expect(subject.use_rugged?(repository, feature_flag_name)).to be false
|
||||
end
|
||||
|
||||
expect(subject.use_rugged?(repository, feature_flag_name)).to be_falsey
|
||||
it "returns false when gitaly doesn't match disk" do
|
||||
allow(Gitlab::GitalyClient).to receive(:storage_metadata_file_path).and_return(temp_gitaly_metadata_file)
|
||||
|
||||
File.delete(temp_gitaly_metadata_file)
|
||||
end
|
||||
expect(subject.use_rugged?(repository, feature_flag_name)).to be_falsey
|
||||
|
||||
it "doesn't lead to a second rpc call because gitaly client should use the cached value" do
|
||||
expect(subject.use_rugged?(repository, feature_flag_name)).to be true
|
||||
File.delete(temp_gitaly_metadata_file)
|
||||
end
|
||||
|
||||
expect(Gitlab::GitalyClient).not_to receive(:filesystem_id)
|
||||
it "doesn't lead to a second rpc call because gitaly client should use the cached value" do
|
||||
expect(subject.use_rugged?(repository, feature_flag_name)).to be true
|
||||
|
||||
subject.use_rugged?(repository, feature_flag_name)
|
||||
expect(Gitlab::GitalyClient).not_to receive(:filesystem_id)
|
||||
|
||||
subject.use_rugged?(repository, feature_flag_name)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -655,6 +655,10 @@ merge_request_assignees:
|
|||
- merge_request
|
||||
- assignee
|
||||
- updated_state_by
|
||||
merge_request_reviewers:
|
||||
- merge_request
|
||||
- reviewer
|
||||
- updated_state_by
|
||||
lfs_file_locks:
|
||||
- user
|
||||
project_badges:
|
||||
|
|
|
@ -139,6 +139,30 @@ RSpec.describe Gitlab::ImportExport::Base::RelationFactory do
|
|||
expect(subject.value).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'with duplicate assignees' do
|
||||
let(:relation_sym) { :issues }
|
||||
let(:relation_hash) do
|
||||
{ "title" => "title", "state" => "opened" }.merge(issue_assignees)
|
||||
end
|
||||
|
||||
context 'when duplicate assignees are present' do
|
||||
let(:issue_assignees) do
|
||||
{
|
||||
"issue_assignees" => [
|
||||
IssueAssignee.new(user_id: 1),
|
||||
IssueAssignee.new(user_id: 2),
|
||||
IssueAssignee.new(user_id: 1),
|
||||
{ user_id: 3 }
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
it 'removes duplicate assignees' do
|
||||
expect(subject.issue_assignees.map(&:user_id)).to contain_exactly(1, 2)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -259,6 +259,11 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer do
|
|||
expect(MergeRequest.find_by(title: 'MR2').assignees).to be_empty
|
||||
end
|
||||
|
||||
it 'has multiple merge request reviewers' do
|
||||
expect(MergeRequest.find_by(title: 'MR1').reviewers).to contain_exactly(@user, *@existing_members)
|
||||
expect(MergeRequest.find_by(title: 'MR2').reviewers).to be_empty
|
||||
end
|
||||
|
||||
it 'has labels associated to label links, associated to issues' do
|
||||
expect(Label.first.label_links.first.target).not_to be_nil
|
||||
end
|
||||
|
|
|
@ -110,6 +110,13 @@ RSpec.describe Gitlab::ImportExport::Project::TreeSaver do
|
|||
expect(reviewer).not_to be_nil
|
||||
expect(reviewer['user_id']).to eq(user.id)
|
||||
end
|
||||
|
||||
it 'has merge request reviewers' do
|
||||
reviewer = subject.first['merge_request_reviewers'].first
|
||||
|
||||
expect(reviewer).not_to be_nil
|
||||
expect(reviewer['user_id']).to eq(user.id)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with snippets' do
|
||||
|
@ -475,7 +482,7 @@ RSpec.describe Gitlab::ImportExport::Project::TreeSaver do
|
|||
create(:label_link, label: group_label, target: issue)
|
||||
create(:label_priority, label: group_label, priority: 1)
|
||||
milestone = create(:milestone, project: project)
|
||||
merge_request = create(:merge_request, source_project: project, milestone: milestone, assignees: [user])
|
||||
merge_request = create(:merge_request, source_project: project, milestone: milestone, assignees: [user], reviewers: [user])
|
||||
|
||||
ci_build = create(:ci_build, project: project, when: nil)
|
||||
ci_build.pipeline.update!(project: project)
|
||||
|
|
|
@ -745,6 +745,12 @@ MergeRequestAssignee:
|
|||
- merge_request_id
|
||||
- created_at
|
||||
- state
|
||||
MergeRequestReviewer:
|
||||
- id
|
||||
- user_id
|
||||
- merge_request_id
|
||||
- created_at
|
||||
- state
|
||||
ProjectMetricsSetting:
|
||||
- project_id
|
||||
- external_dashboard_url
|
||||
|
|
|
@ -74,6 +74,17 @@ RSpec.describe Groups::DestroyService do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'event store', :sidekiq_might_not_need_inline do
|
||||
it 'publishes a GroupDeletedEvent' do
|
||||
expect { destroy_group(group, user, async) }
|
||||
.to publish_event(Groups::GroupDeletedEvent)
|
||||
.with(
|
||||
group_id: group.id,
|
||||
root_namespace_id: group.root_ancestor.id
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'asynchronous delete' do
|
||||
|
|
|
@ -90,6 +90,18 @@ module Spec
|
|||
"[data-token-id='#{id}']"
|
||||
end
|
||||
|
||||
def more_invite_errors_button_selector
|
||||
"[data-testid='accordion-button']"
|
||||
end
|
||||
|
||||
def limited_invite_error_selector
|
||||
"[data-testid='errors-limited-item']"
|
||||
end
|
||||
|
||||
def expanded_invite_error_selector
|
||||
"[data-testid='errors-expanded-item']"
|
||||
end
|
||||
|
||||
def remove_token(id)
|
||||
page.within member_token_selector(id) do
|
||||
find('[data-testid="close-icon"]').click
|
||||
|
@ -101,10 +113,10 @@ module Spec
|
|||
expect(page).not_to have_text("#{user.name}: ")
|
||||
end
|
||||
|
||||
def expect_to_have_invalid_invite_indicator(page, user)
|
||||
def expect_to_have_invalid_invite_indicator(page, user, message: true)
|
||||
expect(page).to have_selector("#{member_token_selector(user.id)} .gl-bg-red-100")
|
||||
expect(page).to have_selector(member_token_error_selector(user.id))
|
||||
expect(page).to have_text("#{user.name}: Access level should be greater than or equal to")
|
||||
expect(page).to have_text("#{user.name}: Access level should be greater than or equal to") if message
|
||||
end
|
||||
|
||||
def expect_to_have_normal_invite_indicator(page, user)
|
||||
|
|
|
@ -169,16 +169,47 @@ RSpec.shared_examples 'inviting members' do |snowplow_invite_label|
|
|||
it 'shows the partial user error and success and then removes them from the form', :js do
|
||||
user4 = create(:user)
|
||||
user5 = create(:user)
|
||||
user6 = create(:user)
|
||||
user7 = create(:user)
|
||||
|
||||
group.add_maintainer(user6)
|
||||
group.add_maintainer(user7)
|
||||
|
||||
visit subentity_members_page_path
|
||||
|
||||
invite_member([user2.name, user3.name, user4.name], role: role, refresh: false)
|
||||
invite_member([user2.name, user3.name, user4.name, user6.name, user7.name], role: role, refresh: false)
|
||||
|
||||
# we have more than 2 errors, so one will be hidden
|
||||
invite_modal = page.find(invite_modal_selector)
|
||||
expect(invite_modal).to have_text("The following 2 members couldn't be invited")
|
||||
expect_to_have_invalid_invite_indicator(invite_modal, user2)
|
||||
expect_to_have_invalid_invite_indicator(invite_modal, user3)
|
||||
expect(invite_modal).to have_text("The following 4 members couldn't be invited")
|
||||
expect(invite_modal).to have_selector(limited_invite_error_selector, count: 2, visible: :visible)
|
||||
expect(invite_modal).to have_selector(expanded_invite_error_selector, count: 2, visible: :hidden)
|
||||
# unpredictability of return order means we can't rely on message showing in any order here
|
||||
# so we will not expect on the message
|
||||
expect_to_have_invalid_invite_indicator(invite_modal, user2, message: false)
|
||||
expect_to_have_invalid_invite_indicator(invite_modal, user3, message: false)
|
||||
expect_to_have_invalid_invite_indicator(invite_modal, user6, message: false)
|
||||
expect_to_have_invalid_invite_indicator(invite_modal, user7, message: false)
|
||||
expect_to_have_successful_invite_indicator(invite_modal, user4)
|
||||
expect(invite_modal).to have_button('Show more (2)')
|
||||
|
||||
# now we want to test the show more errors count logic
|
||||
remove_token(user7.id)
|
||||
|
||||
# count decreases from 4 to 3 and 2 to 1
|
||||
expect(invite_modal).to have_text("The following 3 members couldn't be invited")
|
||||
expect(invite_modal).to have_button('Show more (1)')
|
||||
|
||||
# we want to show this error now for user6
|
||||
invite_modal.find(more_invite_errors_button_selector).click
|
||||
|
||||
# now we should see the error for all users and our collapse button text
|
||||
expect(invite_modal).to have_selector(limited_invite_error_selector, count: 2, visible: :visible)
|
||||
expect(invite_modal).to have_selector(expanded_invite_error_selector, count: 1, visible: :visible)
|
||||
expect_to_have_invalid_invite_indicator(invite_modal, user2, message: true)
|
||||
expect_to_have_invalid_invite_indicator(invite_modal, user3, message: true)
|
||||
expect_to_have_invalid_invite_indicator(invite_modal, user6, message: true)
|
||||
expect(invite_modal).to have_button('Show less')
|
||||
|
||||
# adds new token, but doesn't submit
|
||||
select_members(user5.name)
|
||||
|
@ -187,9 +218,19 @@ RSpec.shared_examples 'inviting members' do |snowplow_invite_label|
|
|||
|
||||
remove_token(user2.id)
|
||||
|
||||
expect(invite_modal).to have_text("The following member couldn't be invited")
|
||||
expect(invite_modal).to have_text("The following 2 members couldn't be invited")
|
||||
expect(invite_modal).not_to have_selector(more_invite_errors_button_selector)
|
||||
expect_to_have_invite_removed(invite_modal, user2)
|
||||
expect_to_have_invalid_invite_indicator(invite_modal, user3)
|
||||
expect_to_have_invalid_invite_indicator(invite_modal, user6)
|
||||
expect_to_have_successful_invite_indicator(invite_modal, user4)
|
||||
expect_to_have_normal_invite_indicator(invite_modal, user5)
|
||||
|
||||
remove_token(user6.id)
|
||||
|
||||
expect(invite_modal).to have_text("The following member couldn't be invited")
|
||||
expect_to_have_invite_removed(invite_modal, user6)
|
||||
expect_to_have_invalid_invite_indicator(invite_modal, user3)
|
||||
expect_to_have_successful_invite_indicator(invite_modal, user4)
|
||||
expect_to_have_normal_invite_indicator(invite_modal, user5)
|
||||
|
||||
|
|
Loading…
Reference in New Issue