Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-11-24 03:12:33 +00:00
parent 1c27dcaf69
commit 492f99eac8
27 changed files with 729 additions and 99 deletions

View File

@ -1 +1 @@
4e18794f846ad0d27bea3443caa2b51cd9afd722
1dcc934bcbe0f712306913a5a3d7963d77ae2e70

View File

@ -100,13 +100,13 @@ export default {
<div :class="`issuable-status-box status-box ${statusBoxClass}`">
{{ stateHumanName }}
</div>
<span class="text-secondary">Opened <time v-text="formattedTime"></time></span>
<span class="gl-text-secondary">Opened <time v-text="formattedTime"></time></span>
</div>
<ci-icon v-if="detailedStatus" :status="detailedStatus" />
</div>
<h5 v-if="!$apollo.queries.mergeRequest.loading" class="my-2">{{ title }}</h5>
<!-- eslint-disable @gitlab/vue-require-i18n-strings -->
<div class="text-secondary">
<div class="gl-text-secondary">
{{ `${projectPath}!${mergeRequestIID}` }}
</div>
<!-- eslint-enable @gitlab/vue-require-i18n-strings -->

View File

@ -1,11 +1,12 @@
<script>
import {
GlAlert,
GlEmptyState,
GlFormGroup,
GlFormInputGroup,
GlLink,
GlSkeletonLoader,
GlSprintf,
GlEmptyState,
} from '@gitlab/ui';
import { s__ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
@ -15,7 +16,10 @@ import {
DEPENDENCY_PROXY_SETTINGS_DESCRIPTION,
DEPENDENCY_PROXY_DOCS_PATH,
} from '~/packages_and_registries/settings/group/constants';
import { GRAPHQL_PAGE_SIZE } from '~/packages_and_registries/dependency_proxy/constants';
import {
GRAPHQL_PAGE_SIZE,
ENABLE_DEPENDENCY_PROXY_DOCS_PATH,
} from '~/packages_and_registries/dependency_proxy/constants';
import getDependencyProxyDetailsQuery from '~/packages_and_registries/dependency_proxy/graphql/queries/get_dependency_proxy_details.query.graphql';
@ -25,6 +29,7 @@ export default {
GlEmptyState,
GlFormGroup,
GlFormInputGroup,
GlLink,
GlSkeletonLoader,
GlSprintf,
ClipboardButton,
@ -37,7 +42,7 @@ export default {
'DependencyProxy|Dependency Proxy feature is limited to public groups for now.',
),
proxyDisabledText: s__(
'DependencyProxy|Dependency Proxy disabled. To enable it, contact the group owner.',
'DependencyProxy|The Dependency Proxy is disabled. %{docLinkStart}Learn how to enable it%{docLinkEnd}.',
),
proxyImagePrefix: s__('DependencyProxy|Dependency Proxy image prefix'),
copyImagePrefixText: s__('DependencyProxy|Copy prefix'),
@ -45,6 +50,10 @@ export default {
pageTitle: s__('DependencyProxy|Dependency Proxy'),
noManifestTitle: s__('DependencyProxy|There are no images in the cache'),
},
links: {
DEPENDENCY_PROXY_DOCS_PATH,
ENABLE_DEPENDENCY_PROXY_DOCS_PATH,
},
data() {
return {
group: {},
@ -162,7 +171,11 @@ export default {
/>
</div>
<gl-alert v-else :dismissible="false" data-testid="proxy-disabled">
{{ $options.i18n.proxyDisabledText }}
<gl-sprintf :message="$options.i18n.proxyDisabledText">
<template #docLink="{ content }">
<gl-link :href="$options.links.ENABLE_DEPENDENCY_PROXY_DOCS_PATH">{{ content }}</gl-link>
</template>
</gl-sprintf>
</gl-alert>
</div>
</template>

View File

@ -1 +1,7 @@
import { helpPagePath } from '~/helpers/help_page_helper';
export const GRAPHQL_PAGE_SIZE = 20;
export const ENABLE_DEPENDENCY_PROXY_DOCS_PATH = helpPagePath(
'user/packages/dependency_proxy/index',
{ anchor: 'enable-or-disable-the-dependency-proxy-for-a-group' },
);

View File

@ -18,9 +18,10 @@ export default () => {
el,
apolloProvider,
provide: {
groupPath: el.dataset.groupPath,
groupDependencyProxyPath: el.dataset.groupDependencyProxyPath,
defaultExpanded: parseBoolean(el.dataset.defaultExpanded),
dependencyProxyAvailable: parseBoolean(el.dataset.dependencyProxyAvailable),
groupPath: el.dataset.groupPath,
},
render(createElement) {
return createElement(SettingsApp);

View File

@ -23,12 +23,15 @@ export default {
i18n: {
DEPENDENCY_PROXY_HEADER,
DEPENDENCY_PROXY_SETTINGS_DESCRIPTION,
label: s__('DependencyProxy|Enable Proxy'),
label: s__('DependencyProxy|Enable Dependency Proxy'),
enabledProxyHelpText: s__(
'DependencyProxy|To see the image prefix and what is in the cache, visit the %{linkStart}Dependency Proxy%{linkEnd}',
),
},
links: {
DEPENDENCY_PROXY_DOCS_PATH,
},
inject: ['defaultExpanded', 'groupPath'],
inject: ['defaultExpanded', 'groupPath', 'groupDependencyProxyPath'],
props: {
dependencyProxySettings: {
type: Object,
@ -49,6 +52,9 @@ export default {
this.updateSettings({ enabled });
},
},
helpText() {
return this.enabled ? this.$options.i18n.enabledProxyHelpText : '';
},
},
methods: {
async updateSettings(payload) {
@ -91,7 +97,11 @@ export default {
<span data-testid="description">
<gl-sprintf :message="$options.i18n.DEPENDENCY_PROXY_SETTINGS_DESCRIPTION">
<template #docLink="{ content }">
<gl-link :href="$options.links.DEPENDENCY_PROXY_DOCS_PATH">{{ content }}</gl-link>
<gl-link
data-testid="description-link"
:href="$options.links.DEPENDENCY_PROXY_DOCS_PATH"
>{{ content }}</gl-link
>
</template>
</gl-sprintf>
</span>
@ -102,9 +112,22 @@ export default {
v-model="enabled"
:disabled="isLoading"
:label="$options.i18n.label"
:help="helpText"
data-qa-selector="dependency_proxy_setting_toggle"
data-testid="dependency-proxy-setting-toggle"
/>
>
<template #help>
<span class="gl-overflow-break-word gl-max-w-100vw gl-display-inline-block">
<gl-sprintf :message="$options.i18n.enabledProxyHelpText">
<template #link="{ content }">
<gl-link data-testid="toggle-help-link" :href="groupDependencyProxyPath">{{
content
}}</gl-link>
</template>
</gl-sprintf>
</span>
</template>
</gl-toggle>
</div>
</template>
</settings-block>

View File

@ -73,7 +73,7 @@ export default {
});
},
onReset() {
this.$emit('cancel');
this.$emit('resetContent');
},
scrollIntoView() {
this.$el.scrollIntoView({ behavior: 'smooth' });
@ -86,7 +86,7 @@ export default {
startMergeRequest: __('Start a %{new_merge_request} with these changes'),
newMergeRequest: __('new merge request'),
commitChanges: __('Commit changes'),
cancel: __('Cancel'),
resetContent: __('Reset'),
},
};
</script>
@ -148,7 +148,7 @@ export default {
{{ $options.i18n.commitChanges }}
</gl-button>
<gl-button type="reset" category="secondary" class="gl-mr-3">
{{ $options.i18n.cancel }}
{{ $options.i18n.resetContent }}
</gl-button>
</div>
</gl-form>

View File

@ -127,9 +127,6 @@ export default {
this.isSaving = false;
}
},
onCommitCancel() {
this.$emit('resetContent');
},
updateCurrentBranch(currentBranch) {
this.$apollo.mutate({
mutation: updateCurrentBranchMutation,
@ -153,7 +150,6 @@ export default {
:is-saving="isSaving"
:scroll-to-commit-form="scrollToCommitForm"
v-on="$listeners"
@cancel="onCommitCancel"
@submit="onCommitSubmit"
/>
</template>

View File

@ -1,8 +1,8 @@
<script>
import { GlLoadingIcon } from '@gitlab/ui';
import { GlLoadingIcon, GlModal } from '@gitlab/ui';
import { fetchPolicies } from '~/lib/graphql';
import { queryToObject } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
import { __, s__ } from '~/locale';
import { unwrapStagesWithNeeds } from '~/pipelines/components/unwrapping_utils';
@ -30,6 +30,7 @@ export default {
components: {
ConfirmUnsavedChangesDialog,
GlLoadingIcon,
GlModal,
PipelineEditorEmptyState,
PipelineEditorHome,
PipelineEditorMessages,
@ -54,6 +55,7 @@ export default {
lastCommittedContent: '',
shouldSkipStartScreen: false,
showFailure: false,
showResetComfirmationModal: false,
showStartScreen: false,
showSuccess: false,
starterTemplate: '',
@ -224,6 +226,18 @@ export default {
tabGraph: s__('Pipelines|Visualize'),
tabLint: s__('Pipelines|Lint'),
},
resetModal: {
actionPrimary: {
text: __('Reset file'),
},
actionCancel: {
text: __('Cancel'),
},
body: s__(
'Pipeline Editor|Are you sure you want to reset the file to its last committed version?',
),
title: __('Discard changes'),
},
watch: {
isEmpty(flag) {
if (flag) {
@ -242,6 +256,11 @@ export default {
hideSuccess() {
this.showSuccess = false;
},
confirmReset() {
if (this.hasUnsavedChanges) {
this.showResetComfirmationModal = true;
}
},
async refetchContent() {
this.$apollo.queries.initialCiFileContent.skip = false;
await this.$apollo.queries.initialCiFileContent.refetch();
@ -262,6 +281,7 @@ export default {
this.successType = type;
},
resetContent() {
this.showResetComfirmationModal = false;
this.currentCiFileContent = this.lastCommittedContent;
},
setAppStatus(appStatus) {
@ -335,12 +355,22 @@ export default {
:has-unsaved-changes="hasUnsavedChanges"
:is-new-ci-config-file="isNewCiConfigFile"
@commit="updateOnCommit"
@resetContent="resetContent"
@resetContent="confirmReset"
@showError="showErrorAlert"
@refetchContent="refetchContent"
@updateCiConfig="updateCiConfig"
@updateCommitSha="updateCommitSha"
/>
<gl-modal
v-model="showResetComfirmationModal"
modal-id="reset-content"
:title="$options.resetModal.title"
:action-cancel="$options.resetModal.actionCancel"
:action-primary="$options.resetModal.actionPrimary"
@primary="resetContent"
>
{{ $options.resetModal.body }}
</gl-modal>
<confirm-unsaved-changes-dialog :has-unsaved-changes="hasUnsavedChanges" />
</div>
</div>

View File

@ -5,4 +5,5 @@
%section#js-packages-and-registries-settings{ data: { default_expanded: expanded_by_default?.to_s,
group_path: @group.full_path,
dependency_proxy_available: dependency_proxy_available.to_s } }
dependency_proxy_available: dependency_proxy_available.to_s,
group_dependency_proxy_path: group_dependency_proxy_path(@group) } }

View File

@ -0,0 +1,23 @@
# frozen_string_literal: true
class ScheduleRemoveOccurrencePipelinesAndDuplicateVulnerabilitiesFindings < Gitlab::Database::Migration[1.0]
MIGRATION = 'RemoveOccurrencePipelinesAndDuplicateVulnerabilitiesFindings'
DELAY_INTERVAL = 2.minutes.to_i
BATCH_SIZE = 10_000
disable_ddl_transaction!
def up
queue_background_migration_jobs_by_range_at_intervals(
define_batchable_model('vulnerability_occurrences'),
MIGRATION,
DELAY_INTERVAL,
batch_size: BATCH_SIZE,
track_jobs: true
)
end
def down
# no-op
end
end

View File

@ -0,0 +1 @@
664c7fa75d3283b6e984fcca4ffcefab6dba24a78e4cc24ac86f791ab4495def

View File

@ -4,7 +4,7 @@ group: Import
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
# Group Import/Export API **(FREE)**
# Group import/export API **(FREE)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/20353) in GitLab 12.8.

View File

@ -9,49 +9,49 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/6861) in GitLab 11.6.
[Group owners](../permissions.md#group-members-permissions) can set a subgroup to
be the source of project templates that are selectable when a new project is created
in the group. These templates can be selected when you go to **New project > Create from template**
in the group and select the **Group** tab.
When you create a project, you can [choose from a list of templates](../project/working_with_projects.md#project-templates).
These templates, for things like GitLab Pages or Ruby, populate the new project with a copy of the files contained in the
template. This information is identical to the information used by [GitLab project import/export](../project/settings/import_export.md)
and can help you start a new project more quickly.
Every project in the subgroup, but not nested subgroups, can be selected by members
of the group when a new project is created.
You can [customize the list](../project/working_with_projects.md#custom-project-templates) of available templates, so
that all projects in your group have the same list. To do this, you populate a subgroup with the projects you want to
use as templates.
- Public projects can be selected by any signed-in user as a template for a new project,
if all enabled [project features](../project/settings/index.md#sharing-and-permissions)
except for **GitLab Pages** and **Security & Compliance** are set to **Everyone With Access**.
The same applies to internal projects.
- Private projects can be selected only by users who are members of the projects.
Repository and database information that is copied over to each new project is identical to the
data exported with the [GitLab Project Import/Export](../project/settings/import_export.md).
To set custom project templates at the instance level, see [Custom instance-level project templates](../admin_area/custom_project_templates.md).
You can also configure [custom templates for the instance](../admin_area/custom_project_templates.md).
## Set up group-level project templates
Prerequisite:
- You must have the [Owner role for the group](../permissions.md#group-members-permissions).
To set up custom project templates in a group, add the subgroup that contains the
project templates to the group settings:
1. In the group, create a [subgroup](subgroups/index.md).
1. [Add projects to the new subgroup](index.md#add-projects-to-a-group) as your templates.
1. In the left menu for the group, go to **Settings > General**.
1. In the left menu for the group, select **Settings > General**.
1. Expand **Custom project templates** and select the subgroup.
If all enabled [project features](../project/settings/index.md#sharing-and-permissions)
(except for GitLab Pages) are set to **Everyone With Access**, then every project
template in the subgroup is available to every member of the group.
The next time a group member creates a project, they can select any of the projects in the subgroup.
Any projects added to the subgroup later can be selected the next time a group member
creates a new project.
Projects in nested subgroups are not included in the template list.
### Example structure
## Which projects are available as templates
Here's a sample group/project structure for project templates, for a hypothetical _Acme Co_:
- Public and internal projects can be selected by any signed-in user as a template for a new project,
if all [project features](../project/settings/index.md#sharing-and-permissions)
except for **GitLab Pages** and **Security & Compliance** are set to **Everyone With Access**.
- Private projects can be selected only by users who are members of the projects.
## Example structure
Here's a sample group and project structure for project templates, for `myorganization`:
```plaintext
# GitLab instance and group
gitlab.com/acmeco/
gitlab.com/myorganization/
# Subgroups
internal
tools
@ -61,7 +61,7 @@ gitlab.com/acmeco/
# Project templates
client-site-django
client-site-gatsby
client-site-hTML
client-site-html
# Other projects
client-site-a

View File

@ -9,19 +9,16 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/2888) in GitLab 13.0 as an experimental feature. May change in future releases.
Existing groups running on any GitLab instance or GitLab.com can be exported with all their related data and moved to a
new GitLab instance.
You can export groups, with all their related data, from one GitLab instance to another.
You can also [export projects](../../project/settings/import_export.md).
The **GitLab import/export** button is displayed if the group import option in enabled.
## Enable export for a group
See also:
Prerequisite:
- [Group Import/Export API](../../../api/group_import_export.md)
- [Project Import/Export](../../project/settings/import_export.md)
- [Project Import/Export API](../../../api/project_import_export.md)
- You must have the [Owner role](../../permissions.md) for the group.
Users with the [Owner role](../../permissions.md) for a group can enable
import and export for that group:
To enable import and export for a group:
1. On the top bar, select **Menu > Admin**.
1. On the left sidebar, select **Settings > General**.
@ -70,8 +67,11 @@ WARNING:
This feature will be [deprecated](https://gitlab.com/groups/gitlab-org/-/epics/4619)
in GitLab 14.6 and replaced by [GitLab Migration](../import/).
Users with the [Owner role](../../permissions.md) for a group can export the
contents of that group:
Prerequisites:
- You must have the [Owner role](../../permissions.md) for the group.
To export the contents of a group:
1. On the top bar, select **Menu > Groups** and find your group.
1. On the left sidebar, select **Settings > General**.
@ -88,6 +88,8 @@ NOTE:
The maximum import file size can be set by the Administrator, default is `0` (unlimited).
As an administrator, you can modify the maximum import file size. To do so, use the `max_import_size` option in the [Application settings API](../../../api/settings.md#change-application-settings) or the [Admin UI](../../admin_area/settings/account_and_limit_settings.md). Default [modified](https://gitlab.com/gitlab-org/gitlab/-/issues/251106) from 50MB to 0 in GitLab 13.8.
You can also use the [group import/export API](../../../api/group_import_export.md).
### Between CE and EE
You can export groups from the [Community Edition to the Enterprise Edition](https://about.gitlab.com/install/ce-or-ee/) and vice versa.

View File

@ -0,0 +1,59 @@
# frozen_string_literal: true
# This migration will look for Vulnerabilities::Finding objects that would have a duplicate UUIDv5 if the UUID was
# recalculated. Then it removes Vulnerabilities::FindingPipeline objects associated with those Findings.
# We can't just drop those Findings directly since the cascade drop will timeout if any given Finding has too many
# associated FindingPipelines
class Gitlab::BackgroundMigration::RemoveOccurrencePipelinesAndDuplicateVulnerabilitiesFindings
# rubocop:disable Gitlab/NamespacedClass, Style/Documentation
class VulnerabilitiesFinding < ActiveRecord::Base
self.table_name = "vulnerability_occurrences"
end
class VulnerabilitiesFindingPipeline < ActiveRecord::Base
include EachBatch
self.table_name = "vulnerability_occurrence_pipelines"
end
# rubocop:enable Gitlab/NamespacedClass, Style/Documentation
def perform(start_id, end_id)
ids_to_look_for = findings_that_would_produce_duplicate_uuids(start_id, end_id)
ids_to_look_for.each do |finding_id|
VulnerabilitiesFindingPipeline.where(occurrence_id: finding_id).each_batch(of: 1000) do |pipelines|
pipelines.delete_all
end
end
VulnerabilitiesFinding.where(id: ids_to_look_for).delete_all
mark_job_as_succeeded(start_id, end_id)
end
private
def findings_that_would_produce_duplicate_uuids(start_id, end_id)
VulnerabilitiesFinding
.from("vulnerability_occurrences to_delete")
.where("to_delete.id BETWEEN ? AND ?", start_id, end_id)
.where(
"EXISTS (
SELECT 1
FROM vulnerability_occurrences duplicates
WHERE duplicates.report_type = to_delete.report_type
AND duplicates.location_fingerprint = to_delete.location_fingerprint
AND duplicates.primary_identifier_id = to_delete.primary_identifier_id
AND duplicates.project_id = to_delete.project_id
AND duplicates.id > to_delete.id
)"
)
.pluck("to_delete.id")
end
def mark_job_as_succeeded(*arguments)
Gitlab::Database::BackgroundMigrationJob.mark_all_as_succeeded(
self.class.name.demodulize,
arguments
)
end
end

View File

@ -42,7 +42,7 @@ module Gitlab
def renamed_tables_cache
@renamed_tables ||= begin
Gitlab::Database::TABLES_TO_BE_RENAMED.select do |old_name, new_name|
ActiveRecord::Base.connection.view_exists?(old_name)
connection.view_exists?(old_name)
end
end
end

View File

@ -11426,24 +11426,27 @@ msgstr ""
msgid "DependencyProxy|Dependency Proxy"
msgstr ""
msgid "DependencyProxy|Dependency Proxy disabled. To enable it, contact the group owner."
msgstr ""
msgid "DependencyProxy|Dependency Proxy feature is limited to public groups for now."
msgstr ""
msgid "DependencyProxy|Dependency Proxy image prefix"
msgstr ""
msgid "DependencyProxy|Enable Proxy"
msgid "DependencyProxy|Enable Dependency Proxy"
msgstr ""
msgid "DependencyProxy|Image list"
msgstr ""
msgid "DependencyProxy|The Dependency Proxy is disabled. %{docLinkStart}Learn how to enable it%{docLinkEnd}."
msgstr ""
msgid "DependencyProxy|There are no images in the cache"
msgstr ""
msgid "DependencyProxy|To see the image prefix and what is in the cache, visit the %{linkStart}Dependency Proxy%{linkEnd}"
msgstr ""
msgid "Depends on %d merge request being merged"
msgid_plural "Depends on %d merge requests being merged"
msgstr[0] ""
@ -25301,6 +25304,9 @@ msgstr ""
msgid "Pipeline Editor"
msgstr ""
msgid "Pipeline Editor|Are you sure you want to reset the file to its last committed version?"
msgstr ""
msgid "Pipeline ID"
msgstr ""
@ -29535,6 +29541,9 @@ msgstr ""
msgid "Reset authorization key?"
msgstr ""
msgid "Reset file"
msgstr ""
msgid "Reset filters"
msgstr ""

View File

@ -52,4 +52,53 @@ RSpec.describe 'Pipeline Editor', :js do
end
end
end
context 'Editor content' do
it 'user can reset their CI configuration' do
click_button 'Collapse'
page.within('#source-editor-') do
find('textarea').send_keys '123'
end
# It takes some time after sending keys for the reset
# btn to register the changes inside the editor
sleep 1
click_button 'Reset'
expect(page).to have_css('#reset-content')
page.within('#reset-content') do
click_button 'Reset file'
end
page.within('#source-editor-') do
expect(page).to have_content('Default Content')
expect(page).not_to have_content('Default Content123')
end
end
it 'user can cancel reseting their CI configuration' do
click_button 'Collapse'
page.within('#source-editor-') do
find('textarea').send_keys '123'
end
# It takes some time after sending keys for the reset
# btn to register the changes inside the editor
sleep 1
click_button 'Reset'
expect(page).to have_css('#reset-content')
page.within('#reset-content') do
click_button 'Cancel'
end
page.within('#source-editor-') do
expect(page).to have_content('Default Content123')
end
end
end
end

View File

@ -26,7 +26,7 @@ exports[`MR Popover loaded state matches the snapshot 1`] = `
</div>
<span
class="text-secondary"
class="gl-text-secondary"
>
Opened
<time>
@ -49,7 +49,7 @@ exports[`MR Popover loaded state matches the snapshot 1`] = `
</h5>
<div
class="text-secondary"
class="gl-text-secondary"
>
foo/bar!1
@ -80,7 +80,7 @@ exports[`MR Popover shows skeleton-loader while apollo is loading 1`] = `
<!---->
<div
class="text-secondary"
class="gl-text-secondary"
>
foo/bar!1

View File

@ -3,6 +3,7 @@ import {
GlFormGroup,
GlSkeletonLoader,
GlSprintf,
GlLink,
GlEmptyState,
} from '@gitlab/ui';
import { createLocalVue } from '@vue/test-utils';
@ -11,7 +12,10 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { stripTypenames } from 'helpers/graphql_helpers';
import waitForPromises from 'helpers/wait_for_promises';
import { GRAPHQL_PAGE_SIZE } from '~/packages_and_registries/dependency_proxy/constants';
import {
GRAPHQL_PAGE_SIZE,
ENABLE_DEPENDENCY_PROXY_DOCS_PATH,
} from '~/packages_and_registries/dependency_proxy/constants';
import DependencyProxyApp from '~/packages_and_registries/dependency_proxy/app.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
@ -55,6 +59,7 @@ describe('DependencyProxyApp', () => {
const findProxyNotAvailableAlert = () => wrapper.findByTestId('proxy-not-available');
const findProxyDisabledAlert = () => wrapper.findByTestId('proxy-disabled');
const findDisabledAlertLink = () => findProxyDisabledAlert().findComponent(GlLink);
const findClipBoardButton = () => wrapper.findComponent(ClipboardButton);
const findFormGroup = () => wrapper.findComponent(GlFormGroup);
const findFormInputGroup = () => wrapper.findComponent(GlFormInputGroup);
@ -238,7 +243,15 @@ describe('DependencyProxyApp', () => {
});
it('shows a proxy disabled alert', () => {
expect(findProxyDisabledAlert().text()).toBe(DependencyProxyApp.i18n.proxyDisabledText);
expect(findProxyDisabledAlert().text()).toMatchInterpolatedText(
DependencyProxyApp.i18n.proxyDisabledText,
);
});
it('disabled alert has a link to the docs', () => {
expect(findDisabledAlertLink().attributes()).toMatchObject({
href: ENABLE_DEPENDENCY_PROXY_DOCS_PATH,
});
});
});
});

View File

@ -1,6 +1,7 @@
import { GlSprintf, GlLink, GlToggle } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlSprintf, GlToggle } from '@gitlab/ui';
import { createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
@ -16,7 +17,7 @@ import getGroupPackagesSettingsQuery from '~/packages_and_registries/settings/gr
import SettingsBlock from '~/vue_shared/components/settings/settings_block.vue';
import { updateGroupDependencyProxySettingsOptimisticResponse } from '~/packages_and_registries/settings/group/graphql/utils/optimistic_responses';
import {
dependencyProxySettings,
dependencyProxySettings as dependencyProxySettingsMock,
dependencyProxySettingMutationMock,
groupPackageSettingsMock,
dependencyProxySettingMutationErrorMock,
@ -34,6 +35,7 @@ describe('DependencyProxySettings', () => {
const defaultProvide = {
defaultExpanded: false,
groupPath: 'foo_group_path',
groupDependencyProxyPath: 'group_dependency_proxy_path',
};
localVue.use(VueApollo);
@ -42,21 +44,23 @@ describe('DependencyProxySettings', () => {
provide = defaultProvide,
mutationResolver = jest.fn().mockResolvedValue(dependencyProxySettingMutationMock()),
isLoading = false,
dependencyProxySettings = dependencyProxySettingsMock(),
} = {}) => {
const requestHandlers = [[updateDependencyProxySettings, mutationResolver]];
apolloProvider = createMockApollo(requestHandlers);
wrapper = shallowMount(component, {
wrapper = shallowMountExtended(component, {
localVue,
apolloProvider,
provide,
propsData: {
dependencyProxySettings: dependencyProxySettings(),
dependencyProxySettings,
isLoading,
},
stubs: {
GlSprintf,
GlToggle,
SettingsBlock,
},
});
@ -67,9 +71,10 @@ describe('DependencyProxySettings', () => {
});
const findSettingsBlock = () => wrapper.findComponent(SettingsBlock);
const findDescription = () => wrapper.find('[data-testid="description"');
const findLink = () => wrapper.findComponent(GlLink);
const findDescription = () => wrapper.findByTestId('description');
const findDescriptionLink = () => wrapper.findByTestId('description-link');
const findToggle = () => wrapper.findComponent(GlToggle);
const findToggleHelpLink = () => wrapper.findByTestId('toggle-help-link');
const fillApolloCache = () => {
apolloProvider.defaultClient.cache.writeQuery({
@ -112,10 +117,59 @@ describe('DependencyProxySettings', () => {
it('has the correct link', () => {
mountComponent();
expect(findLink().attributes()).toMatchObject({
expect(findDescriptionLink().attributes()).toMatchObject({
href: DEPENDENCY_PROXY_DOCS_PATH,
});
expect(findLink().text()).toBe('Learn more');
expect(findDescriptionLink().text()).toBe('Learn more');
});
describe('enable toggle', () => {
it('exists', () => {
mountComponent();
expect(findToggle().props()).toMatchObject({
label: component.i18n.label,
});
});
describe('when enabled', () => {
beforeEach(() => {
mountComponent();
});
it('has the help prop correctly set', () => {
expect(findToggle().props()).toMatchObject({
help: component.i18n.enabledProxyHelpText,
});
});
it('has help text with a link', () => {
expect(findToggle().text()).toContain(
'To see the image prefix and what is in the cache, visit the Dependency Proxy',
);
expect(findToggleHelpLink().attributes()).toMatchObject({
href: defaultProvide.groupDependencyProxyPath,
});
});
});
describe('when disabled', () => {
beforeEach(() => {
mountComponent({
dependencyProxySettings: dependencyProxySettingsMock({ enabled: false }),
});
});
it('has the help prop set to empty', () => {
expect(findToggle().props()).toMatchObject({
help: '',
});
});
it('the help text is not visible', () => {
expect(findToggleHelpLink().exists()).toBe(false);
});
});
});
describe('settings update', () => {

View File

@ -5,8 +5,9 @@ export const packageSettings = () => ({
genericDuplicateExceptionRegex: '',
});
export const dependencyProxySettings = () => ({
export const dependencyProxySettings = (extend) => ({
enabled: true,
...extend,
});
export const groupPackageSettingsMock = {

View File

@ -78,7 +78,7 @@ describe('Pipeline Editor | Commit Form', () => {
it('emits an event when the form resets', () => {
findCancelBtn().trigger('click');
expect(wrapper.emitted('cancel')).toHaveLength(1);
expect(wrapper.emitted('resetContent')).toHaveLength(1);
});
});

View File

@ -103,11 +103,6 @@ describe('Pipeline Editor | Commit section', () => {
await waitForPromises();
};
const cancelCommitForm = async () => {
const findCancelBtn = () => wrapper.find('[type="reset"]');
await findCancelBtn().trigger('click');
};
afterEach(() => {
mockMutate.mockReset();
wrapper.destroy();
@ -266,18 +261,6 @@ describe('Pipeline Editor | Commit section', () => {
});
});
describe('when the commit form is cancelled', () => {
beforeEach(async () => {
createComponent();
});
it('emits an event so that it cab be reseted', async () => {
await cancelCommitForm();
expect(wrapper.emitted('resetContent')).toHaveLength(1);
});
});
it('sets listeners on commit form', () => {
const handler = jest.fn();
createComponent({ options: { listeners: { event: handler } } });

View File

@ -0,0 +1,175 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration::RemoveOccurrencePipelinesAndDuplicateVulnerabilitiesFindings do
let(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }
let(:users) { table(:users) }
let(:user) { create_user! }
let(:project) { table(:projects).create!(id: 14219619, namespace_id: namespace.id) }
let(:scanners) { table(:vulnerability_scanners) }
let!(:scanner1) { scanners.create!(project_id: project.id, external_id: 'test 1', name: 'test scanner 1') }
let!(:scanner2) { scanners.create!(project_id: project.id, external_id: 'test 2', name: 'test scanner 2') }
let!(:scanner3) { scanners.create!(project_id: project.id, external_id: 'test 3', name: 'test scanner 3') }
let!(:unrelated_scanner) { scanners.create!(project_id: project.id, external_id: 'unreleated_scanner', name: 'unrelated scanner') }
let(:vulnerabilities) { table(:vulnerabilities) }
let(:vulnerability_findings) { table(:vulnerability_occurrences) }
let(:vulnerability_finding_pipelines) { table(:vulnerability_occurrence_pipelines) }
let(:vulnerability_identifiers) { table(:vulnerability_identifiers) }
let(:vulnerability_identifier) do
vulnerability_identifiers.create!(
id: 1244459,
project_id: project.id,
external_type: 'vulnerability-identifier',
external_id: 'vulnerability-identifier',
fingerprint: '0a203e8cd5260a1948edbedc76c7cb91ad6a2e45',
name: 'vulnerability identifier')
end
let!(:vulnerability_for_first_duplicate) do
create_vulnerability!(
project_id: project.id,
author_id: user.id
)
end
let!(:first_finding_duplicate) do
create_finding!(
id: 5606961,
uuid: "bd95c085-71aa-51d7-9bb6-08ae669c262e",
vulnerability_id: vulnerability_for_first_duplicate.id,
report_type: 0,
location_fingerprint: '00049d5119c2cb3bfb3d1ee1f6e031fe925aed75',
primary_identifier_id: vulnerability_identifier.id,
scanner_id: scanner1.id,
project_id: project.id
)
end
let!(:vulnerability_for_second_duplicate) do
create_vulnerability!(
project_id: project.id,
author_id: user.id
)
end
let!(:second_finding_duplicate) do
create_finding!(
id: 8765432,
uuid: "5b714f58-1176-5b26-8fd5-e11dfcb031b5",
vulnerability_id: vulnerability_for_second_duplicate.id,
report_type: 0,
location_fingerprint: '00049d5119c2cb3bfb3d1ee1f6e031fe925aed75',
primary_identifier_id: vulnerability_identifier.id,
scanner_id: scanner2.id,
project_id: project.id
)
end
let!(:vulnerability_for_third_duplicate) do
create_vulnerability!(
project_id: project.id,
author_id: user.id
)
end
let!(:third_finding_duplicate) do
create_finding!(
id: 8832995,
uuid: "cfe435fa-b25b-5199-a56d-7b007cc9e2d4",
vulnerability_id: vulnerability_for_third_duplicate.id,
report_type: 0,
location_fingerprint: '00049d5119c2cb3bfb3d1ee1f6e031fe925aed75',
primary_identifier_id: vulnerability_identifier.id,
scanner_id: scanner3.id,
project_id: project.id
)
end
let!(:unrelated_finding) do
create_finding!(
id: 9999999,
uuid: "unreleated_finding",
vulnerability_id: nil,
report_type: 1,
location_fingerprint: 'random_location_fingerprint',
primary_identifier_id: vulnerability_identifier.id,
scanner_id: unrelated_scanner.id,
project_id: project.id
)
end
subject { described_class.new.perform(first_finding_duplicate.id, unrelated_finding.id) }
before do
4.times do
create_finding_pipeline!(project_id: project.id, finding_id: first_finding_duplicate.id)
create_finding_pipeline!(project_id: project.id, finding_id: second_finding_duplicate.id)
create_finding_pipeline!(project_id: project.id, finding_id: third_finding_duplicate.id)
create_finding_pipeline!(project_id: project.id, finding_id: unrelated_finding.id)
end
end
it 'removes Vulnerabilities::OccurrencePipelines for matching Vulnerabilities::Finding' do
expect(vulnerability_findings.count).to eq(4)
expect(vulnerability_finding_pipelines.count).to eq(16)
expect { subject }.to change(vulnerability_finding_pipelines, :count).from(16).to(8)
.and change(vulnerability_findings, :count).from(4).to(2)
end
private
def create_vulnerability!(project_id:, author_id:, title: 'test', severity: 7, confidence: 7, report_type: 0)
vulnerabilities.create!(
project_id: project_id,
author_id: author_id,
title: title,
severity: severity,
confidence: confidence,
report_type: report_type
)
end
# rubocop:disable Metrics/ParameterLists
def create_finding!(
id: nil,
vulnerability_id:, project_id:, scanner_id:, primary_identifier_id:,
name: "test", severity: 7, confidence: 7, report_type: 0,
project_fingerprint: '123qweasdzxc', location_fingerprint: 'test',
metadata_version: 'test', raw_metadata: 'test', uuid: 'test')
params = {
vulnerability_id: vulnerability_id,
project_id: project_id,
name: name,
severity: severity,
confidence: confidence,
report_type: report_type,
project_fingerprint: project_fingerprint,
scanner_id: scanner_id,
primary_identifier_id: vulnerability_identifier.id,
location_fingerprint: location_fingerprint,
metadata_version: metadata_version,
raw_metadata: raw_metadata,
uuid: uuid
}
params[:id] = id unless id.nil?
vulnerability_findings.create!(params)
end
# rubocop:enable Metrics/ParameterLists
def create_user!(name: "Example User", email: "user@example.com", user_type: nil, created_at: Time.zone.now, confirmed_at: Time.zone.now)
table(:users).create!(
name: name,
email: email,
username: name,
projects_limit: 0,
user_type: user_type,
confirmed_at: confirmed_at
)
end
def create_finding_pipeline!(project_id:, finding_id:)
pipeline = table(:ci_pipelines).create!(project_id: project_id)
vulnerability_finding_pipelines.create!(pipeline_id: pipeline.id, occurrence_id: finding_id)
end
end

View File

@ -0,0 +1,191 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!
RSpec.describe ScheduleRemoveOccurrencePipelinesAndDuplicateVulnerabilitiesFindings, :migration do
let_it_be(:background_migration_jobs) { table(:background_migration_jobs) }
let_it_be(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }
let_it_be(:users) { table(:users) }
let_it_be(:user) { create_user! }
let_it_be(:project) { table(:projects).create!(id: 14219619, namespace_id: namespace.id) }
let_it_be(:pipelines) { table(:ci_pipelines) }
let_it_be(:scanners) { table(:vulnerability_scanners) }
let_it_be(:scanner1) { scanners.create!(project_id: project.id, external_id: 'test 1', name: 'test scanner 1') }
let_it_be(:scanner2) { scanners.create!(project_id: project.id, external_id: 'test 2', name: 'test scanner 2') }
let_it_be(:scanner3) { scanners.create!(project_id: project.id, external_id: 'test 3', name: 'test scanner 3') }
let_it_be(:unrelated_scanner) { scanners.create!(project_id: project.id, external_id: 'unreleated_scanner', name: 'unrelated scanner') }
let_it_be(:vulnerabilities) { table(:vulnerabilities) }
let_it_be(:vulnerability_findings) { table(:vulnerability_occurrences) }
let_it_be(:vulnerability_finding_pipelines) { table(:vulnerability_occurrence_pipelines) }
let_it_be(:vulnerability_identifiers) { table(:vulnerability_identifiers) }
let_it_be(:vulnerability_identifier) do
vulnerability_identifiers.create!(
id: 1244459,
project_id: project.id,
external_type: 'vulnerability-identifier',
external_id: 'vulnerability-identifier',
fingerprint: '0a203e8cd5260a1948edbedc76c7cb91ad6a2e45',
name: 'vulnerability identifier')
end
let_it_be(:vulnerability_for_first_duplicate) do
create_vulnerability!(
project_id: project.id,
author_id: user.id
)
end
let_it_be(:first_finding_duplicate) do
create_finding!(
id: 5606961,
uuid: "bd95c085-71aa-51d7-9bb6-08ae669c262e",
vulnerability_id: vulnerability_for_first_duplicate.id,
report_type: 0,
location_fingerprint: '00049d5119c2cb3bfb3d1ee1f6e031fe925aed75',
primary_identifier_id: vulnerability_identifier.id,
scanner_id: scanner1.id,
project_id: project.id
)
end
let_it_be(:vulnerability_for_second_duplicate) do
create_vulnerability!(
project_id: project.id,
author_id: user.id
)
end
let_it_be(:second_finding_duplicate) do
create_finding!(
id: 8765432,
uuid: "5b714f58-1176-5b26-8fd5-e11dfcb031b5",
vulnerability_id: vulnerability_for_second_duplicate.id,
report_type: 0,
location_fingerprint: '00049d5119c2cb3bfb3d1ee1f6e031fe925aed75',
primary_identifier_id: vulnerability_identifier.id,
scanner_id: scanner2.id,
project_id: project.id
)
end
let_it_be(:vulnerability_for_third_duplicate) do
create_vulnerability!(
project_id: project.id,
author_id: user.id
)
end
let_it_be(:third_finding_duplicate) do
create_finding!(
id: 8832995,
uuid: "cfe435fa-b25b-5199-a56d-7b007cc9e2d4",
vulnerability_id: vulnerability_for_third_duplicate.id,
report_type: 0,
location_fingerprint: '00049d5119c2cb3bfb3d1ee1f6e031fe925aed75',
primary_identifier_id: vulnerability_identifier.id,
scanner_id: scanner3.id,
project_id: project.id
)
end
let_it_be(:unrelated_finding) do
create_finding!(
id: 9999999,
uuid: "unreleated_finding",
vulnerability_id: nil,
report_type: 1,
location_fingerprint: 'random_location_fingerprint',
primary_identifier_id: vulnerability_identifier.id,
scanner_id: unrelated_scanner.id,
project_id: project.id
)
end
before do
stub_const("#{described_class}::BATCH_SIZE", 1)
4.times do
create_finding_pipeline!(project_id: project.id, finding_id: first_finding_duplicate.id)
create_finding_pipeline!(project_id: project.id, finding_id: second_finding_duplicate.id)
create_finding_pipeline!(project_id: project.id, finding_id: third_finding_duplicate.id)
create_finding_pipeline!(project_id: project.id, finding_id: unrelated_finding.id)
end
end
around do |example|
freeze_time { Sidekiq::Testing.fake! { example.run } }
end
it 'schedules background migrations' do
migrate!
expect(background_migration_jobs.count).to eq(4)
expect(background_migration_jobs.first.arguments).to match_array([first_finding_duplicate.id, first_finding_duplicate.id])
expect(background_migration_jobs.second.arguments).to match_array([second_finding_duplicate.id, second_finding_duplicate.id])
expect(background_migration_jobs.third.arguments).to match_array([third_finding_duplicate.id, third_finding_duplicate.id])
expect(background_migration_jobs.fourth.arguments).to match_array([unrelated_finding.id, unrelated_finding.id])
expect(BackgroundMigrationWorker.jobs.size).to eq(4)
expect(described_class::MIGRATION).to be_scheduled_delayed_migration(2.minutes, first_finding_duplicate.id, first_finding_duplicate.id)
expect(described_class::MIGRATION).to be_scheduled_delayed_migration(4.minutes, second_finding_duplicate.id, second_finding_duplicate.id)
expect(described_class::MIGRATION).to be_scheduled_delayed_migration(6.minutes, third_finding_duplicate.id, third_finding_duplicate.id)
expect(described_class::MIGRATION).to be_scheduled_delayed_migration(8.minutes, unrelated_finding.id, unrelated_finding.id)
end
private
def create_vulnerability!(project_id:, author_id:, title: 'test', severity: 7, confidence: 7, report_type: 0)
vulnerabilities.create!(
project_id: project_id,
author_id: author_id,
title: title,
severity: severity,
confidence: confidence,
report_type: report_type
)
end
# rubocop:disable Metrics/ParameterLists
def create_finding!(
id: nil,
vulnerability_id:, project_id:, scanner_id:, primary_identifier_id:,
name: "test", severity: 7, confidence: 7, report_type: 0,
project_fingerprint: '123qweasdzxc', location_fingerprint: 'test',
metadata_version: 'test', raw_metadata: 'test', uuid: 'test')
params = {
vulnerability_id: vulnerability_id,
project_id: project_id,
name: name,
severity: severity,
confidence: confidence,
report_type: report_type,
project_fingerprint: project_fingerprint,
scanner_id: scanner_id,
primary_identifier_id: vulnerability_identifier.id,
location_fingerprint: location_fingerprint,
metadata_version: metadata_version,
raw_metadata: raw_metadata,
uuid: uuid
}
params[:id] = id unless id.nil?
vulnerability_findings.create!(params)
end
# rubocop:enable Metrics/ParameterLists
def create_user!(name: "Example User", email: "user@example.com", user_type: nil, created_at: Time.zone.now, confirmed_at: Time.zone.now)
users.create!(
name: name,
email: email,
username: name,
projects_limit: 0,
user_type: user_type,
confirmed_at: confirmed_at
)
end
def create_finding_pipeline!(project_id:, finding_id:)
pipeline = pipelines.create!(project_id: project_id)
vulnerability_finding_pipelines.create!(pipeline_id: pipeline.id, occurrence_id: finding_id)
end
end