Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-05-04 18:17:13 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-05-04 18:17:13 +0300
commit4bdfcf93f224edb9c4daff90d95b0c6c92766ea3 (patch)
treecedf1f94561571d00033c48846ad3959af64449b
parentfb5d3cceb8d43f8c2dc22a5d8c74327e9397f2e8 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.rubocop.yml1
-rw-r--r--.rubocop_todo/layout/empty_line_after_magic_comment.yml1
-rw-r--r--app/assets/javascripts/boards/components/board_filtered_search.vue10
-rw-r--r--app/assets/javascripts/import_entities/import_projects/components/github_organizations_box.vue73
-rw-r--r--app/assets/javascripts/import_entities/import_projects/components/github_status_table.vue104
-rw-r--r--app/assets/javascripts/import_entities/import_projects/components/import_projects_table.vue83
-rw-r--r--app/assets/javascripts/import_entities/import_projects/index.js2
-rw-r--r--app/assets/javascripts/import_entities/import_projects/store/actions.js4
-rw-r--r--app/assets/javascripts/import_entities/import_projects/store/mutations.js4
-rw-r--r--app/assets/javascripts/import_entities/import_projects/store/state.js2
-rw-r--r--app/assets/javascripts/issues/list/constants.js2
-rw-r--r--app/assets/javascripts/pages/import/github/status/index.js9
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss3
-rw-r--r--app/assets/stylesheets/page_bundles/merge_requests.scss4
-rw-r--r--app/controllers/registrations/welcome_controller.rb9
-rw-r--r--app/mailers/previews/notify_preview.rb6
-rw-r--r--app/models/service_desk/custom_email_verification.rb68
-rw-r--r--app/serializers/note_entity.rb2
-rw-r--r--app/services/packages/npm/create_package_service.rb1
-rw-r--r--app/views/import/_githubish_status.html.haml2
-rw-r--r--app/views/import/github/status.html.haml1
-rw-r--r--app/views/notify/service_desk_verification_result_email.html.haml2
-rw-r--r--app/views/notify/service_desk_verification_result_email.text.erb2
-rw-r--r--app/views/projects/issues/service_desk/_nav_btns.html.haml2
-rw-r--r--app/views/projects/merge_requests/_nav_btns.html.haml2
-rw-r--r--config/feature_flags/development/external_note_author_service_desk.yml (renamed from config/feature_flags/ops/external_note_author_service_desk.yml)2
-rw-r--r--doc/administration/auth/oidc.md9
-rw-r--r--doc/integration/openid_connect_provider.md2
-rw-r--r--doc/user/group/epics/manage_epics.md2
-rw-r--r--doc/user/product_analytics/index.md2
-rw-r--r--doc/user/project/issues/csv_import.md2
-rw-r--r--doc/user/project/issues/managing_issues.md6
-rw-r--r--doc/user/project/merge_requests/reviews/index.md4
-rw-r--r--locale/gitlab.pot30
-rw-r--r--package.json2
-rw-r--r--spec/factories/service_desk/custom_email_verification.rb5
-rw-r--r--spec/features/issues/user_bulk_edits_issues_labels_spec.rb4
-rw-r--r--spec/features/issues/user_bulk_edits_issues_spec.rb14
-rw-r--r--spec/features/merge_requests/user_mass_updates_spec.rb8
-rw-r--r--spec/fixtures/packages/npm/payload_with_empty_attachment.json29
-rw-r--r--spec/frontend/import_entities/import_projects/components/bitbucket_status_table_spec.js17
-rw-r--r--spec/frontend/import_entities/import_projects/components/github_organizations_box_spec.js97
-rw-r--r--spec/frontend/import_entities/import_projects/components/github_status_table_spec.js125
-rw-r--r--spec/frontend/import_entities/import_projects/store/actions_spec.js14
-rw-r--r--spec/frontend/import_entities/import_projects/store/mutations_spec.js13
-rw-r--r--spec/frontend/issues/list/components/issues_list_app_spec.js4
-rw-r--r--spec/frontend/vue_compat_test_setup.js9
-rw-r--r--spec/models/service_desk/custom_email_verification_spec.rb157
-rw-r--r--spec/requests/api/npm_project_packages_spec.rb8
-rw-r--r--spec/services/packages/npm/create_package_service_spec.rb7
-rw-r--r--yarn.lock8
51 files changed, 783 insertions, 196 deletions
diff --git a/.rubocop.yml b/.rubocop.yml
index 6108b2981e9..b3f24e12c22 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -109,6 +109,7 @@ Layout/LineLength:
AllowedPatterns: ['^RSpec\.describe\s.*\sdo']
Exclude:
- 'ee/spec/controllers/concerns/routable_actions_spec.rb'
+ - 'ee/spec/lib/gitlab/auth/group_saml/sso_enforcer_spec.rb'
Lint/LastKeywordArgument:
Safe: false
diff --git a/.rubocop_todo/layout/empty_line_after_magic_comment.yml b/.rubocop_todo/layout/empty_line_after_magic_comment.yml
index 4ba1efe4916..dbd7c038482 100644
--- a/.rubocop_todo/layout/empty_line_after_magic_comment.yml
+++ b/.rubocop_todo/layout/empty_line_after_magic_comment.yml
@@ -206,7 +206,6 @@ Layout/EmptyLineAfterMagicComment:
- 'ee/app/models/dependencies/dependency_list_export.rb'
- 'ee/app/models/ee/issue_assignee.rb'
- 'ee/app/models/geo/ci_secure_file_state.rb'
- - 'ee/app/models/namespaces/storage/cli_notification.rb'
- 'ee/app/models/namespaces/storage/limit_exclusion.rb'
- 'ee/app/models/project_security_setting.rb'
- 'ee/app/models/protected_environment.rb'
diff --git a/app/assets/javascripts/boards/components/board_filtered_search.vue b/app/assets/javascripts/boards/components/board_filtered_search.vue
index 46612320136..b5d3613ca27 100644
--- a/app/assets/javascripts/boards/components/board_filtered_search.vue
+++ b/app/assets/javascripts/boards/components/board_filtered_search.vue
@@ -397,7 +397,6 @@ export default {
generateParams(filters = []) {
const filterParams = {};
const labels = [];
- const plainText = [];
filters.forEach((filter) => {
switch (filter.type) {
@@ -439,7 +438,9 @@ export default {
filterParams.confidential = filter.value.data;
break;
case FILTERED_SEARCH_TERM:
- if (filter.value.data) plainText.push(filter.value.data);
+ if (filter.value.data) {
+ filterParams.search = filter.value.data;
+ }
break;
case TOKEN_TYPE_HEALTH:
filterParams.healthStatus = filter.value.data;
@@ -453,10 +454,6 @@ export default {
filterParams.labelName = labels;
}
- if (plainText.length) {
- filterParams.search = plainText.join(' ');
- }
-
return filterParams;
},
},
@@ -468,6 +465,7 @@ export default {
:key="filteredSearchKey"
class="gl-w-full"
namespace=""
+ terms-as-tokens
:tokens="tokens"
:search-input-placeholder="$options.i18n.search"
:initial-filter-value="getFilteredSearchValue"
diff --git a/app/assets/javascripts/import_entities/import_projects/components/github_organizations_box.vue b/app/assets/javascripts/import_entities/import_projects/components/github_organizations_box.vue
new file mode 100644
index 00000000000..5d5965e33da
--- /dev/null
+++ b/app/assets/javascripts/import_entities/import_projects/components/github_organizations_box.vue
@@ -0,0 +1,73 @@
+<script>
+import * as Sentry from '@sentry/browser';
+import { GlCollapsibleListbox } from '@gitlab/ui';
+import { createAlert } from '~/alert';
+import { __, s__ } from '~/locale';
+import axios from '~/lib/utils/axios_utils';
+
+export default {
+ components: {
+ GlCollapsibleListbox,
+ },
+ inject: ['statusImportGithubGroupPath'],
+ props: {
+ value: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return { organizationsLoading: true, organizations: [], organizationFilter: '' };
+ },
+ computed: {
+ toggleText() {
+ return this.value || this.$options.i18n.allOrganizations;
+ },
+ dropdownItems() {
+ return [
+ { text: this.$options.i18n.allOrganizations, value: '' },
+ ...this.organizations
+ .filter((entry) =>
+ entry.name.toLowerCase().includes(this.organizationFilter.toLowerCase()),
+ )
+ .map((entry) => ({
+ text: entry.name,
+ value: entry.name,
+ })),
+ ];
+ },
+ },
+ async mounted() {
+ try {
+ this.organizationsLoading = true;
+ const {
+ data: { provider_groups: organizations },
+ } = await axios.get(this.statusImportGithubGroupPath);
+ this.organizations = organizations;
+ } catch (e) {
+ createAlert({
+ message: __('Something went wrong on our end.'),
+ });
+ Sentry.captureException(e);
+ } finally {
+ this.organizationsLoading = false;
+ }
+ },
+ i18n: {
+ allOrganizations: s__('ImportProjects|All organizations'),
+ },
+};
+</script>
+<template>
+ <gl-collapsible-listbox
+ :loading="organizationsLoading"
+ :toggle-text="toggleText"
+ :header-text="s__('ImportProjects|Organizations')"
+ :items="dropdownItems"
+ searchable
+ role="button"
+ tabindex="0"
+ @search="organizationFilter = $event"
+ @select="$emit('input', $event)"
+ />
+</template>
diff --git a/app/assets/javascripts/import_entities/import_projects/components/github_status_table.vue b/app/assets/javascripts/import_entities/import_projects/components/github_status_table.vue
new file mode 100644
index 00000000000..20dcd0356cd
--- /dev/null
+++ b/app/assets/javascripts/import_entities/import_projects/components/github_status_table.vue
@@ -0,0 +1,104 @@
+<script>
+import { GlButton, GlSearchBoxByClick, GlTabs, GlTab } from '@gitlab/ui';
+import { mapActions, mapGetters, mapState } from 'vuex';
+import { s__ } from '~/locale';
+import ImportProjectsTable from './import_projects_table.vue';
+import GithubOrganizationsBox from './github_organizations_box.vue';
+
+export default {
+ components: {
+ ImportProjectsTable,
+ GithubOrganizationsBox,
+ GlButton,
+ GlSearchBoxByClick,
+ GlTab,
+ GlTabs,
+ },
+ inheritAttrs: false,
+ data() {
+ return {
+ selectedRelationTypeTabIdx: 0,
+ };
+ },
+ computed: {
+ ...mapState({
+ selectedOrganization: (state) => state.filter.organization_login ?? '',
+ nameFilter: (state) => state.filter.filter ?? '',
+ }),
+ ...mapGetters(['isImportingAnyRepo', 'hasImportableRepos']),
+ isNameFilterDisabled() {
+ return (
+ this.$options.relationTypes[this.selectedRelationTypeTabIdx].showOrganizationFilter &&
+ !this.selectedOrganization
+ );
+ },
+ },
+ watch: {
+ selectedRelationTypeTabIdx: {
+ immediate: true,
+ handler(newIdx) {
+ const { backendFilter } = this.$options.relationTypes[newIdx];
+ this.setFilter({ ...backendFilter, organization_login: '', filter: '' });
+ },
+ },
+ },
+ methods: {
+ ...mapActions(['setFilter']),
+ selectOrganization(org) {
+ this.selectedOrganization = org;
+ this.setFilter();
+ },
+ },
+
+ relationTypes: [
+ { title: s__('ImportProjects|Owned'), backendFilter: { relation_type: 'owned' } },
+ { title: s__('ImportProjects|Collaborated'), backendFilter: { relation_type: 'collaborated' } },
+ {
+ title: s__('ImportProjects|Organization'),
+ backendFilter: { relation_type: 'organization' },
+ showOrganizationFilter: true,
+ },
+ ],
+};
+</script>
+<template>
+ <import-projects-table v-bind="$attrs">
+ <template #filter="{ importAllButtonText, showImportAllModal }">
+ <gl-tabs v-model="selectedRelationTypeTabIdx" content-class="gl-py-0! gl-mb-3">
+ <gl-tab v-for="tab in $options.relationTypes" :key="tab.title" :title="tab.title">
+ <div
+ class="gl-display-flex gl-justify-content-space-between gl-flex-wrap gl-gap-3 gl-p-5 gl-bg-gray-10 gl-border-solid gl-border-0 gl-border-b-gray-100 gl-border-b-1"
+ >
+ <form class="gl-display-flex gl-flex-grow-1 gl-mr-3" novalidate @submit.prevent>
+ <github-organizations-box
+ v-if="tab.showOrganizationFilter"
+ class="gl-mr-3"
+ :value="selectedOrganization"
+ @input="setFilter({ organization_login: $event })"
+ />
+ <gl-search-box-by-click
+ data-qa-selector="githubish_import_filter_field"
+ name="filter"
+ :disabled="isNameFilterDisabled"
+ :value="nameFilter"
+ :placeholder="__('Filter by name')"
+ autofocus
+ @submit="setFilter({ filter: $event })"
+ @clear="setFilter({ filter: '' })"
+ />
+ </form>
+ <gl-button
+ variant="confirm"
+ :loading="isImportingAnyRepo"
+ :disabled="!hasImportableRepos"
+ type="button"
+ @click="showImportAllModal"
+ >
+ {{ importAllButtonText }}
+ </gl-button>
+ </div>
+ </gl-tab>
+ </gl-tabs>
+ </template>
+ </import-projects-table>
+</template>
diff --git a/app/assets/javascripts/import_entities/import_projects/components/import_projects_table.vue b/app/assets/javascripts/import_entities/import_projects/components/import_projects_table.vue
index 55a8bad27b9..a867a1695b9 100644
--- a/app/assets/javascripts/import_entities/import_projects/components/import_projects_table.vue
+++ b/app/assets/javascripts/import_entities/import_projects/components/import_projects_table.vue
@@ -8,6 +8,7 @@ import {
} from '@gitlab/ui';
import { mapActions, mapState, mapGetters } from 'vuex';
import { n__, __, sprintf } from '~/locale';
+
import ProviderRepoTableRow from './provider_repo_table_row.vue';
import AdvancedSettings from './advanced_settings.vue';
@@ -123,6 +124,10 @@ export default {
'setFilter',
'importAll',
]),
+
+ showImportAllModal() {
+ this.$refs.importAllModal.show();
+ },
},
};
</script>
@@ -135,43 +140,30 @@ export default {
<template v-if="hasIncompatibleRepos">
<slot name="incompatible-repos-warning"></slot>
</template>
- <div class="gl-display-flex gl-justify-content-space-between gl-flex-wrap gl-mb-5">
- <gl-button
- variant="confirm"
- :loading="isImportingAnyRepo"
- :disabled="!hasImportableRepos"
- type="button"
- @click="$refs.importAllModal.show()"
- >{{ importAllButtonText }}</gl-button
- >
- <gl-modal
- ref="importAllModal"
- modal-id="import-all-modal"
- :title="s__('ImportProjects|Import repositories')"
- :ok-title="__('Import')"
- @ok="importAll({ optionalStages: optionalStagesSelection })"
- >
- {{
- n__(
- 'Are you sure you want to import %d repository?',
- 'Are you sure you want to import %d repositories?',
- importAllCount,
- )
- }}
- </gl-modal>
-
- <slot name="actions"></slot>
- <form v-if="filterable" class="gl-ml-auto" novalidate @submit.prevent>
- <gl-search-box-by-click
- data-qa-selector="githubish_import_filter_field"
- name="filter"
- :placeholder="__('Filter by name')"
- autofocus
- @submit="setFilter"
- @clear="setFilter('')"
- />
- </form>
- </div>
+ <slot name="filter" v-bind="{ showImportAllModal, importAllButtonText }">
+ <div class="gl-display-flex gl-justify-content-space-between gl-flex-wrap gl-mb-5">
+ <gl-button
+ variant="confirm"
+ :loading="isImportingAnyRepo"
+ :disabled="!hasImportableRepos"
+ type="button"
+ @click="showImportAllModal"
+ >{{ importAllButtonText }}</gl-button
+ >
+
+ <slot name="actions"></slot>
+ <form v-if="filterable" class="gl-ml-auto" novalidate @submit.prevent>
+ <gl-search-box-by-click
+ data-qa-selector="githubish_import_filter_field"
+ name="filter"
+ :placeholder="__('Filter by name')"
+ autofocus
+ @submit="setFilter({ filter: $event })"
+ @clear="setFilter({ filter: '' })"
+ />
+ </form>
+ </div>
+ </slot>
<advanced-settings
v-if="optionalStages && optionalStages.length"
v-model="optionalStagesSelection"
@@ -179,6 +171,21 @@ export default {
:is-initially-expanded="isAdvancedSettingsPanelInitiallyExpanded"
class="gl-mb-5"
/>
+ <gl-modal
+ ref="importAllModal"
+ modal-id="import-all-modal"
+ :title="s__('ImportProjects|Import repositories')"
+ :ok-title="__('Import')"
+ @ok="importAll({ optionalStages: optionalStagesSelection })"
+ >
+ {{
+ n__(
+ 'Are you sure you want to import %d repository?',
+ 'Are you sure you want to import %d repositories?',
+ importAllCount,
+ )
+ }}
+ </gl-modal>
<div v-if="repositories.length" class="gl-w-full">
<table class="table gl-table">
<thead>
@@ -209,7 +216,7 @@ export default {
</table>
</div>
<gl-intersection-observer
- v-if="paginatable && pageInfo.hasNextPage"
+ v-if="!isLoadingRepos && paginatable && pageInfo.hasNextPage"
:key="pagePaginationStateKey"
@appear="fetchRepos"
/>
diff --git a/app/assets/javascripts/import_entities/import_projects/index.js b/app/assets/javascripts/import_entities/import_projects/index.js
index f898e23b47a..6ee637b1ce8 100644
--- a/app/assets/javascripts/import_entities/import_projects/index.js
+++ b/app/assets/javascripts/import_entities/import_projects/index.js
@@ -61,6 +61,7 @@ export default function mountImportProjectsTable({
mountElement,
Component = ImportProjectsTable,
extraProps = () => ({}),
+ extraProvide = () => ({}),
}) {
if (!mountElement) return undefined;
@@ -75,6 +76,7 @@ export default function mountImportProjectsTable({
apolloProvider,
provide: {
detailsPath,
+ ...extraProvide(mountElement.dataset),
},
render(createElement) {
// We are using attrs instead of props so root-level component with inheritAttrs
diff --git a/app/assets/javascripts/import_entities/import_projects/store/actions.js b/app/assets/javascripts/import_entities/import_projects/store/actions.js
index e3c32028b13..4305f8d4db5 100644
--- a/app/assets/javascripts/import_entities/import_projects/store/actions.js
+++ b/app/assets/javascripts/import_entities/import_projects/store/actions.js
@@ -83,7 +83,7 @@ const fetchReposFactory = ({ reposPath = isRequired() }) => ({ state, commit })
.get(
pathWithParams({
path: reposPath,
- filter: filter ?? '',
+ ...(filter ?? {}),
...paginationParams({ state }),
}),
)
@@ -203,7 +203,7 @@ export const fetchJobsFactory = (jobsPath = isRequired()) => ({ state, commit, d
eTagPoll = new Poll({
resource: {
- fetchJobs: () => axios.get(pathWithParams({ path: jobsPath, filter: state.filter })),
+ fetchJobs: () => axios.get(pathWithParams({ path: jobsPath, ...state.filter })),
},
method: 'fetchJobs',
successCallback: ({ data }) =>
diff --git a/app/assets/javascripts/import_entities/import_projects/store/mutations.js b/app/assets/javascripts/import_entities/import_projects/store/mutations.js
index 734e7b10a77..df529449f90 100644
--- a/app/assets/javascripts/import_entities/import_projects/store/mutations.js
+++ b/app/assets/javascripts/import_entities/import_projects/store/mutations.js
@@ -23,8 +23,8 @@ const processLegacyEntries = ({ newRepositories, existingRepositories, factory }
};
export default {
- [types.SET_FILTER](state, filter) {
- state.filter = filter;
+ [types.SET_FILTER](state, newFilter) {
+ state.filter = { ...state.filter, ...newFilter };
state.repositories = [];
state.pageInfo = {
page: 0,
diff --git a/app/assets/javascripts/import_entities/import_projects/store/state.js b/app/assets/javascripts/import_entities/import_projects/store/state.js
index c384848f0a0..62dcefd3339 100644
--- a/app/assets/javascripts/import_entities/import_projects/store/state.js
+++ b/app/assets/javascripts/import_entities/import_projects/store/state.js
@@ -4,7 +4,7 @@ export default () => ({
customImportTargets: {},
isLoadingRepos: false,
ciCdOnly: false,
- filter: '',
+ filter: {},
pageInfo: {
page: 0,
startCursor: null,
diff --git a/app/assets/javascripts/issues/list/constants.js b/app/assets/javascripts/issues/list/constants.js
index cd0679e00bf..56d3a57457b 100644
--- a/app/assets/javascripts/issues/list/constants.js
+++ b/app/assets/javascripts/issues/list/constants.js
@@ -83,7 +83,7 @@ export const i18n = {
confidentialNo: __('No'),
confidentialYes: __('Yes'),
downvotes: __('Downvotes'),
- editIssues: __('Edit issues'),
+ editIssues: __('Bulk edit'),
errorFetchingCounts: __('An error occurred while getting issue counts'),
errorFetchingIssues: __('An error occurred while loading issues'),
importIssues: __('Import issues'),
diff --git a/app/assets/javascripts/pages/import/github/status/index.js b/app/assets/javascripts/pages/import/github/status/index.js
index 30ee468734d..e9c98d0adb5 100644
--- a/app/assets/javascripts/pages/import/github/status/index.js
+++ b/app/assets/javascripts/pages/import/github/status/index.js
@@ -1,5 +1,12 @@
import mountImportProjectsTable from '~/import_entities/import_projects';
+import GithubStatusTable from '~/import_entities/import_projects/components/github_status_table.vue';
const mountElement = document.getElementById('import-projects-mount-element');
-mountImportProjectsTable({ mountElement });
+mountImportProjectsTable({
+ mountElement,
+ Component: GithubStatusTable,
+ extraProvide: (dataset) => ({
+ statusImportGithubGroupPath: dataset.statusImportGithubGroupPath,
+ }),
+});
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index 946f2b28859..74d5c64f57d 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -306,7 +306,6 @@
@mixin right-sidebar {
position: fixed;
- top: calc(#{$header-height} + #{$calc-application-bars-height});
bottom: calc(#{$calc-application-footer-height} + var(--mr-review-bar-height));
right: 0;
transition: width $gl-transition-duration-medium;
@@ -318,6 +317,7 @@
.right-sidebar {
&:not(.right-sidebar-merge-requests) {
@include right-sidebar;
+ top: calc(#{$header-height} + #{$calc-application-bars-height});
@include media-breakpoint-down(sm) {
z-index: 251;
@@ -327,6 +327,7 @@
&.right-sidebar-merge-requests {
@include media-breakpoint-down(md) {
@include right-sidebar;
+ top: $calc-application-header-height;
z-index: 251;
}
}
diff --git a/app/assets/stylesheets/page_bundles/merge_requests.scss b/app/assets/stylesheets/page_bundles/merge_requests.scss
index 5ac4f6f30d3..00ef659dcf4 100644
--- a/app/assets/stylesheets/page_bundles/merge_requests.scss
+++ b/app/assets/stylesheets/page_bundles/merge_requests.scss
@@ -1164,10 +1164,6 @@ $tabs-holder-z-index: 250;
padding-left: $contextual-sidebar-collapsed-width;
}
- .right-sidebar-expanded & {
- padding-right: $gutter_width;
- }
-
@media (max-width: map-get($grid-breakpoints, sm)-1) {
padding-left: 0;
padding-right: 0;
diff --git a/app/controllers/registrations/welcome_controller.rb b/app/controllers/registrations/welcome_controller.rb
index 158536b6ba1..ac8959e0f52 100644
--- a/app/controllers/registrations/welcome_controller.rb
+++ b/app/controllers/registrations/welcome_controller.rb
@@ -49,16 +49,7 @@ module Registrations
params.require(:user).permit(:role, :setup_for_company)
end
- def requires_confirmation?(user)
- return false if user.confirmed?
- return false unless Gitlab::CurrentSettings.email_confirmation_setting_hard?
-
- true
- end
-
def path_for_signed_in_user(user)
- return users_almost_there_path(email: user.email) if requires_confirmation?(user)
-
stored_location_for(user) || members_activity_path(user.members)
end
diff --git a/app/mailers/previews/notify_preview.rb b/app/mailers/previews/notify_preview.rb
index 510f35ee0d2..d91f69cdd4b 100644
--- a/app/mailers/previews/notify_preview.rb
+++ b/app/mailers/previews/notify_preview.rb
@@ -233,7 +233,7 @@ class NotifyPreview < ActionMailer::Preview
cleanup do
setup_service_desk_custom_email_objects
- custom_email_verification.update!(state: 1)
+ custom_email_verification.mark_as_finished!
Notify.service_desk_verification_result_email(service_desk_setting, 'owner@example.com').message
end
@@ -297,7 +297,7 @@ class NotifyPreview < ActionMailer::Preview
cleanup do
setup_service_desk_custom_email_objects
- custom_email_verification.update!(state: 2, error: error)
+ custom_email_verification.mark_as_failed!(error)
Notify.service_desk_verification_result_email(service_desk_setting, 'owner@example.com').message
end
@@ -319,7 +319,7 @@ class NotifyPreview < ActionMailer::Preview
token: 'XXXXXXXXXXXX',
triggerer: user,
triggered_at: Time.current,
- state: 0
+ state: 'started'
)
end
diff --git a/app/models/service_desk/custom_email_verification.rb b/app/models/service_desk/custom_email_verification.rb
index b3b9390bb82..69bd0f5e7f8 100644
--- a/app/models/service_desk/custom_email_verification.rb
+++ b/app/models/service_desk/custom_email_verification.rb
@@ -2,11 +2,8 @@
module ServiceDesk
class CustomEmailVerification < ApplicationRecord
- enum state: {
- running: 0,
- verified: 1,
- error: 2
- }, _default: 'running'
+ TIMEFRAME = 30.minutes
+ STATES = { started: 0, finished: 1, failed: 2 }.freeze
enum error: {
incorrect_token: 0,
@@ -16,8 +13,6 @@ module ServiceDesk
smtp_host_issue: 4
}
- TIMEFRAME = 30.minutes
-
attr_encrypted :token,
mode: :per_attribute_iv,
algorithm: 'aes-256-gcm',
@@ -33,6 +28,61 @@ module ServiceDesk
delegate :service_desk_setting, to: :project
+ state_machine :state do
+ state :started do
+ validates :token, presence: true, length: { is: 12 }
+ validates :triggerer, presence: true
+ validates :triggered_at, presence: true
+ validates :error, absence: true
+ end
+
+ state :finished do
+ validates :token, absence: true
+ validates :error, absence: true
+ end
+
+ state :failed do
+ validates :token, absence: true
+ validates :error, presence: true
+ end
+
+ event :mark_as_started do
+ transition all => :started
+ end
+
+ event :mark_as_finished do
+ transition started: :finished
+ end
+
+ event :mark_as_failed do
+ transition all => :failed
+ end
+
+ before_transition any => :started do |verification, transition|
+ triggerer = transition.args.first
+
+ verification.triggerer = triggerer
+ verification.token = verification.class.generate_token
+ verification.triggered_at = Time.current
+ verification.error = nil
+ end
+
+ before_transition started: :finished do |verification|
+ verification.token = nil
+ end
+
+ before_transition started: :failed do |verification, transition|
+ error = transition.args.first
+
+ verification.error = error
+ verification.token = nil
+ end
+ end
+
+ # Needs to be below `state_machine` definition to suppress
+ # its method override warnings
+ enum state: STATES
+
class << self
def generate_token
SecureRandom.alphanumeric(12)
@@ -40,14 +90,14 @@ module ServiceDesk
end
def accepted_until
- return unless running?
+ return unless started?
return unless triggered_at.present?
TIMEFRAME.since(triggered_at)
end
def in_timeframe?
- return false unless running?
+ return false unless started?
!!accepted_until&.future?
end
diff --git a/app/serializers/note_entity.rb b/app/serializers/note_entity.rb
index e80b3be98bd..6058c89d347 100644
--- a/app/serializers/note_entity.rb
+++ b/app/serializers/note_entity.rb
@@ -109,7 +109,7 @@ class NoteEntity < API::Entities::Note
end
def external_author
- return unless Feature.enabled?(:external_note_author_service_desk, type: :ops)
+ return unless Feature.enabled?(:external_note_author_service_desk)
return unless object.note_metadata&.external_author
diff --git a/app/services/packages/npm/create_package_service.rb b/app/services/packages/npm/create_package_service.rb
index aa535fc7775..c71ae060dd9 100644
--- a/app/services/packages/npm/create_package_service.rb
+++ b/app/services/packages/npm/create_package_service.rb
@@ -10,6 +10,7 @@ module Packages
def execute
return error('Version is empty.', 400) if version.blank?
+ return error('Attachment data is empty.', 400) if attachment['data'].blank?
return error('Package already exists.', 403) if current_package_exists?
return error('File is too large.', 400) if file_size_exceeded?
diff --git a/app/views/import/_githubish_status.html.haml b/app/views/import/_githubish_status.html.haml
index 8545b5fd71d..5eae48fd237 100644
--- a/app/views/import/_githubish_status.html.haml
+++ b/app/views/import/_githubish_status.html.haml
@@ -8,6 +8,7 @@
- details_path = local_assigns.fetch(:details_path, nil)
- provider_title = Gitlab::ImportSources.title(local_assigns.fetch(:provider))
- optional_stages = local_assigns.fetch(:optional_stages, [])
+- status_import_github_group_path = local_assigns.fetch(:status_import_github_group_path, '')
- header_title _("New project"), new_project_path
- add_to_breadcrumbs s_('ProjectsNew|Import project'), new_project_path(anchor: 'import_project')
@@ -19,6 +20,7 @@
jobs_path: url_for([:realtime_changes, :import, provider, { format: :json }]),
default_target_namespace: default_namespace_path,
import_path: url_for([:import, provider, { format: :json }]),
+ status_import_github_group_path: status_import_github_group_path,
cancel_path: cancel_path,
details_path: details_path,
filterable: filterable.to_s,
diff --git a/app/views/import/github/status.html.haml b/app/views/import/github/status.html.haml
index 45b5a9408be..b07374e5b5f 100644
--- a/app/views/import/github/status.html.haml
+++ b/app/views/import/github/status.html.haml
@@ -12,4 +12,5 @@
default_namespace: @namespace,
cancel_path: cancel_import_github_path,
details_path: details_import_github_path,
+ status_import_github_group_path: status_import_github_group_path(format: :json),
optional_stages: Gitlab::GithubImport::Settings.stages_array
diff --git a/app/views/notify/service_desk_verification_result_email.html.haml b/app/views/notify/service_desk_verification_result_email.html.haml
index d63177e4a42..c072744c43c 100644
--- a/app/views/notify/service_desk_verification_result_email.html.haml
+++ b/app/views/notify/service_desk_verification_result_email.html.haml
@@ -14,7 +14,7 @@
%tr
%td.text-content
- - if @verification.verified?
+ - if @verification.finished?
%h1{ :style => "margin-top:0;" }
= s_("Notify|Email successfully verified")
%p
diff --git a/app/views/notify/service_desk_verification_result_email.text.erb b/app/views/notify/service_desk_verification_result_email.text.erb
index a78e3b19d1e..65b0cba5616 100644
--- a/app/views/notify/service_desk_verification_result_email.text.erb
+++ b/app/views/notify/service_desk_verification_result_email.text.erb
@@ -2,7 +2,7 @@
<% email_address = @service_desk_setting.custom_email %>
<% verify_email_address = @service_desk_setting.custom_email_address_for_verification %>
-<% if @verification.verified? %>
+<% if @verification.finished? %>
<%= s_("Notify|Email successfully verified") %>
<%= s_('Notify|Your email address %{strong_open}%{email_address}%{strong_close} for the Service Desk of %{project_link_start}%{project_name}%{project_link_end} was verified successfully.') % { email_address: email_address, project_link_start: '', project_name: project_name, project_link_end: '', strong_open: '', strong_close: '' } %>
diff --git a/app/views/projects/issues/service_desk/_nav_btns.html.haml b/app/views/projects/issues/service_desk/_nav_btns.html.haml
index 9b16dbe761f..a0a290f340a 100644
--- a/app/views/projects/issues/service_desk/_nav_btns.html.haml
+++ b/app/views/projects/issues/service_desk/_nav_btns.html.haml
@@ -7,7 +7,7 @@
.nav-controls.issues-nav-controls.gl-font-size-0
- if @can_bulk_update
- = button_tag _("Edit issues"), class: "gl-button btn btn-default gl-mr-3 js-bulk-update-toggle"
+ = button_tag _("Bulk edit"), class: "gl-button btn btn-default gl-mr-3 js-bulk-update-toggle"
- if show_new_issue_link?(@project)
= link_to _("New issue"), new_project_issue_path(@project,
issue: { milestone_id: finder.milestones.first.try(:id) }),
diff --git a/app/views/projects/merge_requests/_nav_btns.html.haml b/app/views/projects/merge_requests/_nav_btns.html.haml
index 6cb1eee01c4..80085cc6a34 100644
--- a/app/views/projects/merge_requests/_nav_btns.html.haml
+++ b/app/views/projects/merge_requests/_nav_btns.html.haml
@@ -3,7 +3,7 @@
- if @can_bulk_update
= render Pajamas::ButtonComponent.new(type: :submit, button_options: { class: 'gl-mr-3 js-bulk-update-toggle' }) do
- = _("Edit merge requests")
+ = _("Bulk edit")
- if merge_project
= render Pajamas::ButtonComponent.new(href: new_merge_request_path, variant: :confirm) do
= _("New merge request")
diff --git a/config/feature_flags/ops/external_note_author_service_desk.yml b/config/feature_flags/development/external_note_author_service_desk.yml
index 044dc59262b..bd5b5d9d350 100644
--- a/config/feature_flags/ops/external_note_author_service_desk.yml
+++ b/config/feature_flags/development/external_note_author_service_desk.yml
@@ -3,6 +3,6 @@ name: external_note_author_service_desk
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/117149
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/408932
milestone: '16.0'
-type: ops
+type: development
group: group::respond
default_enabled: false
diff --git a/doc/administration/auth/oidc.md b/doc/administration/auth/oidc.md
index 90f485da6f7..6df721c2e09 100644
--- a/doc/administration/auth/oidc.md
+++ b/doc/administration/auth/oidc.md
@@ -89,10 +89,7 @@ The OpenID Connect provides you with a client's details and secret for you to us
```
NOTE:
- For more information on each configuration option, refer to the:
-
- - [OmniAuth OpenID Connect usage documentation](https://github.com/m0n9oose/omniauth_openid_connect#usage).
- - [OpenID Connect Core 1.0 specification](https://openid.net/specs/openid-connect-core-1_0.html).
+ For more information on each configuration option, refer to the [OmniAuth OpenID Connect usage documentation](https://github.com/omniauth/omniauth_openid_connect#usage) and [OpenID Connect Core 1.0 specification](https://openid.net/specs/openid-connect-core-1_0.html).
1. For the provider configuration, change the values for the provider to match your
OpenID Connect client setup. Use the following as a guide:
@@ -129,7 +126,7 @@ The OpenID Connect provides you with a client's details and secret for you to us
- `client_options` are the OpenID Connect client-specific options. Specifically:
- `identifier` is the client identifier as configured in the OpenID Connect service provider.
- `secret` is the client secret as configured in the OpenID Connect service provider. For example,
- [OmniAuth OpenIDConnect](https://github.com/omniauth/omniauth_openid_connect)) requires this. If the service provider doesn't require a secret,
+ [OmniAuth OpenID Connect](https://github.com/omniauth/omniauth_openid_connect) requires this. If the service provider doesn't require a secret,
provide any value and it is ignored.
- `redirect_uri` is the GitLab URL to redirect the user to after successful login
(for example, `http://example.com/users/auth/openid_connect/callback`).
@@ -963,7 +960,7 @@ For installation from source:
1. Check your system clock to ensure the time is synchronized properly.
-1. As mentioned in [the OmniAuth OpenID Connect documentation](https://github.com/m0n9oose/omniauth_openid_connect),
+1. As mentioned in [the OmniAuth OpenID Connect documentation](https://github.com/omniauth/omniauth_openid_connect),
make sure `issuer` corresponds to the base URL of the Discovery URL. For
example, `https://accounts.google.com` is used for the URL
`https://accounts.google.com/.well-known/openid-configuration`.
diff --git a/doc/integration/openid_connect_provider.md b/doc/integration/openid_connect_provider.md
index b0d85fae8c2..ff81ec49a9f 100644
--- a/doc/integration/openid_connect_provider.md
+++ b/doc/integration/openid_connect_provider.md
@@ -20,7 +20,7 @@ OAuth 2.0 protocol. It allows clients to:
OIDC performs many of the same tasks as OpenID 2.0, but is API-friendly and usable by native and
mobile applications.
-On the client side, you can use [OmniAuth::OpenIDConnect](https://github.com/jjbohn/omniauth-openid-connect/) for Rails
+On the client side, you can use [OmniAuth::OpenIDConnect](https://github.com/omniauth/omniauth_openid_connect) for Rails
applications, or any of the other available [client implementations](https://openid.net/developers/libraries/#connect).
The GitLab implementation uses the [doorkeeper-openid_connect](https://github.com/doorkeeper-gem/doorkeeper-openid_connect "Doorkeeper::OpenidConnect website") gem, refer
diff --git a/doc/user/group/epics/manage_epics.md b/doc/user/group/epics/manage_epics.md
index 1349d28c6a1..98316188496 100644
--- a/doc/user/group/epics/manage_epics.md
+++ b/doc/user/group/epics/manage_epics.md
@@ -132,7 +132,7 @@ Prerequisites:
To update multiple epics at the same time:
1. In a group, go to **Epics > List**.
-1. Select **Edit epics**. A sidebar on the right appears with editable fields.
+1. Select **Bulk edit**. A sidebar on the right appears with editable fields.
1. Select the checkboxes next to each epic you want to edit.
1. Select the appropriate fields and their values from the sidebar.
1. Select **Update all**.
diff --git a/doc/user/product_analytics/index.md b/doc/user/product_analytics/index.md
index 554de7731ff..f03ed9ecb90 100644
--- a/doc/user/product_analytics/index.md
+++ b/doc/user/product_analytics/index.md
@@ -158,7 +158,7 @@ You can define different charts, and add visualization options to some of them:
- Line chart, with the options listed in the [ECharts documentation](https://echarts.apache.org/en/option.html).
- Column chart, with the options listed in the [ECharts documentation](https://echarts.apache.org/en/option.html).
-- Data table, without options.
+- Data table, with the only option to render `links` (array of objects, each with `text` and `href` properties to specify the dimensions to be used in links). See [example](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/app/validators/json_schemas/analytics_visualization.json?ref_type=heads#L112)).
- Single stat, with the only option to set `decimalPlaces` (number, default value is 0).
To define a chart for your dashboards:
diff --git a/doc/user/project/issues/csv_import.md b/doc/user/project/issues/csv_import.md
index c918793dcef..d23650e973a 100644
--- a/doc/user/project/issues/csv_import.md
+++ b/doc/user/project/issues/csv_import.md
@@ -36,7 +36,7 @@ To import issues:
1. Go to your project's Issues list page.
1. Open the import feature, depending if the project has issues:
- - The project has existing issues: in the upper-right corner, next to **Edit issues**, select **Actions** (**{ellipsis_v}**) **> Import CSV**.
+ - The project has existing issues: in the upper-right corner, next to **Bulk edit**, select **Actions** (**{ellipsis_v}**) **> Import CSV**.
- The project has no issues: in the middle of the page, select **Import CSV**.
1. Select the file you want to import, and then select **Import issues**.
diff --git a/doc/user/project/issues/managing_issues.md b/doc/user/project/issues/managing_issues.md
index b532fd0c5b8..276cc3bc7a5 100644
--- a/doc/user/project/issues/managing_issues.md
+++ b/doc/user/project/issues/managing_issues.md
@@ -54,7 +54,7 @@ To edit multiple issues at the same time:
1. On the top bar, select **Main menu > Projects** and find your project.
1. On the left sidebar, select **Issues**.
-1. Select **Edit issues**. A sidebar on the right of your screen appears.
+1. Select **Bulk edit**. A sidebar on the right of your screen appears.
1. Select the checkboxes next to each issue you want to edit.
1. From the sidebar, edit the available fields.
1. Select **Update all**.
@@ -87,7 +87,7 @@ To edit multiple issues at the same time:
1. On the top bar, select **Main menu > Groups** and find your group.
1. On the left sidebar, select **Issues**.
-1. Select **Edit issues**. A sidebar on the right of your screen appears.
+1. Select **Bulk edit**. A sidebar on the right of your screen appears.
1. Select the checkboxes next to each issue you want to edit.
1. From the sidebar, edit the available fields.
1. Select **Update all**.
@@ -136,7 +136,7 @@ To move multiple issues at the same time:
1. On the top bar, select **Main menu > Projects** and find your project.
1. On the left sidebar, select **Issues**.
-1. Select **Edit issues**. A sidebar on the right of your screen appears.
+1. Select **Bulk edit**. A sidebar on the right of your screen appears.
1. Select the checkboxes next to each issue you want to move.
1. From the right sidebar, select **Move selected**.
1. From the dropdown list, select the destination project.
diff --git a/doc/user/project/merge_requests/reviews/index.md b/doc/user/project/merge_requests/reviews/index.md
index 6718422865a..a972ae1d604 100644
--- a/doc/user/project/merge_requests/reviews/index.md
+++ b/doc/user/project/merge_requests/reviews/index.md
@@ -234,7 +234,7 @@ When bulk-editing merge requests in a project, you can edit the following attrib
To update multiple project merge requests at the same time:
1. In a project, go to **Merge requests**.
-1. Select **Edit merge requests**. A sidebar on the right-hand side of your screen appears with
+1. Select **Bulk edit**. A sidebar on the right-hand side of your screen appears with
editable fields.
1. Select the checkboxes next to each merge request you want to edit.
1. Select the appropriate fields and their values from the sidebar.
@@ -254,7 +254,7 @@ When bulk editing merge requests in a group, you can edit the following attribut
To update multiple group merge requests at the same time:
1. In a group, go to **Merge requests**.
-1. Select **Edit merge requests**. A sidebar on the right-hand side of your screen appears with
+1. Select **Bulk edit**. A sidebar on the right-hand side of your screen appears with
editable fields.
1. Select the checkboxes next to each merge request you want to edit.
1. Select the appropriate fields and their values from the sidebar.
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index b1fc8e8fa42..04594cc02c3 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -8137,6 +8137,9 @@ msgstr ""
msgid "Built-in"
msgstr ""
+msgid "Bulk edit"
+msgstr ""
+
msgid "Bulk request concurrency"
msgstr ""
@@ -16155,9 +16158,6 @@ msgstr ""
msgid "Edit environment"
msgstr ""
-msgid "Edit epics"
-msgstr ""
-
msgid "Edit files in the editor and commit changes here"
msgstr ""
@@ -16185,15 +16185,9 @@ msgstr ""
msgid "Edit inline"
msgstr ""
-msgid "Edit issues"
-msgstr ""
-
msgid "Edit link"
msgstr ""
-msgid "Edit merge requests"
-msgstr ""
-
msgid "Edit project: %{project_name}"
msgstr ""
@@ -22637,6 +22631,9 @@ msgstr ""
msgid "ImportProjects|Advanced import settings"
msgstr ""
+msgid "ImportProjects|All organizations"
+msgstr ""
+
msgid "ImportProjects|Blocked import URL: %{message}"
msgstr ""
@@ -22649,6 +22646,9 @@ msgstr ""
msgid "ImportProjects|Cancelling project import failed: %{reason}"
msgstr ""
+msgid "ImportProjects|Collaborated"
+msgstr ""
+
msgid "ImportProjects|Error importing repository %{project_safe_import_url} into %{project_full_path} - %{message}"
msgstr ""
@@ -22664,6 +22664,15 @@ msgstr ""
msgid "ImportProjects|Importing the project failed: %{reason}"
msgstr ""
+msgid "ImportProjects|Organization"
+msgstr ""
+
+msgid "ImportProjects|Organizations"
+msgstr ""
+
+msgid "ImportProjects|Owned"
+msgstr ""
+
msgid "ImportProjects|Re-import creates a new project. It does not sync with the existing project."
msgstr ""
@@ -52964,6 +52973,9 @@ msgstr ""
msgid "expires on %{timebox_due_date}"
msgstr ""
+msgid "external link"
+msgstr ""
+
msgid "failed"
msgstr ""
diff --git a/package.json b/package.json
index aaf12068ea2..ec986934347 100644
--- a/package.json
+++ b/package.json
@@ -277,7 +277,7 @@
"swagger-cli": "^4.0.4",
"timezone-mock": "^1.0.8",
"vue-loader-vue3": "npm:vue-loader@17",
- "vue-test-utils-compat": "^0.0.11",
+ "vue-test-utils-compat": "0.0.13",
"webpack-dev-server": "4.13.3",
"xhr-mock": "^2.5.1",
"yarn-check-webpack-plugin": "^1.2.0",
diff --git a/spec/factories/service_desk/custom_email_verification.rb b/spec/factories/service_desk/custom_email_verification.rb
index 614be5da71e..3f3a2ea570d 100644
--- a/spec/factories/service_desk/custom_email_verification.rb
+++ b/spec/factories/service_desk/custom_email_verification.rb
@@ -2,7 +2,10 @@
FactoryBot.define do
factory :service_desk_custom_email_verification, class: '::ServiceDesk::CustomEmailVerification' do
+ state { 'started' }
+ token { 'XXXXXXXXXXXX' }
project
- state { "running" }
+ triggerer factory: :user
+ triggered_at { Time.current }
end
end
diff --git a/spec/features/issues/user_bulk_edits_issues_labels_spec.rb b/spec/features/issues/user_bulk_edits_issues_labels_spec.rb
index 1fc6609d1f5..a01ae9ae0c2 100644
--- a/spec/features/issues/user_bulk_edits_issues_labels_spec.rb
+++ b/spec/features/issues/user_bulk_edits_issues_labels_spec.rb
@@ -406,7 +406,7 @@ RSpec.describe 'Issues > Labels bulk assignment', feature_category: :team_planni
context 'cannot bulk assign labels' do
it do
- expect(page).not_to have_button 'Edit issues'
+ expect(page).not_to have_button 'Bulk edit'
expect(page).not_to have_unchecked_field 'Select all'
expect(page).not_to have_unchecked_field issue1.title
end
@@ -462,7 +462,7 @@ RSpec.describe 'Issues > Labels bulk assignment', feature_category: :team_planni
def enable_bulk_update
visit project_issues_path(project)
wait_for_requests
- click_button 'Edit issues'
+ click_button 'Bulk edit'
end
def disable_bulk_update
diff --git a/spec/features/issues/user_bulk_edits_issues_spec.rb b/spec/features/issues/user_bulk_edits_issues_spec.rb
index 5696bde4069..3e119d86c05 100644
--- a/spec/features/issues/user_bulk_edits_issues_spec.rb
+++ b/spec/features/issues/user_bulk_edits_issues_spec.rb
@@ -16,7 +16,7 @@ RSpec.describe 'Multiple issue updating from issues#index', :js, feature_categor
it 'sets to closed', :js do
visit project_issues_path(project)
- click_button 'Edit issues'
+ click_button 'Bulk edit'
check 'Select all'
click_button 'Select status'
click_button 'Closed'
@@ -29,7 +29,7 @@ RSpec.describe 'Multiple issue updating from issues#index', :js, feature_categor
create_closed
visit project_issues_path(project, state: 'closed')
- click_button 'Edit issues'
+ click_button 'Bulk edit'
check 'Select all'
click_button 'Select status'
click_button 'Open'
@@ -43,7 +43,7 @@ RSpec.describe 'Multiple issue updating from issues#index', :js, feature_categor
it 'updates to current user' do
visit project_issues_path(project)
- click_button 'Edit issues'
+ click_button 'Bulk edit'
check 'Select all'
click_update_assignee_button
click_button user.username
@@ -61,7 +61,7 @@ RSpec.describe 'Multiple issue updating from issues#index', :js, feature_categor
expect(find('.issue:first-of-type')).to have_link "Assigned to #{user.name}"
- click_button 'Edit issues'
+ click_button 'Bulk edit'
check 'Select all'
click_update_assignee_button
click_button 'Unassigned'
@@ -77,7 +77,7 @@ RSpec.describe 'Multiple issue updating from issues#index', :js, feature_categor
it 'updates milestone' do
visit project_issues_path(project)
- click_button 'Edit issues'
+ click_button 'Bulk edit'
check 'Select all'
click_button 'Select milestone'
click_button milestone.title
@@ -94,7 +94,7 @@ RSpec.describe 'Multiple issue updating from issues#index', :js, feature_categor
expect(find('.issue:first-of-type')).to have_text milestone.title
- click_button 'Edit issues'
+ click_button 'Bulk edit'
check 'Select all'
click_button 'Select milestone'
click_button 'No milestone'
@@ -110,7 +110,7 @@ RSpec.describe 'Multiple issue updating from issues#index', :js, feature_categor
it 'after selecting all issues, unchecking one issue only unselects that one issue' do
visit project_issues_path(project)
- click_button 'Edit issues'
+ click_button 'Bulk edit'
check 'Select all'
uncheck issue.title
diff --git a/spec/features/merge_requests/user_mass_updates_spec.rb b/spec/features/merge_requests/user_mass_updates_spec.rb
index b0be76d386a..45d57cf8374 100644
--- a/spec/features/merge_requests/user_mass_updates_spec.rb
+++ b/spec/features/merge_requests/user_mass_updates_spec.rb
@@ -44,7 +44,7 @@ RSpec.describe 'Merge requests > User mass updates', :js, feature_category: :cod
merge_request.close
visit project_merge_requests_path(project, state: 'merged')
- click_button 'Edit merge requests'
+ click_button 'Bulk edit'
expect(page).not_to have_button 'Select status'
end
@@ -108,7 +108,7 @@ RSpec.describe 'Merge requests > User mass updates', :js, feature_category: :cod
end
def change_status(text)
- click_button 'Edit merge requests'
+ click_button 'Bulk edit'
check 'Select all'
click_button 'Select status'
click_button text
@@ -116,7 +116,7 @@ RSpec.describe 'Merge requests > User mass updates', :js, feature_category: :cod
end
def change_assignee(text)
- click_button 'Edit merge requests'
+ click_button 'Bulk edit'
check 'Select all'
within 'aside[aria-label="Bulk update"]' do
click_button 'Select assignee'
@@ -127,7 +127,7 @@ RSpec.describe 'Merge requests > User mass updates', :js, feature_category: :cod
end
def change_milestone(text)
- click_button 'Edit merge requests'
+ click_button 'Bulk edit'
check 'Select all'
click_button 'Select milestone'
click_button text
diff --git a/spec/fixtures/packages/npm/payload_with_empty_attachment.json b/spec/fixtures/packages/npm/payload_with_empty_attachment.json
new file mode 100644
index 00000000000..299ff32baf3
--- /dev/null
+++ b/spec/fixtures/packages/npm/payload_with_empty_attachment.json
@@ -0,0 +1,29 @@
+{
+ "_id": "@root/npm-test",
+ "name": "@root/npm-test",
+ "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
+ "dist-tags": {
+ "latest": "1.0.1"
+ },
+ "versions": {
+ "1.0.1": {
+ "name": "@root/npm-test",
+ "version": "1.0.1",
+ "main": "app.js",
+ "dependencies": {
+ "express": "^4.16.4"
+ },
+ "dist": {
+ "shasum": "f572d396fae9206628714fb2ce00f72e94f2258f",
+ "tarball": "http://localhost/npm/package.tgz"
+ }
+ }
+ },
+ "_attachments": {
+ "@root/npm-test-1.0.1.tgz": {
+ "data": ""
+ }
+ },
+ "id": "10",
+ "package_name": "@root/npm-test"
+}
diff --git a/spec/frontend/import_entities/import_projects/components/bitbucket_status_table_spec.js b/spec/frontend/import_entities/import_projects/components/bitbucket_status_table_spec.js
index 5773ab33e05..246c6499a97 100644
--- a/spec/frontend/import_entities/import_projects/components/bitbucket_status_table_spec.js
+++ b/spec/frontend/import_entities/import_projects/components/bitbucket_status_table_spec.js
@@ -14,11 +14,11 @@ const ImportProjectsTableStub = {
describe('BitbucketStatusTable', () => {
let wrapper;
- function createComponent(propsData, importProjectsTableStub = true, slots) {
+ function createComponent(propsData, slots) {
wrapper = shallowMount(BitbucketStatusTable, {
propsData,
stubs: {
- ImportProjectsTable: importProjectsTableStub,
+ ImportProjectsTable: ImportProjectsTableStub,
},
slots,
});
@@ -30,20 +30,23 @@ describe('BitbucketStatusTable', () => {
});
it('passes alert in incompatible-repos-warning slot', () => {
- createComponent({ providerTitle: 'Test' }, ImportProjectsTableStub);
+ createComponent({ providerTitle: 'Test' });
expect(wrapper.findComponent(GlAlert).exists()).toBe(true);
});
it('passes actions slot to import project table component', () => {
const actionsSlotContent = 'DEMO';
- createComponent({ providerTitle: 'Test' }, ImportProjectsTableStub, {
- actions: actionsSlotContent,
- });
+ createComponent(
+ { providerTitle: 'Test' },
+ {
+ actions: actionsSlotContent,
+ },
+ );
expect(wrapper.findComponent(ImportProjectsTable).text()).toBe(actionsSlotContent);
});
it('dismisses alert when requested', async () => {
- createComponent({ providerTitle: 'Test' }, ImportProjectsTableStub);
+ createComponent({ providerTitle: 'Test' });
wrapper.findComponent(GlAlert).vm.$emit('dismiss');
await nextTick();
diff --git a/spec/frontend/import_entities/import_projects/components/github_organizations_box_spec.js b/spec/frontend/import_entities/import_projects/components/github_organizations_box_spec.js
new file mode 100644
index 00000000000..b6f96cd6a23
--- /dev/null
+++ b/spec/frontend/import_entities/import_projects/components/github_organizations_box_spec.js
@@ -0,0 +1,97 @@
+import { GlCollapsibleListbox } from '@gitlab/ui';
+import MockAdapter from 'axios-mock-adapter';
+import { mount } from '@vue/test-utils';
+import { captureException } from '@sentry/browser';
+import { nextTick } from 'vue';
+
+import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
+import { createAlert } from '~/alert';
+
+import GithubOrganizationsBox from '~/import_entities/import_projects/components/github_organizations_box.vue';
+
+jest.mock('@sentry/browser');
+jest.mock('~/alert');
+
+const MOCK_RESPONSE = {
+ provider_groups: [{ name: 'alpha-1' }, { name: 'alpha-2' }, { name: 'beta-1' }],
+};
+
+describe('GithubOrganizationsBox component', () => {
+ let wrapper;
+ let mockAxios;
+
+ const findListbox = () => wrapper.findComponent(GlCollapsibleListbox);
+ const mockGithubGroupPath = '/mock/groups.json';
+
+ const createComponent = (props) => {
+ wrapper = mount(GithubOrganizationsBox, {
+ propsData: {
+ value: 'some-org',
+ ...props,
+ },
+ provide: () => ({
+ statusImportGithubGroupPath: mockGithubGroupPath,
+ }),
+ });
+ };
+
+ beforeEach(() => {
+ mockAxios = new MockAdapter(axios);
+ mockAxios.onGet(mockGithubGroupPath).reply(HTTP_STATUS_OK, MOCK_RESPONSE);
+ });
+
+ afterEach(() => {
+ mockAxios.restore();
+ });
+
+ it('has underlying listbox as loading while loading organizations', () => {
+ createComponent();
+ expect(findListbox().props('loading')).toBe(true);
+ });
+
+ it('clears underlying listbox when loading is complete', async () => {
+ createComponent();
+ await axios.waitForAll();
+ expect(findListbox().props('loading')).toBe(false);
+ });
+
+ it('sets toggle-text to all organizations when selection is not provided', () => {
+ createComponent({ value: '' });
+ expect(findListbox().props('toggleText')).toBe(GithubOrganizationsBox.i18n.allOrganizations);
+ });
+
+ it('sets toggle-text to organization name when it is provided', () => {
+ const ORG_NAME = 'org';
+ createComponent({ value: ORG_NAME });
+
+ expect(findListbox().props('toggleText')).toBe(ORG_NAME);
+ });
+
+ it('emits selected organization from underlying listbox', () => {
+ createComponent();
+
+ findListbox().vm.$emit('select', 'org-id');
+ expect(wrapper.emitted('input').at(-1)).toStrictEqual(['org-id']);
+ });
+
+ it('filters list for underlying listbox', async () => {
+ createComponent();
+ await axios.waitForAll();
+
+ findListbox().vm.$emit('search', 'alpha');
+ await nextTick();
+
+ // 2 matches + 'All organizations'
+ expect(findListbox().props('items')).toHaveLength(3);
+ });
+
+ it('reports error to sentry on load', async () => {
+ mockAxios.onGet(mockGithubGroupPath).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
+ createComponent();
+ await axios.waitForAll();
+
+ expect(captureException).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalled();
+ });
+});
diff --git a/spec/frontend/import_entities/import_projects/components/github_status_table_spec.js b/spec/frontend/import_entities/import_projects/components/github_status_table_spec.js
new file mode 100644
index 00000000000..7eebff7364c
--- /dev/null
+++ b/spec/frontend/import_entities/import_projects/components/github_status_table_spec.js
@@ -0,0 +1,125 @@
+import { GlTabs, GlSearchBoxByClick } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
+import Vuex from 'vuex';
+
+import { stubComponent } from 'helpers/stub_component';
+import GithubStatusTable from '~/import_entities/import_projects/components/github_status_table.vue';
+import GithubOrganizationsBox from '~/import_entities/import_projects/components/github_organizations_box.vue';
+import ImportProjectsTable from '~/import_entities/import_projects/components/import_projects_table.vue';
+import initialState from '~/import_entities/import_projects/store/state';
+import * as getters from '~/import_entities/import_projects/store/getters';
+
+const ImportProjectsTableStub = stubComponent(ImportProjectsTable, {
+ importAllButtonText: 'IMPORT_ALL_TEXT',
+ methods: {
+ showImportAllModal: jest.fn(),
+ },
+ template:
+ '<div><slot name="filter" v-bind="{ importAllButtonText: $options.importAllButtonText, showImportAllModal }"></slot></div>',
+});
+
+Vue.use(Vuex);
+
+describe('GithubStatusTable', () => {
+ let wrapper;
+
+ const setFilterAction = jest.fn().mockImplementation(({ state }, filter) => {
+ state.filter = { ...state.filter, ...filter };
+ });
+
+ const findFilterField = () => wrapper.findComponent(GlSearchBoxByClick);
+ const selectTab = (idx) => {
+ wrapper.findComponent(GlTabs).vm.$emit('input', idx);
+ return nextTick();
+ };
+
+ function createComponent() {
+ const store = new Vuex.Store({
+ state: { ...initialState() },
+ getters,
+ actions: {
+ setFilter: setFilterAction,
+ },
+ });
+
+ wrapper = mount(GithubStatusTable, {
+ store,
+ propsData: {
+ providerTitle: 'Github',
+ },
+ stubs: {
+ ImportProjectsTable: ImportProjectsTableStub,
+ GithubOrganizationsBox: stubComponent(GithubOrganizationsBox),
+ GlTabs: false,
+ GlTab: false,
+ },
+ });
+ }
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders import table component', () => {
+ expect(wrapper.findComponent(ImportProjectsTable).exists()).toBe(true);
+ });
+
+ it('sets relation_type filter to owned repositories by default', () => {
+ expect(setFilterAction).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.objectContaining({ relation_type: 'owned' }),
+ );
+ });
+
+ it('updates relation_type and resets organization filter when tab is switched', async () => {
+ const NEW_ACTIVE_TAB_IDX = 1;
+ await selectTab(NEW_ACTIVE_TAB_IDX);
+
+ expect(setFilterAction).toHaveBeenCalledTimes(2);
+ expect(setFilterAction).toHaveBeenCalledWith(expect.anything(), {
+ ...GithubStatusTable.relationTypes[NEW_ACTIVE_TAB_IDX].backendFilter,
+ organization_login: '',
+ filter: '',
+ });
+ });
+
+ it('renders name filter disabled when tab with organization filter is selected and organization is not set', async () => {
+ const NEW_ACTIVE_TAB_IDX = GithubStatusTable.relationTypes.findIndex(
+ (entry) => entry.showOrganizationFilter,
+ );
+ await selectTab(NEW_ACTIVE_TAB_IDX);
+ expect(findFilterField().props('disabled')).toBe(true);
+ });
+
+ it('enables name filter disabled when organization is set', async () => {
+ const NEW_ACTIVE_TAB_IDX = GithubStatusTable.relationTypes.findIndex(
+ (entry) => entry.showOrganizationFilter,
+ );
+ await selectTab(NEW_ACTIVE_TAB_IDX);
+
+ wrapper.findComponent(GithubOrganizationsBox).vm.$emit('input', 'some-org');
+ await nextTick();
+
+ expect(findFilterField().props('disabled')).toBe(false);
+ });
+
+ it('updates filter when search box is changed', async () => {
+ const NEW_FILTER = 'test';
+ findFilterField().vm.$emit('submit', NEW_FILTER);
+ await nextTick();
+
+ expect(setFilterAction).toHaveBeenCalledWith(expect.anything(), {
+ filter: NEW_FILTER,
+ });
+ });
+
+ it('updates organization_login filter when GithubOrganizationsBox emits input', () => {
+ const NEW_ORG = 'some-org';
+ wrapper.findComponent(GithubOrganizationsBox).vm.$emit('input', NEW_ORG);
+
+ expect(setFilterAction).toHaveBeenCalledWith(expect.anything(), {
+ organization_login: NEW_ORG,
+ });
+ });
+});
diff --git a/spec/frontend/import_entities/import_projects/store/actions_spec.js b/spec/frontend/import_entities/import_projects/store/actions_spec.js
index f78016eefcf..3b94db37801 100644
--- a/spec/frontend/import_entities/import_projects/store/actions_spec.js
+++ b/spec/frontend/import_entities/import_projects/store/actions_spec.js
@@ -220,12 +220,14 @@ describe('import_projects store actions', () => {
describe('when rate limited', () => {
it('commits RECEIVE_REPOS_ERROR and shows rate limited error message', async () => {
- mock.onGet(`${TEST_HOST}/endpoint.json?filter=filter`).reply(HTTP_STATUS_TOO_MANY_REQUESTS);
+ mock
+ .onGet(`${TEST_HOST}/endpoint.json?filtered_field=filter`)
+ .reply(HTTP_STATUS_TOO_MANY_REQUESTS);
await testAction(
fetchRepos,
null,
- { ...localState, filter: 'filter' },
+ { ...localState, filter: { filtered_field: 'filter' } },
[{ type: REQUEST_REPOS }, { type: RECEIVE_REPOS_ERROR }],
[],
);
@@ -238,12 +240,12 @@ describe('import_projects store actions', () => {
describe('when filtered', () => {
it('fetches repos with filter applied', () => {
- mock.onGet(`${TEST_HOST}/endpoint.json?filter=filter`).reply(HTTP_STATUS_OK, payload);
+ mock.onGet(`${TEST_HOST}/endpoint.json?some_filter=filter`).reply(HTTP_STATUS_OK, payload);
return testAction(
fetchRepos,
null,
- { ...localState, filter: 'filter' },
+ { ...localState, filter: { some_filter: 'filter' } },
[
{ type: REQUEST_REPOS },
{ type: SET_PAGE, payload: 1 },
@@ -374,12 +376,12 @@ describe('import_projects store actions', () => {
describe('when filtered', () => {
beforeEach(() => {
- localState.filter = 'filter';
+ localState.filter = { some_filter: 'filter' };
});
it('fetches realtime changes with filter applied', () => {
mock
- .onGet(`${TEST_HOST}/endpoint.json?filter=filter`)
+ .onGet(`${TEST_HOST}/endpoint.json?some_filter=filter`)
.reply(HTTP_STATUS_OK, updatedProjects);
return testAction(
diff --git a/spec/frontend/import_entities/import_projects/store/mutations_spec.js b/spec/frontend/import_entities/import_projects/store/mutations_spec.js
index 514a168553a..07d247630cc 100644
--- a/spec/frontend/import_entities/import_projects/store/mutations_spec.js
+++ b/spec/frontend/import_entities/import_projects/store/mutations_spec.js
@@ -25,7 +25,7 @@ describe('import_projects store mutations', () => {
beforeEach(() => {
state = {
- filter: 'some-value',
+ filter: { someField: 'some-value' },
repositories: ['some', ' repositories'],
pageInfo: {
page: 1,
@@ -47,6 +47,17 @@ describe('import_projects store mutations', () => {
expect(state.pageInfo.endCursor).toBe(null);
expect(state.pageInfo.hasNextPage).toBe(true);
});
+
+ it('merges filter updates', () => {
+ const originalFilter = { ...state.filter };
+ const anotherFilter = { anotherField: 'another-value' };
+ mutations[types.SET_FILTER](state, anotherFilter);
+
+ expect(state.filter).toStrictEqual({
+ ...originalFilter,
+ ...anotherFilter,
+ });
+ });
});
describe(`${types.REQUEST_REPOS}`, () => {
diff --git a/spec/frontend/issues/list/components/issues_list_app_spec.js b/spec/frontend/issues/list/components/issues_list_app_spec.js
index 076fdc4a991..af24b547545 100644
--- a/spec/frontend/issues/list/components/issues_list_app_spec.js
+++ b/spec/frontend/issues/list/components/issues_list_app_spec.js
@@ -304,13 +304,13 @@ describe('CE IssuesListApp component', () => {
it('renders when user has permissions', () => {
wrapper = mountComponent({ provide: { canBulkUpdate: true }, mountFn: mount });
- expect(findGlButton().text()).toBe('Edit issues');
+ expect(findGlButton().text()).toBe('Bulk edit');
});
it('does not render when user does not have permissions', () => {
wrapper = mountComponent({ provide: { canBulkUpdate: false }, mountFn: mount });
- expect(findGlButtons().filter((button) => button.text() === 'Edit issues')).toHaveLength(0);
+ expect(findGlButtons().filter((button) => button.text() === 'Bulk edit')).toHaveLength(0);
});
it('emits "issuables:enableBulkEdit" event to legacy bulk edit class', async () => {
diff --git a/spec/frontend/vue_compat_test_setup.js b/spec/frontend/vue_compat_test_setup.js
index b7ac7449045..8c0346e6198 100644
--- a/spec/frontend/vue_compat_test_setup.js
+++ b/spec/frontend/vue_compat_test_setup.js
@@ -96,15 +96,14 @@ if (global.document) {
$slots: slots = {},
$scopedSlots: scopedSlots = {},
$parent: parent,
- $vnode: { children },
+ $vnode: vnode,
} = this;
- const hasDefaultSlot = 'default' in slots || 'default' in scopedSlots;
- const isTheOnlyChild = parent && parent.$.subTree.children === children;
-
+ const hasStaticDefaultSlot = 'default' in slots && !('default' in scopedSlots);
+ const isTheOnlyChild = parent?.$.subTree === vnode;
// this condition should be altered when https://github.com/vuejs/vue-test-utils/pull/2068 is merged
// and our codebase will be updated to include it (@vue/test-utils@1.3.6 I assume)
- const shouldRenderAllSlots = !hasDefaultSlot && isTheOnlyChild;
+ const shouldRenderAllSlots = !hasStaticDefaultSlot && isTheOnlyChild;
const renderSlotByName = (slotName) => {
const slot = scopedSlots[slotName] || slots[slotName];
diff --git a/spec/models/service_desk/custom_email_verification_spec.rb b/spec/models/service_desk/custom_email_verification_spec.rb
index f0a6028f21d..f114367cfbf 100644
--- a/spec/models/service_desk/custom_email_verification_spec.rb
+++ b/spec/models/service_desk/custom_email_verification_spec.rb
@@ -3,89 +3,149 @@
require 'spec_helper'
RSpec.describe ServiceDesk::CustomEmailVerification, feature_category: :service_desk do
- let(:user) { build_stubbed(:user) }
- let(:project) { build_stubbed(:project) }
- let(:verification) { build_stubbed(:service_desk_custom_email_verification, project: project) }
- let(:token) { 'XXXXXXXXXXXX' }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:user) { create(:user) }
+
+ let(:generate_token_pattern) { /\A\p{Alnum}{12}\z/ }
describe '.generate_token' do
it 'matches expected output' do
- expect(described_class.generate_token).to match(/\A\p{Alnum}{12}\z/)
+ expect(described_class.generate_token).to match(generate_token_pattern)
end
end
describe 'validations' do
+ subject { build(:service_desk_custom_email_verification, project: project) }
+
it { is_expected.to validate_presence_of(:project) }
it { is_expected.to validate_presence_of(:state) }
+
+ context 'when status is :started' do
+ before do
+ subject.mark_as_started!(user)
+ end
+
+ it { is_expected.to validate_presence_of(:token) }
+ it { is_expected.to validate_length_of(:token).is_equal_to(12) }
+
+ it 'matches .generate_token pattern' do
+ expect(subject.token).to match(generate_token_pattern)
+ end
+
+ it { is_expected.to validate_presence_of(:triggerer) }
+ it { is_expected.to validate_presence_of(:triggered_at) }
+ it { is_expected.to validate_absence_of(:error) }
+ end
+
+ context 'when status is :finished' do
+ before do
+ subject.mark_as_started!(user)
+ subject.mark_as_finished!
+ end
+
+ it { is_expected.to validate_absence_of(:token) }
+ it { is_expected.to validate_absence_of(:error) }
+ end
+
+ context 'when status is :failed' do
+ before do
+ subject.mark_as_started!(user)
+ subject.mark_as_failed!(:smtp_host_issue)
+ end
+
+ it { is_expected.to validate_presence_of(:error) }
+ it { is_expected.to validate_absence_of(:token) }
+ end
end
- describe '#accepted_until' do
- context 'when no custom email is set up' do
- it 'returns nil' do
- expect(subject.accepted_until).to be_nil
+ describe 'status state machine' do
+ subject { build(:service_desk_custom_email_verification, project: project) }
+
+ describe 'transitioning to started' do
+ it 'records the started at time and generates token' do
+ subject.mark_as_started!(user)
+
+ is_expected.to be_started
+ expect(subject.token).to be_present
+ expect(subject.triggered_at).to be_present
+ expect(subject.triggerer).to eq(user)
end
end
- context 'when custom email is set up' do
- subject { verification.accepted_until }
+ describe 'transitioning to finished' do
+ it 'removes the generated token' do
+ subject.mark_as_started!(user)
+ subject.mark_as_finished!
- it { is_expected.to be_nil }
+ is_expected.to be_finished
+ expect(subject.token).not_to be_present
+ end
+ end
- context 'when verification process started' do
- let(:triggered_at) { 2.minutes.ago }
+ describe 'transitioning to failed' do
+ let(:error) { :smtp_host_issue }
- before do
- verification.assign_attributes(
- state: "running",
- triggered_at: triggered_at,
- triggerer: user,
- token: token
- )
- end
+ it 'removes the generated token' do
+ subject.mark_as_started!(user)
+ subject.mark_as_failed!(error)
- it { is_expected.to eq(described_class::TIMEFRAME.since(triggered_at)) }
+ is_expected.to be_failed
+ expect(subject.token).not_to be_present
+ expect(subject.error).to eq(error.to_s)
end
end
end
- describe '#in_timeframe?' do
- context 'when no custom email is set up' do
- it 'returns false' do
- expect(subject).not_to be_in_timeframe
- end
+ describe '#accepted_until' do
+ it 'returns nil' do
+ expect(subject.accepted_until).to be_nil
end
- context 'when custom email is set up' do
- it { is_expected.not_to be_in_timeframe }
+ context 'when state is :started and successfully transitioned' do
+ let(:triggered_at) { 2.minutes.ago }
- context 'when verification process started' do
- let(:triggered_at) { 1.second.ago }
+ before do
+ subject.project = project
+ subject.mark_as_started!(user)
+ end
- before do
- subject.assign_attributes(
- state: "running",
- triggered_at: triggered_at,
- triggerer: user,
- token: token
- )
+ it 'returns correct timeframe end time' do
+ expect(subject.accepted_until).to eq(described_class::TIMEFRAME.since(subject.triggered_at))
+ end
+
+ context 'when triggered_at is not set' do
+ it 'returns nil' do
+ subject.triggered_at = nil
+ expect(subject.accepted_until).to be nil
end
+ end
+ end
+ end
- it { is_expected.to be_in_timeframe }
+ describe '#in_timeframe?' do
+ it { is_expected.not_to be_in_timeframe }
- context 'and timeframe was missed' do
- let(:triggered_at) { (described_class::TIMEFRAME + 1).ago }
+ context 'when state is :started and successfully transitioned' do
+ before do
+ subject.project = project
+ subject.mark_as_started!(user)
+ end
- before do
- subject.triggered_at = triggered_at
- end
+ it { is_expected.to be_in_timeframe }
- it { is_expected.not_to be_in_timeframe }
+ context 'and timeframe was missed' do
+ before do
+ subject.triggered_at = (described_class::TIMEFRAME + 1).ago
end
+
+ it { is_expected.not_to be_in_timeframe }
end
end
end
describe 'encrypted #token' do
+ let(:token) { 'XXXXXXXXXXXX' }
+
subject { build_stubbed(:service_desk_custom_email_verification, token: token) }
it 'saves and retrieves the encrypted token and iv correctly' do
@@ -101,9 +161,10 @@ RSpec.describe ServiceDesk::CustomEmailVerification, feature_category: :service_
it { is_expected.to belong_to(:triggerer) }
it 'can access service desk setting from project' do
- setting = build_stubbed(:service_desk_setting, project: project)
+ subject.project = project
+ setting = build_stubbed(:service_desk_setting, project: subject.project)
- expect(verification.service_desk_setting).to eq(setting)
+ expect(subject.service_desk_setting).to eq(setting)
end
end
end
diff --git a/spec/requests/api/npm_project_packages_spec.rb b/spec/requests/api/npm_project_packages_spec.rb
index f621af5d968..1f5ebc80824 100644
--- a/spec/requests/api/npm_project_packages_spec.rb
+++ b/spec/requests/api/npm_project_packages_spec.rb
@@ -214,6 +214,14 @@ RSpec.describe API::NpmProjectPackages, feature_category: :package_registry do
it_behaves_like 'not a package tracking event'
end
end
+
+ context 'invalid package attachment data' do
+ let(:package_name) { "@#{group.path}/my_package_name" }
+ let(:params) { upload_params(package_name: package_name, file: 'npm/payload_with_empty_attachment.json') }
+
+ it_behaves_like 'handling invalid record with 400 error'
+ it_behaves_like 'not a package tracking event'
+ end
end
context 'valid package params' do
diff --git a/spec/services/packages/npm/create_package_service_spec.rb b/spec/services/packages/npm/create_package_service_spec.rb
index b2ae89f0f9d..a12d86412d8 100644
--- a/spec/services/packages/npm/create_package_service_spec.rb
+++ b/spec/services/packages/npm/create_package_service_spec.rb
@@ -294,6 +294,13 @@ RSpec.describe Packages::Npm::CreatePackageService, feature_category: :package_r
end
end
+ context 'with empty attachment data' do
+ let(:params) { super().merge({ _attachments: { "#{package_name}-#{version}.tgz" => { data: '' } } }) }
+
+ it { expect(subject[:http_status]).to eq 400 }
+ it { expect(subject[:message]).to eq 'Attachment data is empty.' }
+ end
+
it 'obtains a lease to create a new package' do
expect_to_obtain_exclusive_lease(lease_key, timeout: described_class::DEFAULT_LEASE_TIMEOUT)
diff --git a/yarn.lock b/yarn.lock
index 31cbc014ef8..1a0b2612033 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -12708,10 +12708,10 @@ vue-template-es2015-compiler@^1.9.0:
resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz#1ee3bc9a16ecbf5118be334bb15f9c46f82f5825"
integrity sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==
-vue-test-utils-compat@^0.0.11:
- version "0.0.11"
- resolved "https://registry.yarnpkg.com/vue-test-utils-compat/-/vue-test-utils-compat-0.0.11.tgz#d3e92bcee6893e7506b5d6463f774678f144fe7a"
- integrity sha512-vAc19M4GS0qxGZGB0UZg7mG6j90hyNoDQXWYCEG4Sjahyi+v+CHq9xHa6js70S8HExNkm2f4LF3x2+gbCMFVUQ==
+vue-test-utils-compat@0.0.13:
+ version "0.0.13"
+ resolved "https://registry.yarnpkg.com/vue-test-utils-compat/-/vue-test-utils-compat-0.0.13.tgz#31cf91746601c2cafb0f4e560d32e7d6782c1380"
+ integrity sha512-h4EBx87YmYTZF8xZvRmX04YsuyTHz4kkmcJS6J7NBMgWQecjq/fmAfLzpYnVF32E7Yus4uB3xv6iaaVtJp4YGQ==
vue-virtual-scroll-list@^1.4.7:
version "1.4.7"