Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
733f1d8bb1
commit
c9d1b77888
|
@ -403,25 +403,25 @@ export default {
|
|||
>{{ i18n.cancel }}
|
||||
</gl-button>
|
||||
|
||||
<gl-button
|
||||
v-if="isKasEnabledInEmptyStateModal"
|
||||
:href="repositoryPath"
|
||||
variant="confirm"
|
||||
category="secondary"
|
||||
data-testid="agent-secondary-button"
|
||||
>{{ i18n.secondaryButton }}
|
||||
</gl-button>
|
||||
|
||||
<gl-button
|
||||
v-if="isEmptyStateModal"
|
||||
variant="confirm"
|
||||
category="primary"
|
||||
category="secondary"
|
||||
:data-track-action="$options.EVENT_ACTIONS_CLICK"
|
||||
:data-track-label="$options.EVENT_LABEL_MODAL"
|
||||
data-track-property="done"
|
||||
@click="closeModal"
|
||||
>{{ i18n.done }}
|
||||
</gl-button>
|
||||
|
||||
<gl-button
|
||||
v-if="isKasEnabledInEmptyStateModal"
|
||||
:href="repositoryPath"
|
||||
variant="confirm"
|
||||
category="primary"
|
||||
data-testid="agent-primary-button"
|
||||
>{{ i18n.primaryButton }}
|
||||
</gl-button>
|
||||
</template>
|
||||
</gl-modal>
|
||||
</template>
|
||||
|
|
|
@ -112,7 +112,7 @@ export const I18N_AGENT_MODAL = {
|
|||
"ClusterAgents|Your instance doesn't have the %{linkStart}GitLab Agent Server (KAS)%{linkEnd} set up. Ask a GitLab Administrator to install it.",
|
||||
),
|
||||
altText: s__('ClusterAgents|GitLab Agent for Kubernetes'),
|
||||
secondaryButton: s__('ClusterAgents|Go to the repository files'),
|
||||
primaryButton: s__('ClusterAgents|Go to the repository files'),
|
||||
done: __('Cancel'),
|
||||
},
|
||||
};
|
||||
|
|
|
@ -28,6 +28,7 @@ const groupsSelect = () => {
|
|||
const skipGroups = $select.data('skipGroups') || [];
|
||||
const parentGroupID = $select.data('parentId');
|
||||
const groupsFilter = $select.data('groupsFilter');
|
||||
const minAccessLevel = $select.data('minAccessLevel');
|
||||
|
||||
$select.select2({
|
||||
placeholder: __('Search for a group'),
|
||||
|
@ -45,6 +46,7 @@ const groupsSelect = () => {
|
|||
page,
|
||||
per_page: window.GROUP_SELECT_PER_PAGE,
|
||||
all_available: allAvailable,
|
||||
min_access_level: minAccessLevel,
|
||||
};
|
||||
},
|
||||
results(data, page) {
|
||||
|
|
|
@ -24,6 +24,10 @@ export default {
|
|||
prop: 'selectedGroup',
|
||||
},
|
||||
props: {
|
||||
accessLevels: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
groupsFilter: {
|
||||
type: String,
|
||||
required: false,
|
||||
|
@ -50,6 +54,13 @@ export default {
|
|||
isFetchResultEmpty() {
|
||||
return this.groups.length === 0;
|
||||
},
|
||||
defaultFetchOptions() {
|
||||
return {
|
||||
exclude_internal: true,
|
||||
active: true,
|
||||
min_access_level: this.accessLevels.Guest,
|
||||
};
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
searchTerm() {
|
||||
|
@ -84,13 +95,9 @@ export default {
|
|||
fetchGroups() {
|
||||
switch (this.groupsFilter) {
|
||||
case GROUP_FILTERS.DESCENDANT_GROUPS:
|
||||
return getDescendentGroups(
|
||||
this.parentGroupId,
|
||||
this.searchTerm,
|
||||
this.$options.defaultFetchOptions,
|
||||
);
|
||||
return getDescendentGroups(this.parentGroupId, this.searchTerm, this.defaultFetchOptions);
|
||||
default:
|
||||
return getGroups(this.searchTerm, this.$options.defaultFetchOptions);
|
||||
return getGroups(this.searchTerm, this.defaultFetchOptions);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
@ -99,10 +106,6 @@ export default {
|
|||
searchPlaceholder: s__('GroupSelect|Search groups'),
|
||||
emptySearchResult: s__('GroupSelect|No matching results'),
|
||||
},
|
||||
defaultFetchOptions: {
|
||||
exclude_internal: true,
|
||||
active: true,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
|
|
|
@ -428,6 +428,7 @@ export default {
|
|||
<group-select
|
||||
v-if="isInviteGroup"
|
||||
v-model="groupToBeSharedWith"
|
||||
:access-levels="accessLevels"
|
||||
:groups-filter="groupSelectFilter"
|
||||
:parent-group-id="groupSelectParentId"
|
||||
@input="handleMembersTokenSelectClear"
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
import initProjectNew from '~/projects/project_new';
|
||||
|
||||
initProjectNew.bindEvents();
|
|
@ -300,7 +300,7 @@ export default {
|
|||
v-gl-tooltip
|
||||
:title="
|
||||
__(
|
||||
'Pipelines for merge requests are configured. A detached pipeline runs in the context of the merge request, and not against the merged result. Learn more in the documentation for Pipelines for Merged Results.',
|
||||
'Merge request pipelines are configured. A detached pipeline runs in the context of the merge request, and not against the merged result. Learn more in the documentation for merge request pipelines.',
|
||||
)
|
||||
"
|
||||
variant="info"
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import $ from 'jquery';
|
||||
import { debounce } from 'lodash';
|
||||
import DEFAULT_PROJECT_TEMPLATES from 'ee_else_ce/projects/default_project_templates';
|
||||
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '../lib/utils/constants';
|
||||
import axios from '../lib/utils/axios_utils';
|
||||
import {
|
||||
convertToTitleCase,
|
||||
|
@ -13,20 +14,26 @@ let hasUserDefinedProjectPath = false;
|
|||
let hasUserDefinedProjectName = false;
|
||||
const invalidInputClass = 'gl-field-error-outline';
|
||||
|
||||
const cancelSource = axios.CancelToken.source();
|
||||
const endpoint = `${gon.relative_url_root}/import/url/validate`;
|
||||
let importCredentialsValidationPromise = null;
|
||||
const validateImportCredentials = (url, user, password) => {
|
||||
const endpoint = `${gon.relative_url_root}/import/url/validate`;
|
||||
return axios
|
||||
.post(endpoint, {
|
||||
url,
|
||||
user,
|
||||
password,
|
||||
})
|
||||
cancelSource.cancel();
|
||||
importCredentialsValidationPromise = axios
|
||||
.post(endpoint, { url, user, password }, { cancelToken: cancelSource.cancel() })
|
||||
.then(({ data }) => data)
|
||||
.catch(() => ({
|
||||
// intentionally reporting success in case of validation error
|
||||
// we do not want to block users from trying import in case of validation exception
|
||||
success: true,
|
||||
}));
|
||||
.catch((thrown) =>
|
||||
axios.isCancel(thrown)
|
||||
? {
|
||||
cancelled: true,
|
||||
}
|
||||
: {
|
||||
// intentionally reporting success in case of validation error
|
||||
// we do not want to block users from trying import in case of validation exception
|
||||
success: true,
|
||||
},
|
||||
);
|
||||
return importCredentialsValidationPromise;
|
||||
};
|
||||
|
||||
const onProjectNameChange = ($projectNameInput, $projectPathInput) => {
|
||||
|
@ -72,7 +79,7 @@ const deriveProjectPathFromUrl = ($projectImportUrl) => {
|
|||
.parents('.toggle-import-form')
|
||||
.find('#project_path');
|
||||
|
||||
if (hasUserDefinedProjectPath) {
|
||||
if (hasUserDefinedProjectPath || $currentProjectPath.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -114,7 +121,7 @@ const bindEvents = () => {
|
|||
const $projectImportUrlUser = $('#project_import_url_user');
|
||||
const $projectImportUrlPassword = $('#project_import_url_password');
|
||||
const $projectImportUrlError = $('.js-import-url-error');
|
||||
const $projectImportForm = $('.project-import form');
|
||||
const $projectImportForm = $('form.js-project-import');
|
||||
const $projectPath = $('.tab-pane.active #project_path');
|
||||
const $useTemplateBtn = $('.template-button > input');
|
||||
const $projectFieldsForm = $('.project-fields-form');
|
||||
|
@ -124,7 +131,7 @@ const bindEvents = () => {
|
|||
const $projectTemplateButtons = $('.project-templates-buttons');
|
||||
const $projectName = $('.tab-pane.active #project_name');
|
||||
|
||||
if ($newProjectForm.length !== 1) {
|
||||
if ($newProjectForm.length !== 1 && $projectImportForm.length !== 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -168,20 +175,28 @@ const bindEvents = () => {
|
|||
$projectPath.val($projectPath.val().trim());
|
||||
});
|
||||
|
||||
const updateUrlPathWarningVisibility = debounce(async () => {
|
||||
const { success: isUrlValid } = await validateImportCredentials(
|
||||
const updateUrlPathWarningVisibility = async () => {
|
||||
const { success: isUrlValid, cancelled } = await validateImportCredentials(
|
||||
$projectImportUrl.val(),
|
||||
$projectImportUrlUser.val(),
|
||||
$projectImportUrlPassword.val(),
|
||||
);
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
$projectImportUrl.toggleClass(invalidInputClass, !isUrlValid);
|
||||
$projectImportUrlError.toggleClass('hide', isUrlValid);
|
||||
}, 500);
|
||||
};
|
||||
const debouncedUpdateUrlPathWarningVisibility = debounce(
|
||||
updateUrlPathWarningVisibility,
|
||||
DEFAULT_DEBOUNCE_AND_THROTTLE_MS,
|
||||
);
|
||||
|
||||
let isProjectImportUrlDirty = false;
|
||||
$projectImportUrl.on('blur', () => {
|
||||
isProjectImportUrlDirty = true;
|
||||
updateUrlPathWarningVisibility();
|
||||
debouncedUpdateUrlPathWarningVisibility();
|
||||
});
|
||||
$projectImportUrl.on('keyup', () => {
|
||||
deriveProjectPathFromUrl($projectImportUrl);
|
||||
|
@ -190,17 +205,33 @@ const bindEvents = () => {
|
|||
[$projectImportUrl, $projectImportUrlUser, $projectImportUrlPassword].forEach(($f) => {
|
||||
$f.on('input', () => {
|
||||
if (isProjectImportUrlDirty) {
|
||||
updateUrlPathWarningVisibility();
|
||||
debouncedUpdateUrlPathWarningVisibility();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$projectImportForm.on('submit', (e) => {
|
||||
$projectImportForm.on('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (importCredentialsValidationPromise === null) {
|
||||
// we didn't validate credentials yet
|
||||
debouncedUpdateUrlPathWarningVisibility.cancel();
|
||||
updateUrlPathWarningVisibility();
|
||||
}
|
||||
|
||||
const submitBtn = $projectImportForm.find('input[type="submit"]');
|
||||
|
||||
submitBtn.disable();
|
||||
await importCredentialsValidationPromise;
|
||||
submitBtn.enable();
|
||||
|
||||
const $invalidFields = $projectImportForm.find(`.${invalidInputClass}`);
|
||||
if ($invalidFields.length > 0) {
|
||||
$invalidFields[0].focus();
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
} else {
|
||||
// calling .submit() on HTMLFormElement does not trigger 'submit' event
|
||||
// We are using this behavior to bypass this handler and avoid infinite loop
|
||||
$projectImportForm[0].submit();
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -1,11 +1,16 @@
|
|||
<script>
|
||||
import { GlBanner } from '@gitlab/ui';
|
||||
import { s__ } from '~/locale';
|
||||
import Tracking from '~/tracking';
|
||||
|
||||
export const SECURITY_UPGRADE_BANNER = 'security_upgrade_banner';
|
||||
export const UPGRADE_OR_FREE_TRIAL = 'upgrade_or_free_trial';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlBanner,
|
||||
},
|
||||
mixins: [Tracking.mixin({ property: SECURITY_UPGRADE_BANNER })],
|
||||
inject: ['upgradePath'],
|
||||
i18n: {
|
||||
title: s__('SecurityConfiguration|Secure your project'),
|
||||
|
@ -22,6 +27,17 @@ export default {
|
|||
],
|
||||
buttonText: s__('SecurityConfiguration|Upgrade or start a free trial'),
|
||||
},
|
||||
mounted() {
|
||||
this.track('display_banner', { label: SECURITY_UPGRADE_BANNER });
|
||||
},
|
||||
methods: {
|
||||
bannerClosed() {
|
||||
this.track('dismiss_banner', { label: SECURITY_UPGRADE_BANNER });
|
||||
},
|
||||
bannerButtonClicked() {
|
||||
this.track('click_button', { label: UPGRADE_OR_FREE_TRIAL });
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
@ -31,6 +47,8 @@ export default {
|
|||
:button-text="$options.i18n.buttonText"
|
||||
:button-link="upgradePath"
|
||||
variant="introduction"
|
||||
@close="bannerClosed"
|
||||
@primary="bannerButtonClicked"
|
||||
v-on="$listeners"
|
||||
>
|
||||
<p>{{ $options.i18n.bodyStart }}</p>
|
||||
|
|
|
@ -148,7 +148,11 @@ class UsersController < ApplicationController
|
|||
end
|
||||
|
||||
def exists
|
||||
render json: { exists: !!Namespace.find_by_path_or_name(params[:username]) }
|
||||
if Gitlab::CurrentSettings.signup_enabled? || current_user
|
||||
render json: { exists: !!Namespace.find_by_path_or_name(params[:username]) }
|
||||
else
|
||||
render json: { error: _('You must be authenticated to access this path.') }, status: :unauthorized
|
||||
end
|
||||
end
|
||||
|
||||
def follow
|
||||
|
|
|
@ -82,7 +82,7 @@
|
|||
|
||||
|
||||
.js-toggle-content.toggle-import-form{ class: ('hide' if active_tab != 'import') }
|
||||
= form_for @project, html: { class: 'new_project gl-show-field-errors' } do |f|
|
||||
= form_for @project, html: { class: 'new_project gl-show-field-errors js-project-import' } do |f|
|
||||
%hr
|
||||
= render "shared/import_form", f: f
|
||||
= render 'projects/new_project_fields', f: f, project_name_id: "import-url-name", hide_init_with_readme: true, track_label: track_label
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
.text-secondary
|
||||
- configuring_pipelines_for_merge_requests_help_link_url = help_page_path('ci/pipelines/merge_request_pipelines.md', anchor: 'prerequisites')
|
||||
- configuring_pipelines_for_merge_requests_help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: configuring_pipelines_for_merge_requests_help_link_url }
|
||||
= s_('ProjectSettings|To enable this feature, configure pipelines. %{link_start}How to configure pipelines for merge requests?%{link_end}').html_safe % { link_start: configuring_pipelines_for_merge_requests_help_link_start, link_end: '</a>'.html_safe }
|
||||
= s_('ProjectSettings|To enable this feature, configure pipelines. %{link_start}How to configure merge request pipelines?%{link_end}').html_safe % { link_start: configuring_pipelines_for_merge_requests_help_link_start, link_end: '</a>'.html_safe }
|
||||
.form-check.mb-2
|
||||
.gl-pl-6
|
||||
= form.check_box :allow_merge_on_skipped_pipeline, class: 'form-check-input'
|
||||
|
|
|
@ -12,8 +12,8 @@
|
|||
:preserve
|
||||
#{h(@project.import_state.last_error)}
|
||||
|
||||
= form_for @project, url: project_import_path(@project), method: :post do |f|
|
||||
= form_for @project, url: project_import_path(@project), method: :post, html: { class: 'js-project-import' } do |f|
|
||||
= render "shared/import_form", f: f
|
||||
|
||||
.form-actions
|
||||
= f.submit 'Start import', class: "gl-button btn btn-confirm"
|
||||
= f.submit 'Start import', class: "gl-button btn btn-confirm", data: { disable_with: false }
|
||||
|
|
|
@ -38,7 +38,7 @@
|
|||
- popover_content_text = _('Learn more about Auto DevOps')
|
||||
= gl_badge_tag s_('Pipelines|Auto DevOps'), { variant: :info, size: :sm }, { class: 'js-pipeline-url-autodevops', href: "#", tabindex: "0", role: "button", data: { container: 'body', toggle: 'popover', placement: 'top', html: 'true', triggers: 'focus', title: "<div class='gl-font-weight-normal gl-line-height-normal'>#{popover_title_text}</div>", content: "<a href='#{popover_content_url}' target='_blank' rel='noopener noreferrer nofollow'>#{popover_content_text}</a>" } }
|
||||
- if @pipeline.detached_merge_request_pipeline?
|
||||
= gl_badge_tag s_('Pipelines|detached'), { variant: :info, size: :sm }, { class: 'js-pipeline-url-mergerequest has-tooltip', title: _('Pipelines for merge requests are configured. A detached pipeline runs in the context of the merge request, and not against the merged result. Learn more in the documentation for Pipelines for Merged Results.') }
|
||||
= gl_badge_tag s_('Pipelines|detached'), { variant: :info, size: :sm }, { class: 'js-pipeline-url-mergerequest has-tooltip', title: _('Merge request pipelines are configured. A detached pipeline runs in the context of the merge request, and not against the merged result. Learn more in the documentation for merge request pipelines.') }
|
||||
- if @pipeline.stuck?
|
||||
= gl_badge_tag s_('Pipelines|stuck'), { variant: :warning, size: :sm }, { class: 'js-pipeline-url-stuck has-tooltip' }
|
||||
|
||||
|
|
|
@ -61,7 +61,7 @@
|
|||
default_access_level: ProjectGroupLink.default_access,
|
||||
group_link_field: 'link_group_id',
|
||||
group_access_field: 'link_group_access',
|
||||
groups_select_tag_data: { skip_groups: @skip_groups }
|
||||
groups_select_tag_data: { min_access_level: Gitlab::Access::GUEST, skip_groups: @skip_groups }
|
||||
- elsif !membership_locked?
|
||||
.invite-member
|
||||
= render 'shared/members/invite_member',
|
||||
|
@ -78,7 +78,7 @@
|
|||
submit_url: project_group_links_path(@project),
|
||||
group_link_field: 'link_group_id',
|
||||
group_access_field: 'link_group_access',
|
||||
groups_select_tag_data: { skip_groups: @skip_groups }
|
||||
groups_select_tag_data: { min_access_level: Gitlab::Access::GUEST, skip_groups: @skip_groups }
|
||||
.js-project-members-list-app{ data: { members_data: project_members_app_data_json(@project,
|
||||
members: @project_members,
|
||||
group_links: @group_links,
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddDependencyProxySizeToNamespaceStatistics < Gitlab::Database::Migration[1.0]
|
||||
def change
|
||||
add_column :namespace_statistics, :dependency_proxy_size, :bigint, default: 0, null: false
|
||||
end
|
||||
end
|
|
@ -0,0 +1,19 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class RemoveProjectsCiProjectMonthlyUsagesProjectIdFk < Gitlab::Database::Migration[1.0]
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
return unless foreign_key_exists?(:ci_project_monthly_usages, :projects, name: "fk_rails_508bcd4aa6")
|
||||
|
||||
with_lock_retries do
|
||||
execute('LOCK projects, ci_project_monthly_usages IN ACCESS EXCLUSIVE MODE') if transaction_open?
|
||||
|
||||
remove_foreign_key_if_exists(:ci_project_monthly_usages, :projects, name: "fk_rails_508bcd4aa6")
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
add_concurrent_foreign_key(:ci_project_monthly_usages, :projects, name: "fk_rails_508bcd4aa6", column: :project_id, target_column: :id, on_delete: :cascade)
|
||||
end
|
||||
end
|
|
@ -0,0 +1 @@
|
|||
18b3ef459c3633ebd4b418b4436d3d50b0dc697fa7c4ef4d2e1f62b01d656bce
|
|
@ -0,0 +1 @@
|
|||
4b1dad4fc34717c9b89a770e4e0682b0cee7d421da68223011bb9fde9460d1f8
|
|
@ -16544,7 +16544,8 @@ CREATE TABLE namespace_statistics (
|
|||
shared_runners_seconds integer DEFAULT 0 NOT NULL,
|
||||
shared_runners_seconds_last_reset timestamp without time zone,
|
||||
storage_size bigint DEFAULT 0 NOT NULL,
|
||||
wiki_size bigint DEFAULT 0 NOT NULL
|
||||
wiki_size bigint DEFAULT 0 NOT NULL,
|
||||
dependency_proxy_size bigint DEFAULT 0 NOT NULL
|
||||
);
|
||||
|
||||
CREATE SEQUENCE namespace_statistics_id_seq
|
||||
|
@ -30487,9 +30488,6 @@ ALTER TABLE ONLY resource_iteration_events
|
|||
ALTER TABLE ONLY status_page_settings
|
||||
ADD CONSTRAINT fk_rails_506e5ba391 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY ci_project_monthly_usages
|
||||
ADD CONSTRAINT fk_rails_508bcd4aa6 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY project_repository_storage_moves
|
||||
ADD CONSTRAINT fk_rails_5106dbd44a FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
|
||||
|
||||
|
|
|
@ -8,9 +8,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/332747) in GitLab 14.5 [with a flag](../administration/feature_flags.md) named `ff_external_audit_events_namespace`. Disabled by default.
|
||||
> - [Enabled on GitLab.com and by default on self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/338939) in GitLab 14.7.
|
||||
|
||||
FLAG:
|
||||
On self-managed GitLab, by default this feature is available. To hide the feature per group, ask an administrator to [disable the feature flag](../administration/feature_flags.md) named `ff_external_audit_events_namespace`. On GitLab.com, this feature is available.
|
||||
> - [Feature flag `ff_external_audit_events_namespace`](https://gitlab.com/gitlab-org/gitlab/-/issues/349588) removed in GitLab 14.8.
|
||||
|
||||
Event streaming allows owners of top-level groups to set an HTTP endpoint to receive **all** audit events about the group, and its
|
||||
subgroups and projects as structured JSON.
|
||||
|
|
|
@ -4356,6 +4356,27 @@ Input type: `TimelineEventDestroyInput`
|
|||
| <a id="mutationtimelineeventdestroyerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
|
||||
| <a id="mutationtimelineeventdestroytimelineevent"></a>`timelineEvent` | [`TimelineEventType`](#timelineeventtype) | Timeline event. |
|
||||
|
||||
### `Mutation.timelineEventUpdate`
|
||||
|
||||
Input type: `TimelineEventUpdateInput`
|
||||
|
||||
#### Arguments
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="mutationtimelineeventupdateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
|
||||
| <a id="mutationtimelineeventupdateid"></a>`id` | [`IncidentManagementTimelineEventID!`](#incidentmanagementtimelineeventid) | ID of the timeline event to update. |
|
||||
| <a id="mutationtimelineeventupdatenote"></a>`note` | [`String`](#string) | Text note of the timeline event. |
|
||||
| <a id="mutationtimelineeventupdateoccurredat"></a>`occurredAt` | [`Time`](#time) | Timestamp when the event occurred. |
|
||||
|
||||
#### Fields
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="mutationtimelineeventupdateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
|
||||
| <a id="mutationtimelineeventupdateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
|
||||
| <a id="mutationtimelineeventupdatetimelineevent"></a>`timelineEvent` | [`TimelineEventType`](#timelineeventtype) | Timeline event. |
|
||||
|
||||
### `Mutation.todoCreate`
|
||||
|
||||
Input type: `TodoCreateInput`
|
||||
|
|
|
@ -144,6 +144,14 @@ end
|
|||
After the feature flag is removed, clean up the resource class and delete the variable.
|
||||
All methods should use the condition procedures of the now-default state.
|
||||
|
||||
## Managing flakiness due to caching
|
||||
|
||||
All application settings, and all feature flags, are cached inside GitLab for one minute.
|
||||
All caching is disabled during testing, except on static environments.
|
||||
|
||||
When a test changes a feature flag, it can cause flaky behavior if elements are visible only with an
|
||||
active feature flag. To circumvent this behavior, add a wait for elements behind a feature flag.
|
||||
|
||||
## Running a scenario with a feature flag enabled
|
||||
|
||||
It's also possible to run an entire scenario with a feature flag enabled, without having to edit
|
||||
|
|
|
@ -0,0 +1,134 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module BulkImports
|
||||
module Common
|
||||
module Pipelines
|
||||
class LfsObjectsPipeline
|
||||
include Pipeline
|
||||
|
||||
def extract(_context)
|
||||
download_service.execute
|
||||
decompression_service.execute
|
||||
extraction_service.execute
|
||||
|
||||
file_paths = Dir.glob(File.join(tmpdir, '*'))
|
||||
|
||||
BulkImports::Pipeline::ExtractedData.new(data: file_paths)
|
||||
end
|
||||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def load(_context, file_path)
|
||||
Gitlab::Utils.check_path_traversal!(file_path)
|
||||
Gitlab::Utils.check_allowed_absolute_path!(file_path, [Dir.tmpdir])
|
||||
|
||||
return if tar_filepath?(file_path)
|
||||
return if lfs_json_filepath?(file_path)
|
||||
return if File.directory?(file_path)
|
||||
return if File.lstat(file_path).symlink?
|
||||
|
||||
size = File.size(file_path)
|
||||
oid = LfsObject.calculate_oid(file_path)
|
||||
|
||||
lfs_object = LfsObject.find_or_initialize_by(oid: oid, size: size)
|
||||
lfs_object.file = File.open(file_path) unless lfs_object.file&.exists?
|
||||
lfs_object.save! if lfs_object.changed?
|
||||
|
||||
repository_types(oid)&.each do |type|
|
||||
create_lfs_objects_project(lfs_object, type)
|
||||
end
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
def after_run(_)
|
||||
FileUtils.remove_entry(tmpdir) if Dir.exist?(tmpdir)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def download_service
|
||||
BulkImports::FileDownloadService.new(
|
||||
configuration: context.configuration,
|
||||
relative_url: context.entity.relation_download_url_path(relation),
|
||||
tmpdir: tmpdir,
|
||||
filename: targz_filename
|
||||
)
|
||||
end
|
||||
|
||||
def decompression_service
|
||||
BulkImports::FileDecompressionService.new(tmpdir: tmpdir, filename: targz_filename)
|
||||
end
|
||||
|
||||
def extraction_service
|
||||
BulkImports::ArchiveExtractionService.new(tmpdir: tmpdir, filename: tar_filename)
|
||||
end
|
||||
|
||||
def lfs_json
|
||||
@lfs_json ||= Gitlab::Json.parse(File.read(lfs_json_filepath))
|
||||
rescue StandardError
|
||||
raise BulkImports::Error, 'LFS Objects JSON read failed'
|
||||
end
|
||||
|
||||
def tmpdir
|
||||
@tmpdir ||= Dir.mktmpdir('bulk_imports')
|
||||
end
|
||||
|
||||
def relation
|
||||
BulkImports::FileTransfer::ProjectConfig::LFS_OBJECTS_RELATION
|
||||
end
|
||||
|
||||
def tar_filename
|
||||
"#{relation}.tar"
|
||||
end
|
||||
|
||||
def targz_filename
|
||||
"#{tar_filename}.gz"
|
||||
end
|
||||
|
||||
def lfs_json_filepath?(file_path)
|
||||
file_path == lfs_json_filepath
|
||||
end
|
||||
|
||||
def tar_filepath?(file_path)
|
||||
File.join(tmpdir, tar_filename) == file_path
|
||||
end
|
||||
|
||||
def lfs_json_filepath
|
||||
File.join(tmpdir, "#{relation}.json")
|
||||
end
|
||||
|
||||
def create_lfs_objects_project(lfs_object, repository_type)
|
||||
return unless allowed_repository_types.include?(repository_type)
|
||||
|
||||
lfs_objects_project = LfsObjectsProject.create(
|
||||
project: portable,
|
||||
lfs_object: lfs_object,
|
||||
repository_type: repository_type
|
||||
)
|
||||
|
||||
return if lfs_objects_project.persisted?
|
||||
|
||||
logger.warn(
|
||||
project_id: portable.id,
|
||||
message: 'Failed to save lfs objects project',
|
||||
errors: lfs_objects_project.errors.full_messages.to_sentence,
|
||||
**Gitlab::ApplicationContext.current
|
||||
)
|
||||
end
|
||||
|
||||
def repository_types(oid)
|
||||
types = lfs_json[oid]
|
||||
|
||||
return [] unless types
|
||||
return [] unless types.is_a?(Array)
|
||||
|
||||
# only return allowed repository types
|
||||
types.uniq & allowed_repository_types
|
||||
end
|
||||
|
||||
def allowed_repository_types
|
||||
@allowed_repository_types ||= LfsObjectsProject.repository_types.values.push(nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -87,6 +87,10 @@ module BulkImports
|
|||
pipeline: BulkImports::Common::Pipelines::UploadsPipeline,
|
||||
stage: 5
|
||||
},
|
||||
lfs_objects: {
|
||||
pipeline: BulkImports::Common::Pipelines::LfsObjectsPipeline,
|
||||
stage: 5
|
||||
},
|
||||
auto_devops: {
|
||||
pipeline: BulkImports::Projects::Pipelines::AutoDevopsPipeline,
|
||||
stage: 5
|
||||
|
|
|
@ -129,6 +129,10 @@ packages_package_file_build_infos:
|
|||
- table: ci_pipelines
|
||||
column: pipeline_id
|
||||
on_delete: async_nullify
|
||||
ci_project_monthly_usages:
|
||||
- table: projects
|
||||
column: project_id
|
||||
on_delete: async_delete
|
||||
pages_deployments:
|
||||
- table: ci_builds
|
||||
column: ci_build_id
|
||||
|
|
|
@ -9811,12 +9811,24 @@ msgstr ""
|
|||
msgid "CorpusManagement|Corpus are used in fuzz testing as mutation source to Improve future testing."
|
||||
msgstr ""
|
||||
|
||||
msgid "CorpusManagement|Corpus file"
|
||||
msgstr ""
|
||||
|
||||
msgid "CorpusManagement|Corpus files must be in *.zip format. Maximum 5 GB"
|
||||
msgstr ""
|
||||
|
||||
msgid "CorpusManagement|Corpus name"
|
||||
msgstr ""
|
||||
|
||||
msgid "CorpusManagement|Currently, there are no uploaded or generated corpuses."
|
||||
msgstr ""
|
||||
|
||||
msgid "CorpusManagement|File too large, Maximum 5 GB"
|
||||
msgstr ""
|
||||
|
||||
msgid "CorpusManagement|Filename can contain only lowercase letters (a-z), uppercase letter (A-Z), numbers (0-9), dots (.), hyphens (-), or underscores (_)."
|
||||
msgstr ""
|
||||
|
||||
msgid "CorpusManagement|Fuzz testing corpus management"
|
||||
msgstr ""
|
||||
|
||||
|
@ -9829,9 +9841,6 @@ msgstr ""
|
|||
msgid "CorpusManagement|Latest Job:"
|
||||
msgstr ""
|
||||
|
||||
msgid "CorpusManagement|New corpus needs to be a upload in *.zip format. Maximum 5GB"
|
||||
msgstr ""
|
||||
|
||||
msgid "CorpusManagement|New upload"
|
||||
msgstr ""
|
||||
|
||||
|
@ -22372,6 +22381,9 @@ msgstr ""
|
|||
msgid "Merge request events"
|
||||
msgstr ""
|
||||
|
||||
msgid "Merge request pipelines are configured. A detached pipeline runs in the context of the merge request, and not against the merged result. Learn more in the documentation for merge request pipelines."
|
||||
msgstr ""
|
||||
|
||||
msgid "Merge request reports"
|
||||
msgstr ""
|
||||
|
||||
|
@ -26201,9 +26213,6 @@ msgstr ""
|
|||
msgid "Pipelines charts"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pipelines for merge requests are configured. A detached pipeline runs in the context of the merge request, and not against the merged result. Learn more in the documentation for Pipelines for Merged Results."
|
||||
msgstr ""
|
||||
|
||||
msgid "Pipelines settings for '%{project_name}' were successfully updated."
|
||||
msgstr ""
|
||||
|
||||
|
@ -28232,7 +28241,7 @@ msgstr ""
|
|||
msgid "ProjectSettings|This setting will be applied to all projects unless overridden by an admin."
|
||||
msgstr ""
|
||||
|
||||
msgid "ProjectSettings|To enable this feature, configure pipelines. %{link_start}How to configure pipelines for merge requests?%{link_end}"
|
||||
msgid "ProjectSettings|To enable this feature, configure pipelines. %{link_start}How to configure merge request pipelines?%{link_end}"
|
||||
msgstr ""
|
||||
|
||||
msgid "ProjectSettings|Transfer project"
|
||||
|
@ -28274,7 +28283,7 @@ msgstr ""
|
|||
msgid "ProjectSettings|What are badges?"
|
||||
msgstr ""
|
||||
|
||||
msgid "ProjectSettings|When pipelines for merge requests are enabled in the CI/CD configuration file, pipelines validate the combined results of the source and target branches. %{link_start}How to configure pipelines for merge requests?%{link_end}"
|
||||
msgid "ProjectSettings|When merge request pipelines are enabled in the CI/CD configuration file, pipelines validate the combined results of the source and target branches. %{link_start}How to configure merge request pipelines?%{link_end}"
|
||||
msgstr ""
|
||||
|
||||
msgid "ProjectSettings|When there is a merge conflict, the user is given the option to rebase."
|
||||
|
@ -41145,6 +41154,9 @@ msgstr ""
|
|||
msgid "You may close the milestone now."
|
||||
msgstr ""
|
||||
|
||||
msgid "You must be authenticated to access this path."
|
||||
msgstr ""
|
||||
|
||||
msgid "You must be logged in to search across all of GitLab"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ module RuboCop
|
|||
If there is no relevant metadata, please disable the cop with a comment
|
||||
explaining this.
|
||||
|
||||
Read more about it https://docs.gitlab.com/ee/development/sidekiq_style_guide.html#worker-context
|
||||
Read more about it https://docs.gitlab.com/ee/development/sidekiq/logging.html#worker-context
|
||||
MSG
|
||||
|
||||
def_node_matcher :includes_cronjob_queue?, <<~PATTERN
|
||||
|
|
|
@ -214,31 +214,88 @@ RSpec.describe 'Project > Members > Invite group', :js do
|
|||
end
|
||||
|
||||
context 'for a project in a nested group' do
|
||||
let(:group) { create(:group) }
|
||||
let!(:nested_group) { create(:group, parent: group) }
|
||||
let!(:group_to_share_with) { create(:group) }
|
||||
let!(:project) { create(:project, namespace: nested_group) }
|
||||
let!(:parent_group) { create(:group, :public) }
|
||||
let!(:public_subgroup) { create(:group, :public, parent: parent_group) }
|
||||
let!(:public_sub_subgroup) { create(:group, :public, parent: public_subgroup) }
|
||||
let!(:private_subgroup) { create(:group, :private, parent: parent_group) }
|
||||
let!(:project) { create(:project, :public, namespace: public_subgroup) }
|
||||
|
||||
let!(:membership_group) { create(:group, :public) }
|
||||
|
||||
before do
|
||||
project.add_maintainer(maintainer)
|
||||
membership_group.add_guest(maintainer)
|
||||
|
||||
sign_in(maintainer)
|
||||
group.add_maintainer(maintainer)
|
||||
group_to_share_with.add_maintainer(maintainer)
|
||||
end
|
||||
|
||||
# This behavior should be changed to exclude the ancestor and project
|
||||
# group from the options once issue is fixed for the modal:
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/issues/329835
|
||||
it 'the groups dropdown does show ancestors and the project group' do
|
||||
visit project_project_members_path(project)
|
||||
context 'when invite_members_group_modal feature enabled' do
|
||||
it 'does not show the groups inherited from projects' do
|
||||
visit project_project_members_path(project)
|
||||
|
||||
click_on 'Invite a group'
|
||||
click_on 'Select a group'
|
||||
wait_for_requests
|
||||
click_on 'Invite a group'
|
||||
click_on 'Select a group'
|
||||
wait_for_requests
|
||||
|
||||
expect(page).to have_button(group_to_share_with.name)
|
||||
expect(page).to have_button(group.name)
|
||||
expect(page).to have_button(nested_group.name)
|
||||
expect(page).to have_button(membership_group.name)
|
||||
expect(page).not_to have_button(parent_group.name)
|
||||
expect(page).not_to have_button(public_subgroup.name)
|
||||
expect(page).not_to have_button(public_sub_subgroup.name)
|
||||
expect(page).not_to have_button(private_subgroup.name)
|
||||
end
|
||||
|
||||
# This behavior should be changed to exclude the ancestor and project
|
||||
# group from the options once issue is fixed for the modal:
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/issues/329835
|
||||
it 'does show ancestors and the project group' do
|
||||
parent_group.add_maintainer(maintainer)
|
||||
|
||||
visit project_project_members_path(project)
|
||||
|
||||
click_on 'Invite a group'
|
||||
click_on 'Select a group'
|
||||
wait_for_requests
|
||||
|
||||
expect(page).to have_button(membership_group.name)
|
||||
expect(page).to have_button(parent_group.name)
|
||||
expect(page).to have_button(public_subgroup.name)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when invite_members_group_modal feature disabled' do
|
||||
let(:group_invite_dropdown) { find('#select2-results-2') }
|
||||
|
||||
before do
|
||||
stub_feature_flags(invite_members_group_modal: false)
|
||||
end
|
||||
|
||||
it 'does not show the groups inherited from projects' do
|
||||
visit project_project_members_path(project)
|
||||
|
||||
click_on 'Invite group'
|
||||
click_on 'Search for a group'
|
||||
wait_for_requests
|
||||
|
||||
expect(group_invite_dropdown).to have_text(membership_group.name)
|
||||
expect(group_invite_dropdown).not_to have_text(parent_group.name)
|
||||
expect(group_invite_dropdown).not_to have_text(public_subgroup.name)
|
||||
expect(group_invite_dropdown).not_to have_text(public_sub_subgroup.name)
|
||||
expect(group_invite_dropdown).not_to have_text(private_subgroup.name)
|
||||
end
|
||||
|
||||
it 'does not show ancestors and the project group' do
|
||||
parent_group.add_maintainer(maintainer)
|
||||
|
||||
visit project_project_members_path(project)
|
||||
|
||||
click_on 'Invite group'
|
||||
click_on 'Search for a group'
|
||||
wait_for_requests
|
||||
|
||||
expect(group_invite_dropdown).to have_text(membership_group.name)
|
||||
expect(group_invite_dropdown).not_to have_text(parent_group.name, exact: true)
|
||||
expect(group_invite_dropdown).not_to have_text(public_subgroup.name, exact: true)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -306,10 +306,24 @@ RSpec.describe 'New project', :js do
|
|||
expect(page).to have_text('There is not a valid Git repository at this URL')
|
||||
end
|
||||
|
||||
it 'reports error if repo URL is not a valid Git repository and submit button is clicked immediately' do
|
||||
stub_request(:get, "http://foo/bar/info/refs?service=git-upload-pack").to_return(status: 200, body: "not-a-git-repo")
|
||||
|
||||
fill_in 'project_import_url', with: 'http://foo/bar'
|
||||
click_on 'Create project'
|
||||
|
||||
wait_for_requests
|
||||
|
||||
expect(page).to have_text('There is not a valid Git repository at this URL')
|
||||
end
|
||||
|
||||
it 'keeps "Import project" tab open after form validation error' do
|
||||
collision_project = create(:project, name: 'test-name-collision', namespace: user.namespace)
|
||||
stub_request(:get, "http://foo/bar/info/refs?service=git-upload-pack").to_return({ status: 200,
|
||||
body: '001e# service=git-upload-pack',
|
||||
headers: { 'Content-Type': 'application/x-git-upload-pack-advertisement' } })
|
||||
|
||||
fill_in 'project_import_url', with: collision_project.http_url_to_repo
|
||||
fill_in 'project_import_url', with: 'http://foo/bar'
|
||||
fill_in 'project_name', with: collision_project.name
|
||||
|
||||
click_on 'Create project'
|
||||
|
@ -319,6 +333,38 @@ RSpec.describe 'New project', :js do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when import is initiated from project page' do
|
||||
before do
|
||||
project_without_repo = create(:project, name: 'project-without-repo', namespace: user.namespace)
|
||||
visit project_path(project_without_repo)
|
||||
click_on 'Import repository'
|
||||
end
|
||||
|
||||
it 'reports error when invalid url is provided' do
|
||||
stub_request(:get, "http://foo/bar/info/refs?service=git-upload-pack").to_return(status: 200, body: "not-a-git-repo")
|
||||
|
||||
fill_in 'project_import_url', with: 'http://foo/bar'
|
||||
|
||||
click_on 'Start import'
|
||||
wait_for_requests
|
||||
|
||||
expect(page).to have_text('There is not a valid Git repository at this URL')
|
||||
end
|
||||
|
||||
it 'initiates import when valid repo url is provided' do
|
||||
stub_request(:get, "http://foo/bar/info/refs?service=git-upload-pack").to_return({ status: 200,
|
||||
body: '001e# service=git-upload-pack',
|
||||
headers: { 'Content-Type': 'application/x-git-upload-pack-advertisement' } })
|
||||
|
||||
fill_in 'project_import_url', with: 'http://foo/bar'
|
||||
|
||||
click_on 'Start import'
|
||||
wait_for_requests
|
||||
|
||||
expect(page).to have_text('Import in progress')
|
||||
end
|
||||
end
|
||||
|
||||
context 'from GitHub' do
|
||||
before do
|
||||
first('.js-import-github').click
|
||||
|
|
|
@ -65,7 +65,7 @@ describe('InstallAgentModal', () => {
|
|||
.wrappers.find((button) => button.props('variant') === variant);
|
||||
const findActionButton = () => findButtonByVariant('confirm');
|
||||
const findCancelButton = () => findButtonByVariant('default');
|
||||
const findSecondaryButton = () => wrapper.findByTestId('agent-secondary-button');
|
||||
const findPrimaryButton = () => wrapper.findByTestId('agent-primary-button');
|
||||
const findImage = () => wrapper.findByRole('img', { alt: I18N_AGENT_MODAL.empty_state.altText });
|
||||
|
||||
const expectDisabledAttribute = (element, disabled) => {
|
||||
|
@ -293,9 +293,9 @@ describe('InstallAgentModal', () => {
|
|||
expect(findImage().attributes('src')).toBe(emptyStateImage);
|
||||
});
|
||||
|
||||
it('renders a secondary button', () => {
|
||||
expect(findSecondaryButton().isVisible()).toBe(true);
|
||||
expect(findSecondaryButton().text()).toBe(i18n.secondaryButton);
|
||||
it('renders a primary button', () => {
|
||||
expect(findPrimaryButton().isVisible()).toBe(true);
|
||||
expect(findPrimaryButton().text()).toBe(i18n.primaryButton);
|
||||
});
|
||||
|
||||
it('sends the event with the modalType', () => {
|
||||
|
@ -333,7 +333,7 @@ describe('InstallAgentModal', () => {
|
|||
});
|
||||
|
||||
it("doesn't render a secondary button", () => {
|
||||
expect(findSecondaryButton().exists()).toBe(false);
|
||||
expect(findPrimaryButton().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,8 +4,14 @@ import waitForPromises from 'helpers/wait_for_promises';
|
|||
import * as groupsApi from '~/api/groups_api';
|
||||
import GroupSelect from '~/invite_members/components/group_select.vue';
|
||||
|
||||
const accessLevels = { Guest: 10, Reporter: 20, Developer: 30, Maintainer: 40, Owner: 50 };
|
||||
|
||||
const createComponent = () => {
|
||||
return mount(GroupSelect, {});
|
||||
return mount(GroupSelect, {
|
||||
propsData: {
|
||||
accessLevels,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const group1 = { id: 1, full_name: 'Group One', avatar_url: 'test' };
|
||||
|
@ -61,6 +67,7 @@ describe('GroupSelect', () => {
|
|||
expect(groupsApi.getGroups).toHaveBeenCalledWith(group1.name, {
|
||||
active: true,
|
||||
exclude_internal: true,
|
||||
min_access_level: accessLevels.Guest,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -1,15 +1,22 @@
|
|||
import { GlBanner } from '@gitlab/ui';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import UpgradeBanner from '~/security_configuration/components/upgrade_banner.vue';
|
||||
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
|
||||
import UpgradeBanner, {
|
||||
SECURITY_UPGRADE_BANNER,
|
||||
UPGRADE_OR_FREE_TRIAL,
|
||||
} from '~/security_configuration/components/upgrade_banner.vue';
|
||||
|
||||
const upgradePath = '/upgrade';
|
||||
|
||||
describe('UpgradeBanner component', () => {
|
||||
let wrapper;
|
||||
let closeSpy;
|
||||
let primarySpy;
|
||||
let trackingSpy;
|
||||
|
||||
const createComponent = (propsData) => {
|
||||
closeSpy = jest.fn();
|
||||
primarySpy = jest.fn();
|
||||
|
||||
wrapper = shallowMountExtended(UpgradeBanner, {
|
||||
provide: {
|
||||
|
@ -18,43 +25,83 @@ describe('UpgradeBanner component', () => {
|
|||
propsData,
|
||||
listeners: {
|
||||
close: closeSpy,
|
||||
primary: primarySpy,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const findGlBanner = () => wrapper.findComponent(GlBanner);
|
||||
|
||||
const expectTracking = (action, label) => {
|
||||
return expect(trackingSpy).toHaveBeenCalledWith(undefined, action, {
|
||||
label,
|
||||
property: SECURITY_UPGRADE_BANNER,
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
trackingSpy = mockTracking(undefined, undefined, jest.spyOn);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
unmockTracking();
|
||||
});
|
||||
|
||||
it('passes the expected props to GlBanner', () => {
|
||||
expect(findGlBanner().props()).toMatchObject({
|
||||
title: UpgradeBanner.i18n.title,
|
||||
buttonText: UpgradeBanner.i18n.buttonText,
|
||||
buttonLink: upgradePath,
|
||||
describe('when the component renders', () => {
|
||||
it('tracks an event', () => {
|
||||
expect(trackingSpy).not.toHaveBeenCalled();
|
||||
|
||||
createComponent();
|
||||
|
||||
expectTracking('display_banner', SECURITY_UPGRADE_BANNER);
|
||||
});
|
||||
});
|
||||
|
||||
it('renders the list of benefits', () => {
|
||||
const wrapperText = wrapper.text();
|
||||
describe('when ready', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
trackingSpy.mockClear();
|
||||
});
|
||||
|
||||
expect(wrapperText).toContain('Immediately begin risk analysis and remediation');
|
||||
expect(wrapperText).toContain('statistics in the merge request');
|
||||
expect(wrapperText).toContain('statistics across projects');
|
||||
expect(wrapperText).toContain('Runtime security metrics');
|
||||
expect(wrapperText).toContain('More scan types, including Container Scanning,');
|
||||
});
|
||||
it('passes the expected props to GlBanner', () => {
|
||||
expect(findGlBanner().props()).toMatchObject({
|
||||
title: UpgradeBanner.i18n.title,
|
||||
buttonText: UpgradeBanner.i18n.buttonText,
|
||||
buttonLink: upgradePath,
|
||||
});
|
||||
});
|
||||
|
||||
it(`re-emits GlBanner's close event`, () => {
|
||||
expect(closeSpy).not.toHaveBeenCalled();
|
||||
it('renders the list of benefits', () => {
|
||||
const wrapperText = wrapper.text();
|
||||
|
||||
wrapper.findComponent(GlBanner).vm.$emit('close');
|
||||
expect(wrapperText).toContain('Immediately begin risk analysis and remediation');
|
||||
expect(wrapperText).toContain('statistics in the merge request');
|
||||
expect(wrapperText).toContain('statistics across projects');
|
||||
expect(wrapperText).toContain('Runtime security metrics');
|
||||
expect(wrapperText).toContain('More scan types, including Container Scanning,');
|
||||
});
|
||||
|
||||
expect(closeSpy).toHaveBeenCalledTimes(1);
|
||||
describe('when user interacts', () => {
|
||||
it(`re-emits GlBanner's close event & tracks an event`, () => {
|
||||
expect(closeSpy).not.toHaveBeenCalled();
|
||||
expect(trackingSpy).not.toHaveBeenCalled();
|
||||
|
||||
wrapper.findComponent(GlBanner).vm.$emit('close');
|
||||
|
||||
expect(closeSpy).toHaveBeenCalledTimes(1);
|
||||
expectTracking('dismiss_banner', SECURITY_UPGRADE_BANNER);
|
||||
});
|
||||
|
||||
it(`re-emits GlBanner's primary event & tracks an event`, () => {
|
||||
expect(primarySpy).not.toHaveBeenCalled();
|
||||
expect(trackingSpy).not.toHaveBeenCalled();
|
||||
|
||||
wrapper.findComponent(GlBanner).vm.$emit('primary');
|
||||
|
||||
expect(primarySpy).toHaveBeenCalledTimes(1);
|
||||
expectTracking('click_button', UPGRADE_OR_FREE_TRIAL);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,210 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe BulkImports::Common::Pipelines::LfsObjectsPipeline do
|
||||
let_it_be(:portable) { create(:project) }
|
||||
let_it_be(:oid) { 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855' }
|
||||
|
||||
let(:tmpdir) { Dir.mktmpdir }
|
||||
let(:entity) { create(:bulk_import_entity, :project_entity, project: portable, source_full_path: 'test') }
|
||||
let(:tracker) { create(:bulk_import_tracker, entity: entity) }
|
||||
let(:context) { BulkImports::Pipeline::Context.new(tracker) }
|
||||
let(:lfs_dir_path) { tmpdir }
|
||||
let(:lfs_json_file_path) { File.join(lfs_dir_path, 'lfs_objects.json')}
|
||||
let(:lfs_file_path) { File.join(lfs_dir_path, oid)}
|
||||
|
||||
subject(:pipeline) { described_class.new(context) }
|
||||
|
||||
before do
|
||||
FileUtils.mkdir_p(lfs_dir_path)
|
||||
FileUtils.touch(lfs_json_file_path)
|
||||
FileUtils.touch(lfs_file_path)
|
||||
File.write(lfs_json_file_path, { oid => [0, 1, 2, nil] }.to_json )
|
||||
|
||||
allow(Dir).to receive(:mktmpdir).with('bulk_imports').and_return(tmpdir)
|
||||
end
|
||||
|
||||
after do
|
||||
FileUtils.remove_entry(tmpdir) if Dir.exist?(tmpdir)
|
||||
end
|
||||
|
||||
describe '#run' do
|
||||
it 'imports lfs objects into destination project and removes tmpdir' do
|
||||
allow(pipeline)
|
||||
.to receive(:extract)
|
||||
.and_return(BulkImports::Pipeline::ExtractedData.new(data: [lfs_json_file_path, lfs_file_path]))
|
||||
|
||||
pipeline.run
|
||||
|
||||
expect(portable.lfs_objects.count).to eq(1)
|
||||
expect(portable.lfs_objects_projects.count).to eq(4)
|
||||
expect(Dir.exist?(tmpdir)).to eq(false)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#extract' do
|
||||
it 'downloads & extracts lfs objects filepaths' do
|
||||
download_service = instance_double("BulkImports::FileDownloadService")
|
||||
decompression_service = instance_double("BulkImports::FileDecompressionService")
|
||||
extraction_service = instance_double("BulkImports::ArchiveExtractionService")
|
||||
|
||||
expect(BulkImports::FileDownloadService)
|
||||
.to receive(:new)
|
||||
.with(
|
||||
configuration: context.configuration,
|
||||
relative_url: "/#{entity.pluralized_name}/test/export_relations/download?relation=lfs_objects",
|
||||
tmpdir: tmpdir,
|
||||
filename: 'lfs_objects.tar.gz')
|
||||
.and_return(download_service)
|
||||
expect(BulkImports::FileDecompressionService).to receive(:new).with(tmpdir: tmpdir, filename: 'lfs_objects.tar.gz').and_return(decompression_service)
|
||||
expect(BulkImports::ArchiveExtractionService).to receive(:new).with(tmpdir: tmpdir, filename: 'lfs_objects.tar').and_return(extraction_service)
|
||||
|
||||
expect(download_service).to receive(:execute)
|
||||
expect(decompression_service).to receive(:execute)
|
||||
expect(extraction_service).to receive(:execute)
|
||||
|
||||
extracted_data = pipeline.extract(context)
|
||||
|
||||
expect(extracted_data.data).to contain_exactly(lfs_json_file_path, lfs_file_path)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#load' do
|
||||
before do
|
||||
allow(pipeline)
|
||||
.to receive(:extract)
|
||||
.and_return(BulkImports::Pipeline::ExtractedData.new(data: [lfs_json_file_path, lfs_file_path]))
|
||||
end
|
||||
|
||||
context 'when file path is lfs json' do
|
||||
it 'returns' do
|
||||
filepath = File.join(tmpdir, 'lfs_objects.json')
|
||||
|
||||
allow(Gitlab::Json).to receive(:parse).with(filepath).and_return({})
|
||||
|
||||
expect { pipeline.load(context, filepath) }.not_to change { portable.lfs_objects.count }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when file path is tar file' do
|
||||
it 'returns' do
|
||||
filepath = File.join(tmpdir, 'lfs_objects.tar')
|
||||
|
||||
expect { pipeline.load(context, filepath) }.not_to change { portable.lfs_objects.count }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when lfs json read failed' do
|
||||
it 'raises an error' do
|
||||
File.write(lfs_json_file_path, 'invalid json')
|
||||
|
||||
expect { pipeline.load(context, lfs_file_path) }.to raise_error(BulkImports::Error, 'LFS Objects JSON read failed')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when file path is being traversed' do
|
||||
it 'raises an error' do
|
||||
expect { pipeline.load(context, File.join(tmpdir, '..')) }.to raise_error(Gitlab::Utils::PathTraversalAttackError, 'Invalid path')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when file path is not under tmpdir' do
|
||||
it 'returns' do
|
||||
expect { pipeline.load(context, '/home/test.txt') }.to raise_error(StandardError, 'path /home/test.txt is not allowed')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when file path is symlink' do
|
||||
it 'returns' do
|
||||
symlink = File.join(tmpdir, 'symlink')
|
||||
|
||||
FileUtils.ln_s(File.join(tmpdir, lfs_file_path), symlink)
|
||||
|
||||
expect { pipeline.load(context, symlink) }.not_to change { portable.lfs_objects.count }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when path is a directory' do
|
||||
it 'returns' do
|
||||
expect { pipeline.load(context, Dir.tmpdir) }.not_to change { portable.lfs_objects.count }
|
||||
end
|
||||
end
|
||||
|
||||
context 'lfs objects project' do
|
||||
context 'when lfs objects json is invalid' do
|
||||
context 'when oid value is not Array' do
|
||||
it 'does not create lfs objects project' do
|
||||
File.write(lfs_json_file_path, { oid => 'test' }.to_json )
|
||||
|
||||
expect { pipeline.load(context, lfs_file_path) }.not_to change { portable.lfs_objects_projects.count }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when oid value is nil' do
|
||||
it 'does not create lfs objects project' do
|
||||
File.write(lfs_json_file_path, { oid => nil }.to_json )
|
||||
|
||||
expect { pipeline.load(context, lfs_file_path) }.not_to change { portable.lfs_objects_projects.count }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when oid value is not allowed' do
|
||||
it 'does not create lfs objects project' do
|
||||
File.write(lfs_json_file_path, { oid => ['invalid'] }.to_json )
|
||||
|
||||
expect { pipeline.load(context, lfs_file_path) }.not_to change { portable.lfs_objects_projects.count }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when repository type is duplicated' do
|
||||
it 'creates only one lfs objects project' do
|
||||
File.write(lfs_json_file_path, { oid => [0, 0, 1, 1, 2, 2] }.to_json )
|
||||
|
||||
expect { pipeline.load(context, lfs_file_path) }.to change { portable.lfs_objects_projects.count }.by(3)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when lfs objects project fails to be created' do
|
||||
it 'logs the failure' do
|
||||
allow_next_instance_of(LfsObjectsProject) do |object|
|
||||
allow(object).to receive(:persisted?).and_return(false)
|
||||
end
|
||||
|
||||
expect_next_instance_of(Gitlab::Import::Logger) do |logger|
|
||||
expect(logger)
|
||||
.to receive(:warn)
|
||||
.with(project_id: portable.id,
|
||||
message: 'Failed to save lfs objects project',
|
||||
errors: '', **Gitlab::ApplicationContext.current)
|
||||
.exactly(4).times
|
||||
end
|
||||
|
||||
pipeline.load(context, lfs_file_path)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#after_run' do
|
||||
it 'removes tmpdir' do
|
||||
allow(FileUtils).to receive(:remove_entry).and_call_original
|
||||
expect(FileUtils).to receive(:remove_entry).with(tmpdir).and_call_original
|
||||
|
||||
pipeline.after_run(nil)
|
||||
|
||||
expect(Dir.exist?(tmpdir)).to eq(false)
|
||||
end
|
||||
|
||||
context 'when tmpdir does not exist' do
|
||||
it 'does not attempt to remove tmpdir' do
|
||||
FileUtils.remove_entry(tmpdir)
|
||||
|
||||
expect(FileUtils).not_to receive(:remove_entry).with(tmpdir)
|
||||
|
||||
pipeline.after_run(nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -26,6 +26,7 @@ RSpec.describe BulkImports::Projects::Stage do
|
|||
[4, BulkImports::Projects::Pipelines::ServiceDeskSettingPipeline],
|
||||
[5, BulkImports::Common::Pipelines::WikiPipeline],
|
||||
[5, BulkImports::Common::Pipelines::UploadsPipeline],
|
||||
[5, BulkImports::Common::Pipelines::LfsObjectsPipeline],
|
||||
[5, BulkImports::Projects::Pipelines::AutoDevopsPipeline],
|
||||
[5, BulkImports::Projects::Pipelines::PipelineSchedulesPipeline],
|
||||
[6, BulkImports::Common::Pipelines::EntityFinisher]
|
||||
|
|
|
@ -24,7 +24,6 @@ RSpec.describe 'cross-database foreign keys' do
|
|||
ci_pipeline_schedules.owner_id
|
||||
ci_pipeline_schedules.project_id
|
||||
ci_pipelines.project_id
|
||||
ci_project_monthly_usages.project_id
|
||||
ci_resource_groups.project_id
|
||||
ci_runner_namespaces.namespace_id
|
||||
ci_running_builds.project_id
|
||||
|
|
|
@ -634,13 +634,13 @@ RSpec.describe UsersController do
|
|||
end
|
||||
|
||||
describe 'GET #exists' do
|
||||
before do
|
||||
sign_in(user)
|
||||
|
||||
allow(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).and_return(false)
|
||||
end
|
||||
|
||||
context 'when user exists' do
|
||||
before do
|
||||
sign_in(user)
|
||||
|
||||
allow(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).and_return(false)
|
||||
end
|
||||
|
||||
it 'returns JSON indicating the user exists' do
|
||||
get user_exists_url user.username
|
||||
|
||||
|
@ -661,6 +661,15 @@ RSpec.describe UsersController do
|
|||
end
|
||||
|
||||
context 'when the user does not exist' do
|
||||
it 'will not show a signup page if registration is disabled' do
|
||||
stub_application_setting(signup_enabled: false)
|
||||
get user_exists_url 'foo'
|
||||
|
||||
expected_json = { error: "You must be authenticated to access this path." }.to_json
|
||||
expect(response).to have_gitlab_http_status(:unauthorized)
|
||||
expect(response.body).to eq(expected_json)
|
||||
end
|
||||
|
||||
it 'returns JSON indicating the user does not exist' do
|
||||
get user_exists_url 'foo'
|
||||
|
||||
|
|
Loading…
Reference in New Issue