Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-08-15 21:09:52 +00:00
parent efbd20fd1e
commit a2fd863d3b
72 changed files with 720 additions and 232 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,4 +5,4 @@ rollout_issue_url:
milestone: '15.1'
type: development
group: group::code review
default_enabled: false
default_enabled: true

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -99,6 +99,8 @@
- 1
- - cluster_agent
- 1
- - compliance_management_chain_of_custody_report
- 1
- - compliance_management_merge_requests_compliance_violations
- 1
- - container_repository

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -42,7 +42,7 @@ describe('RemoveGroupLinkButton', () => {
});
};
const findButton = () => wrapper.find(GlButton);
const findButton = () => wrapper.findComponent(GlButton);
beforeEach(() => {
createComponent();

View File

@ -44,7 +44,7 @@ describe('ResendInviteButton', () => {
};
const findForm = () => wrapper.find('form');
const findButton = () => findForm().find(GlButton);
const findButton = () => findForm().findComponent(GlButton);
beforeEach(() => {
createComponent();

View File

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

View File

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

View File

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

View File

@ -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', () => {

View File

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

View File

@ -56,7 +56,7 @@ describe('MembersFilteredSearchBar', () => {
});
};
const findFilteredSearchBar = () => wrapper.find(FilteredSearchBar);
const findFilteredSearchBar = () => wrapper.findComponent(FilteredSearchBar);
it('passes correct props to `FilteredSearchBar` component', () => {
createComponent();

View File

@ -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(() => {

View File

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

View File

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

View File

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

View File

@ -56,7 +56,7 @@ describe('ExpirationDatepicker', () => {
};
const findInput = () => wrapper.find('input');
const findDatepicker = () => wrapper.find(GlDatepicker);
const findDatepicker = () => wrapper.findComponent(GlDatepicker);
afterEach(() => {
wrapper.destroy();

View File

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

View File

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

View File

@ -69,7 +69,7 @@ describe('MembersTableCell', () => {
});
};
const findWrappedComponent = () => wrapper.find(WrappedComponent);
const findWrappedComponent = () => wrapper.findComponent(WrappedComponent);
const memberCurrentUser = {
...memberMock,

View File

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

View File

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

View File

@ -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', () => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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