Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-08-23 18:10:41 +00:00
parent 9919ffe0c8
commit 6a6e9bec88
77 changed files with 1370 additions and 136 deletions

View File

@ -1,7 +1,9 @@
<script>
import { GlAlert } from '@gitlab/ui';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import ReportHeader from './report_header.vue';
import UserDetails from './user_details.vue';
import ReportDetails from './report_details.vue';
import ReportedContent from './reported_content.vue';
import HistoryItems from './history_items.vue';
@ -17,9 +19,11 @@ export default {
GlAlert,
ReportHeader,
UserDetails,
ReportDetails,
ReportedContent,
HistoryItems,
},
mixins: [glFeatureFlagsMixin()],
props: {
abuseReport: {
type: Object,
@ -63,6 +67,12 @@ export default {
/>
<user-details v-if="abuseReport.user" :user="abuseReport.user" />
<report-details
v-if="glFeatures.abuseReportLabels"
:report-id="abuseReport.report.globalId"
class="gl-mt-6"
/>
<reported-content :report="abuseReport.report" data-testid="reported-content" />
<div v-for="report in similarOpenReports" :key="report.id" data-testid="similar-open-reports">

View File

@ -0,0 +1,13 @@
query abuseReportQuery($id: AbuseReportID!) {
abuseReport(id: $id) {
labels {
nodes {
id
title
description
color
textColor
}
}
}
}

View File

@ -0,0 +1,11 @@
query abuseReportLabelsQuery($searchTerm: String) {
abuseReportLabels(searchTerm: $searchTerm) {
nodes {
id
title
description
color
textColor
}
}
}

View File

@ -0,0 +1,203 @@
<script>
import { GlButton, GlLoadingIcon } from '@gitlab/ui';
import { debounce } from 'lodash';
import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { __, s__, sprintf } from '~/locale';
import LabelItem from '~/sidebar/components/labels/labels_select_widget/label_item.vue';
import DropdownValue from '~/sidebar/components/labels/labels_select_widget/dropdown_value.vue';
import DropdownHeader from '~/sidebar/components/labels/labels_select_widget/dropdown_header.vue';
import DropdownWidget from '~/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue';
import abuseReportLabelsQuery from './graphql/abuse_report_labels.query.graphql';
export default {
components: {
DropdownWidget,
GlButton,
GlLoadingIcon,
LabelItem,
DropdownValue,
DropdownHeader,
},
inject: ['updatePath'],
props: {
report: {
type: Object,
required: true,
},
},
data() {
return {
search: '',
labels: [],
selected: this.report.labels,
initialLoading: true,
isEditing: false,
isUpdating: false,
};
},
apollo: {
labels: {
query() {
return abuseReportLabelsQuery;
},
variables() {
return { searchTerm: this.search };
},
skip() {
return !this.isEditing;
},
update(data) {
return data.abuseReportLabels?.nodes;
},
error() {
createAlert({ message: this.$options.i18n.searchError });
},
},
},
computed: {
isLabelsEmpty() {
return this.selected.length === 0;
},
selectedLabelIds() {
return this.selected.map((label) => label.id);
},
isLoading() {
return this.$apollo.queries.labels.loading;
},
selectText() {
if (!this.selected.length) {
return this.$options.i18n.labelsListTitle;
} else if (this.selected.length > 1) {
return sprintf(s__('LabelSelect|%{firstLabelName} +%{remainingLabelCount} more'), {
firstLabelName: this.selected[0].title,
remainingLabelCount: this.selected.length - 1,
});
}
return this.selected[0].title;
},
},
watch: {
report({ labels }) {
this.selected = labels;
this.initialLoading = false;
},
},
created() {
const setSearch = (search) => {
this.search = search;
};
this.debouncedSetSearch = debounce(setSearch, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
},
methods: {
toggleEdit() {
return this.isEditing ? this.hideDropdown() : this.showDropdown();
},
showDropdown() {
this.isEditing = true;
this.$refs.editDropdown.showDropdown();
},
hideDropdown() {
this.saveSelectedLabels();
this.isEditing = false;
},
saveSelectedLabels() {
this.isUpdating = true;
axios
.put(this.updatePath, { label_ids: this.selectedLabelIds })
.catch((error) => {
createAlert({
message: __('An error occurred while updating labels.'),
captureError: true,
error,
});
})
.finally(() => {
this.isUpdating = false;
});
},
isLabelSelected(label) {
return this.selectedLabelIds.includes(label.id);
},
filterSelected(id) {
return this.selected.filter(({ id: labelId }) => labelId !== id);
},
toggleLabelSelection(label) {
this.selected = this.isLabelSelected(label)
? this.filterSelected(label.id)
: [...this.selected, label];
},
removeLabel(labelId) {
this.selected = this.filterSelected(labelId);
this.saveSelectedLabels();
},
},
i18n: {
label: __('Labels'),
noLabels: __('None'),
labelsListTitle: __('Assign labels'),
searchError: __('An error occurred while searching for labels, please try again.'),
edit: __('Edit'),
},
};
</script>
<template>
<div class="labels-select-wrapper">
<div class="gl-display-flex gl-align-items-center gl-gap-3 gl-mb-2">
<span>{{ $options.i18n.label }}</span>
<gl-loading-icon v-if="initialLoading" size="sm" inline class="gl-ml-2" />
<gl-button
category="tertiary"
size="small"
:disabled="isUpdating || initialLoading"
class="edit-link gl-ml-auto"
@click="toggleEdit"
>
{{ $options.i18n.edit }}
</gl-button>
</div>
<div class="gl-text-gray-500 gl-mb-2" data-testid="selected-labels">
<template v-if="isLabelsEmpty">{{ $options.i18n.noLabels }}</template>
<dropdown-value
v-else
:disable-labels="isLoading"
:selected-labels="selected"
:allow-label-remove="!isUpdating"
:labels-filter-base-path="''"
:labels-filter-param="'label_name'"
@onLabelRemove="removeLabel"
/>
</div>
<dropdown-widget
v-show="isEditing"
ref="editDropdown"
:select-text="selectText"
:options="labels"
:is-loading="isLoading"
:selected="selected"
:search-term="search"
:allow-multiselect="true"
@hide="hideDropdown"
@set-option="toggleLabelSelection"
@set-search="debouncedSetSearch"
>
<template #header>
<dropdown-header
ref="header"
:search-key="search"
labels-create-title=""
:labels-list-title="$options.i18n.labelsListTitle"
:show-dropdown-contents-create-view="false"
@closeDropdown="hideDropdown"
@input="debouncedSetSearch"
/>
</template>
<template #item="{ item }">
<label-item :label="item" />
</template>
</dropdown-widget>
</div>
</template>

View File

@ -0,0 +1,49 @@
<script>
import { __ } from '~/locale';
import { createAlert } from '~/alert';
import LabelsSelect from './labels_select.vue';
import abuseReportQuery from './graphql/abuse_report.query.graphql';
export default {
name: 'ReportDetails',
components: {
LabelsSelect,
},
props: {
reportId: {
type: String,
required: true,
},
},
data() {
return {
report: { labels: [] },
};
},
apollo: {
report: {
query() {
return abuseReportQuery;
},
variables() {
return { id: this.reportId };
},
update({ abuseReport }) {
return {
labels: abuseReport.labels?.nodes,
};
},
error() {
createAlert({ message: this.$options.i18n.fetchError });
},
},
},
i18n: {
fetchError: __('An error occurred while fetching labels, please try again.'),
},
};
</script>
<template>
<labels-select :report="report" />
</template>

View File

@ -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: {

View File

@ -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"

View File

@ -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;

View File

@ -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

View File

@ -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"

View File

@ -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'),

View File

@ -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"

View File

@ -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.',
);

View File

@ -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();
}

View File

@ -73,7 +73,8 @@ export default {
},
searchInputPlaceholder: {
type: String,
required: true,
required: false,
default: __('Search or filter results…'),
},
suggestionsListClass: {
type: String,

View File

@ -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…'),
};

View File

@ -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 {
<div class="filtered-search-wrapper">
<filtered-search-bar
:namespace="projectPath"
:search-input-placeholder="$options.defaultI18n.searchPlaceholder"
:tokens="filteredSearchTokens"
:initial-filter-value="filteredSearchValue"
initial-sortby="created_desc"

View File

@ -5,6 +5,7 @@ import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import PageSizeSelector from '~/vue_shared/components/page_size_selector.vue';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { updateHistory, setUrlParams } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
@ -51,7 +52,8 @@ export default {
},
searchInputPlaceholder: {
type: String,
required: true,
required: false,
default: __('Search or filter results…'),
},
searchTokens: {
type: Array,

View File

@ -88,6 +88,10 @@ export default {
showSuperSidebarToggle() {
return gon.use_new_navigation && sidebarState.isCollapsed;
},
topBarClasses() {
return gon.use_new_navigation ? 'top-bar-fixed container-fluid' : '';
},
},
created() {
@ -120,15 +124,17 @@ export default {
<template>
<div>
<div
class="top-bar-container gl-display-flex gl-align-items-center gl-border-b-1 gl-border-b-gray-100 gl-border-b-solid"
>
<super-sidebar-toggle
v-if="showSuperSidebarToggle"
class="gl-mr-2"
:class="$options.JS_TOGGLE_EXPAND_CLASS"
/>
<gl-breadcrumb :items="breadcrumbs" data-testid="breadcrumb-links" />
<div :class="topBarClasses" data-testid="top-bar">
<div
class="top-bar-container gl-display-flex gl-align-items-center gl-border-b-1 gl-border-b-gray-100 gl-border-b-solid"
>
<super-sidebar-toggle
v-if="showSuperSidebarToggle"
class="gl-mr-2"
:class="$options.JS_TOGGLE_EXPAND_CLASS"
/>
<gl-breadcrumb :items="breadcrumbs" data-testid="breadcrumb-links" />
</div>
</div>
<template v-if="activePanel">

View File

@ -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"

View File

@ -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

View File

@ -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?,

View File

@ -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] },

View File

@ -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

View File

@ -8,6 +8,7 @@ module Types
graphql_name 'CiJob'
present_using ::Ci::BuildPresenter
field_class Types::Ci::JobBaseField
connection_type_class Types::LimitedCountableConnectionType

View File

@ -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?

View File

@ -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}",

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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}",

View File

@ -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

View File

@ -29,3 +29,5 @@ class MergeWorker # rubocop:disable Scalability/IdempotentWorker
.execute(merge_request)
end
end
MergeWorker.prepend_mod

View File

@ -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

View File

@ -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

View File

@ -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'

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -0,0 +1 @@
0dd37cf1da3ff0f56f24a41dd76ef7cb789e0833a6ea73b773f56a8a3793c465

View File

@ -24835,6 +24835,7 @@ Represents a vulnerability.
| <a id="vulnerabilityupdatedat"></a>`updatedAt` | [`Time`](#time) | Timestamp of when the vulnerability was last updated. |
| <a id="vulnerabilityusernotescount"></a>`userNotesCount` | [`Int!`](#int) | Number of user notes attached to the vulnerability. |
| <a id="vulnerabilityuserpermissions"></a>`userPermissions` | [`VulnerabilityPermissions!`](#vulnerabilitypermissions) | Permissions for the current user on the resource. |
| <a id="vulnerabilityuuid"></a>`uuid` | [`String!`](#string) | UUID of the vulnerability finding. Can be used to look up the associated security report finding. |
| <a id="vulnerabilityvulnerabilitypath"></a>`vulnerabilityPath` | [`String`](#string) | Path to the vulnerability's details page. |
| <a id="vulnerabilityweburl"></a>`webUrl` | [`String`](#string) | URL to the vulnerability's details page. |

View File

@ -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: <your_access_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: <your_access_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: <your_access_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: <your_access_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: <your_access_token>" "https://gitlab.example.com/api/v4/groups/5/-/debian_distributions/unstable"

View File

@ -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: <your_access_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: <your_access_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: <your_access_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: <your_access_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: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/debian_distributions/unstable"

View File

@ -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).

View File

@ -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 `<PackageName>_<VersionNumber>-<DebianRevisionNumber>_<DebianArchitecture>.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 <deb_file>`
The `.deb` file follows the naming convention `<PackageName>_<VersionNumber>-<DebianRevisionNumber>_<DebianArchitecture>.deb`.
It includes a `control file` that contains metadata about the package. You can view the control file by using `dpkg --info <deb_file>`.
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))

View File

@ -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)**

View File

@ -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.

View File

@ -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 <your role name>` 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=<your secret name>`.
- Creates a `Deployment` resource for the `agentk` pod.

View File

@ -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

View File

@ -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:

View File

@ -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)**

View File

@ -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 ""

View File

@ -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

View File

@ -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')

View File

@ -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);
});

View File

@ -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.',
});
});
});
});

View File

@ -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.',
});
});
});
});

View File

@ -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',
},
},
};

View File

@ -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);

View File

@ -137,7 +137,6 @@ describe('IssuesDashboardApp component', () => {
issuablesLoading: false,
namespace: 'dashboard',
recentSearchesStorageKey: 'issues',
searchInputPlaceholder: i18n.searchPlaceholder,
showPaginationControls: true,
sortOptions: getSortOptions({
hasBlockedIssuesFeature: defaultProvide.hasBlockedIssuesFeature,

View File

@ -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,

View File

@ -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,

View File

@ -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([]);
});
});
});

View File

@ -44,7 +44,6 @@ describe('WorkItemsListApp component', () => {
issuablesLoading: true,
namespace: 'work-items',
recentSearchesStorageKey: 'issues',
searchInputPlaceholder: 'Search or filter results...',
searchTokens: [],
showWorkItemTypeIcon: true,
sortOptions: [],

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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) { {} }

View File

@ -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
%(

View File

@ -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,

View File

@ -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]

View File

@ -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

View File

@ -155,7 +155,7 @@ module FilteredSearchHelpers
end
def default_placeholder
'Search or filter results...'
'Search or filter results'
end
def get_filtered_search_placeholder