summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-03-07 00:07:42 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2023-03-07 00:07:42 +0000
commitdefeeba1a8d6fa8784db1c50ca4ff9e8b56f539c (patch)
treeda50633fb0b41bd238a1b972c69488073599fe28
parent8ec004d6d8d92f00d0598e94ede4d31ab1e8f18e (diff)
downloadgitlab-ce-defeeba1a8d6fa8784db1c50ca4ff9e8b56f539c.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--app/assets/javascripts/analytics/cycle_analytics/components/value_stream_filters.vue2
-rw-r--r--app/assets/javascripts/analytics/shared/components/projects_dropdown_filter.vue5
-rw-r--r--app/assets/javascripts/behaviors/markdown/render_json_table.js2
-rw-r--r--app/assets/javascripts/behaviors/markdown/render_sandboxed_mermaid.js2
-rw-r--r--app/assets/javascripts/behaviors/preview_markdown.js2
-rw-r--r--app/assets/javascripts/deploy_tokens/components/new_deploy_token.vue2
-rw-r--r--app/assets/javascripts/deprecated_notes.js2
-rw-r--r--app/assets/javascripts/design_management/pages/design/index.vue4
-rw-r--r--app/assets/javascripts/diffs/components/app.vue2
-rw-r--r--app/assets/javascripts/diffs/components/diff_expansion_cell.vue2
-rw-r--r--app/assets/javascripts/diffs/components/diff_file.vue2
-rw-r--r--app/assets/javascripts/diffs/store/actions.js2
-rw-r--r--app/assets/javascripts/import_entities/import_groups/components/import_table.vue2
-rw-r--r--app/assets/javascripts/import_entities/import_groups/services/status_poller.js2
-rw-r--r--app/assets/javascripts/import_entities/import_projects/store/actions.js10
-rw-r--r--app/assets/javascripts/notes/components/note_edited_text.vue19
-rw-r--r--app/assets/javascripts/notes/i18n.js2
-rw-r--r--app/assets/javascripts/service_ping_consent.js2
-rw-r--r--app/assets/javascripts/task_list.js2
-rw-r--r--app/assets/javascripts/token_access/components/inbound_token_access.vue2
-rw-r--r--app/assets/javascripts/token_access/components/opt_in_jwt.vue2
-rw-r--r--app/assets/javascripts/token_access/components/outbound_token_access.vue2
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue2
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_tree_children.vue2
-rw-r--r--app/assets/stylesheets/page_bundles/issuable.scss5
-rw-r--r--app/controllers/admin/projects_controller.rb27
-rw-r--r--app/controllers/profiles/personal_access_tokens_controller.rb5
-rw-r--r--app/finders/abuse_reports_finder.rb10
-rw-r--r--app/helpers/application_helper.rb2
-rw-r--r--app/helpers/routing/projects_helper.rb5
-rw-r--r--app/models/alert_management/alert.rb2
-rw-r--r--app/models/commit.rb3
-rw-r--r--app/models/commit_range.rb2
-rw-r--r--app/models/concerns/noteable.rb1
-rw-r--r--app/models/concerns/referable.rb6
-rw-r--r--app/models/design_management/design.rb4
-rw-r--r--app/models/group.rb13
-rw-r--r--app/models/issue.rb10
-rw-r--r--app/models/merge_request.rb2
-rw-r--r--app/models/milestone.rb2
-rw-r--r--app/models/namespace.rb45
-rw-r--r--app/models/operations/feature_flag.rb2
-rw-r--r--app/models/project.rb13
-rw-r--r--app/models/snippet.rb2
-rw-r--r--app/services/groups/autocomplete_service.rb2
-rw-r--r--app/services/personal_access_tokens/create_service.rb8
-rw-r--r--app/views/admin/projects/_form.html.haml23
-rw-r--r--app/views/admin/projects/_projects.html.haml2
-rw-r--r--app/views/admin/projects/edit.html.haml4
-rw-r--r--app/views/admin/projects/show.html.haml4
-rw-r--r--app/views/notify/merge_request_status_email.text.haml2
-rw-r--r--config/routes/admin.rb4
-rw-r--r--doc/development/code_review.md2
-rw-r--r--doc/user/admin_area/index.md10
-rw-r--r--doc/user/application_security/policies/img/scheduled_scan_execution_policies_diagram.pngbin0 -> 35066 bytes
-rw-r--r--doc/user/application_security/policies/scan-execution-policies.md16
-rw-r--r--doc/user/compliance/compliance_report/index.md2
-rw-r--r--lib/banzai/filter/references/design_reference_filter.rb2
-rw-r--r--lib/banzai/filter/references/issue_reference_filter.rb2
-rw-r--r--lib/banzai/reference_parser/issue_parser.rb1
-rw-r--r--lib/gitlab/github_import/importer/events/cross_referenced.rb1
-rw-r--r--locale/gitlab.pot21
-rw-r--r--spec/factories/work_items.rb7
-rw-r--r--spec/features/admin/admin_projects_spec.rb25
-rw-r--r--spec/finders/abuse_reports_finder_spec.rb30
-rw-r--r--spec/helpers/issuables_helper_spec.rb18
-rw-r--r--spec/lib/banzai/filter/issuable_reference_expansion_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/references/reference_cache_spec.rb3
-rw-r--r--spec/lib/gitlab/issuable_sorter_spec.rb42
-rw-r--r--spec/models/abuse_report_spec.rb5
-rw-r--r--spec/models/design_management/design_spec.rb4
-rw-r--r--spec/models/issue_spec.rb125
-rw-r--r--spec/models/namespace_spec.rb111
-rw-r--r--spec/models/project_spec.rb54
-rw-r--r--spec/requests/admin/projects_controller_spec.rb58
-rw-r--r--spec/serializers/group_issuable_autocomplete_entity_spec.rb3
-rw-r--r--spec/services/personal_access_tokens/create_service_spec.rb20
77 files changed, 714 insertions, 138 deletions
diff --git a/app/assets/javascripts/analytics/cycle_analytics/components/value_stream_filters.vue b/app/assets/javascripts/analytics/cycle_analytics/components/value_stream_filters.vue
index 17decb6b448..82bb1f77cbb 100644
--- a/app/assets/javascripts/analytics/cycle_analytics/components/value_stream_filters.vue
+++ b/app/assets/javascripts/analytics/cycle_analytics/components/value_stream_filters.vue
@@ -82,9 +82,7 @@ export default {
<div>
<projects-dropdown-filter
v-if="hasProjectFilter"
- :key="groupId"
class="js-projects-dropdown-filter project-select gl-mb-2 gl-lg-mb-0"
- :group-id="groupId"
:group-namespace="groupPath"
:query-params="projectsQueryParams"
:multi-select="$options.multiProjectSelect"
diff --git a/app/assets/javascripts/analytics/shared/components/projects_dropdown_filter.vue b/app/assets/javascripts/analytics/shared/components/projects_dropdown_filter.vue
index 5bb60d91f1e..98193de4a12 100644
--- a/app/assets/javascripts/analytics/shared/components/projects_dropdown_filter.vue
+++ b/app/assets/javascripts/analytics/shared/components/projects_dropdown_filter.vue
@@ -32,11 +32,6 @@ export default {
GlTruncate,
},
props: {
- groupId: {
- type: Number,
- required: false,
- default: null,
- },
groupNamespace: {
type: String,
required: true,
diff --git a/app/assets/javascripts/behaviors/markdown/render_json_table.js b/app/assets/javascripts/behaviors/markdown/render_json_table.js
index 4d9ac1d266b..aa0e7d38113 100644
--- a/app/assets/javascripts/behaviors/markdown/render_json_table.js
+++ b/app/assets/javascripts/behaviors/markdown/render_json_table.js
@@ -1,7 +1,7 @@
import { memoize } from 'lodash';
import Vue from 'vue';
import { __ } from '~/locale';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
// Async import component since we might not need it...
const JSONTable = memoize(() =>
diff --git a/app/assets/javascripts/behaviors/markdown/render_sandboxed_mermaid.js b/app/assets/javascripts/behaviors/markdown/render_sandboxed_mermaid.js
index 66007aa9e3d..bd9e41ac0ba 100644
--- a/app/assets/javascripts/behaviors/markdown/render_sandboxed_mermaid.js
+++ b/app/assets/javascripts/behaviors/markdown/render_sandboxed_mermaid.js
@@ -8,7 +8,7 @@ import {
} from '~/lib/utils/url_utility';
import { darkModeEnabled } from '~/lib/utils/color_utils';
import { setAttributes, isElementVisible } from '~/lib/utils/dom_utils';
-import { createAlert, VARIANT_WARNING } from '~/flash';
+import { createAlert, VARIANT_WARNING } from '~/alert';
import { unrestrictedPages } from './constants';
// Renders diagrams and flowcharts from text using Mermaid in any element with the
diff --git a/app/assets/javascripts/behaviors/preview_markdown.js b/app/assets/javascripts/behaviors/preview_markdown.js
index 32e395e4f3c..dc408f5a950 100644
--- a/app/assets/javascripts/behaviors/preview_markdown.js
+++ b/app/assets/javascripts/behaviors/preview_markdown.js
@@ -2,7 +2,7 @@
import $ from 'jquery';
import { renderGFM } from '~/behaviors/markdown/render_gfm';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
diff --git a/app/assets/javascripts/deploy_tokens/components/new_deploy_token.vue b/app/assets/javascripts/deploy_tokens/components/new_deploy_token.vue
index 57fae608efa..486baccfad0 100644
--- a/app/assets/javascripts/deploy_tokens/components/new_deploy_token.vue
+++ b/app/assets/javascripts/deploy_tokens/components/new_deploy_token.vue
@@ -9,7 +9,7 @@ import {
GlSprintf,
GlLink,
} from '@gitlab/ui';
-import { createAlert, VARIANT_INFO } from '~/flash';
+import { createAlert, VARIANT_INFO } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { formatDate } from '~/lib/utils/datetime_utility';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
diff --git a/app/assets/javascripts/deprecated_notes.js b/app/assets/javascripts/deprecated_notes.js
index 7503df9194b..5b398623164 100644
--- a/app/assets/javascripts/deprecated_notes.js
+++ b/app/assets/javascripts/deprecated_notes.js
@@ -16,7 +16,7 @@ import $ from 'jquery';
import { escape, uniqueId } from 'lodash';
import Vue from 'vue';
import { renderGFM } from '~/behaviors/markdown/render_gfm';
-import { createAlert, VARIANT_INFO } from '~/flash';
+import { createAlert, VARIANT_INFO } from '~/alert';
import { sanitize } from '~/lib/dompurify';
import '~/lib/utils/jquery_at_who';
import AjaxCache from '~/lib/utils/ajax_cache';
diff --git a/app/assets/javascripts/design_management/pages/design/index.vue b/app/assets/javascripts/design_management/pages/design/index.vue
index 2780ae70cc7..c099c06df47 100644
--- a/app/assets/javascripts/design_management/pages/design/index.vue
+++ b/app/assets/javascripts/design_management/pages/design/index.vue
@@ -3,7 +3,7 @@ import { GlAlert } from '@gitlab/ui';
import { isNull } from 'lodash';
import Mousetrap from 'mousetrap';
import { keysFor, ISSUE_CLOSE_DESIGN } from '~/behaviors/shortcuts/keybindings';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { fetchPolicies } from '~/lib/graphql';
import { updateGlobalTodoCount } from '~/sidebar/utils';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
@@ -253,7 +253,7 @@ export default {
},
onQueryError(message) {
// because we redirect user to /designs (the issue page),
- // we want to create these flashes on the issue page
+ // we want to create these alerts on the issue page
createAlert({ message });
this.$router.push({ name: this.$options.DESIGNS_ROUTE_NAME });
},
diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue
index 9a6b36ad2e8..9ccba88f7e6 100644
--- a/app/assets/javascripts/diffs/components/app.vue
+++ b/app/assets/javascripts/diffs/components/app.vue
@@ -11,7 +11,7 @@ import {
MR_COMMITS_NEXT_COMMIT,
MR_COMMITS_PREVIOUS_COMMIT,
} from '~/behaviors/shortcuts/keybindings';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { isSingleViewStyle } from '~/helpers/diffs_helper';
import { helpPagePath } from '~/helpers/help_page_helper';
import { parseBoolean } from '~/lib/utils/common_utils';
diff --git a/app/assets/javascripts/diffs/components/diff_expansion_cell.vue b/app/assets/javascripts/diffs/components/diff_expansion_cell.vue
index 8fcbc4b5cce..53a55aac1ec 100644
--- a/app/assets/javascripts/diffs/components/diff_expansion_cell.vue
+++ b/app/assets/javascripts/diffs/components/diff_expansion_cell.vue
@@ -2,7 +2,7 @@
import { GlTooltipDirective, GlIcon, GlLoadingIcon } from '@gitlab/ui';
import { mapActions } from 'vuex';
import SafeHtml from '~/vue_shared/directives/safe_html';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { s__, sprintf } from '~/locale';
import { UNFOLD_COUNT, INLINE_DIFF_LINES_KEY } from '../constants';
import * as utils from '../store/utils';
diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue
index 564f776edd2..c19174dda8a 100644
--- a/app/assets/javascripts/diffs/components/diff_file.vue
+++ b/app/assets/javascripts/diffs/components/diff_file.vue
@@ -5,7 +5,7 @@ import { mapActions, mapGetters, mapState } from 'vuex';
import SafeHtml from '~/vue_shared/directives/safe_html';
import { IdState } from 'vendor/vue-virtual-scroller';
import DiffContent from 'jh_else_ce/diffs/components/diff_content.vue';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { hasDiff } from '~/helpers/diffs_helper';
import { diffViewerErrors } from '~/ide/constants';
import { scrollToElement } from '~/lib/utils/common_utils';
diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js
index 561e2a4f134..9236e14beb1 100644
--- a/app/assets/javascripts/diffs/store/actions.js
+++ b/app/assets/javascripts/diffs/store/actions.js
@@ -5,7 +5,7 @@ import {
historyPushState,
scrollToElement,
} from '~/lib/utils/common_utils';
-import { createAlert, VARIANT_WARNING } from '~/flash';
+import { createAlert, VARIANT_WARNING } from '~/alert';
import { diffViewerModes } from '~/ide/constants';
import axios from '~/lib/utils/axios_utils';
diff --git a/app/assets/javascripts/import_entities/import_groups/components/import_table.vue b/app/assets/javascripts/import_entities/import_groups/components/import_table.vue
index 5ef6c974920..1c196e05ff0 100644
--- a/app/assets/javascripts/import_entities/import_groups/components/import_table.vue
+++ b/app/assets/javascripts/import_entities/import_groups/components/import_table.vue
@@ -15,7 +15,7 @@ import {
GlTooltipDirective,
} from '@gitlab/ui';
import { debounce } from 'lodash';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { s__, __, n__, sprintf } from '~/locale';
import { HTTP_STATUS_TOO_MANY_REQUESTS } from '~/lib/utils/http_status';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
diff --git a/app/assets/javascripts/import_entities/import_groups/services/status_poller.js b/app/assets/javascripts/import_entities/import_groups/services/status_poller.js
index 6ad5e448a40..10496fce11b 100644
--- a/app/assets/javascripts/import_entities/import_groups/services/status_poller.js
+++ b/app/assets/javascripts/import_entities/import_groups/services/status_poller.js
@@ -1,5 +1,5 @@
import Visibility from 'visibilityjs';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import Poll from '~/lib/utils/poll';
import { s__ } from '~/locale';
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 e0db585eb3e..e3c32028b13 100644
--- a/app/assets/javascripts/import_entities/import_projects/store/actions.js
+++ b/app/assets/javascripts/import_entities/import_projects/store/actions.js
@@ -1,6 +1,6 @@
import Visibility from 'visibilityjs';
import _ from 'lodash';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { HTTP_STATUS_TOO_MANY_REQUESTS } from '~/lib/utils/http_status';
@@ -141,7 +141,7 @@ const fetchImportFactory = (importPath = isRequired()) => (
})
.catch((e) => {
const serverErrorMessage = e?.response?.data?.errors;
- const flashMessage = serverErrorMessage
+ const alertMessage = serverErrorMessage
? sprintf(
s__('ImportProjects|Importing the project failed: %{reason}'),
{
@@ -152,7 +152,7 @@ const fetchImportFactory = (importPath = isRequired()) => (
: s__('ImportProjects|Importing the project failed');
createAlert({
- message: flashMessage,
+ message: alertMessage,
});
commit(types.RECEIVE_IMPORT_ERROR, repoId);
@@ -179,7 +179,7 @@ export const cancelImportFactory = (cancelImportPath) => ({ state, commit }, { r
})
.catch((e) => {
const serverErrorMessage = e?.response?.data?.errors;
- const flashMessage = serverErrorMessage
+ const alertMessage = serverErrorMessage
? sprintf(
s__('ImportProjects|Cancelling project import failed: %{reason}'),
{
@@ -190,7 +190,7 @@ export const cancelImportFactory = (cancelImportPath) => ({ state, commit }, { r
: s__('ImportProjects|Cancelling project import failed');
createAlert({
- message: flashMessage,
+ message: alertMessage,
});
});
};
diff --git a/app/assets/javascripts/notes/components/note_edited_text.vue b/app/assets/javascripts/notes/components/note_edited_text.vue
index bdff2e6317c..25c82c29a29 100644
--- a/app/assets/javascripts/notes/components/note_edited_text.vue
+++ b/app/assets/javascripts/notes/components/note_edited_text.vue
@@ -46,14 +46,21 @@ export default {
<template #actionText>
{{ actionText }}
</template>
+ <template #actionDetail>
+ {{ actionDetailText }}
+ </template>
+ <template #timeago>
+ <time-ago-tooltip :time="editedAt" tooltip-placement="bottom" />
+ </template>
<template #author>
- <gl-link :href="editedBy.path" :data-user-id="editedBy.id" class="js-user-link author-link">
+ <gl-link
+ :href="editedBy.path"
+ :data-user-id="editedBy.id"
+ class="js-user-link author-link gl-hover-text-decoration-underline"
+ >
{{ editedBy.name }}
</gl-link>
</template>
- <template #actionDetail>
- {{ actionDetailText }}
- </template>
</gl-sprintf>
<gl-sprintf v-else :message="$options.i18n.actionWithoutAuthor">
<template #actionText>
@@ -62,7 +69,9 @@ export default {
<template #actionDetail>
{{ actionDetailText }}
</template>
+ <template #timeago>
+ <time-ago-tooltip :time="editedAt" tooltip-placement="bottom" />
+ </template>
</gl-sprintf>
- <time-ago-tooltip :time="editedAt" tooltip-placement="bottom" />
</div>
</template>
diff --git a/app/assets/javascripts/notes/i18n.js b/app/assets/javascripts/notes/i18n.js
index 8aad3ec07cb..4bf2a8d70a7 100644
--- a/app/assets/javascripts/notes/i18n.js
+++ b/app/assets/javascripts/notes/i18n.js
@@ -51,6 +51,6 @@ export const COMMENT_FORM = {
};
export const EDITED_TEXT = {
- actionWithAuthor: __('%{actionText} by %{author} %{actionDetail}'),
+ actionWithAuthor: __('%{actionText} %{actionDetail} %{timeago} by %{author}'),
actionWithoutAuthor: __('%{actionText} %{actionDetail}'),
};
diff --git a/app/assets/javascripts/service_ping_consent.js b/app/assets/javascripts/service_ping_consent.js
index 1cb4e188e54..7d6e7e81f3b 100644
--- a/app/assets/javascripts/service_ping_consent.js
+++ b/app/assets/javascripts/service_ping_consent.js
@@ -1,5 +1,5 @@
import $ from 'jquery';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import axios from './lib/utils/axios_utils';
import { parseBoolean } from './lib/utils/common_utils';
import { __ } from './locale';
diff --git a/app/assets/javascripts/task_list.js b/app/assets/javascripts/task_list.js
index a7760ad5d0b..bb344ade344 100644
--- a/app/assets/javascripts/task_list.js
+++ b/app/assets/javascripts/task_list.js
@@ -1,7 +1,7 @@
import $ from 'jquery';
import 'deckar01-task_list';
import { __ } from '~/locale';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import axios from './lib/utils/axios_utils';
export default class TaskList {
diff --git a/app/assets/javascripts/token_access/components/inbound_token_access.vue b/app/assets/javascripts/token_access/components/inbound_token_access.vue
index feaf9072ee2..1904846fcbc 100644
--- a/app/assets/javascripts/token_access/components/inbound_token_access.vue
+++ b/app/assets/javascripts/token_access/components/inbound_token_access.vue
@@ -9,7 +9,7 @@ import {
GlSprintf,
GlToggle,
} from '@gitlab/ui';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { __, s__ } from '~/locale';
import { helpPagePath } from '~/helpers/help_page_helper';
import inboundAddProjectCIJobTokenScopeMutation from '../graphql/mutations/inbound_add_project_ci_job_token_scope.mutation.graphql';
diff --git a/app/assets/javascripts/token_access/components/opt_in_jwt.vue b/app/assets/javascripts/token_access/components/opt_in_jwt.vue
index c774f37b1e4..9485e0c3667 100644
--- a/app/assets/javascripts/token_access/components/opt_in_jwt.vue
+++ b/app/assets/javascripts/token_access/components/opt_in_jwt.vue
@@ -1,7 +1,7 @@
<script>
import { GlLink, GlLoadingIcon, GlSprintf, GlToggle } from '@gitlab/ui';
import CodeInstruction from '~/vue_shared/components/registry/code_instruction.vue';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { __, s__ } from '~/locale';
import updateOptInJwtMutation from '../graphql/mutations/update_opt_in_jwt.mutation.graphql';
import getOptInJwtSettingQuery from '../graphql/queries/get_opt_in_jwt_setting.query.graphql';
diff --git a/app/assets/javascripts/token_access/components/outbound_token_access.vue b/app/assets/javascripts/token_access/components/outbound_token_access.vue
index 0deae1a1d82..d9c23c6c7f3 100644
--- a/app/assets/javascripts/token_access/components/outbound_token_access.vue
+++ b/app/assets/javascripts/token_access/components/outbound_token_access.vue
@@ -9,7 +9,7 @@ import {
GlSprintf,
GlToggle,
} from '@gitlab/ui';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { __, s__ } from '~/locale';
import { helpPagePath } from '~/helpers/help_page_helper';
import addProjectCIJobTokenScopeMutation from '../graphql/mutations/add_project_ci_job_token_scope.mutation.graphql';
diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue
index 3a3a846bce5..49574fe8846 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue
+++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue
@@ -2,7 +2,7 @@
import { GlButton, GlLink, GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { __, s__ } from '~/locale';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import RichTimestampTooltip from '~/vue_shared/components/rich_timestamp_tooltip.vue';
import WorkItemLinkChildMetadata from 'ee_else_ce/work_items/components/work_item_links/work_item_link_child_metadata.vue';
diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_tree_children.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_tree_children.vue
index 71de6867680..e233a2219fa 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/work_item_tree_children.vue
+++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_tree_children.vue
@@ -1,5 +1,5 @@
<script>
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { s__ } from '~/locale';
import updateWorkItemMutation from '../../graphql/update_work_item.mutation.graphql';
diff --git a/app/assets/stylesheets/page_bundles/issuable.scss b/app/assets/stylesheets/page_bundles/issuable.scss
index f364170c99f..7321da1526d 100644
--- a/app/assets/stylesheets/page_bundles/issuable.scss
+++ b/app/assets/stylesheets/page_bundles/issuable.scss
@@ -92,10 +92,11 @@
color: var(--gray-500, $gray-500);
display: block;
margin: 16px 0 0;
- font-size: 85%;
+ font-size: $gl-font-size-small;
.author-link {
- color: var(--gray-500, $gray-500);
+ color: var(--gray-700, $gray-700);
+ font-size: $gl-font-size-small;
}
}
diff --git a/app/controllers/admin/projects_controller.rb b/app/controllers/admin/projects_controller.rb
index 5d37bd27302..70c2d262b72 100644
--- a/app/controllers/admin/projects_controller.rb
+++ b/app/controllers/admin/projects_controller.rb
@@ -3,10 +3,10 @@
class Admin::ProjectsController < Admin::ApplicationController
include MembersPresentation
- before_action :project, only: [:show, :transfer, :repository_check, :destroy]
+ before_action :project, only: [:show, :transfer, :repository_check, :destroy, :edit, :update]
before_action :group, only: [:show, :transfer]
- feature_category :projects, [:index, :show, :transfer, :destroy]
+ feature_category :projects, [:index, :show, :transfer, :destroy, :edit, :update]
feature_category :source_code_management, [:repository_check]
def index
@@ -62,6 +62,18 @@ class Admin::ProjectsController < Admin::ApplicationController
end
# rubocop: enable CodeReuse/ActiveRecord
+ def edit; end
+
+ def update
+ result = ::Projects::UpdateService.new(@project, current_user, project_params).execute
+
+ if result[:status] == :success
+ redirect_to [:admin, @project], notice: format(_("Project '%{project_name}' was successfully updated."), project_name: @project.name)
+ else
+ render "edit"
+ end
+ end
+
def repository_check
RepositoryCheck::SingleRepositoryWorker.perform_async(@project.id) # rubocop:disable CodeReuse/Worker
@@ -83,6 +95,17 @@ class Admin::ProjectsController < Admin::ApplicationController
def group
@group ||= @project.group
end
+
+ def project_params
+ params.require(:project).permit(allowed_project_params)
+ end
+
+ def allowed_project_params
+ [
+ :description,
+ :name
+ ]
+ end
end
Admin::ProjectsController.prepend_mod_with('Admin::ProjectsController')
diff --git a/app/controllers/profiles/personal_access_tokens_controller.rb b/app/controllers/profiles/personal_access_tokens_controller.rb
index 8d5c690fbfe..4b6e2f768fa 100644
--- a/app/controllers/profiles/personal_access_tokens_controller.rb
+++ b/app/controllers/profiles/personal_access_tokens_controller.rb
@@ -25,7 +25,10 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
def create
result = ::PersonalAccessTokens::CreateService.new(
- current_user: current_user, target_user: current_user, params: personal_access_token_params
+ current_user: current_user,
+ target_user: current_user,
+ params: personal_access_token_params,
+ concatenate_errors: false
).execute
@personal_access_token = result.payload[:personal_access_token]
diff --git a/app/finders/abuse_reports_finder.rb b/app/finders/abuse_reports_finder.rb
index 90d09a2d6ed..1f313534552 100644
--- a/app/finders/abuse_reports_finder.rb
+++ b/app/finders/abuse_reports_finder.rb
@@ -21,6 +21,7 @@ class AbuseReportsFinder
def filter_reports
filter_by_user_id
+ filter_by_user
filter_by_status
filter_by_category
end
@@ -42,6 +43,15 @@ class AbuseReportsFinder
@reports = @reports.by_category(params[:category])
end
+ def filter_by_user
+ return unless params[:user].present?
+
+ user_id = User.by_username(params[:user]).pick(:id)
+ return unless user_id
+
+ @reports = @reports.by_user_id(user_id)
+ end
+
def filter_by_user_id
return unless params[:user_id].present?
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 23a21bce456..d822a297856 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -200,7 +200,7 @@ module ApplicationHelper
if !exclude_author && object.last_edited_by
output << content_tag(:span, ' by ')
- output << link_to_member(object.project, object.last_edited_by, avatar: false, author_class: nil)
+ output << link_to_member(object.project, object.last_edited_by, avatar: false, extra_class: 'gl-hover-text-decoration-underline', author_class: nil)
end
output
diff --git a/app/helpers/routing/projects_helper.rb b/app/helpers/routing/projects_helper.rb
index f4732e398f0..d632bccf43e 100644
--- a/app/helpers/routing/projects_helper.rb
+++ b/app/helpers/routing/projects_helper.rb
@@ -43,6 +43,11 @@ module Routing
end
def work_item_url(entity, *args)
+ # TODO: we do not have a route to access group level work items yet.
+ # That is to be done as part of view group level work item issue:
+ # see https://gitlab.com/gitlab-org/gitlab/-/work_items/393987?iid_path=true
+ return unless entity.project.present?
+
unless Feature.enabled?(:use_iid_in_work_items_path, entity.project.group)
return project_work_items_url(entity.project, entity.id, *args)
end
diff --git a/app/models/alert_management/alert.rb b/app/models/alert_management/alert.rb
index 4af9b644362..74edcf12ac2 100644
--- a/app/models/alert_management/alert.rb
+++ b/app/models/alert_management/alert.rb
@@ -140,7 +140,7 @@ module AlertManagement
end
def self.link_reference_pattern
- @link_reference_pattern ||= super("alert_management", %r{(?<alert>\d+)/details(\#)?})
+ @link_reference_pattern ||= compose_link_reference_pattern('alert_management', %r{(?<alert>\d+)/details(\#)?})
end
def self.reference_valid?(reference)
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 4517b3ef216..ea90b4e4dda 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -206,7 +206,8 @@ class Commit
def self.link_reference_pattern
@link_reference_pattern ||=
- super("commit", /(?<commit>#{COMMIT_SHA_PATTERN})?(\.(?<extension>#{LINK_EXTENSION_PATTERN}))?/o)
+ compose_link_reference_pattern('commit',
+ /(?<commit>#{COMMIT_SHA_PATTERN})?(\.(?<extension>#{LINK_EXTENSION_PATTERN}))?/o)
end
def to_reference(from = nil, full: false)
diff --git a/app/models/commit_range.rb b/app/models/commit_range.rb
index 87029cb2033..90cdd267cbd 100644
--- a/app/models/commit_range.rb
+++ b/app/models/commit_range.rb
@@ -50,7 +50,7 @@ class CommitRange
end
def self.link_reference_pattern
- @link_reference_pattern ||= super("compare", /(?<commit_range>#{PATTERN})/o)
+ @link_reference_pattern ||= compose_link_reference_pattern('compare', /(?<commit_range>#{PATTERN})/o)
end
# Initialize a CommitRange
diff --git a/app/models/concerns/noteable.rb b/app/models/concerns/noteable.rb
index 7addcf9e2ec..0333cfc5f9e 100644
--- a/app/models/concerns/noteable.rb
+++ b/app/models/concerns/noteable.rb
@@ -169,6 +169,7 @@ module Noteable
def expire_note_etag_cache
return unless discussions_rendered_on_frontend?
return unless etag_caching_enabled?
+ return unless project.present?
Gitlab::EtagCaching::Store.new.touch(note_etag_key)
end
diff --git a/app/models/concerns/referable.rb b/app/models/concerns/referable.rb
index 9a17131c91c..5303d110078 100644
--- a/app/models/concerns/referable.rb
+++ b/app/models/concerns/referable.rb
@@ -76,7 +76,11 @@ module Referable
true
end
- def link_reference_pattern(route, pattern)
+ def link_reference_pattern
+ raise NotImplementedError, "#{self} does not implement #{__method__}"
+ end
+
+ def compose_link_reference_pattern(route, pattern)
%r{
(?<url>
#{Regexp.escape(Gitlab.config.gitlab.url)}
diff --git a/app/models/design_management/design.rb b/app/models/design_management/design.rb
index f9c748c299d..cb6d4e72c80 100644
--- a/app/models/design_management/design.rb
+++ b/app/models/design_management/design.rb
@@ -47,7 +47,7 @@ module DesignManagement
# Pre-fetching scope to include the data necessary to construct a
# reference using `to_reference`.
- scope :for_reference, -> { includes(issue: [{ project: [:route, :namespace] }]) }
+ scope :for_reference, -> { includes(issue: [{ namespace: :project }, { project: [:route, :namespace] }]) }
# A design can be uniquely identified by issue_id and filename
# Takes one or more sets of composite IDs of the form:
@@ -178,7 +178,7 @@ module DesignManagement
(?<url_filename> #{valid_char}+ \. #{ext})
}x
- super(path_segment, filename_pattern)
+ compose_link_reference_pattern(path_segment, filename_pattern)
end
end
diff --git a/app/models/group.rb b/app/models/group.rb
index 0dcc35b2a39..732e082c3af 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -7,7 +7,6 @@ class Group < Namespace
include AfterCommitQueue
include AccessRequestable
include Avatarable
- include Referable
include SelectForProjectAuthorization
include LoadedInGroupList
include GroupDescendant
@@ -242,14 +241,6 @@ class Group < Namespace
end
end
- def reference_prefix
- User.reference_prefix
- end
-
- def reference_pattern
- User.reference_pattern
- end
-
# WARNING: This method should never be used on its own
# please do make sure the number of rows you are filtering is small
# enough for this query
@@ -366,10 +357,6 @@ class Group < Namespace
notification_settings.find { |n| n.notification_email.present? }&.notification_email
end
- def to_reference(_from = nil, target_project: nil, full: nil)
- "#{self.class.reference_prefix}#{full_path}"
- end
-
def web_url(only_path: nil)
Gitlab::UrlBuilder.build(self, only_path: only_path)
end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 7fdededb8aa..352aa89b4c8 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -163,15 +163,15 @@ class Issue < ApplicationRecord
scope :order_closed_at_desc, -> { reorder(arel_table[:closed_at].desc.nulls_last) }
scope :preload_associated_models, -> { preload(:assignees, :labels, project: :namespace) }
- scope :with_web_entity_associations, -> { preload(:author, project: [:project_feature, :route, namespace: :route]) }
+ scope :with_web_entity_associations, -> { preload(:author, :namespace, project: [:project_feature, :route, namespace: :route]) }
scope :preload_awardable, -> { preload(:award_emoji) }
scope :with_alert_management_alerts, -> { joins(:alert_management_alert) }
scope :with_prometheus_alert_events, -> { joins(:issues_prometheus_alert_events) }
scope :with_self_managed_prometheus_alert_events, -> { joins(:issues_self_managed_prometheus_alert_events) }
scope :with_api_entity_associations, -> {
- preload(:timelogs, :closed_by, :assignees, :author, :labels, :issuable_severity,
+ preload(:timelogs, :closed_by, :assignees, :author, :labels, :issuable_severity, namespace: [{ parent: :route }, :route],
milestone: { project: [:route, { namespace: :route }] },
- project: [:project_feature, :route, { namespace: :route }],
+ project: [:project_namespace, :project_feature, :route, { group: :route }, { namespace: :route }],
duplicated_to: { project: [:project_feature] })
}
scope :with_issue_type, ->(types) { where(issue_type: types) }
@@ -346,7 +346,7 @@ class Issue < ApplicationRecord
end
def self.link_reference_pattern
- @link_reference_pattern ||= super(%r{issues(?:\/incident)?}, Gitlab::Regex.issue)
+ @link_reference_pattern ||= compose_link_reference_pattern(%r{issues(?:\/incident)?}, Gitlab::Regex.issue)
end
def self.reference_valid?(reference)
@@ -451,7 +451,7 @@ class Issue < ApplicationRecord
def to_reference(from = nil, full: false)
reference = "#{self.class.reference_prefix}#{iid}"
- "#{project.to_reference_base(from, full: full)}#{reference}"
+ "#{namespace.to_reference_base(from, full: full)}#{reference}"
end
def suggested_branch_name
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index e59393f7533..dd810b924a1 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -563,7 +563,7 @@ class MergeRequest < ApplicationRecord
end
def self.link_reference_pattern
- @link_reference_pattern ||= super("merge_requests", Gitlab::Regex.merge_request)
+ @link_reference_pattern ||= compose_link_reference_pattern('merge_requests', Gitlab::Regex.merge_request)
end
def self.reference_valid?(reference)
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index 8ae1fab963f..10d70eaa24e 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -114,7 +114,7 @@ class Milestone < ApplicationRecord
end
def self.link_reference_pattern
- @link_reference_pattern ||= super("milestones", /(?<milestone>\d+)/)
+ @link_reference_pattern ||= compose_link_reference_pattern('milestones', /(?<milestone>\d+)/)
end
def self.upcoming_ids(projects, groups)
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index cfe5f927e33..13466f147bd 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -16,6 +16,7 @@ class Namespace < ApplicationRecord
include EachBatch
include BlocksUnsafeSerialization
include Ci::NamespaceSettings
+ include Referable
# Tells ActiveRecord not to store the full class name, in order to save some space
# https://gitlab.com/gitlab-org/gitlab/-/merge_requests/69794
@@ -266,6 +267,23 @@ class Namespace < ApplicationRecord
def top_most
by_parent(nil)
end
+
+ def reference_prefix
+ User.reference_prefix
+ end
+
+ def reference_pattern
+ User.reference_pattern
+ end
+ end
+
+ def to_reference_base(from = nil, full: false)
+ return full_path if full || cross_namespace_reference?(from)
+ return path if cross_project_reference?(from)
+ end
+
+ def to_reference(*)
+ "#{self.class.reference_prefix}#{full_path}"
end
def container_registry_namespace_path_validation
@@ -619,6 +637,33 @@ class Namespace < ApplicationRecord
private
+ def cross_namespace_reference?(from)
+ return false if from == self
+
+ comparable_namespace_id = project_namespace? ? parent_id : id
+
+ case from
+ when Project
+ from.namespace_id != comparable_namespace_id
+ when Namespaces::ProjectNamespace
+ from.parent_id != comparable_namespace_id
+ when Namespace
+ parent != from
+ when User
+ true
+ end
+ end
+
+ # Check if a reference is being done cross-project
+ def cross_project_reference?(from)
+ case from
+ when Project
+ from.project_namespace_id != id
+ else
+ from && self != from
+ end
+ end
+
def update_new_emails_created_column
return if namespace_settings.nil?
return if namespace_settings.emails_enabled == !emails_disabled
diff --git a/app/models/operations/feature_flag.rb b/app/models/operations/feature_flag.rb
index 0df8c87f73f..6876af09c2c 100644
--- a/app/models/operations/feature_flag.rb
+++ b/app/models/operations/feature_flag.rb
@@ -72,7 +72,7 @@ module Operations
end
def link_reference_pattern
- @link_reference_pattern ||= super("feature_flags", %r{(?<feature_flag>\d+)/edit})
+ @link_reference_pattern ||= compose_link_reference_pattern('feature_flags', %r{(?<feature_flag>\d+)/edit})
end
def reference_postfix
diff --git a/app/models/project.rb b/app/models/project.rb
index e2f5e51453d..21f81f70f18 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -3243,6 +3243,8 @@ class Project < ApplicationRecord
case from
when Project
namespace_id != from.namespace_id
+ when Namespaces::ProjectNamespace
+ namespace_id != from.parent_id
when Namespace
namespace != from
when User
@@ -3252,9 +3254,14 @@ class Project < ApplicationRecord
# Check if a reference is being done cross-project
def cross_project_reference?(from)
- return true if from.is_a?(Namespace)
-
- from && self != from
+ case from
+ when Namespaces::ProjectNamespace
+ project_namespace_id != from.id
+ when Namespace
+ true
+ else
+ from && self != from
+ end
end
def update_project_statistics
diff --git a/app/models/snippet.rb b/app/models/snippet.rb
index 9ec685c5580..66d5c3ac801 100644
--- a/app/models/snippet.rb
+++ b/app/models/snippet.rb
@@ -183,7 +183,7 @@ class Snippet < ApplicationRecord
end
def link_reference_pattern
- @link_reference_pattern ||= super("snippets", /(?<snippet>\d+)/)
+ @link_reference_pattern ||= compose_link_reference_pattern('snippets', /(?<snippet>\d+)/)
end
def find_by_id_and_project(id:, project:)
diff --git a/app/services/groups/autocomplete_service.rb b/app/services/groups/autocomplete_service.rb
index 92b05d9ac08..5b9d60495e9 100644
--- a/app/services/groups/autocomplete_service.rb
+++ b/app/services/groups/autocomplete_service.rb
@@ -13,7 +13,7 @@ module Groups
IssuesFinder.new(current_user, finder_params)
.execute
.preload(project: :namespace)
- .select(:iid, :title, :project_id)
+ .select(:iid, :title, :project_id, :namespace_id)
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/services/personal_access_tokens/create_service.rb b/app/services/personal_access_tokens/create_service.rb
index e2f2e220750..adb7924f35e 100644
--- a/app/services/personal_access_tokens/create_service.rb
+++ b/app/services/personal_access_tokens/create_service.rb
@@ -2,11 +2,12 @@
module PersonalAccessTokens
class CreateService < BaseService
- def initialize(current_user:, target_user:, params: {})
+ def initialize(current_user:, target_user:, params: {}, concatenate_errors: true)
@current_user = current_user
@target_user = target_user
@params = params.dup
@ip_address = @params.delete(:ip_address)
+ @concatenate_errors = concatenate_errors
end
def execute
@@ -19,7 +20,10 @@ module PersonalAccessTokens
notification_service.access_token_created(target_user, token.name)
ServiceResponse.success(payload: { personal_access_token: token })
else
- ServiceResponse.error(message: token.errors.full_messages.to_sentence, payload: { personal_access_token: token })
+ message = token.errors.full_messages
+ message = message.to_sentence if @concatenate_errors
+
+ ServiceResponse.error(message: message, payload: { personal_access_token: token })
end
end
diff --git a/app/views/admin/projects/_form.html.haml b/app/views/admin/projects/_form.html.haml
new file mode 100644
index 00000000000..18bef523168
--- /dev/null
+++ b/app/views/admin/projects/_form.html.haml
@@ -0,0 +1,23 @@
+= gitlab_ui_form_for [:admin, @project] do |f|
+ = form_errors(@project)
+ = render ::Layouts::HorizontalSectionComponent.new(options: { class: 'gl-pb-3 gl-mb-6' }) do |c|
+ = c.title { _('Naming') }
+ = c.description do
+ = _('Update your project name and description.')
+ = c.body do
+ .form-group.gl-form-group
+ = f.label :name, _('Project name')
+ = f.text_field :name, class: 'form-control gl-form-input gl-md-form-input-md'
+
+ .form-group.gl-form-group
+ = f.label :id, _('Project ID')
+ = f.text_field :id, class: 'form-control gl-form-input gl-md-form-input-sm', readonly: true
+
+ .form-group.gl-form-group
+ = f.label :description, _('Project description (optional)')
+ = f.text_area :description, class: 'form-control gl-form-input gl-form-textarea gl-lg-form-input-xl', rows: 5
+
+ .gl-mt-5
+ = f.submit _('Save changes'), pajamas_button: true
+ = render Pajamas::ButtonComponent.new(href: admin_project_path(@project)) do
+ = _('Cancel')
diff --git a/app/views/admin/projects/_projects.html.haml b/app/views/admin/projects/_projects.html.haml
index cf1bd2a8022..72dcd604f06 100644
--- a/app/views/admin/projects/_projects.html.haml
+++ b/app/views/admin/projects/_projects.html.haml
@@ -24,7 +24,7 @@
= render_if_exists 'admin/projects/archived', project: project
.controls.gl-flex-shrink-0.gl-ml-5
- = render Pajamas::ButtonComponent.new(href: edit_project_path(project), button_options: { id: dom_id(project, :edit) }) do
+ = render Pajamas::ButtonComponent.new(href: edit_admin_namespace_project_path({ id: project.to_param, namespace_id: project.namespace.to_param }), button_options: { id: dom_id(project) }) do
= _('Edit')
= render Pajamas::ButtonComponent.new(variant: :danger, button_options: { class: 'delete-project-button', data: { delete_project_url: admin_project_path(project), project_name: project.name } }) do
= s_('AdminProjects|Delete')
diff --git a/app/views/admin/projects/edit.html.haml b/app/views/admin/projects/edit.html.haml
new file mode 100644
index 00000000000..ade0f543d58
--- /dev/null
+++ b/app/views/admin/projects/edit.html.haml
@@ -0,0 +1,4 @@
+- page_title _("Edit"), @project.name, _("Projects")
+%h1.page-title.gl-font-size-h-display= _('Edit project: %{project_name}') % { project_name: @project.name }
+%hr
+= render 'form'
diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml
index 464027e73f4..2803bec49c3 100644
--- a/app/views/admin/projects/show.html.haml
+++ b/app/views/admin/projects/show.html.haml
@@ -6,7 +6,9 @@
%h1.page-title.gl-font-size-h-display
= _('Project: %{name}') % { name: @project.full_name }
- = render Pajamas::ButtonComponent.new(href: edit_project_path(@project), icon: 'pencil', button_options: { class: 'gl-float-right' }) do
+ = render Pajamas::ButtonComponent.new(href: edit_admin_namespace_project_path({ id: @project.to_param, namespace_id: @project.namespace.to_param }),
+ icon: 'pencil',
+ button_options: { class: 'gl-float-right'}) do
= _('Edit')
%hr
- if @project.last_repository_check_failed?
diff --git a/app/views/notify/merge_request_status_email.text.haml b/app/views/notify/merge_request_status_email.text.haml
index 61c9b130da8..7d10bc77126 100644
--- a/app/views/notify/merge_request_status_email.text.haml
+++ b/app/views/notify/merge_request_status_email.text.haml
@@ -1,4 +1,4 @@
-= sprintf(s_('Notify|Merge request %{merge_request} was %{mr_status}'), { merge_request: @merge_request.to_reference, mr_status: sanitize_name(@updated_by.name) })
+= sprintf(s_('Notify|Merge request %{merge_request} was %{mr_status} by %{updated_by}'), { merge_request: @merge_request.to_reference, mr_status: @mr_status, updated_by: sanitize_name(@updated_by.name) })
= sprintf(s_('Notify|Merge request URL: %{merge_request_url}'), { merge_request_url: project_merge_request_url(@merge_request.target_project, @merge_request) })
diff --git a/config/routes/admin.rb b/config/routes/admin.rb
index 70d02609523..85c7951e1ea 100644
--- a/config/routes/admin.rb
+++ b/config/routes/admin.rb
@@ -123,6 +123,10 @@ namespace :admin do
member do
put :transfer
post :repository_check
+ get :edit, action: :edit
+ get '/', action: :show
+ patch '/', action: :update
+ put '/', action: :update
end
resources :runner_projects, only: [:create, :destroy]
diff --git a/doc/development/code_review.md b/doc/development/code_review.md
index 2457207823b..ed9404187ae 100644
--- a/doc/development/code_review.md
+++ b/doc/development/code_review.md
@@ -68,7 +68,7 @@ When a suitable [domain expert](#domain-experts) isn't available, you can choose
To find a domain expert:
-- In the Merge Request approvals widget, select [**View eligible approvers**](../user/project/merge_requests/approvals/rules.md#eligible-approvers).
+- In the Merge Request approvals widget, select [View eligible approvers](../user/project/merge_requests/approvals/rules.md#eligible-approvers).
This widget shows recommended and required approvals per area of the codebase.
These rules are defined in [Code Owners](../user/project/merge_requests/approvals/rules.md#code-owners-as-eligible-approvers).
- View the list of team members who work in the [stage or group](https://about.gitlab.com/handbook/product/categories/#devops-stages) related to the merge request.
diff --git a/doc/user/admin_area/index.md b/doc/user/admin_area/index.md
index 0375232334f..7227da3ce0d 100644
--- a/doc/user/admin_area/index.md
+++ b/doc/user/admin_area/index.md
@@ -86,6 +86,16 @@ project, the following information is listed:
Projects can be edited or deleted.
+To edit a project's name or description:
+
+1. In the Projects overview, next to the project you want to edit, select **Edit**.
+1. Edit the **Project name** or **Project description**.
+1. Select **Save Changes**.
+
+To delete a project:
+
+1. In the Projects overview, next to the project you want to delete, select **Delete**.
+
The list of projects can be sorted by:
- Updated date
diff --git a/doc/user/application_security/policies/img/scheduled_scan_execution_policies_diagram.png b/doc/user/application_security/policies/img/scheduled_scan_execution_policies_diagram.png
new file mode 100644
index 00000000000..b183013ff7a
--- /dev/null
+++ b/doc/user/application_security/policies/img/scheduled_scan_execution_policies_diagram.png
Binary files differ
diff --git a/doc/user/application_security/policies/scan-execution-policies.md b/doc/user/application_security/policies/scan-execution-policies.md
index 3b59fd46931..96048bb2308 100644
--- a/doc/user/application_security/policies/scan-execution-policies.md
+++ b/doc/user/application_security/policies/scan-execution-policies.md
@@ -88,7 +88,7 @@ This rule enforces the defined actions and schedules a scan on the provided date
|------------|------|-----------------|-------------|
| `type` | `string` | `schedule` | The rule's type. |
| `branches` | `array` of `string` | `*` or the branch's name | The branch the given policy applies to (supports wildcard). This field is required if the `agents` field is not set. |
-| `cadence` | `string` | CRON expression (for example, `0 0 * * *`) | A whitespace-separated string containing five fields that represents the scheduled time. |
+| `cadence` | `string` | CRON expression (for example, `0 0 * * *`) | A whitespace-separated string containing five fields that represents the scheduled time. Minimum of 15 minute intervals when used together with the `branches` field. |
| `agents` | `object` | | The name of the [GitLab agents](../../clusters/agent/index.md) where [Operational Container Scanning](../../clusters/agent/vulnerabilities.md) runs. The object key is the name of the Kubernetes agent configured for your project in GitLab. This field is required if the `branches` field is not set. |
GitLab supports the following types of CRON syntax for the `cadence` field:
@@ -99,8 +99,18 @@ GitLab supports the following types of CRON syntax for the `cadence` field:
NOTE:
Other elements of the [CRON syntax](https://docs.oracle.com/cd/E12058_01/doc/doc.1014/e12030/cron_expressions.htm) may work in the cadence field if supported by the [cron](https://github.com/robfig/cron) we are using in our implementation, however, GitLab does not officially test or support them.
-NOTE:
-If using the `agents` field, required for `Operational Container Scanning`, the CRON expression is evaluated in [UTC](https://www.timeanddate.com/worldclock/timezone/utc) using the system-time of the Kubernetes-agent pod. If not using the `agents` field, the CRON expression is evaluated in standard [UTC](https://www.timeanddate.com/worldclock/timezone/utc) time from GitLab.com. If you have a self-managed GitLab instance and have [changed the server time zone](../../../administration/timezone.md), the CRON expression is evaluated with the new time zone.
+When using the `schedule` rule type in conjunction with the `agents` field, note the following:
+
+- The GitLab Agent for Kubernetes checks every 30 seconds to see if there is an applicable policy. When a policy is found, the scans are executed according to the `cadence` defined.
+- The CRON expression is evaluated using the system-time of the Kubernetes-agent pod.
+
+When using the `schedule` rule type in conjunction with the `branches` field, note the following:
+
+- The cron worker runs on 15 minute intervals and starts any pipelines that were scheduled to run during the previous 15 minutes.
+- Based on your rule, you might expect scheduled pipelines to run with an offset of up to 15 minutes.
+- The CRON expression is evaluated in standard [UTC](https://www.timeanddate.com/worldclock/timezone/utc) time from GitLab.com. If you have a self-managed GitLab instance and have [changed the server time zone](../../../administration/timezone.md), the CRON expression is evaluated with the new time zone.
+
+![CRON worker diagram](img/scheduled_scan_execution_policies_diagram.png)
### `agent` schema
diff --git a/doc/user/compliance/compliance_report/index.md b/doc/user/compliance/compliance_report/index.md
index 6415cdeaf05..0d794b843af 100644
--- a/doc/user/compliance/compliance_report/index.md
+++ b/doc/user/compliance/compliance_report/index.md
@@ -77,7 +77,7 @@ From [GitLab 14.10](https://gitlab.com/groups/gitlab-org/-/epics/6870), these ar
| Committers approved merge request | High | [Separation of duties](#separation-of-duties) | Committers of the merge request approved the merge request they contributed to. For more information, see [Prevent approvals by users who add commits](../../project/merge_requests/approvals/settings.md#prevent-approvals-by-users-who-add-commits). |
| Fewer than two approvals | High | [Separation of duties](#separation-of-duties) | Merge request was merged with fewer than two approvals. For more information, see [Merge request approval rules](../../project/merge_requests/approvals/rules.md). |
-The following are unavailable compliance violations that are tracked in [issue 346011](https://gitlab.com/gitlab-org/gitlab/-/issues/346011).
+The following are unavailable compliance violations that are tracked in [epic 5237](https://gitlab.com/groups/gitlab-org/-/epics/5237).
<!-- vale gitlab.SubstitutionWarning = NO -->
diff --git a/lib/banzai/filter/references/design_reference_filter.rb b/lib/banzai/filter/references/design_reference_filter.rb
index 01e1036dcec..16a2a2835e9 100644
--- a/lib/banzai/filter/references/design_reference_filter.rb
+++ b/lib/banzai/filter/references/design_reference_filter.rb
@@ -43,7 +43,7 @@ module Banzai
return [] unless project.design_management_enabled?
iids = identifiers.map(&:issue_iid).to_set
- issues = project.issues.where(iid: iids)
+ issues = project.issues.where(iid: iids).includes(:project, :namespace)
id_for_iid = issues.index_by(&:iid).transform_values(&:id)
issue_by_id = issues.index_by(&:id)
diff --git a/lib/banzai/filter/references/issue_reference_filter.rb b/lib/banzai/filter/references/issue_reference_filter.rb
index b536d900a02..e186c8b1f73 100644
--- a/lib/banzai/filter/references/issue_reference_filter.rb
+++ b/lib/banzai/filter/references/issue_reference_filter.rb
@@ -22,7 +22,7 @@ module Banzai
end
def parent_records(parent, ids)
- parent.issues.where(iid: ids.to_a)
+ parent.issues.where(iid: ids.to_a).includes(:project, :namespace)
end
def object_link_text_extras(issue, matches)
diff --git a/lib/banzai/reference_parser/issue_parser.rb b/lib/banzai/reference_parser/issue_parser.rb
index ecf77191c13..a5862fbaac4 100644
--- a/lib/banzai/reference_parser/issue_parser.rb
+++ b/lib/banzai/reference_parser/issue_parser.rb
@@ -58,6 +58,7 @@ module Banzai
def records_for_nodes(nodes)
node_includes = [
+ :namespace,
:author,
:assignees,
{
diff --git a/lib/gitlab/github_import/importer/events/cross_referenced.rb b/lib/gitlab/github_import/importer/events/cross_referenced.rb
index b56ae186d3c..4fe371e5900 100644
--- a/lib/gitlab/github_import/importer/events/cross_referenced.rb
+++ b/lib/gitlab/github_import/importer/events/cross_referenced.rb
@@ -55,6 +55,7 @@ module Gitlab
record = record_class.new(id: db_id, iid: iid)
record.project = project
+ record.namespace = project.project_namespace if record.respond_to?(:namespace)
record.readonly!
record
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index e7954873267..29eb4f9dd47 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -479,6 +479,9 @@ msgstr[1] ""
msgid "%{actionText} %{actionDetail}"
msgstr ""
+msgid "%{actionText} %{actionDetail} %{timeago} by %{author}"
+msgstr ""
+
msgid "%{actionText} & %{openOrClose} %{noteable}"
msgstr ""
@@ -488,9 +491,6 @@ msgstr ""
msgid "%{actionText} & reopen %{noteable}"
msgstr ""
-msgid "%{actionText} by %{author} %{actionDetail}"
-msgstr ""
-
msgid "%{address} is an invalid IP address range"
msgstr ""
@@ -15388,6 +15388,9 @@ msgstr ""
msgid "Edit merge requests"
msgstr ""
+msgid "Edit project: %{project_name}"
+msgstr ""
+
msgid "Edit public deploy key"
msgstr ""
@@ -28037,6 +28040,9 @@ msgstr ""
msgid "Namespaces to index"
msgstr ""
+msgid "Naming"
+msgstr ""
+
msgid "Naming, topics, avatar"
msgstr ""
@@ -29138,9 +29144,6 @@ msgstr ""
msgid "Notify|Merge request %{merge_request} can no longer be merged due to conflict."
msgstr ""
-msgid "Notify|Merge request %{merge_request} was %{mr_status}"
-msgstr ""
-
msgid "Notify|Merge request %{merge_request} was %{mr_status} by %{updated_by}"
msgstr ""
@@ -41517,6 +41520,9 @@ msgstr ""
msgid "StatusCheck|Service name"
msgstr ""
+msgid "StatusCheck|Status Check ID"
+msgstr ""
+
msgid "StatusCheck|Status checks"
msgstr ""
@@ -46166,6 +46172,9 @@ msgstr ""
msgid "Update your group name, description, avatar, and visibility."
msgstr ""
+msgid "Update your project name and description."
+msgstr ""
+
msgid "Update your project name, topics, description, and avatar."
msgstr ""
diff --git a/spec/factories/work_items.rb b/spec/factories/work_items.rb
index cff246d4071..3cb4d8cd8bc 100644
--- a/spec/factories/work_items.rb
+++ b/spec/factories/work_items.rb
@@ -37,5 +37,12 @@ FactoryBot.define do
issue_type { :key_result }
association :work_item_type, :default, :key_result
end
+
+ before(:create, :build) do |work_item, evaluator|
+ if evaluator.namespace.present?
+ work_item.project = nil
+ work_item.namespace = evaluator.namespace
+ end
+ end
end
end
diff --git a/spec/features/admin/admin_projects_spec.rb b/spec/features/admin/admin_projects_spec.rb
index f08e6521184..405a254dc84 100644
--- a/spec/features/admin/admin_projects_spec.rb
+++ b/spec/features/admin/admin_projects_spec.rb
@@ -161,4 +161,29 @@ RSpec.describe "Admin::Projects", feature_category: :projects do
expect(page).to have_current_path(dashboard_projects_path, ignore_query: true, url: false)
end
end
+
+ describe 'project edit' do
+ it 'updates project details' do
+ project = create(:project, :private, name: 'Garfield', description: 'Funny Cat')
+
+ visit edit_admin_namespace_project_path({ id: project.to_param, namespace_id: project.namespace.to_param })
+
+ aggregate_failures do
+ expect(page).to have_content(project.name)
+ expect(page).to have_content(project.description)
+ end
+
+ fill_in 'Project name', with: 'Scooby-Doo'
+ fill_in 'Project description (optional)', with: 'Funny Dog'
+
+ click_button 'Save changes'
+
+ visit edit_admin_namespace_project_path({ id: project.to_param, namespace_id: project.namespace.to_param })
+
+ aggregate_failures do
+ expect(page).to have_content('Scooby-Doo')
+ expect(page).to have_content('Funny Dog')
+ end
+ end
+ end
end
diff --git a/spec/finders/abuse_reports_finder_spec.rb b/spec/finders/abuse_reports_finder_spec.rb
index 297f3487227..3dec6bf3eda 100644
--- a/spec/finders/abuse_reports_finder_spec.rb
+++ b/spec/finders/abuse_reports_finder_spec.rb
@@ -3,11 +3,12 @@
require 'spec_helper'
RSpec.describe AbuseReportsFinder, '#execute' do
+ let_it_be(:user1) { create(:user) }
+ let_it_be(:user2) { create(:user) }
+ let_it_be(:abuse_report_1) { create(:abuse_report, category: 'spam', user: user1) }
+ let_it_be(:abuse_report_2) { create(:abuse_report, :closed, category: 'phishing', user: user2) }
+
let(:params) { {} }
- let!(:user1) { create(:user) }
- let!(:user2) { create(:user) }
- let!(:abuse_report_1) { create(:abuse_report, category: 'spam', user: user1, reporter: user2) }
- let!(:abuse_report_2) { create(:abuse_report, :closed, category: 'phishing', user: user2) }
subject { described_class.new(params).execute }
@@ -25,6 +26,27 @@ RSpec.describe AbuseReportsFinder, '#execute' do
end
end
+ context 'when params[:user] is present' do
+ let(:params) { { user: abuse_report_1.user.username } }
+
+ it 'returns abuse reports for the specified user' do
+ expect(subject).to match_array([abuse_report_1])
+ end
+
+ context 'when no user has username = params[:user]' do
+ before do
+ allow(User).to receive_message_chain(:by_username, :pick)
+ .with(params[:user])
+ .with(:id)
+ .and_return(nil)
+ end
+
+ it 'returns all abuse reports' do
+ expect(subject).to match_array([abuse_report_1, abuse_report_2])
+ end
+ end
+ end
+
context 'when params[:status] is present' do
context 'when value is "open"' do
let(:params) { { status: 'open' } }
diff --git a/spec/helpers/issuables_helper_spec.rb b/spec/helpers/issuables_helper_spec.rb
index d5fff7f3a31..fd10b204e50 100644
--- a/spec/helpers/issuables_helper_spec.rb
+++ b/spec/helpers/issuables_helper_spec.rb
@@ -293,10 +293,13 @@ RSpec.describe IssuablesHelper, feature_category: :team_planning do
end
describe '#issuable_reference' do
+ let(:project_namespace) { build_stubbed(:project_namespace) }
+ let(:project) { build_stubbed(:project, project_namespace: project_namespace) }
+
context 'when show_full_reference truthy' do
it 'display issuable full reference' do
assign(:show_full_reference, true)
- issue = build_stubbed(:issue)
+ issue = build_stubbed(:issue, project: project)
expect(helper.issuable_reference(issue)).to eql(issue.to_reference(full: true))
end
@@ -305,12 +308,10 @@ RSpec.describe IssuablesHelper, feature_category: :team_planning do
context 'when show_full_reference falsey' do
context 'when @group present' do
it 'display issuable reference to @group' do
- project = build_stubbed(:project)
-
assign(:show_full_reference, nil)
assign(:group, project.namespace)
- issue = build_stubbed(:issue)
+ issue = build_stubbed(:issue, project: project)
expect(helper.issuable_reference(issue)).to eql(issue.to_reference(project.namespace))
end
@@ -318,13 +319,11 @@ RSpec.describe IssuablesHelper, feature_category: :team_planning do
context 'when @project present' do
it 'display issuable reference to @project' do
- project = build_stubbed(:project)
-
assign(:show_full_reference, nil)
assign(:group, nil)
assign(:project, project)
- issue = build_stubbed(:issue)
+ issue = build_stubbed(:issue, project: project)
expect(helper.issuable_reference(issue)).to eql(issue.to_reference(project))
end
@@ -333,8 +332,11 @@ RSpec.describe IssuablesHelper, feature_category: :team_planning do
end
describe '#issuable_project_reference' do
+ let(:project_namespace) { build_stubbed(:project_namespace) }
+ let(:project) { build_stubbed(:project, project_namespace: project_namespace) }
+
it 'display project name and simple reference with `#` to an issue' do
- issue = build_stubbed(:issue)
+ issue = build_stubbed(:issue, project: project)
expect(helper.issuable_project_reference(issue)).to eq("#{issue.project.full_name} ##{issue.iid}")
end
diff --git a/spec/lib/banzai/filter/issuable_reference_expansion_filter_spec.rb b/spec/lib/banzai/filter/issuable_reference_expansion_filter_spec.rb
index cc3918b9678..80061539a0b 100644
--- a/spec/lib/banzai/filter/issuable_reference_expansion_filter_spec.rb
+++ b/spec/lib/banzai/filter/issuable_reference_expansion_filter_spec.rb
@@ -202,7 +202,7 @@ RSpec.describe Banzai::Filter::IssuableReferenceExpansionFilter, feature_categor
filter(link, context)
end.count
- expect(control_count).to eq 9
+ expect(control_count).to eq 10
expect do
filter("#{link} #{link2}", context)
diff --git a/spec/lib/banzai/filter/references/reference_cache_spec.rb b/spec/lib/banzai/filter/references/reference_cache_spec.rb
index 7307daca516..7e5ca00f118 100644
--- a/spec/lib/banzai/filter/references/reference_cache_spec.rb
+++ b/spec/lib/banzai/filter/references/reference_cache_spec.rb
@@ -76,8 +76,7 @@ RSpec.describe Banzai::Filter::References::ReferenceCache, feature_category: :te
cache_single.load_records_per_parent
end.count
- expect(control_count).to eq 1
-
+ expect(control_count).to eq 2
# Since this is an issue filter that is not batching issue queries
# across projects, we have to account for that.
# 1 for original issue, 2 for second route/project, 1 for other issue
diff --git a/spec/lib/gitlab/issuable_sorter_spec.rb b/spec/lib/gitlab/issuable_sorter_spec.rb
index b8d0c7b0609..0d9940bab6f 100644
--- a/spec/lib/gitlab/issuable_sorter_spec.rb
+++ b/spec/lib/gitlab/issuable_sorter_spec.rb
@@ -4,16 +4,42 @@ require 'spec_helper'
RSpec.describe Gitlab::IssuableSorter do
let(:namespace1) { build_stubbed(:namespace, id: 1) }
- let(:project1) { build_stubbed(:project, id: 1, namespace: namespace1) }
-
- let(:project2) { build_stubbed(:project, id: 2, path: "a", namespace: project1.namespace) }
- let(:project3) { build_stubbed(:project, id: 3, path: "b", namespace: project1.namespace) }
-
let(:namespace2) { build_stubbed(:namespace, id: 2, path: "a") }
let(:namespace3) { build_stubbed(:namespace, id: 3, path: "b") }
- let(:project4) { build_stubbed(:project, id: 4, path: "a", namespace: namespace2) }
- let(:project5) { build_stubbed(:project, id: 5, path: "b", namespace: namespace2) }
- let(:project6) { build_stubbed(:project, id: 6, path: "a", namespace: namespace3) }
+
+ let(:project1) do
+ build_stubbed(:project, id: 1, namespace: namespace1, project_namespace: build_stubbed(:project_namespace))
+ end
+
+ let(:project2) do
+ build_stubbed(
+ :project, id: 2, path: "a", namespace: project1.namespace, project_namespace: build_stubbed(:project_namespace)
+ )
+ end
+
+ let(:project3) do
+ build_stubbed(
+ :project, id: 3, path: "b", namespace: project1.namespace, project_namespace: build_stubbed(:project_namespace)
+ )
+ end
+
+ let(:project4) do
+ build_stubbed(
+ :project, id: 4, path: "a", namespace: namespace2, project_namespace: build_stubbed(:project_namespace)
+ )
+ end
+
+ let(:project5) do
+ build_stubbed(
+ :project, id: 5, path: "b", namespace: namespace2, project_namespace: build_stubbed(:project_namespace)
+ )
+ end
+
+ let(:project6) do
+ build_stubbed(
+ :project, id: 6, path: "a", namespace: namespace3, project_namespace: build_stubbed(:project_namespace)
+ )
+ end
let(:unsorted) { [sorted[2], sorted[3], sorted[0], sorted[1]] }
diff --git a/spec/models/abuse_report_spec.rb b/spec/models/abuse_report_spec.rb
index 87a0a2a492c..30639f5a580 100644
--- a/spec/models/abuse_report_spec.rb
+++ b/spec/models/abuse_report_spec.rb
@@ -71,9 +71,8 @@ RSpec.describe AbuseReport, feature_category: :insider_threat do
end
describe 'scopes' do
- let!(:reporter) { create(:user, username: 'reporter') }
- let!(:report1) { create(:abuse_report) }
- let!(:report2) { create(:abuse_report, :closed, reporter: reporter, category: 'phishing') }
+ let_it_be(:report1) { create(:abuse_report) }
+ let_it_be(:report2) { create(:abuse_report, :closed, category: 'phishing') }
describe '.open' do
subject(:results) { described_class.open }
diff --git a/spec/models/design_management/design_spec.rb b/spec/models/design_management/design_spec.rb
index 5217b2a4114..cb122fa09fc 100644
--- a/spec/models/design_management/design_spec.rb
+++ b/spec/models/design_management/design_spec.rb
@@ -513,11 +513,11 @@ RSpec.describe DesignManagement::Design, feature_category: :design_management do
end
describe '#to_reference' do
+ let(:filename) { 'homescreen.jpg' }
let(:namespace) { build(:namespace, id: non_existing_record_id, path: 'sample-namespace') }
let(:project) { build(:project, name: 'sample-project', namespace: namespace) }
- let(:group) { create(:group, name: 'Group', path: 'sample-group') }
+ let(:group) { build(:group, name: 'Group', path: 'sample-group') }
let(:issue) { build(:issue, iid: 1, project: project) }
- let(:filename) { 'homescreen.jpg' }
let(:design) { build(:design, filename: filename, issue: issue, project: project) }
context 'when nil argument' do
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index 6e54ac3bd3f..a61a3c5e2ab 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -580,38 +580,38 @@ RSpec.describe Issue, feature_category: :team_planning do
end
describe '#to_reference' do
- let(:namespace) { build(:namespace, path: 'sample-namespace') }
- let(:project) { build(:project, name: 'sample-project', namespace: namespace) }
- let(:issue) { build(:issue, iid: 1, project: project) }
+ let_it_be(:namespace) { create(:namespace, path: 'sample-namespace') }
+ let_it_be(:project) { create(:project, name: 'sample-project', namespace: namespace) }
+ let_it_be(:issue) { create(:issue, project: project) }
context 'when nil argument' do
it 'returns issue id' do
- expect(issue.to_reference).to eq "#1"
+ expect(issue.to_reference).to eq "##{issue.iid}"
end
it 'returns complete path to the issue with full: true' do
- expect(issue.to_reference(full: true)).to eq 'sample-namespace/sample-project#1'
+ expect(issue.to_reference(full: true)).to eq "#{project.full_path}##{issue.iid}"
end
end
context 'when argument is a project' do
context 'when same project' do
it 'returns issue id' do
- expect(issue.to_reference(project)).to eq("#1")
+ expect(issue.to_reference(project)).to eq("##{issue.iid}")
end
it 'returns full reference with full: true' do
- expect(issue.to_reference(project, full: true)).to eq 'sample-namespace/sample-project#1'
+ expect(issue.to_reference(project, full: true)).to eq "#{project.full_path}##{issue.iid}"
end
end
context 'when cross-project in same namespace' do
let(:another_project) do
- build(:project, name: 'another-project', namespace: project.namespace)
+ create(:project, name: 'another-project', namespace: project.namespace)
end
it 'returns a cross-project reference' do
- expect(issue.to_reference(another_project)).to eq "sample-project#1"
+ expect(issue.to_reference(another_project)).to eq "sample-project##{issue.iid}"
end
end
@@ -620,7 +620,7 @@ RSpec.describe Issue, feature_category: :team_planning do
let(:another_namespace_project) { build(:project, path: 'another-project', namespace: another_namespace) }
it 'returns complete path to the issue' do
- expect(issue.to_reference(another_namespace_project)).to eq 'sample-namespace/sample-project#1'
+ expect(issue.to_reference(another_namespace_project)).to eq "#{project.full_path}##{issue.iid}"
end
end
end
@@ -628,11 +628,11 @@ RSpec.describe Issue, feature_category: :team_planning do
context 'when argument is a namespace' do
context 'when same as issue' do
it 'returns path to the issue with the project name' do
- expect(issue.to_reference(namespace)).to eq 'sample-project#1'
+ expect(issue.to_reference(namespace)).to eq "sample-project##{issue.iid}"
end
it 'returns full reference with full: true' do
- expect(issue.to_reference(namespace, full: true)).to eq 'sample-namespace/sample-project#1'
+ expect(issue.to_reference(namespace, full: true)).to eq "#{project.full_path}##{issue.iid}"
end
end
@@ -640,12 +640,111 @@ RSpec.describe Issue, feature_category: :team_planning do
let(:group) { build(:group, name: 'Group', path: 'sample-group') }
it 'returns full path to the issue with full: true' do
- expect(issue.to_reference(group)).to eq 'sample-namespace/sample-project#1'
+ expect(issue.to_reference(group)).to eq "#{project.full_path}##{issue.iid}"
end
end
end
end
+ describe '#to_reference with table syntax' do
+ using RSpec::Parameterized::TableSyntax
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:user_namespace) { user.namespace }
+
+ let_it_be(:parent) { create(:group) }
+ let_it_be(:group) { create(:group, parent: parent) }
+ let_it_be(:another_group) { create(:group) }
+
+ let_it_be(:project) { create(:project, namespace: group) }
+ let_it_be(:project_namespace) { project.project_namespace }
+ let_it_be(:same_namespace_project) { create(:project, namespace: group) }
+ let_it_be(:same_namespace_project_namespace) { same_namespace_project.project_namespace }
+
+ let_it_be(:another_namespace_project) { create(:project) }
+ let_it_be(:another_namespace_project_namespace) { another_namespace_project.project_namespace }
+
+ let_it_be(:project_issue) { build(:issue, project: project, iid: 123) }
+ let_it_be(:project_issue_full_reference) { "#{project.full_path}##{project_issue.iid}" }
+
+ let_it_be(:group_issue) { build(:issue, namespace: group, iid: 123) }
+ let_it_be(:group_issue_full_reference) { "#{group.full_path}##{group_issue.iid}" }
+
+ # this one is just theoretically possible, not smth to be supported for real
+ let_it_be(:user_issue) { build(:issue, namespace: user_namespace, iid: 123) }
+ let_it_be(:user_issue_full_reference) { "#{user_namespace.full_path}##{user_issue.iid}" }
+
+ # namespace would be group, project namespace or user namespace
+ where(:issue, :full, :from, :result) do
+ ref(:project_issue) | false | nil | lazy { "##{issue.iid}" }
+ ref(:project_issue) | true | nil | ref(:project_issue_full_reference)
+ ref(:project_issue) | false | ref(:group) | lazy { "#{project.path}##{issue.iid}" }
+ ref(:project_issue) | true | ref(:group) | ref(:project_issue_full_reference)
+ ref(:project_issue) | false | ref(:parent) | ref(:project_issue_full_reference)
+ ref(:project_issue) | true | ref(:parent) | ref(:project_issue_full_reference)
+ ref(:project_issue) | false | ref(:project) | lazy { "##{issue.iid}" }
+ ref(:project_issue) | true | ref(:project) | ref(:project_issue_full_reference)
+ ref(:project_issue) | false | ref(:project_namespace) | lazy { "##{issue.iid}" }
+ ref(:project_issue) | true | ref(:project_namespace) | ref(:project_issue_full_reference)
+ ref(:project_issue) | false | ref(:same_namespace_project) | lazy { "#{project.path}##{issue.iid}" }
+ ref(:project_issue) | true | ref(:same_namespace_project) | ref(:project_issue_full_reference)
+ ref(:project_issue) | false | ref(:same_namespace_project_namespace) | lazy { "#{project.path}##{issue.iid}" }
+ ref(:project_issue) | true | ref(:same_namespace_project_namespace) | ref(:project_issue_full_reference)
+ ref(:project_issue) | false | ref(:another_group) | ref(:project_issue_full_reference)
+ ref(:project_issue) | true | ref(:another_group) | ref(:project_issue_full_reference)
+ ref(:project_issue) | false | ref(:another_namespace_project) | ref(:project_issue_full_reference)
+ ref(:project_issue) | true | ref(:another_namespace_project) | ref(:project_issue_full_reference)
+ ref(:project_issue) | false | ref(:another_namespace_project_namespace) | ref(:project_issue_full_reference)
+ ref(:project_issue) | true | ref(:another_namespace_project_namespace) | ref(:project_issue_full_reference)
+ ref(:project_issue) | false | ref(:user_namespace) | ref(:project_issue_full_reference)
+ ref(:project_issue) | true | ref(:user_namespace) | ref(:project_issue_full_reference)
+
+ ref(:group_issue) | false | nil | lazy { "##{issue.iid}" }
+ ref(:group_issue) | true | nil | ref(:group_issue_full_reference)
+ ref(:group_issue) | false | ref(:user_namespace) | ref(:group_issue_full_reference)
+ ref(:group_issue) | true | ref(:user_namespace) | ref(:group_issue_full_reference)
+ ref(:group_issue) | false | ref(:group) | lazy { "##{issue.iid}" }
+ ref(:group_issue) | true | ref(:group) | ref(:group_issue_full_reference)
+ ref(:group_issue) | false | ref(:parent) | lazy { "#{group.path}##{issue.iid}" }
+ ref(:group_issue) | true | ref(:parent) | ref(:group_issue_full_reference)
+ ref(:group_issue) | false | ref(:project) | lazy { "#{group.path}##{issue.iid}" }
+ ref(:group_issue) | true | ref(:project) | ref(:group_issue_full_reference)
+ ref(:group_issue) | false | ref(:project_namespace) | lazy { "#{group.path}##{issue.iid}" }
+ ref(:group_issue) | true | ref(:project_namespace) | ref(:group_issue_full_reference)
+ ref(:group_issue) | false | ref(:another_group) | ref(:group_issue_full_reference)
+ ref(:group_issue) | true | ref(:another_group) | ref(:group_issue_full_reference)
+ ref(:group_issue) | false | ref(:another_namespace_project) | ref(:group_issue_full_reference)
+ ref(:group_issue) | true | ref(:another_namespace_project) | ref(:group_issue_full_reference)
+ ref(:group_issue) | false | ref(:another_namespace_project_namespace) | ref(:group_issue_full_reference)
+ ref(:group_issue) | true | ref(:another_namespace_project_namespace) | ref(:group_issue_full_reference)
+
+ ref(:user_issue) | false | nil | lazy { "##{issue.iid}" }
+ ref(:user_issue) | true | nil | ref(:user_issue_full_reference)
+ ref(:user_issue) | false | ref(:user_namespace) | lazy { "##{issue.iid}" }
+ ref(:user_issue) | true | ref(:user_namespace) | ref(:user_issue_full_reference)
+ ref(:user_issue) | false | ref(:group) | ref(:user_issue_full_reference)
+ ref(:user_issue) | true | ref(:group) | ref(:user_issue_full_reference)
+ ref(:user_issue) | false | ref(:parent) | ref(:user_issue_full_reference)
+ ref(:user_issue) | true | ref(:parent) | ref(:user_issue_full_reference)
+ ref(:user_issue) | false | ref(:project) | ref(:user_issue_full_reference)
+ ref(:user_issue) | true | ref(:project) | ref(:user_issue_full_reference)
+ ref(:user_issue) | false | ref(:project_namespace) | ref(:user_issue_full_reference)
+ ref(:user_issue) | true | ref(:project_namespace) | ref(:user_issue_full_reference)
+ ref(:user_issue) | false | ref(:another_group) | ref(:user_issue_full_reference)
+ ref(:user_issue) | true | ref(:another_group) | ref(:user_issue_full_reference)
+ ref(:user_issue) | false | ref(:another_namespace_project) | ref(:user_issue_full_reference)
+ ref(:user_issue) | true | ref(:another_namespace_project) | ref(:user_issue_full_reference)
+ ref(:user_issue) | false | ref(:another_namespace_project_namespace) | ref(:user_issue_full_reference)
+ ref(:user_issue) | true | ref(:another_namespace_project_namespace) | ref(:user_issue_full_reference)
+ end
+
+ with_them do
+ it 'returns correct reference' do
+ expect(issue.to_reference(from, full: full)).to eq(result)
+ end
+ end
+ end
+
describe '#assignee_or_author?' do
let(:issue) { create(:issue, project: reusable_project) }
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index 2d6ddd74dfd..0553a7d7152 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -266,6 +266,117 @@ RSpec.describe Namespace, feature_category: :subgroups do
end
end
+ describe "ReferencePatternValidation" do
+ subject { described_class.reference_pattern }
+
+ it { is_expected.to match("@group1") }
+ it { is_expected.to match("@group1/group2/group3") }
+ it { is_expected.to match("@1234/1234/1234") }
+ it { is_expected.to match("@.q-w_e") }
+ end
+
+ describe '#to_reference_base' do
+ using RSpec::Parameterized::TableSyntax
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:user_namespace) { user.namespace }
+
+ let_it_be(:parent) { create(:group) }
+ let_it_be(:group) { create(:group, parent: parent) }
+ let_it_be(:another_group) { create(:group) }
+
+ let_it_be(:project) { create(:project, namespace: group) }
+ let_it_be(:project_namespace) { project.project_namespace }
+
+ let_it_be(:another_namespace_project) { create(:project) }
+ let_it_be(:another_namespace_project_namespace) { another_namespace_project.project_namespace }
+
+ # testing references with namespace being: group, project namespace and user namespace
+ where(:namespace, :full, :from, :result) do
+ ref(:parent) | false | nil | nil
+ ref(:parent) | true | nil | lazy { parent.full_path }
+ ref(:parent) | false | ref(:group) | lazy { parent.path }
+ ref(:parent) | true | ref(:group) | lazy { parent.full_path }
+ ref(:parent) | false | ref(:parent) | nil
+ ref(:parent) | true | ref(:parent) | lazy { parent.full_path }
+ ref(:parent) | false | ref(:project) | lazy { parent.path }
+ ref(:parent) | true | ref(:project) | lazy { parent.full_path }
+ ref(:parent) | false | ref(:project_namespace) | lazy { parent.path }
+ ref(:parent) | true | ref(:project_namespace) | lazy { parent.full_path }
+ ref(:parent) | false | ref(:another_group) | lazy { parent.full_path }
+ ref(:parent) | true | ref(:another_group) | lazy { parent.full_path }
+ ref(:parent) | false | ref(:another_namespace_project) | lazy { parent.full_path }
+ ref(:parent) | true | ref(:another_namespace_project) | lazy { parent.full_path }
+ ref(:parent) | false | ref(:another_namespace_project_namespace) | lazy { parent.full_path }
+ ref(:parent) | true | ref(:another_namespace_project_namespace) | lazy { parent.full_path }
+ ref(:parent) | false | ref(:user_namespace) | lazy { parent.full_path }
+ ref(:parent) | true | ref(:user_namespace) | lazy { parent.full_path }
+
+ ref(:group) | false | nil | nil
+ ref(:group) | true | nil | lazy { group.full_path }
+ ref(:group) | false | ref(:group) | nil
+ ref(:group) | true | ref(:group) | lazy { group.full_path }
+ ref(:group) | false | ref(:parent) | lazy { group.path }
+ ref(:group) | true | ref(:parent) | lazy { group.full_path }
+ ref(:group) | false | ref(:project) | lazy { group.path }
+ ref(:group) | true | ref(:project) | lazy { group.full_path }
+ ref(:group) | false | ref(:project_namespace) | lazy { group.path }
+ ref(:group) | true | ref(:project_namespace) | lazy { group.full_path }
+ ref(:group) | false | ref(:another_group) | lazy { group.full_path }
+ ref(:group) | true | ref(:another_group) | lazy { group.full_path }
+ ref(:group) | false | ref(:another_namespace_project) | lazy { group.full_path }
+ ref(:group) | true | ref(:another_namespace_project) | lazy { group.full_path }
+ ref(:group) | false | ref(:another_namespace_project_namespace) | lazy { group.full_path }
+ ref(:group) | true | ref(:another_namespace_project_namespace) | lazy { group.full_path }
+ ref(:group) | false | ref(:user_namespace) | lazy { group.full_path }
+ ref(:group) | true | ref(:user_namespace) | lazy { group.full_path }
+
+ ref(:project_namespace) | false | nil | nil
+ ref(:project_namespace) | true | nil | lazy { project_namespace.full_path }
+ ref(:project_namespace) | false | ref(:group) | lazy { project_namespace.path }
+ ref(:project_namespace) | true | ref(:group) | lazy { project_namespace.full_path }
+ ref(:project_namespace) | false | ref(:parent) | lazy { project_namespace.full_path }
+ ref(:project_namespace) | true | ref(:parent) | lazy { project_namespace.full_path }
+ ref(:project_namespace) | false | ref(:project) | nil
+ ref(:project_namespace) | true | ref(:project) | lazy { project_namespace.full_path }
+ ref(:project_namespace) | false | ref(:project_namespace) | nil
+ ref(:project_namespace) | true | ref(:project_namespace) | lazy { project_namespace.full_path }
+ ref(:project_namespace) | false | ref(:another_group) | lazy { project_namespace.full_path }
+ ref(:project_namespace) | true | ref(:another_group) | lazy { project_namespace.full_path }
+ ref(:project_namespace) | false | ref(:another_namespace_project) | lazy { project_namespace.full_path }
+ ref(:project_namespace) | true | ref(:another_namespace_project) | lazy { project_namespace.full_path }
+ ref(:project_namespace) | false | ref(:another_namespace_project_namespace) | lazy { project_namespace.full_path }
+ ref(:project_namespace) | true | ref(:another_namespace_project_namespace) | lazy { project_namespace.full_path }
+ ref(:project_namespace) | false | ref(:user_namespace) | lazy { project_namespace.full_path }
+ ref(:project_namespace) | true | ref(:user_namespace) | lazy { project_namespace.full_path }
+
+ ref(:user_namespace) | false | nil | nil
+ ref(:user_namespace) | true | nil | lazy { user_namespace.full_path }
+ ref(:user_namespace) | false | ref(:user_namespace) | nil
+ ref(:user_namespace) | true | ref(:user_namespace) | lazy { user_namespace.full_path }
+ ref(:user_namespace) | false | ref(:group) | lazy { user_namespace.full_path }
+ ref(:user_namespace) | true | ref(:group) | lazy { user_namespace.full_path }
+ ref(:user_namespace) | false | ref(:parent) | lazy { user_namespace.full_path }
+ ref(:user_namespace) | true | ref(:parent) | lazy { user_namespace.full_path }
+ ref(:user_namespace) | false | ref(:project) | lazy { user_namespace.full_path }
+ ref(:user_namespace) | true | ref(:project) | lazy { user_namespace.full_path }
+ ref(:user_namespace) | false | ref(:project_namespace) | lazy { user_namespace.full_path }
+ ref(:user_namespace) | true | ref(:project_namespace) | lazy { user_namespace.full_path }
+ ref(:user_namespace) | false | ref(:another_group) | lazy { user_namespace.full_path }
+ ref(:user_namespace) | true | ref(:another_group) | lazy { user_namespace.full_path }
+ ref(:user_namespace) | false | ref(:another_namespace_project) | lazy { user_namespace.full_path }
+ ref(:user_namespace) | true | ref(:another_namespace_project) | lazy { user_namespace.full_path }
+ ref(:user_namespace) | false | ref(:another_namespace_project_namespace) | lazy { user_namespace.full_path }
+ ref(:user_namespace) | true | ref(:another_namespace_project_namespace) | lazy { user_namespace.full_path }
+ end
+
+ with_them do
+ it 'returns correct path' do
+ expect(namespace.to_reference_base(from, full: full)).to eq(result)
+ end
+ end
+ end
+
describe 'handling STI', :aggregate_failures do
let(:namespace_type) { nil }
let(:parent) { nil }
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index d11257cbf89..e6f2d373128 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -1392,6 +1392,60 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
end
end
+ describe '#to_reference_base' do
+ using RSpec::Parameterized::TableSyntax
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:user_namespace) { user.namespace }
+
+ let_it_be(:parent) { create(:group) }
+ let_it_be(:group) { create(:group, parent: parent) }
+ let_it_be(:another_group) { create(:group) }
+
+ let_it_be(:project1) { create(:project, namespace: group) }
+ let_it_be(:project_namespace) { project1.project_namespace }
+
+ # different project same group
+ let_it_be(:project2) { create(:project, namespace: group) }
+ let_it_be(:project_namespace2) { project2.project_namespace }
+
+ # different project from different group
+ let_it_be(:project3) { create(:project) }
+ let_it_be(:project_namespace3) { project3.project_namespace }
+
+ # testing references with namespace being: group, project namespace and user namespace
+ where(:project, :full, :from, :result) do
+ ref(:project1) | false | nil | nil
+ ref(:project1) | true | nil | lazy { project.full_path }
+ ref(:project1) | false | ref(:group) | lazy { project.path }
+ ref(:project1) | true | ref(:group) | lazy { project.full_path }
+ ref(:project1) | false | ref(:parent) | lazy { project.full_path }
+ ref(:project1) | true | ref(:parent) | lazy { project.full_path }
+ ref(:project1) | false | ref(:project1) | nil
+ ref(:project1) | true | ref(:project1) | lazy { project.full_path }
+ ref(:project1) | false | ref(:project_namespace) | nil
+ ref(:project1) | true | ref(:project_namespace) | lazy { project.full_path }
+ ref(:project1) | false | ref(:project2) | lazy { project.path }
+ ref(:project1) | true | ref(:project2) | lazy { project.full_path }
+ ref(:project1) | false | ref(:project_namespace2) | lazy { project.path }
+ ref(:project1) | true | ref(:project_namespace2) | lazy { project.full_path }
+ ref(:project1) | false | ref(:another_group) | lazy { project.full_path }
+ ref(:project1) | true | ref(:another_group) | lazy { project.full_path }
+ ref(:project1) | false | ref(:project3) | lazy { project.full_path }
+ ref(:project1) | true | ref(:project3) | lazy { project.full_path }
+ ref(:project1) | false | ref(:project_namespace3) | lazy { project.full_path }
+ ref(:project1) | true | ref(:project_namespace3) | lazy { project.full_path }
+ ref(:project1) | false | ref(:user_namespace) | lazy { project.full_path }
+ ref(:project1) | true | ref(:user_namespace) | lazy { project.full_path }
+ end
+
+ with_them do
+ it 'returns correct path' do
+ expect(project.to_reference_base(from, full: full)).to eq(result)
+ end
+ end
+ end
+
describe '#merge_method' do
where(:ff, :rebase, :method) do
true | true | :ff
diff --git a/spec/requests/admin/projects_controller_spec.rb b/spec/requests/admin/projects_controller_spec.rb
new file mode 100644
index 00000000000..5ff49a30ed8
--- /dev/null
+++ b/spec/requests/admin/projects_controller_spec.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Admin::ProjectsController, :enable_admin_mode, feature_category: :projects do
+ let_it_be(:project) { create(:project, :public, name: 'test', description: 'test') }
+ let_it_be(:admin) { create(:admin) }
+
+ describe 'PUT #update' do
+ let(:project_params) { {} }
+ let(:params) { { project: project_params } }
+ let(:path_params) { { namespace_id: project.namespace.to_param, id: project.to_param } }
+
+ before do
+ sign_in(admin)
+ end
+
+ subject do
+ put admin_namespace_project_path(path_params), params: params
+ end
+
+ context 'when changing the name' do
+ let(:project_params) { { name: 'new name' } }
+
+ it 'returns success' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:found)
+ end
+
+ it 'changes the name' do
+ expect { subject }.to change { project.reload.name }.to('new name')
+ end
+ end
+
+ context 'when changing the description' do
+ let(:project_params) { { description: 'new description' } }
+
+ it 'returns success' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:found)
+ end
+
+ it 'changes the project description' do
+ expect { subject }.to change { project.reload.description }.to('new description')
+ end
+ end
+
+ context 'when changing the name to an invalid name' do
+ let(:project_params) { { name: 'invalid/project/name' } }
+
+ it 'does not change the name' do
+ expect { subject }.not_to change { project.reload.name }
+ end
+ end
+ end
+end
diff --git a/spec/serializers/group_issuable_autocomplete_entity_spec.rb b/spec/serializers/group_issuable_autocomplete_entity_spec.rb
index 86ef9dea23b..977239c67da 100644
--- a/spec/serializers/group_issuable_autocomplete_entity_spec.rb
+++ b/spec/serializers/group_issuable_autocomplete_entity_spec.rb
@@ -4,7 +4,8 @@ require 'spec_helper'
RSpec.describe GroupIssuableAutocompleteEntity do
let(:group) { build_stubbed(:group) }
- let(:project) { build_stubbed(:project, group: group) }
+ let(:project_namespace) { build_stubbed(:project_namespace) }
+ let(:project) { build_stubbed(:project, group: group, project_namespace: project_namespace) }
let(:issue) { build_stubbed(:issue, project: project) }
describe '#represent' do
diff --git a/spec/services/personal_access_tokens/create_service_spec.rb b/spec/services/personal_access_tokens/create_service_spec.rb
index b8a4c8f30d2..d80be5cccce 100644
--- a/spec/services/personal_access_tokens/create_service_spec.rb
+++ b/spec/services/personal_access_tokens/create_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe PersonalAccessTokens::CreateService do
+RSpec.describe PersonalAccessTokens::CreateService, feature_category: :system_access do
shared_examples_for 'a successfully created token' do
it 'creates personal access token record' do
expect(subject.success?).to be true
@@ -40,7 +40,7 @@ RSpec.describe PersonalAccessTokens::CreateService do
let(:current_user) { create(:user) }
let(:user) { create(:user) }
let(:params) { { name: 'Test token', impersonation: false, scopes: [:api], expires_at: Date.today + 1.month } }
- let(:service) { described_class.new(current_user: current_user, target_user: user, params: params) }
+ let(:service) { described_class.new(current_user: current_user, target_user: user, params: params, concatenate_errors: false) }
let(:token) { subject.payload[:personal_access_token] }
context 'when current_user is an administrator' do
@@ -66,5 +66,21 @@ RSpec.describe PersonalAccessTokens::CreateService do
it_behaves_like 'a successfully created token'
end
end
+
+ context 'when invalid scope' do
+ let(:params) { { name: 'Test token', impersonation: false, scopes: [:no_valid], expires_at: Date.today + 1.month } }
+
+ context 'when concatenate_errors: true' do
+ let(:service) { described_class.new(current_user: user, target_user: user, params: params) }
+
+ it { expect(subject.message).to be_an_instance_of(String) }
+ end
+
+ context 'when concatenate_errors: false' do
+ let(:service) { described_class.new(current_user: user, target_user: user, params: params, concatenate_errors: false) }
+
+ it { expect(subject.message).to be_an_instance_of(Array) }
+ end
+ end
end
end