diff --git a/app/assets/javascripts/admin/abuse_report/components/abuse_report_app.vue b/app/assets/javascripts/admin/abuse_report/components/abuse_report_app.vue
index fb6bc38848c..3aa1289261d 100644
--- a/app/assets/javascripts/admin/abuse_report/components/abuse_report_app.vue
+++ b/app/assets/javascripts/admin/abuse_report/components/abuse_report_app.vue
@@ -1,7 +1,9 @@
+
+
+
+ {{ $options.i18n.label }}
+
+
+ {{ $options.i18n.edit }}
+
+
+
+ {{ $options.i18n.noLabels }}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/assets/javascripts/admin/abuse_report/components/report_details.vue b/app/assets/javascripts/admin/abuse_report/components/report_details.vue
new file mode 100644
index 00000000000..10e1dca7f91
--- /dev/null
+++ b/app/assets/javascripts/admin/abuse_report/components/report_details.vue
@@ -0,0 +1,49 @@
+
+
+
+
+
diff --git a/app/assets/javascripts/admin/abuse_report/index.js b/app/assets/javascripts/admin/abuse_report/index.js
index 8ff3e690127..5c3a20029f4 100644
--- a/app/assets/javascripts/admin/abuse_report/index.js
+++ b/app/assets/javascripts/admin/abuse_report/index.js
@@ -1,7 +1,15 @@
import Vue from 'vue';
+import VueApollo from 'vue-apollo';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import { defaultClient } from '~/graphql_shared/issuable_client';
import AbuseReportApp from './components/abuse_report_app.vue';
+Vue.use(VueApollo);
+
+const apolloProvider = new VueApollo({
+ defaultClient,
+});
+
export const initAbuseReportApp = () => {
const el = document.querySelector('#js-abuse-reports-detail-view');
@@ -16,7 +24,12 @@ export const initAbuseReportApp = () => {
return new Vue({
el,
+ apolloProvider,
name: 'AbuseReportAppRoot',
+ provide: {
+ allowScopedLabels: false,
+ updatePath: abuseReport.report.updatePath,
+ },
render: (createElement) =>
createElement(AbuseReportApp, {
props: {
diff --git a/app/assets/javascripts/ci/runner/components/runner_filtered_search_bar.vue b/app/assets/javascripts/ci/runner/components/runner_filtered_search_bar.vue
index 3634dcf1c93..81b2a17631e 100644
--- a/app/assets/javascripts/ci/runner/components/runner_filtered_search_bar.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_filtered_search_bar.vue
@@ -92,7 +92,6 @@ export default {
:initial-filter-value="initialFilterValue"
:tokens="validTokens"
:initial-sort-by="initialSortBy"
- :search-input-placeholder="__('Search or filter results...')"
:search-text-option-label="s__('Runners|Search description...')"
terms-as-tokens
data-testid="runners-filtered-search"
diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js
index 684375177bb..8ccf7ba92a5 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js
@@ -42,7 +42,7 @@ export default class FilteredSearchManager {
useDefaultState = false,
filteredSearchTokenKeys = IssuableFilteredSearchTokenKeys,
stateFiltersSelector = '.issues-state-filters',
- placeholder = __('Search or filter results...'),
+ placeholder = __('Search or filter results…'),
anchor = null,
}) {
this.isGroup = isGroup;
diff --git a/app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue b/app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue
index 9febebf7e55..a756229e6ca 100644
--- a/app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue
+++ b/app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue
@@ -495,7 +495,6 @@ export default {
:issuables-loading="isLoading"
namespace="dashboard"
recent-searches-storage-key="issues"
- :search-input-placeholder="$options.i18n.searchPlaceholder"
:search-tokens="searchTokens"
:show-pagination-controls="showPaginationControls"
show-work-item-type-icon
diff --git a/app/assets/javascripts/issues/list/components/issues_list_app.vue b/app/assets/javascripts/issues/list/components/issues_list_app.vue
index f396a8d1928..1a8151901d1 100644
--- a/app/assets/javascripts/issues/list/components/issues_list_app.vue
+++ b/app/assets/javascripts/issues/list/components/issues_list_app.vue
@@ -966,7 +966,6 @@ export default {
v-if="hasAnyIssues"
:namespace="fullPath"
recent-searches-storage-key="issues"
- :search-input-placeholder="$options.i18n.searchPlaceholder"
:search-tokens="searchTokens"
:has-scoped-labels-feature="hasScopedLabelsFeature"
:initial-filter-value="filterTokens"
diff --git a/app/assets/javascripts/issues/list/constants.js b/app/assets/javascripts/issues/list/constants.js
index 85e300b6474..682c7629962 100644
--- a/app/assets/javascripts/issues/list/constants.js
+++ b/app/assets/javascripts/issues/list/constants.js
@@ -121,7 +121,6 @@ export const i18n = {
reorderError: __('An error occurred while reordering issues.'),
deleteError: __('An error occurred while deleting an issuable.'),
rssLabel: __('Subscribe to RSS feed'),
- searchPlaceholder: __('Search or filter results...'),
upvotes: __('Upvotes'),
titles: __('Titles'),
descriptions: __('Descriptions'),
diff --git a/app/assets/javascripts/service_desk/components/service_desk_list_app.vue b/app/assets/javascripts/service_desk/components/service_desk_list_app.vue
index a831fb12d5e..51e632d36a9 100644
--- a/app/assets/javascripts/service_desk/components/service_desk_list_app.vue
+++ b/app/assets/javascripts/service_desk/components/service_desk_list_app.vue
@@ -50,7 +50,6 @@ import reorderServiceDeskIssuesMutation from '../queries/reorder_service_desk_is
import {
errorFetchingCounts,
errorFetchingIssues,
- searchPlaceholder,
issueRepositioningMessage,
reorderError,
SERVICE_DESK_BOT_USERNAME,
@@ -77,7 +76,6 @@ export default {
i18n: {
errorFetchingCounts,
errorFetchingIssues,
- searchPlaceholder,
issueRepositioningMessage,
reorderError,
},
@@ -559,7 +557,6 @@ export default {
namespace="service-desk"
recent-searches-storage-key="service-desk-issues"
:error="issuesError"
- :search-input-placeholder="$options.i18n.searchPlaceholder"
:search-tokens="searchTokens"
:issuables-loading="isLoading"
:initial-filter-value="filterTokens"
diff --git a/app/assets/javascripts/service_desk/constants.js b/app/assets/javascripts/service_desk/constants.js
index dbd2f3edf5d..e498a4f39a1 100644
--- a/app/assets/javascripts/service_desk/constants.js
+++ b/app/assets/javascripts/service_desk/constants.js
@@ -235,7 +235,6 @@ export const noSearchResultsDescription = __(
'To widen your search, change or remove filters above',
);
export const noSearchResultsTitle = __('Sorry, your filter produced no results');
-export const searchPlaceholder = __('Search or filter results...');
export const issueRepositioningMessage = __(
'Issues are being rebalanced at the moment, so manual reordering is disabled.',
);
diff --git a/app/assets/javascripts/settings_panels.js b/app/assets/javascripts/settings_panels.js
index fe5b21713a2..da948cc85b6 100644
--- a/app/assets/javascripts/settings_panels.js
+++ b/app/assets/javascripts/settings_panels.js
@@ -1,4 +1,5 @@
import $ from 'jquery';
+import { InternalEvents } from '~/tracking';
import { __ } from './locale';
/**
@@ -47,6 +48,15 @@ export function toggleSection($section) {
}
}
+export function initTrackProductAnalyticsExpanded() {
+ const $analyticsSection = $('#js-product-analytics-settings');
+ $analyticsSection.on('click.toggleSection', '.js-settings-toggle', () => {
+ if (isExpanded($analyticsSection)) {
+ InternalEvents.track_event('user_viewed_cluster_configuration');
+ }
+ });
+}
+
export default function initSettingsPanels() {
$('.settings').each((i, elm) => {
const $section = $(elm);
@@ -64,4 +74,6 @@ export default function initSettingsPanels() {
}
}
});
+
+ initTrackProductAnalyticsExpanded();
}
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue
index f31d4d53a23..346384e3023 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue
@@ -73,7 +73,8 @@ export default {
},
searchInputPlaceholder: {
type: String,
- required: true,
+ required: false,
+ default: __('Search or filter results…'),
},
suggestionsListClass: {
type: String,
diff --git a/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/constants.js b/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/constants.js
index df1188d365b..77fd197978f 100644
--- a/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/constants.js
+++ b/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/constants.js
@@ -1,5 +1,3 @@
-import { __ } from '~/locale';
-
export const tdClass =
'table-col gl-display-flex d-md-table-cell gl-align-items-center gl-white-space-nowrap';
export const thClass = 'gl-hover-bg-blue-50';
@@ -15,7 +13,3 @@ export const initialPaginationState = {
firstPageSize: defaultPageSize,
lastPageSize: null,
};
-
-export const defaultI18n = {
- searchPlaceholder: __('Search or filter results…'),
-};
diff --git a/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue b/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue
index ab9e6e092d9..0c3d175684c 100644
--- a/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue
+++ b/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue
@@ -14,11 +14,10 @@ import {
} from '~/vue_shared/components/filtered_search_bar/constants';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import UserToken from '~/vue_shared/components/filtered_search_bar/tokens/user_token.vue';
-import { initialPaginationState, defaultI18n, defaultPageSize } from './constants';
+import { initialPaginationState, defaultPageSize } from './constants';
import { isAny } from './utils';
export default {
- defaultI18n,
components: {
GlAlert,
GlBadge,
@@ -300,7 +299,6 @@ export default {
-
-
-
+
diff --git a/app/assets/javascripts/work_items/list/components/work_items_list_app.vue b/app/assets/javascripts/work_items/list/components/work_items_list_app.vue
index 026c48cf017..0b8dbf86573 100644
--- a/app/assets/javascripts/work_items/list/components/work_items_list_app.vue
+++ b/app/assets/javascripts/work_items/list/components/work_items_list_app.vue
@@ -10,9 +10,6 @@ import { STATE_CLOSED } from '../../constants';
import getWorkItemsQuery from '../queries/get_work_items.query.graphql';
export default {
- i18n: {
- searchPlaceholder: __('Search or filter results...'),
- },
issuableListTabs,
components: {
IssuableList,
@@ -64,7 +61,6 @@ export default {
:issuables-loading="$apollo.queries.workItems.loading"
namespace="work-items"
recent-searches-storage-key="issues"
- :search-input-placeholder="$options.i18n.searchPlaceholder"
:search-tokens="searchTokens"
show-work-item-type-icon
:sort-options="sortOptions"
diff --git a/app/controllers/admin/abuse_reports_controller.rb b/app/controllers/admin/abuse_reports_controller.rb
index 329c4e4921a..fe8b3144525 100644
--- a/app/controllers/admin/abuse_reports_controller.rb
+++ b/app/controllers/admin/abuse_reports_controller.rb
@@ -4,7 +4,10 @@ class Admin::AbuseReportsController < Admin::ApplicationController
feature_category :insider_threat
before_action :set_status_param, only: :index, if: -> { Feature.enabled?(:abuse_reports_list) }
- before_action :find_abuse_report, only: [:show, :moderate_user, :update, :destroy]
+ before_action :find_abuse_report, only: [:show, :update, :moderate_user, :destroy]
+ before_action only: :show do
+ push_frontend_feature_flag(:abuse_report_labels)
+ end
def index
@abuse_reports = AbuseReportsFinder.new(params).execute
@@ -12,14 +15,11 @@ class Admin::AbuseReportsController < Admin::ApplicationController
def show; end
- # Kept for backwards compatibility.
- # TODO: See https://gitlab.com/gitlab-org/modelops/anti-abuse/team-tasks/-/issues/167?work_item_iid=443
- # In 16.4 remove or re-use this endpoint after frontend has migrated to using moderate_user endpoint
def update
- response = Admin::AbuseReports::ModerateUserService.new(@abuse_report, current_user, permitted_params).execute
+ response = Admin::AbuseReports::UpdateService.new(@abuse_report, current_user, permitted_params).execute
if response.success?
- render json: { message: response.message }
+ head :ok
else
render json: { message: response.message }, status: :unprocessable_entity
end
@@ -53,6 +53,6 @@ class Admin::AbuseReportsController < Admin::ApplicationController
end
def permitted_params
- params.permit(:user_action, :close, :reason, :comment)
+ params.permit(:user_action, :close, :reason, :comment, { label_ids: [] })
end
end
diff --git a/app/controllers/projects/service_desk_controller.rb b/app/controllers/projects/service_desk_controller.rb
index b1e30e7a45b..ca3cecf5949 100644
--- a/app/controllers/projects/service_desk_controller.rb
+++ b/app/controllers/projects/service_desk_controller.rb
@@ -36,7 +36,7 @@ class Projects::ServiceDeskController < Projects::ApplicationController
service_desk_settings = project.service_desk_setting
{
- service_desk_address: project.service_desk_address,
+ service_desk_address: project.service_desk_system_address,
service_desk_enabled: project.service_desk_enabled,
issue_template_key: service_desk_settings&.issue_template_key,
template_file_missing: service_desk_settings&.issue_template_missing?,
diff --git a/app/graphql/resolvers/ci/runner_jobs_resolver.rb b/app/graphql/resolvers/ci/runner_jobs_resolver.rb
index 9fe25a4d13d..39908d8fd11 100644
--- a/app/graphql/resolvers/ci/runner_jobs_resolver.rb
+++ b/app/graphql/resolvers/ci/runner_jobs_resolver.rb
@@ -18,6 +18,8 @@ module Resolvers
alias_method :runner, :object
def resolve_with_lookahead(statuses: nil)
+ context[:job_field_authorization] = :read_build # Instruct JobType to perform field-level authorization
+
jobs = ::Ci::JobsFinder.new(current_user: current_user, runner: runner, params: { scope: statuses }).execute
apply_lookahead(jobs)
@@ -30,7 +32,7 @@ module Resolvers
previous_stage_jobs_or_needs: [:needs, :pipeline],
artifacts: [:job_artifacts],
pipeline: [:user],
- project: [{ project: [:route, { namespace: [:route] }] }],
+ project: [{ project: [:route, { namespace: [:route] }, :project_feature] }],
detailed_status: [
:metadata,
{ pipeline: [:merge_request] },
diff --git a/app/graphql/types/ci/job_base_field.rb b/app/graphql/types/ci/job_base_field.rb
new file mode 100644
index 00000000000..f5bdd2260b5
--- /dev/null
+++ b/app/graphql/types/ci/job_base_field.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module Types
+ module Ci
+ # JobBaseField ensures that only allow-listed fields can be returned without a permission check.
+ # All other fields go through a permissions check based on the :job_field_authorization value passed in the context.
+ # rubocop: disable Graphql/AuthorizeTypes
+ class JobBaseField < ::Types::BaseField
+ PUBLIC_FIELDS = %i[allow_failure duration id kind status created_at finished_at queued_at queued_duration
+ updated_at runner short_sha].freeze
+
+ def authorized?(object, args, ctx)
+ current_user = ctx[:current_user]
+ permission = ctx[:job_field_authorization]
+
+ if permission.nil? ||
+ PUBLIC_FIELDS.include?(ctx[:current_field].original_name) ||
+ current_user.can?(permission, object)
+ return super
+ end
+
+ false
+ end
+ end
+ # rubocop: enable Graphql/AuthorizeTypes
+ end
+end
diff --git a/app/graphql/types/ci/job_type.rb b/app/graphql/types/ci/job_type.rb
index 22eb32993c5..976103e1510 100644
--- a/app/graphql/types/ci/job_type.rb
+++ b/app/graphql/types/ci/job_type.rb
@@ -8,6 +8,7 @@ module Types
graphql_name 'CiJob'
present_using ::Ci::BuildPresenter
+ field_class Types::Ci::JobBaseField
connection_type_class Types::LimitedCountableConnectionType
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 2bf239979f7..d01055ec07e 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -315,7 +315,7 @@ module ApplicationHelper
class_names << 'issue-boards-page gl-overflow-auto' if current_controller?(:boards)
class_names << 'epic-boards-page gl-overflow-auto' if current_controller?(:epic_boards)
class_names << 'with-performance-bar' if performance_bar_enabled?
- class_names << 'with-top-bar' if show_super_sidebar? && !@hide_top_bar
+ class_names << 'with-top-bar' if show_super_sidebar? && !@hide_top_bar_padding
class_names << system_message_class
class_names << 'logged-out-marketing-header' if !current_user && ::Gitlab.com? && !show_super_sidebar?
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
index cd32023adb6..29b445e6234 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -479,7 +479,7 @@ module SearchHelper
end.to_json
end
- def search_filter_input_options(type, placeholder = _('Search or filter results...'))
+ def search_filter_input_options(type, placeholder = _('Search or filter results…'))
opts =
{
id: "filtered-search-#{type}",
diff --git a/app/models/project.rb b/app/models/project.rb
index 671b5cabc55..a57a3247c8a 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -2963,7 +2963,11 @@ class Project < ApplicationRecord
alias_method :service_desk_enabled?, :service_desk_enabled
def service_desk_address
- service_desk_custom_address || service_desk_incoming_address
+ service_desk_custom_address || service_desk_system_address
+ end
+
+ def service_desk_system_address
+ service_desk_alias_address || service_desk_incoming_address
end
def service_desk_incoming_address
@@ -2975,7 +2979,7 @@ class Project < ApplicationRecord
config.address&.gsub(wildcard, "#{full_path_slug}-#{default_service_desk_suffix}")
end
- def service_desk_custom_address
+ def service_desk_alias_address
return unless Gitlab::Email::ServiceDeskEmail.enabled?
key = service_desk_setting&.project_key || default_service_desk_suffix
@@ -2983,6 +2987,13 @@ class Project < ApplicationRecord
Gitlab::Email::ServiceDeskEmail.address_for_key("#{full_path_slug}-#{key}")
end
+ def service_desk_custom_address
+ return unless Feature.enabled?(:service_desk_custom_email, self)
+ return unless service_desk_setting&.custom_email_enabled?
+
+ service_desk_setting.custom_email
+ end
+
def default_service_desk_suffix
"#{id}-issue-"
end
diff --git a/app/serializers/admin/reported_content_entity.rb b/app/serializers/admin/reported_content_entity.rb
index 0e86a1434f8..bf690647672 100644
--- a/app/serializers/admin/reported_content_entity.rb
+++ b/app/serializers/admin/reported_content_entity.rb
@@ -5,6 +5,9 @@ module Admin
include RequestAwareEntity
expose :id
+ expose :global_id do |report|
+ Gitlab::GlobalId.build(report, id: report.id).to_s
+ end
expose :status
expose :message
expose :created_at, as: :reported_at
@@ -24,9 +27,6 @@ module Admin
end
end
- # Kept for backwards compatibility.
- # TODO: See https://gitlab.com/gitlab-org/modelops/anti-abuse/team-tasks/-/issues/167?work_item_iid=443
- # In 16.4 remove or re-use this field after frontend has migrated to using moderate_user_path
expose :update_path do |report|
admin_abuse_report_path(report)
end
diff --git a/app/services/admin/abuse_reports/update_service.rb b/app/services/admin/abuse_reports/update_service.rb
new file mode 100644
index 00000000000..36992e1aa25
--- /dev/null
+++ b/app/services/admin/abuse_reports/update_service.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module Admin
+ module AbuseReports
+ class UpdateService < BaseService
+ attr_reader :abuse_report, :params, :current_user
+
+ def initialize(abuse_report, current_user, params)
+ @abuse_report = abuse_report
+ @current_user = current_user
+ @params = params
+ end
+
+ def execute
+ return ServiceResponse.error(message: 'Admin is required') unless current_user&.can_admin_all_resources?
+
+ abuse_report.label_ids = label_ids
+
+ ServiceResponse.success
+ end
+
+ private
+
+ def label_ids
+ params[:label_ids].filter_map do |id|
+ GitlabSchema.parse_gid(id, expected_type: ::Admin::AbuseReportLabel).model_id
+ rescue Gitlab::Graphql::Errors::ArgumentError
+ end
+ end
+ end
+ end
+end
diff --git a/app/services/notification_recipients/builder/base.rb b/app/services/notification_recipients/builder/base.rb
index 3fabec29c0d..afbf5747429 100644
--- a/app/services/notification_recipients/builder/base.rb
+++ b/app/services/notification_recipients/builder/base.rb
@@ -44,6 +44,7 @@ module NotificationRecipients
def add_recipients(users, type, reason)
if users.is_a?(ActiveRecord::Relation)
users = users.includes(:notification_settings)
+ .allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/421821')
end
users = Array(users).compact
diff --git a/app/views/layouts/terms.html.haml b/app/views/layouts/terms.html.haml
index 9a50e3e2eb2..29a561ae1a9 100644
--- a/app/views/layouts/terms.html.haml
+++ b/app/views/layouts/terms.html.haml
@@ -1,6 +1,7 @@
!!! 5
- add_page_specific_style 'page_bundles/terms'
- @hide_top_bar = true
+- @hide_top_bar_padding = true
- body_classes = [user_application_theme]
%html{ lang: I18n.locale, class: page_class }
= render "layouts/head"
diff --git a/app/views/projects/_service_desk_settings.html.haml b/app/views/projects/_service_desk_settings.html.haml
index c2382a66132..3dbc4c0fad7 100644
--- a/app/views/projects/_service_desk_settings.html.haml
+++ b/app/views/projects/_service_desk_settings.html.haml
@@ -12,7 +12,7 @@
enabled: "#{@project.service_desk_enabled}",
issue_tracker_enabled: "#{@project.project_feature.issues_enabled?}",
incoming_email: (@project.service_desk_incoming_address if @project.service_desk_enabled),
- service_desk_email: (@project.service_desk_custom_address if @project.service_desk_enabled),
+ service_desk_email: (@project.service_desk_alias_address if @project.service_desk_enabled),
service_desk_email_enabled: "#{Gitlab::Email::ServiceDeskEmail.enabled?}",
selected_template: "#{@project.service_desk_setting&.issue_template_key}",
selected_file_template_project_id: "#{@project.service_desk_setting&.file_template_project_id}",
diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml
index d590c859945..86aaa5128a8 100644
--- a/app/views/shared/issuable/_search_bar.html.haml
+++ b/app/views/shared/issuable/_search_bar.html.haml
@@ -1,7 +1,7 @@
- type = local_assigns.fetch(:type)
- show_sorting_dropdown = local_assigns.fetch(:show_sorting_dropdown, true)
- disable_target_branch = local_assigns.fetch(:disable_target_branch, false)
-- placeholder = local_assigns[:placeholder] || _('Search or filter results...')
+- placeholder = local_assigns[:placeholder] || _('Search or filter results…')
- block_css_class = type != :productivity_analytics ? 'row-content-block second-block' : ''
.issues-filters
diff --git a/app/workers/merge_worker.rb b/app/workers/merge_worker.rb
index 3fcd7a3ad7a..a0594b15e31 100644
--- a/app/workers/merge_worker.rb
+++ b/app/workers/merge_worker.rb
@@ -29,3 +29,5 @@ class MergeWorker # rubocop:disable Scalability/IdempotentWorker
.execute(merge_request)
end
end
+
+MergeWorker.prepend_mod
diff --git a/config/application.rb b/config/application.rb
index 31304031930..527f4f127a8 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -505,6 +505,15 @@ module Gitlab
methods: %i(get head)
end
end
+
+ # Allow assets to be loaded to web-ide
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/421177
+ allow do
+ origins 'https://*.web-ide.gitlab-static.net'
+ resource '/assets/webpack/*',
+ credentials: false,
+ methods: %i(get head)
+ end
end
# Use caching across all environments
diff --git a/config/feature_flags/development/abuse_report_labels.yml b/config/feature_flags/development/abuse_report_labels.yml
new file mode 100644
index 00000000000..4a600b08230
--- /dev/null
+++ b/config/feature_flags/development/abuse_report_labels.yml
@@ -0,0 +1,8 @@
+---
+name: abuse_report_labels
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/128701/
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/421373
+milestone: '16.4'
+type: development
+group: group::anti-abuse
+default_enabled: false
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index cd23ff555f5..ebd69de7762 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -465,7 +465,7 @@ if Gitlab.ee? && Settings['ee_cron_jobs']
Settings.cron_jobs.merge!(Settings.ee_cron_jobs)
end
-Settings.cron_jobs['poll_interval'] ||= nil
+Settings.cron_jobs['poll_interval'] ||= ENV["GITLAB_CRON_JOBS_POLL_INTERVAL"] ? ENV["GITLAB_CRON_JOBS_POLL_INTERVAL"].to_i : nil
Settings.cron_jobs['stuck_ci_jobs_worker'] ||= {}
Settings.cron_jobs['stuck_ci_jobs_worker']['cron'] ||= '0 * * * *'
Settings.cron_jobs['stuck_ci_jobs_worker']['job_class'] = 'StuckCiJobsWorker'
diff --git a/data/deprecations/15-0-oauth-noexpiry.yml b/data/deprecations/15-0-oauth-noexpiry.yml
index 1eeb912a588..36f59e90908 100644
--- a/data/deprecations/15-0-oauth-noexpiry.yml
+++ b/data/deprecations/15-0-oauth-noexpiry.yml
@@ -7,7 +7,7 @@
had no expiration. In GitLab 15.0, an expiry will be automatically generated for any existing token that does not
already have one.
- You should [opt in](https://docs.gitlab.com/ee/integration/oauth_provider.html#expiring-access-tokens) to expiring
+ You should [opt in](https://docs.gitlab.com/ee/integration/oauth_provider.html#access-token-expiration) to expiring
tokens before GitLab 15.0 is released:
1. Edit the application.
diff --git a/db/docs/notification_settings.yml b/db/docs/notification_settings.yml
index c048163a790..a59131b8fe5 100644
--- a/db/docs/notification_settings.yml
+++ b/db/docs/notification_settings.yml
@@ -7,4 +7,4 @@ feature_categories:
description: User preferences for receiving notifications related to various actions within the application
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/commit/31b0e53015e38e51d9c02cca85c9279600b1bf85
milestone: '8.7'
-gitlab_schema: gitlab_main
+gitlab_schema: gitlab_main_cell
diff --git a/db/post_migrate/20230818055517_prepare_removal_index_deployments_on_id_where_cluster_id_present.rb b/db/post_migrate/20230818055517_prepare_removal_index_deployments_on_id_where_cluster_id_present.rb
new file mode 100644
index 00000000000..c66143809d2
--- /dev/null
+++ b/db/post_migrate/20230818055517_prepare_removal_index_deployments_on_id_where_cluster_id_present.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+class PrepareRemovalIndexDeploymentsOnIdWhereClusterIdPresent < Gitlab::Database::Migration[2.1]
+ INDEX_NAME = 'index_deployments_on_id_where_cluster_id_present'
+
+ # TODO: Index to be destroyed synchronously in https://gitlab.com/gitlab-org/gitlab/-/issues/402510
+ def up
+ prepare_async_index_removal :deployments, :id, name: INDEX_NAME
+ end
+
+ def down
+ unprepare_async_index :deployments, :id, name: INDEX_NAME
+ end
+end
diff --git a/db/schema_migrations/20230818055517 b/db/schema_migrations/20230818055517
new file mode 100644
index 00000000000..14c3821d426
--- /dev/null
+++ b/db/schema_migrations/20230818055517
@@ -0,0 +1 @@
+0dd37cf1da3ff0f56f24a41dd76ef7cb789e0833a6ea73b773f56a8a3793c465
\ No newline at end of file
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index aca651e7f17..ee6da4c774e 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -24835,6 +24835,7 @@ Represents a vulnerability.
| `updatedAt` | [`Time`](#time) | Timestamp of when the vulnerability was last updated. |
| `userNotesCount` | [`Int!`](#int) | Number of user notes attached to the vulnerability. |
| `userPermissions` | [`VulnerabilityPermissions!`](#vulnerabilitypermissions) | Permissions for the current user on the resource. |
+| `uuid` | [`String!`](#string) | UUID of the vulnerability finding. Can be used to look up the associated security report finding. |
| `vulnerabilityPath` | [`String`](#string) | Path to the vulnerability's details page. |
| `webUrl` | [`String`](#string) | URL to the vulnerability's details page. |
diff --git a/doc/api/packages/debian_group_distributions.md b/doc/api/packages/debian_group_distributions.md
index 0c7f4cdfeb8..68763a197aa 100644
--- a/doc/api/packages/debian_group_distributions.md
+++ b/doc/api/packages/debian_group_distributions.md
@@ -83,7 +83,7 @@ GET /groups/:id/-/debian_distributions/:codename
| Attribute | Type | Required | Description |
| ---------- | -------------- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](../rest/index.md#namespaced-path-encoding) owned by the authenticated user. |
-| `codename` | integer | yes | The `codename` of a distribution. |
+| `codename` | string | yes | The `codename` of a distribution. |
```shell
curl --header "PRIVATE-TOKEN: " "https://gitlab.example.com/api/v4/groups/5/-/debian_distributions/unstable"
@@ -122,7 +122,7 @@ GET /groups/:id/-/debian_distributions/:codename/key.asc
| Attribute | Type | Required | Description |
| ---------- | -------------- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](../rest/index.md#namespaced-path-encoding) owned by the authenticated user. |
-| `codename` | integer | yes | The `codename` of a distribution. |
+| `codename` | string | yes | The `codename` of a distribution. |
```shell
curl --header "PRIVATE-TOKEN: " "https://gitlab.example.com/api/v4/groups/5/-/debian_distributions/unstable/key.asc"
@@ -166,8 +166,8 @@ POST /groups/:id/-/debian_distributions
| `version` | string | no | The version of the new Debian distribution. |
| `description` | string | no | The description of the new Debian distribution. |
| `valid_time_duration_seconds` | integer | no | The valid time duration (in seconds) of the new Debian distribution. |
-| `components` | architectures | no | The new Debian distribution's list of components. |
-| `architectures` | architectures | no | The new Debian distribution's list of architectures. |
+| `components` | string array | no | The new Debian distribution's list of components. |
+| `architectures` | string array | no | The new Debian distribution's list of architectures. |
```shell
curl --request POST --header "PRIVATE-TOKEN: " "https://gitlab.example.com/api/v4/groups/5/-/debian_distributions?codename=sid"
@@ -213,8 +213,8 @@ PUT /groups/:id/-/debian_distributions/:codename
| `version` | string | no | The Debian distribution's new version. |
| `description` | string | no | The Debian distribution's new description. |
| `valid_time_duration_seconds` | integer | no | The Debian distribution's new valid time duration (in seconds). |
-| `components` | architectures | no | The Debian distribution's new list of components. |
-| `architectures` | architectures | no | The Debian distribution's new list of architectures. |
+| `components` | string array | no | The Debian distribution's new list of components. |
+| `architectures` | string array | no | The Debian distribution's new list of architectures. |
```shell
curl --request PUT --header "PRIVATE-TOKEN: " "https://gitlab.example.com/api/v4/groups/5/-/debian_distributions/unstable?suite=new-suite&valid_time_duration_seconds=604800"
@@ -253,7 +253,7 @@ DELETE /groups/:id/-/debian_distributions/:codename
| Attribute | Type | Required | Description |
| ---------- | -------------- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](../rest/index.md#namespaced-path-encoding) owned by the authenticated user. |
-| `codename` | integer | yes | The codename of the Debian distribution. |
+| `codename` | string | yes | The codename of the Debian distribution. |
```shell
curl --request DELETE --header "PRIVATE-TOKEN: " "https://gitlab.example.com/api/v4/groups/5/-/debian_distributions/unstable"
diff --git a/doc/api/packages/debian_project_distributions.md b/doc/api/packages/debian_project_distributions.md
index 0a43546e2e1..952b75ceb0a 100644
--- a/doc/api/packages/debian_project_distributions.md
+++ b/doc/api/packages/debian_project_distributions.md
@@ -82,7 +82,7 @@ GET /projects/:id/debian_distributions/:codename
| Attribute | Type | Required | Description |
| ---------- | -------------- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](../rest/index.md#namespaced-path-encoding) owned by the authenticated user. |
-| `codename` | integer | yes | The `codename` of a distribution. |
+| `codename` | string | yes | The `codename` of a distribution. |
```shell
curl --header "PRIVATE-TOKEN: " "https://gitlab.example.com/api/v4/projects/5/debian_distributions/unstable"
@@ -121,7 +121,7 @@ GET /projects/:id/debian_distributions/:codename/key.asc
| Attribute | Type | Required | Description |
| ---------- | -------------- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](../rest/index.md#namespaced-path-encoding) owned by the authenticated user. |
-| `codename` | integer | yes | The `codename` of a distribution. |
+| `codename` | string | yes | The `codename` of a distribution. |
```shell
curl --header "PRIVATE-TOKEN: " "https://gitlab.example.com/api/v4/projects/5/debian_distributions/unstable/key.asc"
@@ -165,8 +165,8 @@ POST /projects/:id/debian_distributions
| `version` | string | no | The new Debian distribution's version. |
| `description` | string | no | The new Debian distribution's description. |
| `valid_time_duration_seconds` | integer | no | The new Debian distribution's valid time duration (in seconds). |
-| `components` | architectures | no | The new Debian distribution's list of components. |
-| `architectures` | architectures | no | The new Debian distribution's list of architectures. |
+| `components` | string array | no | The new Debian distribution's list of components. |
+| `architectures` | string array | no | The new Debian distribution's list of architectures. |
```shell
curl --request POST --header "PRIVATE-TOKEN: " "https://gitlab.example.com/api/v4/projects/5/debian_distributions?codename=sid"
@@ -212,8 +212,8 @@ PUT /projects/:id/debian_distributions/:codename
| `version` | string | no | The Debian distribution's new version. |
| `description` | string | no | The Debian distribution's new description. |
| `valid_time_duration_seconds` | integer | no | The Debian distribution's new valid time duration (in seconds). |
-| `components` | architectures | no | The Debian distribution's new list of components. |
-| `architectures` | architectures | no | The Debian distribution's new list of architectures. |
+| `components` | string array | no | The Debian distribution's new list of components. |
+| `architectures` | string array | no | The Debian distribution's new list of architectures. |
```shell
curl --request PUT --header "PRIVATE-TOKEN: " "https://gitlab.example.com/api/v4/projects/5/debian_distributions/unstable?suite=new-suite&valid_time_duration_seconds=604800"
@@ -252,7 +252,7 @@ DELETE /projects/:id/debian_distributions/:codename
| Attribute | Type | Required | Description |
| ---------- | -------------- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](../rest/index.md#namespaced-path-encoding) owned by the authenticated user. |
-| `codename` | integer | yes | The Debian distribution's codename. |
+| `codename` | string | yes | The Debian distribution's codename. |
```shell
curl --request DELETE --header "PRIVATE-TOKEN: " "https://gitlab.example.com/api/v4/projects/5/debian_distributions/unstable"
diff --git a/doc/architecture/blueprints/organization/index.md b/doc/architecture/blueprints/organization/index.md
index ea8f2b75a14..215fd9b6d70 100644
--- a/doc/architecture/blueprints/organization/index.md
+++ b/doc/architecture/blueprints/organization/index.md
@@ -269,7 +269,7 @@ Today only Users, Projects, Namespaces and container images are considered routa
Initially, Organization routes will be [unscoped](../../../development/routing.md).
Organizations will follow the path `https://gitlab.com/-/organizations/org-name/` as one of the design goals is that the addition of Organizations should not change existing Group and Project paths.
-### Impact of the Organization on Other Features
+## Impact of the Organization on Other Features
We want a minimal amount of infrequently written tables in the shared database.
If we have high write volume or large amounts of data in the shared database then this can become a single bottleneck for scaling and we lose the horizontal scalability objective of Cells.
@@ -277,6 +277,18 @@ With isolation being one of the main requirements to make Cells work, this means
One exception to this are Users, which are stored in the cluster-wide shared database.
For a deeper exploration of the impact on select features, see the [list of features impacted by Cells](../cells/index.md#impacted-features).
+### Alignment between Organization and Fulfillment
+
+Fulfillment is supportive of an entity above top-level groups. Their perspective is outlined in issue [#1138](https://gitlab.com/gitlab-org/fulfillment-meta/-/issues/1138).
+
+#### Goals of Fulfillment
+
+- Fulfillment has a longstanding plan to move billing from the top-level Group to a level above. This would mean that a license applies to an Organization and all its top-level Groups.
+- Fulfillment uses Zuora for billing and would like to have a 1-to-1 relationship between an Organization and their Zuora entity called BillingAccount. They want to move away from tying a license to a single top-level Group.
+- If a customer needs multiple Organizations, they will need to have a separate BillingAccount per each.
+- Ideally, a self-managed instance has a single Organization by default, which should be enough for most customers.
+- Fulfillment prefers only one additional entity.
+
## Iteration Plan
The following iteration plan outlines how we intend to arrive at the Organization MVC. We are following the guidelines for [Experiment, Beta, and Generally Available features](../../../policy/experiment-beta-support.md).
diff --git a/doc/development/packages/debian_repository.md b/doc/development/packages/debian_repository.md
index b3e9bedfdd4..2d8ba98f9ad 100644
--- a/doc/development/packages/debian_repository.md
+++ b/doc/development/packages/debian_repository.md
@@ -17,11 +17,13 @@ This guide explains:
There are two types of [Debian packages](https://www.debian.org/doc/manuals/debian-faq/pkg-basics.en.html): binary and source.
- **Binary** - These are usually `.deb` files and contain executables, config files, and other data. A binary package must match your OS or architecture since it is already compiled. These are usually installed using `dpkg`. Dependencies must already exist on the system when installing a binary package.
-- **Source** - These are usual made up of `.dsc` files and `.gz` files. A source package is compiled on your system. These are fetched and installed with [`apt`](https://manpages.debian.org/bullseye/apt/apt.8.en.html), which then uses `dpkg` after the package is compiled. When you use `apt`, it will fetch and install the necessary dependencies.
+- **Source** - These are usually made up of `.dsc` files and compressed `.tar` files. A source package may be compiled on your system.
-The `.deb` file follows the naming convention `_-_.deb`
+Packages are fetched with [`apt`](https://manpages.debian.org/bullseye/apt/apt.8.en.html) and installed with `dpkg`. When you use `apt`, it also fetches and installs any dependencies.
-It includes a `control file` that contains metadata about the package. You can view the control file by using `dpkg --info `
+The `.deb` file follows the naming convention `_-_.deb`.
+
+It includes a `control file` that contains metadata about the package. You can view the control file by using `dpkg --info `.
The [`.changes` file](https://www.debian.org/doc/debian-policy/ch-controlfields.html#debian-changes-files-changes) is used to tell the Debian repository how to process updates to packages. It contains a variety of metadata for the package, including architecture, distribution, and version. In addition to the metadata, they contain three lists of checksums: `sha1`, `sha256`, and `md5` in the `Files` section. Refer to [sample_1.2.3~alpha2_amd64.changes](https://gitlab.com/gitlab-org/gitlab/-/blob/dd1e70d3676891025534dc4a1e89ca9383178fe7/spec/fixtures/packages/debian/sample_1.2.3~alpha2_amd64.changes) for an example of how these files are structured.
@@ -40,8 +42,8 @@ When it comes to Debian, packages don't exist on their own. They belong to a _di
## What does a Debian Repository look like?
- A [Debian repository](https://wiki.debian.org/DebianRepository) is made up of many releases.
-- Each release is given a **codename**. For the public Debian repository, these are things like "bullseye" and "jesse".
- - There is also the concept of **suites** which are essentially aliases of codenames synonymous with release channels like "stable" and "edge".
+- Each release is given a stable **codename**. For the public Debian repository, these are names like "bullseye" and "jessie".
+ - There is also the concept of **suites** which are essentially aliases of codenames synonymous with release channels like "stable" and "edge". Over time they change and point to different _codenames_.
- Each release has many **components**. In the public repository, these are "main", "contrib", and "non-free".
- Each release has many **architectures** such as "amd64", "arm64", or "i386".
- Each release has a signed **Release** file (see below about [GPG signing](#what-are-gpg-keys-and-what-are-signed-releases))
diff --git a/doc/security/two_factor_authentication.md b/doc/security/two_factor_authentication.md
index 906bf4cd062..09fba9cb23d 100644
--- a/doc/security/two_factor_authentication.md
+++ b/doc/security/two_factor_authentication.md
@@ -7,15 +7,14 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Enforce two-factor authentication **(FREE ALL)**
-Two-factor authentication (2FA) provides an additional level of security to your
-users' GitLab account. When enabled, users are prompted for a code generated by an application in
-addition to supplying their username and password to sign in.
+[Two-factor authentication (2FA)](../user/profile/account/two_factor_authentication.md)
+provides an additional level of security to your users' GitLab account. When enabled,
+users are prompted for a code generated by an application in addition to supplying
+their username and password to sign in.
NOTE:
If you are [using and enforcing SSO](../user/group/saml_sso/index.md#sso-enforcement), you might already be enforcing 2FA on the identity provider (IDP) side. Enforcing 2FA on GitLab as well might be unnecessary.
-Read more about [two-factor authentication (2FA)](../user/profile/account/two_factor_authentication.md).
-
## Enforce 2FA for all users **(FREE SELF)**
Users on GitLab can enable it without any administrator's intervention. If you
@@ -121,16 +120,19 @@ The target user is notified that 2FA has been disabled.
### For all users
-There may be some special situations where you want to disable 2FA for everyone
-even when forced 2FA is disabled. There is a Rake task for that:
+To disable 2FA for all users even when forced 2FA is disabled, use the following Rake task.
-```shell
-# Omnibus installations
-sudo gitlab-rake gitlab:two_factor:disable_for_all_users
+- For installations that use the Linux package:
-# Installations from source
-sudo -u git -H bundle exec rake gitlab:two_factor:disable_for_all_users RAILS_ENV=production
-```
+ ```shell
+ sudo gitlab-rake gitlab:two_factor:disable_for_all_users
+ ```
+
+- For self-compiled installations:
+
+ ```shell
+ sudo -u git -H bundle exec rake gitlab:two_factor:disable_for_all_users RAILS_ENV=production
+ ```
## 2FA for Git over SSH operations **(PREMIUM ALL)**
diff --git a/doc/update/deprecations.md b/doc/update/deprecations.md
index 1138cc8d0dd..22934e62d03 100644
--- a/doc/update/deprecations.md
+++ b/doc/update/deprecations.md
@@ -3423,7 +3423,7 @@ By default, all new applications expire access tokens after 2 hours. In GitLab 1
had no expiration. In GitLab 15.0, an expiry will be automatically generated for any existing token that does not
already have one.
-You should [opt in](https://docs.gitlab.com/ee/integration/oauth_provider.html#expiring-access-tokens) to expiring
+You should [opt in](https://docs.gitlab.com/ee/integration/oauth_provider.html#access-token-expiration) to expiring
tokens before GitLab 15.0 is released:
1. Edit the application.
diff --git a/doc/user/clusters/agent/install/index.md b/doc/user/clusters/agent/install/index.md
index d86cd46235b..522aed16eb6 100644
--- a/doc/user/clusters/agent/install/index.md
+++ b/doc/user/clusters/agent/install/index.md
@@ -114,22 +114,26 @@ To connect to multiple clusters, you must configure, register, and install an ag
#### Install the agent with Helm
+WARNING:
+For simplicity, the default Helm chart configuration sets up a service account for the agent with `cluster-admin` rights. You should not use this on production systems. To deploy to a production system, follow the instructions in [Customize the Helm installation](#customize-the-helm-installation) to create a service account with the minimum permissions required for your deployment and specify that during installation.
+
To install the agent on your cluster using Helm:
1. [Install Helm](https://helm.sh/docs/intro/install/).
1. In your computer, open a terminal and [connect to your cluster](https://kubernetes.io/docs/tasks/access-application-cluster/access-cluster/).
1. Run the command you copied when you [registered your agent with GitLab](#register-the-agent-with-gitlab).
-
-Optionally, you can [customize the Helm installation](#customize-the-helm-installation). If you install the agent on a production system, you should customize the Helm installation to skip creating the service account.
+1. Optional. [Customize the Helm installation](#customize-the-helm-installation).
+ If you install the agent on a production system, you should customize the Helm installation to restrict the permissions of the service account. See [How to deploy the GitLab Agent for Kubernetes with limited permissions](https://about.gitlab.com/blog/2021/09/10/setting-up-the-k-agent/).
##### Customize the Helm installation
By default, the Helm installation command generated by GitLab:
- Creates a namespace `gitlab-agent` for the deployment (`--namespace gitlab-agent`). You can skip creating the namespace by omitting the `--create-namespace` flag.
-- Sets up a service account for the agent with `cluster-admin` rights. You can:
+- Sets up a service account for the agent and assigns it the `cluster-admin` role. You can:
- Skip creating the service account by adding `--set serviceAccount.create=false` to the `helm install` command. In this case, you must set `serviceAccount.name` to a pre-existing service account.
- - Skip creating the RBAC permissions by adding `--set rbac.create=false` to the `helm install` command. In this case, you must bring your own RBAC permissions for the agent. Otherwise, it has no permissions at all.
+ - Customise the role assigned to the service account by adding `--set rbac.useExistingRole ` to the `helm install` command. In this case, you should have a pre-created role with restricted permissions that can be used by the service account.
+ - Skip role assignment altogether by adding `--set rbac.create=false` to your `helm install` command. In this case, you must create `ClusterRoleBinding` manually.
- Creates a `Secret` resource for the agent's access token. To instead bring your own secret with a token, omit the token (`--set token=...`) and instead use `--set config.secretName=`.
- Creates a `Deployment` resource for the `agentk` pod.
diff --git a/doc/user/gitlab_com/index.md b/doc/user/gitlab_com/index.md
index 901b2064ed0..9152f01fffb 100644
--- a/doc/user/gitlab_com/index.md
+++ b/doc/user/gitlab_com/index.md
@@ -18,7 +18,7 @@ GitLab.com has the:
- [`email_confirmation_setting`](../../administration/settings/sign_up_restrictions.md#confirm-user-email)
setting set to **Hard**.
- [`unconfirmed_users_delete_after_days`](../../administration/moderate_users.md#automatically-delete-unconfirmed-users)
- setting set to one day.
+ setting set to three days.
## Password requirements
diff --git a/doc/user/group/saml_sso/troubleshooting_scim.md b/doc/user/group/saml_sso/troubleshooting_scim.md
index 7d2aa8faa99..4a66719b020 100644
--- a/doc/user/group/saml_sso/troubleshooting_scim.md
+++ b/doc/user/group/saml_sso/troubleshooting_scim.md
@@ -13,7 +13,18 @@ This section contains possible solutions for problems you might encounter.
When you remove a user, they are removed from the group but their account is not deleted
(see [remove access](scim_setup.md#remove-access)).
-When the user is added back to the SCIM app, GitLab cannot create a new user because the user already exists.
+When the user is added back to the SCIM app, GitLab does not create a new user because the user already exists.
+
+From August 11, 2023, the `skip_saml_identity_destroy_during_scim_deprovision` feature flag is enabled.
+
+For a user de-provisioned by SCIM from that date, their SAML identity is not removed.
+
+When that user is added back to the SCIM app:
+
+- Their SCIM identity `active` attribute is set to `true`.
+- They can sign in using SSO.
+
+For users de-provisioned by SCIM before that date, their SAML identity is destroyed.
To solve this problem:
diff --git a/doc/user/packages/debian_repository/index.md b/doc/user/packages/debian_repository/index.md
index b7fbeb96202..c3e35fc4f06 100644
--- a/doc/user/packages/debian_repository/index.md
+++ b/doc/user/packages/debian_repository/index.md
@@ -27,6 +27,9 @@ Prerequisites:
- The `dpkg-deb` binary must be installed on the GitLab instance.
This binary is usually provided by the [`dpkg` package](https://wiki.debian.org/Teams/Dpkg/Downstream),
installed by default on Debian and derivatives.
+- Support for compression algorithm ZStandard requires version `dpkg >=
+ 1.21.18` from Debian 12 Bookworm or `dpkg >= 1.19.0.5ubuntu2` from Ubuntu
+ 18.04 Bionic Beaver.
## Enable the Debian API **(FREE SELF)**
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index b52e5c942c1..e611d84054f 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -5017,6 +5017,9 @@ msgstr ""
msgid "An error occurred while fetching label colors."
msgstr ""
+msgid "An error occurred while fetching labels, please try again."
+msgstr ""
+
msgid "An error occurred while fetching participants"
msgstr ""
@@ -5202,6 +5205,9 @@ msgstr[1] ""
msgid "An error occurred while saving your settings. Try saving them again."
msgstr ""
+msgid "An error occurred while searching for labels, please try again."
+msgstr ""
+
msgid "An error occurred while triggering the job."
msgstr ""
@@ -26253,7 +26259,7 @@ msgstr ""
msgid "Issues, merge requests, pushes, and comments."
msgstr ""
-msgid "IssuesAnalytics|After you begin creating issues for your projects, we can start tracking and displaying metrics for them"
+msgid "IssuesAnalytics|After you begin creating issues for your projects, we can start tracking and displaying metrics for them."
msgstr ""
msgid "IssuesAnalytics|Avg/Month:"
@@ -26265,7 +26271,7 @@ msgstr ""
msgid "IssuesAnalytics|Issues created per month"
msgstr ""
-msgid "IssuesAnalytics|Last 12 months"
+msgid "IssuesAnalytics|Last 12 months (%{chartDateRange})"
msgstr ""
msgid "IssuesAnalytics|Sorry, your filter produced no results"
@@ -26274,7 +26280,7 @@ msgstr ""
msgid "IssuesAnalytics|There are no issues for the projects in your group"
msgstr ""
-msgid "IssuesAnalytics|To widen your search, change or remove filters in the filter bar above"
+msgid "IssuesAnalytics|To widen your search, change or remove filters in the filter bar above."
msgstr ""
msgid "IssuesAnalytics|Total:"
@@ -41795,12 +41801,6 @@ msgstr ""
msgid "Search or filter commits"
msgstr ""
-msgid "Search or filter results"
-msgstr ""
-
-msgid "Search or filter results..."
-msgstr ""
-
msgid "Search or filter results…"
msgstr ""
diff --git a/spec/features/groups/group_runners_spec.rb b/spec/features/groups/group_runners_spec.rb
index 2a6f79a5d0f..e15374a8629 100644
--- a/spec/features/groups/group_runners_spec.rb
+++ b/spec/features/groups/group_runners_spec.rb
@@ -249,7 +249,7 @@ RSpec.describe "Group Runners", feature_category: :runner_fleet do
create(:ci_runner, :group, groups: [group], description: 'runner-foo')
end
- let_it_be(:group_runner_job) { create(:ci_build, runner: group_runner) }
+ let_it_be(:group_runner_job) { create(:ci_build, runner: group_runner, project: project) }
context 'when logged in as group maintainer' do
before do
diff --git a/spec/features/projects/settings/service_desk_setting_spec.rb b/spec/features/projects/settings/service_desk_setting_spec.rb
index d068cb219f1..5cc2e2d3c05 100644
--- a/spec/features/projects/settings/service_desk_setting_spec.rb
+++ b/spec/features/projects/settings/service_desk_setting_spec.rb
@@ -56,7 +56,7 @@ RSpec.describe 'Service Desk Setting', :js, :clean_gitlab_redis_cache, feature_c
wait_for_requests
project.reload
- expect(find('[data-testid="incoming-email"]').value).to eq(project.service_desk_custom_address)
+ expect(find('[data-testid="incoming-email"]').value).to eq(project.service_desk_alias_address)
page.within '#js-service-desk' do
fill_in('service-desk-project-suffix', with: 'foo')
diff --git a/spec/frontend/admin/abuse_report/components/abuse_report_app_spec.js b/spec/frontend/admin/abuse_report/components/abuse_report_app_spec.js
index 637f035be8c..d704a91b93d 100644
--- a/spec/frontend/admin/abuse_report/components/abuse_report_app_spec.js
+++ b/spec/frontend/admin/abuse_report/components/abuse_report_app_spec.js
@@ -3,6 +3,7 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import AbuseReportApp from '~/admin/abuse_report/components/abuse_report_app.vue';
import ReportHeader from '~/admin/abuse_report/components/report_header.vue';
import UserDetails from '~/admin/abuse_report/components/user_details.vue';
+import ReportDetails from '~/admin/abuse_report/components/report_details.vue';
import ReportedContent from '~/admin/abuse_report/components/reported_content.vue';
import HistoryItems from '~/admin/abuse_report/components/history_items.vue';
import { SUCCESS_ALERT } from '~/admin/abuse_report/constants';
@@ -19,13 +20,15 @@ describe('AbuseReportApp', () => {
const findSimilarReportedContent = () =>
findSimilarOpenReports().at(0).findComponent(ReportedContent);
const findHistoryItems = () => wrapper.findComponent(HistoryItems);
+ const findReportDetails = () => wrapper.findComponent(ReportDetails);
- const createComponent = (props = {}) => {
+ const createComponent = (props = {}, provide = {}) => {
wrapper = shallowMountExtended(AbuseReportApp, {
propsData: {
abuseReport: mockAbuseReport,
...props,
},
+ provide,
});
};
@@ -104,6 +107,24 @@ describe('AbuseReportApp', () => {
});
});
+ describe('ReportDetails', () => {
+ describe('when abuseReportLabels feature flag is enabled', () => {
+ it('renders ReportDetails', () => {
+ createComponent({}, { glFeatures: { abuseReportLabels: true } });
+
+ expect(findReportDetails().props('reportId')).toBe(mockAbuseReport.report.globalId);
+ });
+ });
+
+ describe('when abuseReportLabels feature flag is disabled', () => {
+ it('does not render ReportDetails', () => {
+ createComponent({}, { glFeatures: { abuseReportLabels: false } });
+
+ expect(findReportDetails().exists()).toBe(false);
+ });
+ });
+ });
+
it('renders ReportedContent', () => {
expect(findReportedContent().props('report')).toBe(mockAbuseReport.report);
});
diff --git a/spec/frontend/admin/abuse_report/components/labels_select_spec.js b/spec/frontend/admin/abuse_report/components/labels_select_spec.js
new file mode 100644
index 00000000000..35d378b7258
--- /dev/null
+++ b/spec/frontend/admin/abuse_report/components/labels_select_spec.js
@@ -0,0 +1,239 @@
+import MockAdapter from 'axios-mock-adapter';
+import { GlButton, GlDropdown, GlDropdownItem, GlLoadingIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK, HTTP_STATUS_INTERNAL_SERVER_ERROR } from '~/lib/utils/http_status';
+import LabelsSelect from '~/admin/abuse_report/components/labels_select.vue';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { stubComponent } from 'helpers/stub_component';
+import labelsQuery from '~/admin/abuse_report/components/graphql/abuse_report_labels.query.graphql';
+import DropdownValue from '~/sidebar/components/labels/labels_select_widget/dropdown_value.vue';
+import DropdownWidget from '~/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue';
+import { createAlert } from '~/alert';
+import { mockLabelsQueryResponse, mockLabel1, mockLabel2 } from '../mock_data';
+
+jest.mock('~/alert');
+
+Vue.use(VueApollo);
+
+describe('Labels select component', () => {
+ let mock;
+ let wrapper;
+ let fakeApollo;
+
+ const selectedText = () => wrapper.find('[data-testid="selected-labels"]').text();
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findEditButton = () => wrapper.findComponent(GlButton);
+ const findDropdown = () => wrapper.findComponent(DropdownWidget);
+ const findDropdownValue = () => wrapper.findComponent(DropdownValue);
+
+ const labelsQueryHandlerSuccess = jest.fn().mockResolvedValue(mockLabelsQueryResponse);
+ const labelsQueryHandlerFailure = jest.fn().mockRejectedValue(new Error());
+
+ const updatePath = '/admin/abuse_reports/1';
+
+ async function openLabelsDropdown() {
+ findEditButton().vm.$emit('click');
+ await waitForPromises();
+ }
+
+ const selectLabel = (label) => {
+ findDropdown().vm.$emit('set-option', label);
+ nextTick();
+ };
+
+ const createComponent = ({ props = {}, labelsQueryHandler = labelsQueryHandlerSuccess } = {}) => {
+ fakeApollo = createMockApollo([[labelsQuery, labelsQueryHandler]]);
+ wrapper = shallowMount(LabelsSelect, {
+ apolloProvider: fakeApollo,
+ propsData: {
+ report: { labels: [] },
+ canEdit: true,
+ ...props,
+ },
+ provide: {
+ updatePath,
+ },
+ stubs: {
+ GlDropdown,
+ GlDropdownItem,
+ DropdownWidget: stubComponent(DropdownWidget, {
+ methods: { showDropdown: jest.fn() },
+ }),
+ },
+ });
+ };
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ fakeApollo = null;
+ mock.restore();
+ });
+
+ describe('initial load', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('displays loading icon', () => {
+ expect(findLoadingIcon().exists()).toEqual(true);
+ });
+
+ it('disables edit button', () => {
+ expect(findEditButton().props('disabled')).toEqual(true);
+ });
+
+ describe('after initial load', () => {
+ beforeEach(() => {
+ wrapper.setProps({ report: { labels: [mockLabel1] } });
+ });
+
+ it('does not display loading icon', () => {
+ expect(findLoadingIcon().exists()).toEqual(false);
+ });
+
+ it('enables edit button', () => {
+ expect(findEditButton().props('disabled')).toEqual(false);
+ });
+
+ it('renders fetched labels in DropdownValue', () => {
+ expect(findDropdownValue().isVisible()).toBe(true);
+ expect(findDropdownValue().props('selectedLabels')).toEqual([mockLabel1]);
+ });
+ });
+ });
+
+ describe('when there are no selected labels', () => {
+ it('displays "None"', () => {
+ createComponent();
+
+ expect(selectedText()).toContain('None');
+ });
+ });
+
+ describe('when there are selected labels', () => {
+ beforeEach(() => {
+ createComponent({ props: { report: { labels: [mockLabel1, mockLabel2] } } });
+
+ mock.onPut(updatePath).reply(HTTP_STATUS_OK, {});
+ jest.spyOn(axios, 'put');
+ });
+
+ it('renders selected labels in DropdownValue', () => {
+ expect(findDropdownValue().isVisible()).toBe(true);
+ expect(findDropdownValue().props('selectedLabels')).toEqual([mockLabel1, mockLabel2]);
+ });
+
+ it('selected labels can be removed', async () => {
+ findDropdownValue().vm.$emit('onLabelRemove', mockLabel1.id);
+ await nextTick();
+
+ expect(findDropdownValue().props('selectedLabels')).toEqual([mockLabel2]);
+ expect(axios.put).toHaveBeenCalledWith(updatePath, {
+ label_ids: [mockLabel2.id],
+ });
+ });
+ });
+
+ describe('when not editing', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('does not trigger abuse report labels query', () => {
+ expect(labelsQueryHandlerSuccess).not.toHaveBeenCalled();
+ });
+
+ it('does not render the dropdown', () => {
+ expect(findDropdown().isVisible()).toBe(false);
+ });
+ });
+
+ describe('when editing', () => {
+ beforeEach(async () => {
+ createComponent();
+ await openLabelsDropdown();
+ });
+
+ it('triggers abuse report labels query', () => {
+ expect(labelsQueryHandlerSuccess).toHaveBeenCalledTimes(1);
+ });
+
+ it('renders dropdown with fetched labels', () => {
+ expect(findDropdown().isVisible()).toBe(true);
+ expect(findDropdown().props('options')).toEqual([mockLabel1, mockLabel2]);
+ });
+
+ it('selects/deselects a label', async () => {
+ await selectLabel(mockLabel1);
+
+ expect(findDropdownValue().props('selectedLabels')).toEqual([mockLabel1]);
+
+ await selectLabel(mockLabel1);
+
+ expect(selectedText()).toContain('None');
+ });
+
+ it('triggers abuse report labels query when search term is set', async () => {
+ findDropdown().vm.$emit('set-search', 'Dos');
+ await waitForPromises();
+
+ expect(labelsQueryHandlerSuccess).toHaveBeenCalledTimes(2);
+ expect(labelsQueryHandlerSuccess).toHaveBeenCalledWith({ searchTerm: 'Dos' });
+ });
+ });
+
+ describe('after edit', () => {
+ const setup = async (response) => {
+ mock.onPut(updatePath).reply(response, {});
+ jest.spyOn(axios, 'put');
+
+ createComponent();
+ await openLabelsDropdown();
+ await selectLabel(mockLabel1);
+
+ findDropdown().vm.$emit('hide');
+ };
+
+ describe('successful save', () => {
+ it('saves', async () => {
+ await setup(HTTP_STATUS_OK);
+
+ expect(axios.put).toHaveBeenCalledWith(updatePath, {
+ label_ids: [mockLabel1.id],
+ });
+ });
+ });
+
+ describe('unsuccessful save', () => {
+ it('creates an alert', async () => {
+ await setup(HTTP_STATUS_INTERNAL_SERVER_ERROR);
+
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith({
+ message: 'An error occurred while updating labels.',
+ captureError: true,
+ error: expect.any(Error),
+ });
+ });
+ });
+ });
+
+ describe('failed abuse report labels query', () => {
+ it('creates an alert', async () => {
+ createComponent({ labelsQueryHandler: labelsQueryHandlerFailure });
+ await openLabelsDropdown();
+
+ expect(createAlert).toHaveBeenCalledWith({
+ message: 'An error occurred while searching for labels, please try again.',
+ });
+ });
+ });
+});
diff --git a/spec/frontend/admin/abuse_report/components/report_details_spec.js b/spec/frontend/admin/abuse_report/components/report_details_spec.js
new file mode 100644
index 00000000000..a5c43dcb82b
--- /dev/null
+++ b/spec/frontend/admin/abuse_report/components/report_details_spec.js
@@ -0,0 +1,74 @@
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import LabelsSelect from '~/admin/abuse_report/components/labels_select.vue';
+import ReportDetails from '~/admin/abuse_report/components/report_details.vue';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import abuseReportQuery from '~/admin/abuse_report/components/graphql/abuse_report.query.graphql';
+import { createAlert } from '~/alert';
+import { mockAbuseReport, mockLabel1, mockReportQueryResponse } from '../mock_data';
+
+jest.mock('~/alert');
+
+Vue.use(VueApollo);
+
+describe('Report Details', () => {
+ let wrapper;
+ let fakeApollo;
+
+ const findLabelsSelect = () => wrapper.findComponent(LabelsSelect);
+
+ const abuseReportQueryHandlerSuccess = jest.fn().mockResolvedValue(mockReportQueryResponse);
+ const abuseReportQueryHandlerFailure = jest.fn().mockRejectedValue(new Error());
+
+ const createComponent = ({ abuseReportQueryHandler = abuseReportQueryHandlerSuccess } = {}) => {
+ fakeApollo = createMockApollo([[abuseReportQuery, abuseReportQueryHandler]]);
+ wrapper = shallowMount(ReportDetails, {
+ apolloProvider: fakeApollo,
+ propsData: {
+ reportId: mockAbuseReport.report.globalId,
+ },
+ });
+ };
+
+ afterEach(() => {
+ fakeApollo = null;
+ });
+
+ describe('successful abuse report query', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('triggers abuse report query', async () => {
+ await waitForPromises();
+
+ expect(abuseReportQueryHandlerSuccess).toHaveBeenCalledWith({
+ id: mockAbuseReport.report.globalId,
+ });
+ });
+
+ it('renders LabelsSelect with the fetched report', async () => {
+ expect(findLabelsSelect().props('report').labels).toEqual([]);
+
+ await waitForPromises();
+
+ expect(findLabelsSelect().props('report').labels).toEqual([mockLabel1]);
+ });
+ });
+
+ describe('failed abuse report query', () => {
+ beforeEach(async () => {
+ createComponent({ abuseReportQueryHandler: abuseReportQueryHandlerFailure });
+
+ await waitForPromises();
+ });
+
+ it('creates an alert', () => {
+ expect(createAlert).toHaveBeenCalledWith({
+ message: 'An error occurred while fetching labels, please try again.',
+ });
+ });
+ });
+});
diff --git a/spec/frontend/admin/abuse_report/mock_data.js b/spec/frontend/admin/abuse_report/mock_data.js
index 4c3f6e7b5ea..4777e5b9263 100644
--- a/spec/frontend/admin/abuse_report/mock_data.js
+++ b/spec/frontend/admin/abuse_report/mock_data.js
@@ -52,6 +52,7 @@ export const mockAbuseReport = {
],
},
report: {
+ globalId: 'gid://gitlab/AbuseReport/1',
status: 'open',
message: 'This is obvious spam',
reportedAt: '2023-03-29T09:39:50.502Z',
@@ -73,3 +74,40 @@ export const mockAbuseReport = {
},
},
};
+
+export const mockLabel1 = {
+ id: 'gid://gitlab/AbuseReportLabel/1',
+ title: 'Uno',
+ color: '#F0AD4E',
+ textColor: '#FFFFFF',
+ description: null,
+};
+
+export const mockLabel2 = {
+ id: 'gid://gitlab/AbuseReportLabel/2',
+ title: 'Dos',
+ color: '#F0AD4E',
+ textColor: '#FFFFFF',
+ description: null,
+};
+
+export const mockLabelsQueryResponse = {
+ data: {
+ abuseReportLabels: {
+ nodes: [mockLabel1, mockLabel2],
+ __typename: 'LabelConnection',
+ },
+ },
+};
+
+export const mockReportQueryResponse = {
+ data: {
+ abuseReport: {
+ labels: {
+ nodes: [mockLabel1],
+ __typename: 'LabelConnection',
+ },
+ __typename: 'AbuseReport',
+ },
+ },
+};
diff --git a/spec/frontend/filtered_search/filtered_search_manager_spec.js b/spec/frontend/filtered_search/filtered_search_manager_spec.js
index 8c16ff100eb..c55099d89d9 100644
--- a/spec/frontend/filtered_search/filtered_search_manager_spec.js
+++ b/spec/frontend/filtered_search/filtered_search_manager_spec.js
@@ -24,7 +24,7 @@ describe('Filtered Search Manager', () => {
let manager;
let tokensContainer;
const page = 'issues';
- const placeholder = 'Search or filter results...';
+ const placeholder = 'Search or filter results…';
function dispatchBackspaceEvent(element, eventType) {
const event = new Event(eventType);
diff --git a/spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js b/spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js
index 4686a4fe0c4..f6c9fab76d1 100644
--- a/spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js
+++ b/spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js
@@ -137,7 +137,6 @@ describe('IssuesDashboardApp component', () => {
issuablesLoading: false,
namespace: 'dashboard',
recentSearchesStorageKey: 'issues',
- searchInputPlaceholder: i18n.searchPlaceholder,
showPaginationControls: true,
sortOptions: getSortOptions({
hasBlockedIssuesFeature: defaultProvide.hasBlockedIssuesFeature,
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 de027a21c8f..f830168ce5d 100644
--- a/spec/frontend/issues/list/components/issues_list_app_spec.js
+++ b/spec/frontend/issues/list/components/issues_list_app_spec.js
@@ -237,7 +237,6 @@ describe('CE IssuesListApp component', () => {
expect(findIssuableList().props()).toMatchObject({
namespace: defaultProvide.fullPath,
recentSearchesStorageKey: 'issues',
- searchInputPlaceholder: IssuesListApp.i18n.searchPlaceholder,
sortOptions: getSortOptions({
hasBlockedIssuesFeature: defaultProvide.hasBlockedIssuesFeature,
hasIssuableHealthStatusFeature: defaultProvide.hasIssuableHealthStatusFeature,
diff --git a/spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js b/spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js
index e5b641c61fd..335c5bdfc46 100644
--- a/spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js
+++ b/spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js
@@ -291,7 +291,7 @@ describe('AlertManagementEmptyState', () => {
it('renders the search component for incidents', () => {
const filteredSearchBar = findFilteredSearchBar();
- expect(filteredSearchBar.props('searchInputPlaceholder')).toBe('Search or filter results…');
+
expect(filteredSearchBar.props('tokens')).toEqual([
{
type: TOKEN_TYPE_AUTHOR,
diff --git a/spec/frontend/vue_shared/new_namespace/new_namespace_page_spec.js b/spec/frontend/vue_shared/new_namespace/new_namespace_page_spec.js
index a7ddcbdd8bc..109b7732539 100644
--- a/spec/frontend/vue_shared/new_namespace/new_namespace_page_spec.js
+++ b/spec/frontend/vue_shared/new_namespace/new_namespace_page_spec.js
@@ -1,6 +1,6 @@
import { GlBreadcrumb } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import LegacyContainer from '~/vue_shared/new_namespace/components/legacy_container.vue';
import WelcomePage from '~/vue_shared/new_namespace/components/welcome.vue';
import NewNamespacePage from '~/vue_shared/new_namespace/new_namespace_page.vue';
@@ -14,6 +14,7 @@ describe('Experimental new namespace creation app', () => {
const findWelcomePage = () => wrapper.findComponent(WelcomePage);
const findLegacyContainer = () => wrapper.findComponent(LegacyContainer);
+ const findTopBar = () => wrapper.findByTestId('top-bar');
const findBreadcrumb = () => wrapper.findComponent(GlBreadcrumb);
const findImage = () => wrapper.find('img');
const findNewTopLevelGroupAlert = () => wrapper.findComponent(NewTopLevelGroupAlert);
@@ -30,7 +31,7 @@ describe('Experimental new namespace creation app', () => {
};
const createComponent = ({ slots, propsData } = {}) => {
- wrapper = shallowMount(NewNamespacePage, {
+ wrapper = shallowMountExtended(NewNamespacePage, {
slots,
propsData: {
...DEFAULT_PROPS,
@@ -167,4 +168,19 @@ describe('Experimental new namespace creation app', () => {
});
});
});
+
+ describe('top bar', () => {
+ it('adds "top-bar-fixed" and "container-fluid" classes when new navigation enabled', () => {
+ gon.use_new_navigation = true;
+ createComponent();
+
+ expect(findTopBar().classes()).toEqual(['top-bar-fixed', 'container-fluid']);
+ });
+
+ it('does not add classes when new navigation is not enabled', () => {
+ createComponent();
+
+ expect(findTopBar().classes()).toEqual([]);
+ });
+ });
});
diff --git a/spec/frontend/work_items/list/components/work_items_list_app_spec.js b/spec/frontend/work_items/list/components/work_items_list_app_spec.js
index 5fe0dbcb497..96083478e77 100644
--- a/spec/frontend/work_items/list/components/work_items_list_app_spec.js
+++ b/spec/frontend/work_items/list/components/work_items_list_app_spec.js
@@ -44,7 +44,6 @@ describe('WorkItemsListApp component', () => {
issuablesLoading: true,
namespace: 'work-items',
recentSearchesStorageKey: 'issues',
- searchInputPlaceholder: 'Search or filter results...',
searchTokens: [],
showWorkItemTypeIcon: true,
sortOptions: [],
diff --git a/spec/graphql/types/ci/job_base_field_spec.rb b/spec/graphql/types/ci/job_base_field_spec.rb
new file mode 100644
index 00000000000..e9b1407d249
--- /dev/null
+++ b/spec/graphql/types/ci/job_base_field_spec.rb
@@ -0,0 +1,144 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Types::Ci::JobBaseField, feature_category: :runner_fleet do
+ describe 'authorized?' do
+ let_it_be(:current_user) { create(:user) }
+
+ let(:object) { double }
+ let(:ctx) { { current_user: current_user, current_field: current_field } }
+ let(:current_field) { instance_double(described_class, original_name: current_field_name.to_sym) }
+ let(:args) { {} }
+
+ subject(:field) do
+ described_class.new(name: current_field_name, type: GraphQL::Types::String, null: true, **args)
+ end
+
+ context 'when :job_field_authorization is specified' do
+ let(:ctx) { { current_user: current_user, current_field: current_field, job_field_authorization: :foo } }
+
+ context 'with public field' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:current_field_name) do
+ %i[allow_failure duration id kind status created_at finished_at queued_at queued_duration updated_at runner
+ short_sha]
+ end
+
+ with_them do
+ it 'returns true without authorizing' do
+ is_expected.to be_authorized(object, nil, ctx)
+ end
+ end
+ end
+
+ context 'with private field' do
+ let(:current_field_name) { 'private_field' }
+
+ context 'when permission is not allowed' do
+ it 'returns false' do
+ expect(Ability).to receive(:allowed?).with(current_user, :foo, object).and_return(false)
+
+ is_expected.not_to be_authorized(object, nil, ctx)
+ end
+ end
+
+ context 'when permission is allowed' do
+ it 'returns true' do
+ expect(Ability).to receive(:allowed?).with(current_user, :foo, object).and_return(true)
+
+ is_expected.to be_authorized(object, nil, ctx)
+ end
+ end
+ end
+ end
+
+ context 'when :job_field_authorization is not specified' do
+ let(:current_field_name) { 'status' }
+
+ it 'defaults to true' do
+ is_expected.to be_authorized(object, nil, ctx)
+ end
+
+ context 'when field is authorized' do
+ let(:args) { { authorize: :foo } }
+
+ it 'tests the field authorization' do
+ expect(Ability).to receive(:allowed?).with(current_user, :foo, object).and_return(false)
+
+ expect(field).not_to be_authorized(object, nil, ctx)
+ end
+
+ it 'tests the field authorization, if provided, when it succeeds' do
+ expect(Ability).to receive(:allowed?).with(current_user, :foo, object).and_return(true)
+
+ expect(field).to be_authorized(object, nil, ctx)
+ end
+ end
+
+ context 'with field resolver' do
+ let(:resolver) { Class.new }
+ let(:args) { { resolver_class: resolver } }
+
+ it 'only tests the resolver authorization if it authorizes_object?' do
+ is_expected.to be_authorized(object, nil, ctx)
+ end
+
+ context 'when resolver authorizes object' do
+ let(:resolver) do
+ Class.new do
+ include Gitlab::Graphql::Authorize::AuthorizeResource
+
+ authorizes_object!
+ end
+ end
+
+ it 'tests the resolver authorization, if provided' do
+ expect(resolver).to receive(:authorized?).with(object, ctx).and_return(false)
+
+ expect(field).not_to be_authorized(object, nil, ctx)
+ end
+
+ context 'when field is authorized' do
+ let(:args) { { authorize: :foo, resolver_class: resolver } }
+
+ it 'tests field authorization before resolver authorization, when field auth fails' do
+ expect(Ability).to receive(:allowed?).with(current_user, :foo, object).and_return(false)
+ expect(resolver).not_to receive(:authorized?)
+
+ expect(field).not_to be_authorized(object, nil, ctx)
+ end
+
+ it 'tests field authorization before resolver authorization, when field auth succeeds' do
+ expect(Ability).to receive(:allowed?).with(current_user, :foo, object).and_return(true)
+ expect(resolver).to receive(:authorized?).with(object, ctx).and_return(false)
+
+ expect(field).not_to be_authorized(object, nil, ctx)
+ end
+ end
+ end
+ end
+ end
+ end
+
+ describe '#resolve' do
+ context 'when late_extensions is given' do
+ it 'registers the late extensions after the regular extensions' do
+ extension_class = Class.new(GraphQL::Schema::Field::ConnectionExtension)
+ field = described_class.new(name: 'private_field', type: GraphQL::Types::String.connection_type,
+ null: true, late_extensions: [extension_class])
+
+ expect(field.extensions.last.class).to be(extension_class)
+ end
+ end
+ end
+
+ include_examples 'Gitlab-style deprecations' do
+ def subject(args = {})
+ base_args = { name: 'private_field', type: GraphQL::Types::String, null: true }
+
+ described_class.new(**base_args.merge(args))
+ end
+ end
+end
diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb
index adae96f34fa..b65a2e6f700 100644
--- a/spec/helpers/application_helper_spec.rb
+++ b/spec/helpers/application_helper_spec.rb
@@ -688,6 +688,34 @@ RSpec.describe ApplicationHelper do
end
it { is_expected.not_to include('logged-out-marketing-header') }
+
+ context 'when show_super_sidebar? is true' do
+ context 'when @hide_top_bar_padding is false' do
+ before do
+ allow(helper).to receive(:show_super_sidebar?).and_return(true)
+ helper.instance_variable_set(:@hide_top_bar_padding, false)
+ end
+
+ it { is_expected.to include('with-top-bar') }
+ end
+
+ context 'when @hide_top_bar_padding is true' do
+ before do
+ allow(helper).to receive(:show_super_sidebar?).and_return(true)
+ helper.instance_variable_set(:@hide_top_bar_padding, true)
+ end
+
+ it { is_expected.not_to include('with-top-bar') }
+ end
+ end
+
+ context 'when show_super_sidebar? is false' do
+ before do
+ allow(helper).to receive(:show_super_sidebar?).and_return(false)
+ end
+
+ it { is_expected.not_to include('with-top-bar') }
+ end
end
describe '#dispensable_render' do
diff --git a/spec/lib/api/entities/project_spec.rb b/spec/lib/api/entities/project_spec.rb
index 5d18b93228f..2c2cabba5e9 100644
--- a/spec/lib/api/entities/project_spec.rb
+++ b/spec/lib/api/entities/project_spec.rb
@@ -26,7 +26,7 @@ RSpec.describe ::API::Entities::Project do
end
end
- describe '.service_desk_address' do
+ describe '.service_desk_address', feature_category: :service_desk do
before do
allow(project).to receive(:service_desk_enabled?).and_return(true)
end
diff --git a/spec/lib/gitlab/database/no_cross_db_foreign_keys_spec.rb b/spec/lib/gitlab/database/no_cross_db_foreign_keys_spec.rb
index 7f0b2845f55..4c00d1a073d 100644
--- a/spec/lib/gitlab/database/no_cross_db_foreign_keys_spec.rb
+++ b/spec/lib/gitlab/database/no_cross_db_foreign_keys_spec.rb
@@ -23,6 +23,7 @@ RSpec.describe 'cross-database foreign keys' do
'merge_requests.updated_by_id', # https://gitlab.com/gitlab-org/gitlab/-/issues/422080
'merge_requests.merge_user_id', # https://gitlab.com/gitlab-org/gitlab/-/issues/422080
'merge_requests.author_id', # https://gitlab.com/gitlab-org/gitlab/-/issues/422080
+ 'notification_settings.user_id', # https://gitlab.com/gitlab-org/gitlab/-/issues/421414
'projects.creator_id', # https://gitlab.com/gitlab-org/gitlab/-/issues/421844
'projects.marked_for_deletion_by_user_id', # https://gitlab.com/gitlab-org/gitlab/-/issues/421844
'routes.namespace_id', # https://gitlab.com/gitlab-org/gitlab/-/issues/420869
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 6d3d5f5f7ae..aee491fd68d 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -2380,7 +2380,7 @@ RSpec.describe Project, factory_default: :keep, feature_category: :groups_and_pr
end
end
- describe '#service_desk_address' do
+ describe '#service_desk_address', feature_category: :service_desk do
let_it_be(:project, reload: true) { create(:project, service_desk_enabled: true) }
subject { project.service_desk_address }
@@ -2424,7 +2424,7 @@ RSpec.describe Project, factory_default: :keep, feature_category: :groups_and_pr
end
context 'when project_key is set' do
- it 'returns custom address including the project_key' do
+ it 'returns Service Desk alias address including the project_key' do
create(:service_desk_setting, project: project, project_key: 'key1')
expect(subject).to eq("foo+#{project.full_path_slug}-key1@bar.com")
@@ -2432,11 +2432,35 @@ RSpec.describe Project, factory_default: :keep, feature_category: :groups_and_pr
end
context 'when project_key is not set' do
- it 'returns custom address including the project full path' do
+ it 'returns Service Desk alias address including the project full path' do
expect(subject).to eq("foo+#{project.full_path_slug}-#{project.project_id}-issue-@bar.com")
end
end
end
+
+ context 'when custom email is enabled' do
+ let(:custom_email) { 'support@example.com' }
+
+ before do
+ setting = ServiceDeskSetting.new(project: project, custom_email: custom_email, custom_email_enabled: true)
+ allow(project).to receive(:service_desk_setting).and_return(setting)
+ end
+
+ it 'returns custom email address' do
+ expect(subject).to eq(custom_email)
+ end
+
+ context 'when feature flag service_desk_custom_email is disabled' do
+ before do
+ stub_feature_flags(service_desk_custom_email: false)
+ end
+
+ it 'returns custom email address' do
+ # Don't check for a specific value. Just make sure it's not the custom email
+ expect(subject).not_to eq(custom_email)
+ end
+ end
+ end
end
describe '.with_service_desk_key' do
diff --git a/spec/requests/admin/abuse_reports_controller_spec.rb b/spec/requests/admin/abuse_reports_controller_spec.rb
index c443a441af8..730306ce42c 100644
--- a/spec/requests/admin/abuse_reports_controller_spec.rb
+++ b/spec/requests/admin/abuse_reports_controller_spec.rb
@@ -53,13 +53,62 @@ RSpec.describe Admin::AbuseReportsController, type: :request, feature_category:
end
end
- shared_examples 'moderates user' do
+ describe 'PUT #update' do
+ let_it_be(:report) { create(:abuse_report) }
+ let_it_be(:label1) { create(:abuse_report_label, title: 'Uno') }
+
+ let(:params) { { label_ids: [Gitlab::GlobalId.build(label1, id: label1.id).to_s] } }
+ let(:expected_params) { ActionController::Parameters.new(params).permit! }
+
+ subject(:request) { put admin_abuse_report_path(report, params) }
+
+ it 'invokes the Admin::AbuseReports::UpdateService' do
+ expect_next_instance_of(Admin::AbuseReports::UpdateService, report, admin, expected_params) do |service|
+ expect(service).to receive(:execute).and_call_original
+ end
+
+ request
+ end
+
+ context 'when the service response is a success' do
+ before do
+ allow_next_instance_of(Admin::AbuseReports::UpdateService, report, admin, expected_params) do |service|
+ allow(service).to receive(:execute).and_return(ServiceResponse.success)
+ end
+
+ request
+ end
+
+ it 'returns with a success status' do
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ context 'when the service response is an error' do
+ let(:error_message) { 'Error updating abuse report' }
+
+ before do
+ allow_next_instance_of(Admin::AbuseReports::UpdateService, report, admin, expected_params) do |service|
+ allow(service).to receive(:execute).and_return(ServiceResponse.error(message: error_message))
+ end
+
+ request
+ end
+
+ it 'returns the service response message with a failed status' do
+ expect(response).to have_gitlab_http_status(:unprocessable_entity)
+ expect(json_response['message']).to eq(error_message)
+ end
+ end
+ end
+
+ describe 'PUT #moderate_user' do
let(:report) { create(:abuse_report) }
let(:params) { { user_action: 'block_user', close: 'true', reason: 'spam', comment: 'obvious spam' } }
let(:expected_params) { ActionController::Parameters.new(params).permit! }
let(:message) { 'Service response' }
- subject(:request) { put path }
+ subject(:request) { put moderate_user_admin_abuse_report_path(report, params) }
it 'invokes the Admin::AbuseReports::ModerateUserService' do
expect_next_instance_of(Admin::AbuseReports::ModerateUserService, report, admin, expected_params) do |service|
@@ -100,18 +149,6 @@ RSpec.describe Admin::AbuseReportsController, type: :request, feature_category:
end
end
- describe 'PUT #update' do
- let(:path) { admin_abuse_report_path(report, params) }
-
- it_behaves_like 'moderates user'
- end
-
- describe 'PUT #moderate_user' do
- let(:path) { moderate_user_admin_abuse_report_path(report, params) }
-
- it_behaves_like 'moderates user'
- end
-
describe 'DELETE #destroy' do
let!(:report) { create(:abuse_report) }
let(:params) { {} }
diff --git a/spec/requests/api/graphql/ci/runner_spec.rb b/spec/requests/api/graphql/ci/runner_spec.rb
index 3cfb98c57fd..3d7020b03b7 100644
--- a/spec/requests/api/graphql/ci/runner_spec.rb
+++ b/spec/requests/api/graphql/ci/runner_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
+RSpec.describe 'Query.runner(id)', :freeze_time, feature_category: :runner_fleet do
include GraphqlHelpers
let_it_be(:user) { create(:user, :admin) }
@@ -228,7 +228,7 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
end
end
- context 'with build running', :freeze_time do
+ context 'with build running' do
let!(:pipeline) { create(:ci_pipeline, project: project1) }
let!(:runner_manager) do
create(:ci_runner_machine,
@@ -357,6 +357,77 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
)
end
end
+
+ describe 'jobs' do
+ let(:query) do
+ %(
+ query {
+ runner(id: "#{project_runner.to_global_id}") { #{runner_query_fragment} }
+ }
+ )
+ end
+
+ context 'with a job from a non-owned project' do
+ let(:runner_query_fragment) do
+ %(
+ id
+ jobs {
+ nodes {
+ id status shortSha finishedAt duration queuedDuration tags webPath
+ project { id }
+ runner { id }
+ }
+ }
+ )
+ end
+
+ let_it_be(:owned_project_owner) { create(:user) }
+ let_it_be(:owned_project) { create(:project) }
+ let_it_be(:other_project) { create(:project) }
+ let_it_be(:project_runner) { create(:ci_runner, :project_type, projects: [other_project, owned_project]) }
+ let_it_be(:owned_project_pipeline) { create(:ci_pipeline, project: owned_project) }
+ let_it_be(:other_project_pipeline) { create(:ci_pipeline, project: other_project) }
+ let_it_be(:owned_build) do
+ create(:ci_build, :running, runner: project_runner, pipeline: owned_project_pipeline,
+ tag_list: %i[a b c], created_at: 1.hour.ago, started_at: 59.minutes.ago, finished_at: 30.minutes.ago)
+ end
+
+ let_it_be(:other_build) do
+ create(:ci_build, :success, runner: project_runner, pipeline: other_project_pipeline,
+ tag_list: %i[d e f], created_at: 30.minutes.ago, started_at: 19.minutes.ago, finished_at: 1.minute.ago)
+ end
+
+ before_all do
+ owned_project.add_owner(owned_project_owner)
+ end
+
+ it 'returns empty values for sensitive fields in non-owned jobs' do
+ post_graphql(query, current_user: owned_project_owner)
+
+ jobs_data = graphql_data_at(:runner, :jobs, :nodes)
+ expect(jobs_data).not_to be_nil
+ expect(jobs_data).to match([
+ a_graphql_entity_for(other_build,
+ status: other_build.status.upcase,
+ project: nil, tags: nil, web_path: nil,
+ runner: a_graphql_entity_for(project_runner),
+ short_sha: other_build.short_sha, finished_at: other_build.finished_at&.iso8601,
+ duration: a_value_within(0.001).of(other_build.duration),
+ queued_duration: a_value_within(0.001).of((other_build.started_at - other_build.queued_at).to_f)),
+ a_graphql_entity_for(owned_build,
+ status: owned_build.status.upcase,
+ project: a_graphql_entity_for(owned_project),
+ tags: owned_build.tag_list.map(&:to_s),
+ web_path: ::Gitlab::Routing.url_helpers.project_job_path(owned_project, owned_build),
+ runner: a_graphql_entity_for(project_runner),
+ short_sha: owned_build.short_sha,
+ finished_at: owned_build.finished_at&.iso8601,
+ duration: a_value_within(0.001).of(owned_build.duration),
+ queued_duration: a_value_within(0.001).of((owned_build.started_at - owned_build.queued_at).to_f))
+ ])
+ end
+ end
+ end
end
describe 'for inactive runner' do
@@ -501,8 +572,14 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
end
describe 'for runner with status' do
- let_it_be(:stale_runner) { create(:ci_runner, description: 'Stale runner 1', created_at: 3.months.ago) }
- let_it_be(:never_contacted_instance_runner) { create(:ci_runner, description: 'Missing runner 1', created_at: 1.month.ago, contacted_at: nil) }
+ let_it_be(:stale_runner) do
+ create(:ci_runner, description: 'Stale runner 1',
+ created_at: (3.months + 1.second).ago, contacted_at: (3.months + 1.second).ago)
+ end
+
+ let_it_be(:never_contacted_instance_runner) do
+ create(:ci_runner, description: 'Missing runner 1', created_at: 1.month.ago, contacted_at: nil)
+ end
let(:query) do
%(
diff --git a/spec/serializers/admin/abuse_report_details_entity_spec.rb b/spec/serializers/admin/abuse_report_details_entity_spec.rb
index 230cdc88fb9..bed9775ac8c 100644
--- a/spec/serializers/admin/abuse_report_details_entity_spec.rb
+++ b/spec/serializers/admin/abuse_report_details_entity_spec.rb
@@ -63,6 +63,7 @@ RSpec.describe Admin::AbuseReportDetailsEntity, feature_category: :insider_threa
similar_open_report_hash = user_hash[:similar_open_reports][0]
expect(similar_open_report_hash.keys).to match_array([
:id,
+ :global_id,
:status,
:message,
:reported_at,
@@ -100,6 +101,7 @@ RSpec.describe Admin::AbuseReportDetailsEntity, feature_category: :insider_threa
expect(report_hash.keys).to match_array([
:id,
+ :global_id,
:status,
:message,
:reported_at,
diff --git a/spec/serializers/admin/reported_content_entity_spec.rb b/spec/serializers/admin/reported_content_entity_spec.rb
index 21ce5cf64af..0af16561005 100644
--- a/spec/serializers/admin/reported_content_entity_spec.rb
+++ b/spec/serializers/admin/reported_content_entity_spec.rb
@@ -15,6 +15,7 @@ RSpec.describe Admin::ReportedContentEntity, feature_category: :insider_threat d
it 'exposes correct attributes' do
expect(entity_hash.keys).to match_array([
:id,
+ :global_id,
:status,
:message,
:reported_at,
@@ -29,6 +30,12 @@ RSpec.describe Admin::ReportedContentEntity, feature_category: :insider_threat d
])
end
+ it 'includes correct value for global_id' do
+ allow(Gitlab::GlobalId).to receive(:build).with(report, { id: report.id }).and_return(:mock_global_id)
+
+ expect(entity_hash[:global_id]).to eq 'mock_global_id'
+ end
+
it 'correctly exposes `reporter`' do
reporter_hash = entity_hash[:reporter]
diff --git a/spec/services/admin/abuse_reports/update_service_spec.rb b/spec/services/admin/abuse_reports/update_service_spec.rb
new file mode 100644
index 00000000000..e53b40979ec
--- /dev/null
+++ b/spec/services/admin/abuse_reports/update_service_spec.rb
@@ -0,0 +1,85 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Admin::AbuseReports::UpdateService, feature_category: :instance_resiliency do
+ let_it_be(:current_user) { create(:admin) }
+ let_it_be(:abuse_report) { create(:abuse_report) }
+ let_it_be(:label) { create(:abuse_report_label) }
+
+ let(:params) { {} }
+ let(:service) { described_class.new(abuse_report, current_user, params) }
+
+ describe '#execute', :enable_admin_mode do
+ subject { service.execute }
+
+ shared_examples 'returns an error response' do |error|
+ it 'returns an error response' do
+ expect(subject).to be_error
+ expect(subject.message).to eq error
+ end
+ end
+
+ context 'with invalid parameters' do
+ describe 'invalid user' do
+ describe 'when no user is given' do
+ let_it_be(:current_user) { nil }
+
+ it_behaves_like 'returns an error response', 'Admin is required'
+ end
+
+ describe 'when given user is not an admin' do
+ let_it_be(:current_user) { create(:user) }
+
+ it_behaves_like 'returns an error response', 'Admin is required'
+ end
+ end
+
+ describe 'invalid label_ids' do
+ let(:params) { { label_ids: ['invalid_global_id', non_existing_record_id] } }
+
+ it 'does not update the abuse report' do
+ expect { subject }.not_to change { abuse_report.labels }
+ end
+
+ it { is_expected.to be_success }
+ end
+ end
+
+ describe 'with valid parameters' do
+ context 'when label_ids is empty' do
+ let(:params) { { label_ids: [] } }
+
+ context 'when abuse report has existing labels' do
+ before do
+ abuse_report.labels = [label]
+ end
+
+ it 'clears the abuse report labels' do
+ expect { subject }.to change { abuse_report.labels.count }.from(1).to(0)
+ end
+
+ it { is_expected.to be_success }
+ end
+
+ context 'when abuse report has no existing labels' do
+ it 'does not update the abuse report' do
+ expect { subject }.not_to change { abuse_report.labels }
+ end
+
+ it { is_expected.to be_success }
+ end
+ end
+
+ context 'when label_ids is not empty' do
+ let(:params) { { label_ids: [Gitlab::GlobalId.build(label, id: label.id).to_s] } }
+
+ it 'updates the abuse report' do
+ expect { subject }.to change { abuse_report.label_ids }.from([]).to([label.id])
+ end
+
+ it { is_expected.to be_success }
+ end
+ end
+ end
+end
diff --git a/spec/support/helpers/filtered_search_helpers.rb b/spec/support/helpers/filtered_search_helpers.rb
index 60638eb06cd..abd5d78e836 100644
--- a/spec/support/helpers/filtered_search_helpers.rb
+++ b/spec/support/helpers/filtered_search_helpers.rb
@@ -155,7 +155,7 @@ module FilteredSearchHelpers
end
def default_placeholder
- 'Search or filter results...'
+ 'Search or filter results…'
end
def get_filtered_search_placeholder