Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
e415571a6e
commit
d1bc31b8d5
|
|
@ -27,5 +27,6 @@ export const TYPENAME_USER = 'User';
|
|||
export const TYPENAME_VULNERABILITIES_SCANNER = 'Vulnerabilities::Scanner';
|
||||
export const TYPENAME_VULNERABILITY = 'Vulnerability';
|
||||
export const TYPENAME_WORK_ITEM = 'WorkItem';
|
||||
export const TYPENAME_ORGANIZATION = 'Organization';
|
||||
export const TYPE_USERS_SAVED_REPLY = 'Users::SavedReply';
|
||||
export const TYPE_WORKSPACE = 'RemoteDevelopment::Workspace';
|
||||
|
|
|
|||
|
|
@ -7,7 +7,11 @@ import Api from '~/api';
|
|||
import Tracking from '~/tracking';
|
||||
import { BV_SHOW_MODAL, BV_HIDE_MODAL } from '~/lib/utils/constants';
|
||||
import { n__, s__, sprintf } from '~/locale';
|
||||
import { memberName, triggerExternalAlert } from 'ee_else_ce/invite_members/utils/member_utils';
|
||||
import {
|
||||
memberName,
|
||||
triggerExternalAlert,
|
||||
inviteMembersTrackingOptions,
|
||||
} from 'ee_else_ce/invite_members/utils/member_utils';
|
||||
import { captureException } from '~/ci/runner/sentry_utils';
|
||||
import {
|
||||
USERS_FILTER_ALL,
|
||||
|
|
@ -135,6 +139,9 @@ export default {
|
|||
isCelebration() {
|
||||
return this.mode === 'celebrate';
|
||||
},
|
||||
baseTrackingDetails() {
|
||||
return { label: this.source, celebrate: this.isCelebration };
|
||||
},
|
||||
isTextForAdmin() {
|
||||
return this.isCurrentUserAdmin && Boolean(this.newUsersUrl);
|
||||
},
|
||||
|
|
@ -252,7 +259,7 @@ export default {
|
|||
this.source = source;
|
||||
|
||||
this.$root.$emit(BV_SHOW_MODAL, this.modalId);
|
||||
this.track('render', { label: this.source });
|
||||
this.track('render', inviteMembersTrackingOptions(this.baseTrackingDetails));
|
||||
},
|
||||
closeModal() {
|
||||
this.$root.$emit(BV_HIDE_MODAL, this.modalId);
|
||||
|
|
@ -321,10 +328,10 @@ export default {
|
|||
return this.newUsersToInvite.find((member) => memberName(member) === username)?.name;
|
||||
},
|
||||
onCancel() {
|
||||
this.track('click_cancel', { label: this.source });
|
||||
this.track('click_cancel', inviteMembersTrackingOptions(this.baseTrackingDetails));
|
||||
},
|
||||
onClose() {
|
||||
this.track('click_x', { label: this.source });
|
||||
this.track('click_x', inviteMembersTrackingOptions(this.baseTrackingDetails));
|
||||
},
|
||||
resetFields() {
|
||||
this.clearValidation();
|
||||
|
|
@ -333,7 +340,7 @@ export default {
|
|||
this.newUsersToInvite = [];
|
||||
},
|
||||
onInviteSuccess() {
|
||||
this.track('invite_successful', { label: this.source });
|
||||
this.track('invite_successful', inviteMembersTrackingOptions(this.baseTrackingDetails));
|
||||
|
||||
if (this.reloadPageOnSubmit) {
|
||||
reloadOnInvitationSuccess();
|
||||
|
|
|
|||
|
|
@ -6,3 +6,7 @@ export function memberName(member) {
|
|||
export function triggerExternalAlert() {
|
||||
return false;
|
||||
}
|
||||
|
||||
export function inviteMembersTrackingOptions(options) {
|
||||
return { label: options.label };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
import Vue from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import createDefaultClient from '~/lib/graphql';
|
||||
import { s__ } from '~/locale';
|
||||
import OrganizationSelect from '~/vue_shared/components/entity_select/organization_select.vue';
|
||||
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
|
||||
import resolvers from '../../shared/graphql/resolvers';
|
||||
|
||||
export const initHomeOrganizationSetting = () => {
|
||||
const el = document.getElementById('js-home-organization-setting');
|
||||
|
||||
if (!el) return false;
|
||||
|
||||
const {
|
||||
dataset: { appData },
|
||||
} = el;
|
||||
const { initialSelection } = convertObjectPropsToCamelCase(JSON.parse(appData));
|
||||
|
||||
const apolloProvider = new VueApollo({
|
||||
defaultClient: createDefaultClient(resolvers),
|
||||
});
|
||||
|
||||
return new Vue({
|
||||
el,
|
||||
name: 'HomeOrganizationSetting',
|
||||
apolloProvider,
|
||||
render(createElement) {
|
||||
return createElement(OrganizationSelect, {
|
||||
props: {
|
||||
block: true,
|
||||
label: s__('Organization|Home organization'),
|
||||
description: s__('Organization|Choose what organization you want to see by default.'),
|
||||
inputName: 'home_organization',
|
||||
inputId: 'home_organization',
|
||||
initialSelection,
|
||||
toggleClass: 'gl-form-input-xl',
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
query getOrganization($id: ID!) {
|
||||
organization(id: $id) @client {
|
||||
id
|
||||
name
|
||||
descriptionHtml
|
||||
avatarUrl
|
||||
webUrl
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
import initProfilePreferences from '~/profile/preferences/profile_preferences_bundle';
|
||||
import initProfilePreferencesDiffsColors from '~/profile/preferences/profile_preferences_diffs_colors';
|
||||
import { initHomeOrganizationSetting } from '~/organizations/profile/preferences';
|
||||
|
||||
initProfilePreferences();
|
||||
initProfilePreferencesDiffsColors();
|
||||
initHomeOrganizationSetting();
|
||||
|
|
|
|||
|
|
@ -14,3 +14,13 @@ export const PROJECT_TOGGLE_TEXT = s__('ProjectSelect|Search for project');
|
|||
export const PROJECT_HEADER_TEXT = s__('ProjectSelect|Select a project');
|
||||
export const FETCH_PROJECTS_ERROR = __('Unable to fetch projects. Reload the page to try again.');
|
||||
export const FETCH_PROJECT_ERROR = __('Unable to fetch project. Reload the page to try again.');
|
||||
|
||||
// Organizations
|
||||
export const ORGANIZATION_TOGGLE_TEXT = s__('Organization|Search for an organization');
|
||||
export const ORGANIZATION_HEADER_TEXT = s__('Organization|Select an organization');
|
||||
export const FETCH_ORGANIZATIONS_ERROR = s__(
|
||||
'Organization|Unable to fetch organizations. Reload the page to try again.',
|
||||
);
|
||||
export const FETCH_ORGANIZATION_ERROR = s__(
|
||||
'Organization|Unable to fetch organizations. Reload the page to try again.',
|
||||
);
|
||||
|
|
|
|||
|
|
@ -22,6 +22,11 @@ export default {
|
|||
type: String,
|
||||
required: true,
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
inputName: {
|
||||
type: String,
|
||||
required: true,
|
||||
|
|
@ -31,7 +36,7 @@ export default {
|
|||
required: true,
|
||||
},
|
||||
initialSelection: {
|
||||
type: String,
|
||||
type: [String, Number],
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
|
|
@ -57,6 +62,11 @@ export default {
|
|||
required: false,
|
||||
default: null,
|
||||
},
|
||||
toggleClass: {
|
||||
type: [String, Array, Object],
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
@ -152,6 +162,7 @@ export default {
|
|||
|
||||
this.searching = true;
|
||||
const name = await this.fetchInitialSelectionText(this.initialSelection);
|
||||
|
||||
this.selectedValue = this.initialSelection;
|
||||
this.selectedText = name;
|
||||
this.pristine = false;
|
||||
|
|
@ -178,7 +189,7 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<gl-form-group :label="label">
|
||||
<gl-form-group :label="label" :description="description">
|
||||
<slot name="error"></slot>
|
||||
<template v-if="Boolean($scopedSlots.label)" #label>
|
||||
<slot name="label"></slot>
|
||||
|
|
@ -196,6 +207,7 @@ export default {
|
|||
:no-results-text="noResultsText"
|
||||
:infinite-scroll="hasMoreItems"
|
||||
:infinite-scroll-loading="infiniteScrollLoading"
|
||||
:toggle-class="toggleClass"
|
||||
searchable
|
||||
@shown="onShown"
|
||||
@search="search"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,150 @@
|
|||
<script>
|
||||
import { GlAlert } from '@gitlab/ui';
|
||||
import * as Sentry from '~/sentry/sentry_browser_wrapper';
|
||||
import getCurrentUserOrganizationsQuery from '~/organizations/index/graphql/organizations.query.graphql';
|
||||
import getOrganizationQuery from '~/organizations/shared/graphql/queries/organization.query.graphql';
|
||||
import { getIdFromGraphQLId, convertToGraphQLId } from '~/graphql_shared/utils';
|
||||
import { TYPENAME_ORGANIZATION } from '~/graphql_shared/constants';
|
||||
import {
|
||||
ORGANIZATION_TOGGLE_TEXT,
|
||||
ORGANIZATION_HEADER_TEXT,
|
||||
FETCH_ORGANIZATIONS_ERROR,
|
||||
FETCH_ORGANIZATION_ERROR,
|
||||
} from './constants';
|
||||
import EntitySelect from './entity_select.vue';
|
||||
|
||||
export default {
|
||||
name: 'OrganizationSelect',
|
||||
components: {
|
||||
GlAlert,
|
||||
EntitySelect,
|
||||
},
|
||||
props: {
|
||||
block: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
inputName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
inputId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
initialSelection: {
|
||||
type: [String, Number],
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
clearable: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
toggleClass: {
|
||||
type: [String, Array, Object],
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
errorMessage: '',
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
async fetchOrganizations() {
|
||||
try {
|
||||
const {
|
||||
data: {
|
||||
currentUser: {
|
||||
organizations: { nodes },
|
||||
},
|
||||
},
|
||||
} = await this.$apollo.query({
|
||||
query: getCurrentUserOrganizationsQuery,
|
||||
// TODO: implement search support - https://gitlab.com/gitlab-org/gitlab/-/issues/429999.
|
||||
});
|
||||
|
||||
return {
|
||||
items: nodes.map((organization) => ({
|
||||
text: organization.name,
|
||||
value: getIdFromGraphQLId(organization.id),
|
||||
})),
|
||||
// TODO: implement pagination - https://gitlab.com/gitlab-org/gitlab/-/issues/429999.
|
||||
totalPages: 1,
|
||||
};
|
||||
} catch (error) {
|
||||
this.handleError({ message: FETCH_ORGANIZATIONS_ERROR, error });
|
||||
|
||||
return { items: [], totalPages: 0 };
|
||||
}
|
||||
},
|
||||
async fetchOrganizationName(id) {
|
||||
try {
|
||||
const {
|
||||
data: {
|
||||
organization: { name },
|
||||
},
|
||||
} = await this.$apollo.query({
|
||||
query: getOrganizationQuery,
|
||||
variables: { id: convertToGraphQLId(TYPENAME_ORGANIZATION, id) },
|
||||
});
|
||||
|
||||
return name;
|
||||
} catch (error) {
|
||||
this.handleError({ message: FETCH_ORGANIZATION_ERROR, error });
|
||||
|
||||
return '';
|
||||
}
|
||||
},
|
||||
handleError({ message, error }) {
|
||||
Sentry.captureException(error);
|
||||
this.errorMessage = message;
|
||||
},
|
||||
dismissError() {
|
||||
this.errorMessage = '';
|
||||
},
|
||||
},
|
||||
i18n: {
|
||||
toggleText: ORGANIZATION_TOGGLE_TEXT,
|
||||
selectGroup: ORGANIZATION_HEADER_TEXT,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<entity-select
|
||||
:block="block"
|
||||
:label="label"
|
||||
:description="description"
|
||||
:input-name="inputName"
|
||||
:input-id="inputId"
|
||||
:initial-selection="initialSelection"
|
||||
:clearable="clearable"
|
||||
:header-text="$options.i18n.selectGroup"
|
||||
:default-toggle-text="$options.i18n.toggleText"
|
||||
:fetch-items="fetchOrganizations"
|
||||
:fetch-initial-selection-text="fetchOrganizationName"
|
||||
:toggle-class="toggleClass"
|
||||
v-on="$listeners"
|
||||
>
|
||||
<template #error>
|
||||
<gl-alert v-if="errorMessage" class="gl-mb-3" variant="danger" @dismiss="dismissError">{{
|
||||
errorMessage
|
||||
}}</gl-alert>
|
||||
</template>
|
||||
</entity-select>
|
||||
</template>
|
||||
|
|
@ -48,6 +48,13 @@ module Organizations
|
|||
}
|
||||
end
|
||||
|
||||
def home_organization_setting_app_data
|
||||
{
|
||||
# TODO: use real setting - https://gitlab.com/gitlab-org/gitlab/-/issues/428668
|
||||
initial_selection: 1
|
||||
}.to_json
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def shared_groups_and_projects_app_data
|
||||
|
|
|
|||
|
|
@ -78,14 +78,17 @@ module Spam
|
|||
when BLOCK_USER
|
||||
target.spam!
|
||||
create_spam_log
|
||||
create_spam_abuse_event(result)
|
||||
ban_user!
|
||||
when DISALLOW
|
||||
target.spam!
|
||||
create_spam_log
|
||||
create_spam_abuse_event(result)
|
||||
when CONDITIONAL_ALLOW
|
||||
# This means "require a CAPTCHA to be solved"
|
||||
target.needs_recaptcha!
|
||||
create_spam_log
|
||||
create_spam_abuse_event(result)
|
||||
when OVERRIDE_VIA_ALLOW_POSSIBLE_SPAM
|
||||
create_spam_log
|
||||
when ALLOW
|
||||
|
|
@ -118,6 +121,22 @@ module Spam
|
|||
target.spam_log = spam_log
|
||||
end
|
||||
|
||||
def create_spam_abuse_event(result)
|
||||
params = {
|
||||
user_id: user.id,
|
||||
title: target.spam_title,
|
||||
description: target.spam_description,
|
||||
source_ip: spam_params&.ip_address,
|
||||
user_agent: spam_params&.user_agent,
|
||||
noteable_type: noteable_type,
|
||||
verdict: result
|
||||
}
|
||||
|
||||
target.run_after_commit_or_now do
|
||||
Abuse::SpamAbuseEventsWorker.perform_async(params)
|
||||
end
|
||||
end
|
||||
|
||||
def ban_user!
|
||||
UserCustomAttribute.set_banned_by_spam_log(target.spam_log)
|
||||
|
||||
|
|
|
|||
|
|
@ -78,6 +78,9 @@
|
|||
= f.gitlab_ui_radio_component :layout, layout_choices[0][1], layout_choices[0][0], help_text: fixed_help_text
|
||||
= f.gitlab_ui_radio_component :layout, layout_choices[1][1], layout_choices[1][0], help_text: fluid_help_text
|
||||
|
||||
- if Feature.enabled?(:ui_for_organizations, current_user)
|
||||
#js-home-organization-setting{ data: { app_data: home_organization_setting_app_data } }
|
||||
|
||||
.js-listbox-input{ data: { label: s_('Preferences|Homepage'), description: s_('Preferences|Choose what content you want to see by default on your homepage.'), name: 'user[dashboard]', items: dashboard_choices.to_json, value: current_user.dashboard, block: true.to_s, toggle_class: 'gl-form-input-xl' } }
|
||||
|
||||
= render_if_exists 'profiles/preferences/group_overview_selector', f: f # EE-specific
|
||||
|
|
|
|||
|
|
@ -10,10 +10,10 @@
|
|||
%div
|
||||
%h1.home-panel-title.gl-font-size-h1.gl-mt-3.gl-mb-2.gl-display-flex.gl-word-break-word{ data: { testid: 'project-name-content' }, itemprop: 'name' }
|
||||
= @project.name
|
||||
= visibility_level_content(@project, css_class: 'visibility-icon gl-text-secondary gl-ml-2', icon_css_class: 'icon')
|
||||
= render_if_exists 'compliance_management/compliance_framework/compliance_framework_badge', project: @project, additional_classes: 'gl-align-self-center gl-ml-2'
|
||||
= visibility_level_content(@project, css_class: 'visibility-icon gl-text-secondary gl-mx-2', icon_css_class: 'icon')
|
||||
= render_if_exists 'compliance_management/compliance_framework/compliance_framework_badge', project: @project, additional_classes: 'gl-align-self-center gl-mx-2'
|
||||
- if @project.catalog_resource
|
||||
= render partial: 'shared/ci_catalog_badge', locals: { href: project_ci_catalog_resource_path(@project, @project.catalog_resource) }
|
||||
= render partial: 'shared/ci_catalog_badge', locals: { href: project_ci_catalog_resource_path(@project, @project.catalog_resource), css_class: 'gl-mx-2' }
|
||||
- if @project.group
|
||||
= render_if_exists 'shared/tier_badge', source: @project, source_type: 'Project'
|
||||
.home-panel-metadata.gl-font-sm.gl-text-secondary.gl-font-base.gl-font-weight-normal.gl-line-height-normal{ data: { testid: 'project-id-content' }, itemprop: 'identifier' }
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
= render Pajamas::BadgeComponent.new(s_('CiCatalog|CI/CD catalog resource'), variant: 'info', icon: 'catalog-checkmark', class: 'gl-mx-2', href: href)
|
||||
= render Pajamas::BadgeComponent.new(s_('CiCatalog|CI/CD catalog resource'), variant: 'info', icon: 'catalog-checkmark', class: css_class, href: href)
|
||||
|
|
|
|||
|
|
@ -35,10 +35,10 @@
|
|||
%span.project-name<
|
||||
= project.name
|
||||
|
||||
= visibility_level_content(project, css_class: 'gl-mr-3')
|
||||
= visibility_level_content(project, css_class: 'gl-mr-2')
|
||||
|
||||
- if project.catalog_resource
|
||||
= render partial: 'shared/ci_catalog_badge', locals: { href: project_ci_catalog_resource_path(project, project.catalog_resource) }
|
||||
= render partial: 'shared/ci_catalog_badge', locals: { href: project_ci_catalog_resource_path(project, project.catalog_resource), css_class: 'gl-mr-2' }
|
||||
|
||||
- if explore_projects_tab? && project_license_name(project)
|
||||
%span.gl-display-inline-flex.gl-align-items-center.gl-mr-3
|
||||
|
|
|
|||
|
|
@ -0,0 +1,60 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Abuse
|
||||
class SpamAbuseEventsWorker
|
||||
include ApplicationWorker
|
||||
|
||||
data_consistency :delayed
|
||||
|
||||
idempotent!
|
||||
feature_category :instance_resiliency
|
||||
urgency :low
|
||||
|
||||
def perform(params)
|
||||
params = params.with_indifferent_access
|
||||
|
||||
@user = User.find_by_id(params[:user_id])
|
||||
unless @user
|
||||
logger.info(structured_payload(message: "User not found.", user_id: params[:user_id]))
|
||||
return
|
||||
end
|
||||
|
||||
report_user(params)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :user
|
||||
|
||||
def report_user(params)
|
||||
category = 'spam'
|
||||
reporter = Users::Internal.security_bot
|
||||
report_params = { user_id: params[:user_id],
|
||||
reporter: reporter,
|
||||
category: category,
|
||||
message: 'User reported for abuse based on spam verdict' }
|
||||
|
||||
abuse_report = AbuseReport.by_category(category).by_reporter_id(reporter.id).by_user_id(params[:user_id]).first
|
||||
|
||||
abuse_report = AbuseReport.create!(report_params) if abuse_report.nil?
|
||||
|
||||
create_abuse_event(abuse_report.id, params)
|
||||
end
|
||||
|
||||
# Associate the abuse report with an abuse event
|
||||
def create_abuse_event(abuse_report_id, params)
|
||||
Abuse::Event.create!(
|
||||
abuse_report_id: abuse_report_id,
|
||||
category: :spam,
|
||||
metadata: { noteable_type: params[:noteable_type],
|
||||
title: params[:title],
|
||||
description: params[:description],
|
||||
source_ip: params[:source_ip],
|
||||
user_agent: params[:user_agent],
|
||||
verdict: params[:verdict] },
|
||||
source: :spamcheck,
|
||||
user: user
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -2280,6 +2280,15 @@
|
|||
:weight: 1
|
||||
:idempotent: true
|
||||
:tags: []
|
||||
- :name: abuse_spam_abuse_events
|
||||
:worker_name: Abuse::SpamAbuseEventsWorker
|
||||
:feature_category: :instance_resiliency
|
||||
:has_external_dependencies: false
|
||||
:urgency: :low
|
||||
:resource_boundary: :unknown
|
||||
:weight: 1
|
||||
:idempotent: true
|
||||
:tags: []
|
||||
- :name: analytics_usage_trends_counter_job
|
||||
:worker_name: Analytics::UsageTrends::CounterJobWorker
|
||||
:feature_category: :devops_reports
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@
|
|||
:queues:
|
||||
- - abuse_new_abuse_report
|
||||
- 1
|
||||
- - abuse_spam_abuse_events
|
||||
- 1
|
||||
- - activity_pub
|
||||
- 1
|
||||
- - adjourned_project_deletion
|
||||
|
|
|
|||
|
|
@ -19,8 +19,6 @@ Fetching large repositories can take a long time for teams located far from a si
|
|||
Geo provides local, read-only sites of your GitLab instances. This can reduce the time it takes
|
||||
to clone and fetch large repositories, speeding up development.
|
||||
|
||||
For a video introduction to Geo, see [Introduction to GitLab Geo - GitLab Features](https://www.youtube.com/watch?v=-HDLxSjEh6w).
|
||||
|
||||
To make sure you're using the right version of the documentation, go to [the Geo page on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/administration/geo/index.md) and choose the appropriate release from the **Switch branch/tag** dropdown list. For example, [`v13.7.6-ee`](https://gitlab.com/gitlab-org/gitlab/-/blob/v13.7.6-ee/doc/administration/geo/index.md).
|
||||
|
||||
Geo uses a set of defined terms that are described in the [Geo Glossary](glossary.md).
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ you must verify your identity or reset your password to sign in to GitLab.
|
|||
<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
|
||||
For a demo, see [Require email verification - demo](https://www.youtube.com/watch?v=wU6BVEGB3Y0).
|
||||
|
||||
On GitLab.com, if you don't receive a verification email, select **Resend Code** before you contact the support team.
|
||||
|
||||
## Accounts without two-factor authentication (2FA)
|
||||
|
||||
An account is locked when either:
|
||||
|
|
|
|||
|
|
@ -597,7 +597,7 @@ rules:
|
|||
|
||||
The following example [enables SAST](index.md#configure-sast-in-your-cicd-yaml) and uses a shared ruleset customization file. The file is:
|
||||
|
||||
- Downloaded from a private project that requires authentication, by using a [Group Access Token](../../group/settings/group_access_tokens.md).
|
||||
- Downloaded from a private project that requires authentication, by using a [Group Access Token](../../group/settings/group_access_tokens.md) securely stored within a CI variable.
|
||||
- Checked out at a specific Git commit SHA instead of the default branch.
|
||||
|
||||
See [group access tokens](../../group/settings/group_access_tokens.md#bot-users-for-groups) for how to find the username associated with a group token.
|
||||
|
|
@ -607,5 +607,5 @@ include:
|
|||
- template: Security/SAST.gitlab-ci.yml
|
||||
|
||||
variables:
|
||||
SAST_RULESET_GIT_REFERENCE: "group_2504721_bot_7c9311ffb83f2850e794d478ccee36f5:glpat-1234567@gitlab.com/example-group/example-ruleset-project@c8ea7e3ff126987fb4819cc35f2310755511c2ab"
|
||||
SAST_RULESET_GIT_REFERENCE: "group_2504721_bot_7c9311ffb83f2850e794d478ccee36f5:$PERSONAL_ACCESS_TOKEN@gitlab.com/example-group/example-ruleset-project@c8ea7e3ff126987fb4819cc35f2310755511c2ab"
|
||||
```
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ With comment templates, create and reuse text for any text area in:
|
|||
Comment templates can be small, like approving a merge request and unassigning yourself from it,
|
||||
or large, like chunks of boilerplate text you use frequently:
|
||||
|
||||

|
||||

|
||||
|
||||
## Use comment templates in a text area
|
||||
|
||||
|
|
@ -65,4 +65,4 @@ To edit or delete a previously comment template:
|
|||
1. On the left sidebar, select **Comment templates** (**{comment-lines}**).
|
||||
1. Scroll to **My comment templates**, and identify the comment template you want to edit.
|
||||
1. To edit, select **Edit** (**{pencil}**).
|
||||
1. To delete, select **Delete** (**{remove}**), then select **Delete** again from the modal window.
|
||||
1. To delete, select **Delete** (**{remove}**), then select **Delete** again on the dialog.
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 16 KiB |
|
|
@ -341,6 +341,9 @@ from the command line by running `git checkout <branch-name>`.
|
|||
|
||||
### Checkout merge requests locally through the `head` ref
|
||||
|
||||
> - Deleting `head` refs 14 days after a merge request closes or merges [enabled on self-managed and GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/130098) in GitLab 16.4.
|
||||
> - Deleting `head` refs 14 days after a merge request closes or merges [generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/336070) in GitLab 16.6. Feature flag `merge_request_refs_cleanup` removed.
|
||||
|
||||
A merge request contains all the history from a repository, plus the additional
|
||||
commits added to the branch associated with the merge request. Here's a few
|
||||
ways to check out a merge request locally.
|
||||
|
|
@ -352,9 +355,8 @@ This relies on the merge request `head` ref (`refs/merge-requests/:iid/head`)
|
|||
that is available for each merge request. It allows checking out a merge
|
||||
request by using its ID instead of its branch.
|
||||
|
||||
[Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/223156) in GitLab
|
||||
13.4, 14 days after a merge request gets closed or merged, the merge request
|
||||
`head` ref is deleted. This means that the merge request isn't available
|
||||
In GitLab 16.6 and later, the merge request `head` ref is deleted 14 days after
|
||||
a merge request is closed or merged. The merge request is then no longer available
|
||||
for local checkout from the merge request `head` ref anymore. The merge request
|
||||
can still be re-opened. If the merge request's branch
|
||||
exists, you can still check out the branch, as it isn't affected.
|
||||
|
|
|
|||
|
|
@ -21623,9 +21623,6 @@ msgstr ""
|
|||
msgid "Geo|URL must be a valid url (ex: https://gitlab.com)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Geo|Undefined"
|
||||
msgstr ""
|
||||
|
||||
msgid "Geo|Unhealthy"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -33394,6 +33391,9 @@ msgstr ""
|
|||
msgid "Organization|An error occurred updating your organization. Please try again."
|
||||
msgstr ""
|
||||
|
||||
msgid "Organization|Choose what organization you want to see by default."
|
||||
msgstr ""
|
||||
|
||||
msgid "Organization|Copy organization ID"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -33412,6 +33412,9 @@ msgstr ""
|
|||
msgid "Organization|Get started with organizations"
|
||||
msgstr ""
|
||||
|
||||
msgid "Organization|Home organization"
|
||||
msgstr ""
|
||||
|
||||
msgid "Organization|Manage"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -33463,9 +33466,18 @@ msgstr ""
|
|||
msgid "Organization|Public - The organization can be accessed without any authentication."
|
||||
msgstr ""
|
||||
|
||||
msgid "Organization|Search for an organization"
|
||||
msgstr ""
|
||||
|
||||
msgid "Organization|Search or filter list"
|
||||
msgstr ""
|
||||
|
||||
msgid "Organization|Select an organization"
|
||||
msgstr ""
|
||||
|
||||
msgid "Organization|Unable to fetch organizations. Reload the page to try again."
|
||||
msgstr ""
|
||||
|
||||
msgid "Organization|Update your organization name, description, and avatar."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -57809,6 +57821,9 @@ msgstr ""
|
|||
msgid "sign in"
|
||||
msgstr ""
|
||||
|
||||
msgid "site"
|
||||
msgstr ""
|
||||
|
||||
msgid "smartcn custom analyzer"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -143,7 +143,7 @@ then
|
|||
ruby -r './tooling/lib/tooling/find_changes' -e "Tooling::FindChanges.new(
|
||||
from: :api,
|
||||
changed_files_pathname: '${DOC_CHANGES_FILE}',
|
||||
file_filter: ->(file) { !file['deleted_file'] && file['new_path'] =~ %r{doc/.*|lint-doc\.sh|docs\.gitlab-ci\.yml} }
|
||||
file_filter: ->(file) { !file['deleted_file'] && file['new_path'] =~ %r{doc/.*\.md|lint-doc\.sh|docs\.gitlab-ci\.yml} }
|
||||
).execute"
|
||||
if grep -E "\.vale|\.markdownlint|lint-doc\.sh|docs\.gitlab-ci\.yml" < $DOC_CHANGES_FILE
|
||||
then
|
||||
|
|
|
|||
|
|
@ -38,7 +38,8 @@ RSpec.describe 'User activates issue tracker', :js, feature_category: :integrati
|
|||
end
|
||||
|
||||
it 'shows the link in the menu' do
|
||||
page.within('.nav-sidebar') do
|
||||
within_testid('super-sidebar') do
|
||||
click_button 'Plan'
|
||||
expect(page).to have_link(tracker, href: url)
|
||||
end
|
||||
end
|
||||
|
|
@ -77,7 +78,8 @@ RSpec.describe 'User activates issue tracker', :js, feature_category: :integrati
|
|||
end
|
||||
|
||||
it 'does not show the external tracker link in the menu' do
|
||||
page.within('.nav-sidebar') do
|
||||
within_testid('super-sidebar') do
|
||||
click_button 'Plan'
|
||||
expect(page).not_to have_link(tracker, href: url)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -25,10 +25,11 @@ RSpec.describe 'User activates Jira', :js, feature_category: :integrations do
|
|||
|
||||
unless Gitlab.ee?
|
||||
it 'adds Jira link to sidebar menu' do
|
||||
page.within('.nav-sidebar') do
|
||||
expect(page).not_to have_link('Jira issues', visible: false)
|
||||
expect(page).not_to have_link('Open Jira', href: url, visible: false)
|
||||
expect(page).to have_link('Jira', href: url)
|
||||
within_testid('super-sidebar') do
|
||||
click_button 'Plan'
|
||||
expect(page).not_to have_link('Jira issues')
|
||||
expect(page).not_to have_link('Open Jira')
|
||||
expect(page).to have_link(exact_text: 'Jira', href: url)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -76,8 +77,9 @@ RSpec.describe 'User activates Jira', :js, feature_category: :integrations do
|
|||
end
|
||||
|
||||
it 'does not show the Jira link in the menu' do
|
||||
page.within('.nav-sidebar') do
|
||||
expect(page).not_to have_link('Jira', href: url)
|
||||
within_testid('super-sidebar') do
|
||||
click_button 'Plan'
|
||||
expect(page).not_to have_link('Jira')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
import { memberName, triggerExternalAlert } from '~/invite_members/utils/member_utils';
|
||||
import {
|
||||
memberName,
|
||||
triggerExternalAlert,
|
||||
inviteMembersTrackingOptions,
|
||||
} from '~/invite_members/utils/member_utils';
|
||||
|
||||
jest.mock('~/lib/utils/url_utility');
|
||||
|
||||
|
|
@ -18,3 +22,13 @@ describe('Trigger External Alert', () => {
|
|||
expect(triggerExternalAlert()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('inviteMembersTrackingOptions', () => {
|
||||
it('returns options with a label', () => {
|
||||
expect(inviteMembersTrackingOptions({ label: '_label_' })).toEqual({ label: '_label_' });
|
||||
});
|
||||
|
||||
it('handles options that has no label', () => {
|
||||
expect(inviteMembersTrackingOptions({})).toEqual({ label: undefined });
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -23,10 +23,13 @@ describe('EntitySelect', () => {
|
|||
|
||||
// Props
|
||||
const label = 'label';
|
||||
const description = 'description';
|
||||
const inputName = 'inputName';
|
||||
const inputId = 'inputId';
|
||||
const headerText = 'headerText';
|
||||
const defaultToggleText = 'defaultToggleText';
|
||||
const toggleClass = 'foo-bar';
|
||||
const block = true;
|
||||
|
||||
// Finders
|
||||
const findListbox = () => wrapper.findComponent(GlCollapsibleListbox);
|
||||
|
|
@ -37,11 +40,14 @@ describe('EntitySelect', () => {
|
|||
wrapper = shallowMountExtended(EntitySelect, {
|
||||
propsData: {
|
||||
label,
|
||||
description,
|
||||
inputName,
|
||||
inputId,
|
||||
headerText,
|
||||
defaultToggleText,
|
||||
fetchItems: fetchItemsMock,
|
||||
toggleClass,
|
||||
block,
|
||||
...props,
|
||||
},
|
||||
stubs: {
|
||||
|
|
@ -65,6 +71,21 @@ describe('EntitySelect', () => {
|
|||
fetchItemsMock = jest.fn().mockImplementation(() => ({ items: [itemMock], totalPages: 1 }));
|
||||
});
|
||||
|
||||
describe('GlCollapsableListbox props', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it.each`
|
||||
prop | expectedValue
|
||||
${'block'} | ${block}
|
||||
${'toggleClass'} | ${toggleClass}
|
||||
${'headerText'} | ${headerText}
|
||||
`('passes the $prop prop to GlCollapsableListbox', ({ prop, expectedValue }) => {
|
||||
expect(findListbox().props(prop)).toBe(expectedValue);
|
||||
});
|
||||
});
|
||||
|
||||
describe('on mount', () => {
|
||||
it('calls the fetch function when the listbox is opened', async () => {
|
||||
createComponent();
|
||||
|
|
@ -114,6 +135,12 @@ describe('EntitySelect', () => {
|
|||
expect(wrapper.findByTestId(testid).exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('passes description prop to GlFormGroup', () => {
|
||||
createComponent();
|
||||
|
||||
expect(wrapper.findComponent(GlFormGroup).attributes('description')).toBe(description);
|
||||
});
|
||||
|
||||
describe('selection', () => {
|
||||
it('uses the default toggle text while no group is selected', () => {
|
||||
createComponent();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,179 @@
|
|||
import VueApollo from 'vue-apollo';
|
||||
import Vue, { nextTick } from 'vue';
|
||||
import { GlCollapsibleListbox } from '@gitlab/ui';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import OrganizationSelect from '~/vue_shared/components/entity_select/organization_select.vue';
|
||||
import EntitySelect from '~/vue_shared/components/entity_select/entity_select.vue';
|
||||
import {
|
||||
ORGANIZATION_TOGGLE_TEXT,
|
||||
ORGANIZATION_HEADER_TEXT,
|
||||
FETCH_ORGANIZATIONS_ERROR,
|
||||
FETCH_ORGANIZATION_ERROR,
|
||||
} from '~/vue_shared/components/entity_select/constants';
|
||||
import resolvers from '~/organizations/shared/graphql/resolvers';
|
||||
import organizationsQuery from '~/organizations/index/graphql/organizations.query.graphql';
|
||||
import { organizations as organizationsMock } from '~/organizations/mock_data';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
|
||||
Vue.use(VueApollo);
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
describe('OrganizationSelect', () => {
|
||||
let wrapper;
|
||||
let mockApollo;
|
||||
|
||||
// Mocks
|
||||
const [organizationMock] = organizationsMock;
|
||||
|
||||
// Stubs
|
||||
const GlAlert = {
|
||||
template: '<div><slot /></div>',
|
||||
};
|
||||
|
||||
// Props
|
||||
const label = 'label';
|
||||
const description = 'description';
|
||||
const inputName = 'inputName';
|
||||
const inputId = 'inputId';
|
||||
const toggleClass = 'foo-bar';
|
||||
|
||||
// Finders
|
||||
const findListbox = () => wrapper.findComponent(GlCollapsibleListbox);
|
||||
const findEntitySelect = () => wrapper.findComponent(EntitySelect);
|
||||
const findAlert = () => wrapper.findComponent(GlAlert);
|
||||
|
||||
const handleInput = jest.fn();
|
||||
|
||||
// Helpers
|
||||
const createComponent = ({ props = {}, mockResolvers = resolvers, handlers } = {}) => {
|
||||
mockApollo = createMockApollo(
|
||||
handlers || [
|
||||
[
|
||||
organizationsQuery,
|
||||
jest.fn().mockResolvedValueOnce({
|
||||
data: { currentUser: { id: 1, organizations: { nodes: organizationsMock } } },
|
||||
}),
|
||||
],
|
||||
],
|
||||
mockResolvers,
|
||||
);
|
||||
|
||||
wrapper = shallowMountExtended(OrganizationSelect, {
|
||||
apolloProvider: mockApollo,
|
||||
propsData: {
|
||||
label,
|
||||
description,
|
||||
inputName,
|
||||
inputId,
|
||||
toggleClass,
|
||||
...props,
|
||||
},
|
||||
stubs: {
|
||||
GlAlert,
|
||||
EntitySelect,
|
||||
},
|
||||
listeners: {
|
||||
input: handleInput,
|
||||
},
|
||||
});
|
||||
};
|
||||
const openListbox = () => findListbox().vm.$emit('shown');
|
||||
|
||||
afterEach(() => {
|
||||
mockApollo = null;
|
||||
});
|
||||
|
||||
describe('entity_select props', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it.each`
|
||||
prop | expectedValue
|
||||
${'label'} | ${label}
|
||||
${'description'} | ${description}
|
||||
${'inputName'} | ${inputName}
|
||||
${'inputId'} | ${inputId}
|
||||
${'defaultToggleText'} | ${ORGANIZATION_TOGGLE_TEXT}
|
||||
${'headerText'} | ${ORGANIZATION_HEADER_TEXT}
|
||||
${'toggleClass'} | ${toggleClass}
|
||||
`('passes the $prop prop to entity-select', ({ prop, expectedValue }) => {
|
||||
expect(findEntitySelect().props(prop)).toBe(expectedValue);
|
||||
});
|
||||
});
|
||||
|
||||
describe('on mount', () => {
|
||||
it('fetches organizations when the listbox is opened', async () => {
|
||||
createComponent();
|
||||
await nextTick();
|
||||
jest.runAllTimers();
|
||||
await waitForPromises();
|
||||
|
||||
openListbox();
|
||||
jest.runAllTimers();
|
||||
await waitForPromises();
|
||||
expect(findListbox().props('items')).toEqual([
|
||||
{ text: organizationsMock[0].name, value: 1 },
|
||||
{ text: organizationsMock[1].name, value: 2 },
|
||||
{ text: organizationsMock[2].name, value: 3 },
|
||||
]);
|
||||
});
|
||||
|
||||
describe('with an initial selection', () => {
|
||||
it("fetches the initially selected value's name", async () => {
|
||||
createComponent({ props: { initialSelection: organizationMock.id } });
|
||||
await nextTick();
|
||||
jest.runAllTimers();
|
||||
await waitForPromises();
|
||||
|
||||
expect(findListbox().props('toggleText')).toBe(organizationMock.name);
|
||||
});
|
||||
|
||||
it('show an error if fetching initially selected fails', async () => {
|
||||
const mockResolvers = {
|
||||
Query: {
|
||||
organization: jest.fn().mockRejectedValueOnce(new Error()),
|
||||
},
|
||||
};
|
||||
|
||||
createComponent({ props: { initialSelection: organizationMock.id }, mockResolvers });
|
||||
await nextTick();
|
||||
jest.runAllTimers();
|
||||
|
||||
expect(findAlert().exists()).toBe(false);
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(findAlert().exists()).toBe(true);
|
||||
expect(findAlert().text()).toBe(FETCH_ORGANIZATION_ERROR);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('shows an error when fetching organizations fails', async () => {
|
||||
createComponent({
|
||||
handlers: [[organizationsQuery, jest.fn().mockRejectedValueOnce(new Error())]],
|
||||
});
|
||||
await nextTick();
|
||||
jest.runAllTimers();
|
||||
await waitForPromises();
|
||||
|
||||
openListbox();
|
||||
expect(findAlert().exists()).toBe(false);
|
||||
|
||||
jest.runAllTimers();
|
||||
await waitForPromises();
|
||||
|
||||
expect(findAlert().exists()).toBe(true);
|
||||
expect(findAlert().text()).toBe(FETCH_ORGANIZATIONS_ERROR);
|
||||
});
|
||||
|
||||
it('forwards events to the parent scope via `v-on="$listeners"`', () => {
|
||||
createComponent();
|
||||
findEntitySelect().vm.$emit('input');
|
||||
|
||||
expect(handleInput).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
|
@ -92,6 +92,16 @@ RSpec.describe Organizations::OrganizationHelper, feature_category: :cell do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#home_organization_setting_app_data' do
|
||||
it 'returns expected json' do
|
||||
expect(Gitlab::Json.parse(helper.home_organization_setting_app_data)).to eq(
|
||||
{
|
||||
'initial_selection' => 1
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#organization_settings_general_app_data' do
|
||||
it 'returns expected json' do
|
||||
expect(Gitlab::Json.parse(helper.organization_settings_general_app_data(organization))).to eq(
|
||||
|
|
|
|||
|
|
@ -85,6 +85,26 @@ RSpec.describe Spam::SpamActionService, feature_category: :instance_resiliency d
|
|||
end
|
||||
end
|
||||
|
||||
shared_examples 'calls SpamAbuseEventsWorker with correct arguments' do
|
||||
let(:params) do
|
||||
{
|
||||
user_id: user.id,
|
||||
title: target.title,
|
||||
description: target.spam_description,
|
||||
source_ip: fake_ip,
|
||||
user_agent: fake_user_agent,
|
||||
noteable_type: target_type,
|
||||
verdict: verdict
|
||||
}
|
||||
end
|
||||
|
||||
it do
|
||||
expect(::Abuse::SpamAbuseEventsWorker).to receive(:perform_async).with(params)
|
||||
|
||||
subject
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'execute spam action service' do |target_type|
|
||||
let(:fake_captcha_verification_service) { double(:captcha_verification_service) }
|
||||
let(:fake_verdict_service) { double(:spam_verdict_service) }
|
||||
|
|
@ -161,6 +181,12 @@ RSpec.describe Spam::SpamActionService, feature_category: :instance_resiliency d
|
|||
it 'does not create a spam log' do
|
||||
expect { subject }.not_to change(SpamLog, :count)
|
||||
end
|
||||
|
||||
it 'does not call SpamAbuseEventsWorker' do
|
||||
expect(::Abuse::SpamAbuseEventsWorker).not_to receive(:perform_async)
|
||||
|
||||
subject
|
||||
end
|
||||
end
|
||||
|
||||
context 'when spammable attributes have changed' do
|
||||
|
|
@ -213,6 +239,11 @@ RSpec.describe Spam::SpamActionService, feature_category: :instance_resiliency d
|
|||
|
||||
it_behaves_like 'creates a spam log', target_type
|
||||
|
||||
it_behaves_like 'calls SpamAbuseEventsWorker with correct arguments' do
|
||||
let(:verdict) { DISALLOW }
|
||||
let(:target_type) { target_type }
|
||||
end
|
||||
|
||||
it 'marks as spam' do
|
||||
response = subject
|
||||
|
||||
|
|
@ -231,6 +262,11 @@ RSpec.describe Spam::SpamActionService, feature_category: :instance_resiliency d
|
|||
|
||||
it_behaves_like 'creates a spam log', target_type
|
||||
|
||||
it_behaves_like 'calls SpamAbuseEventsWorker with correct arguments' do
|
||||
let(:verdict) { BLOCK_USER }
|
||||
let(:target_type) { target_type }
|
||||
end
|
||||
|
||||
it 'marks as spam' do
|
||||
response = subject
|
||||
|
||||
|
|
@ -254,6 +290,11 @@ RSpec.describe Spam::SpamActionService, feature_category: :instance_resiliency d
|
|||
|
||||
it_behaves_like 'creates a spam log', target_type
|
||||
|
||||
it_behaves_like 'calls SpamAbuseEventsWorker with correct arguments' do
|
||||
let(:verdict) { CONDITIONAL_ALLOW }
|
||||
let(:target_type) { target_type }
|
||||
end
|
||||
|
||||
it 'does not mark as spam' do
|
||||
response = subject
|
||||
|
||||
|
|
@ -276,6 +317,12 @@ RSpec.describe Spam::SpamActionService, feature_category: :instance_resiliency d
|
|||
|
||||
it_behaves_like 'creates a spam log', target_type
|
||||
|
||||
it 'does not call SpamAbuseEventsWorker' do
|
||||
expect(::Abuse::SpamAbuseEventsWorker).not_to receive(:perform_async)
|
||||
|
||||
subject
|
||||
end
|
||||
|
||||
it 'does not mark as spam' do
|
||||
response = subject
|
||||
|
||||
|
|
@ -300,6 +347,12 @@ RSpec.describe Spam::SpamActionService, feature_category: :instance_resiliency d
|
|||
expect { subject }.not_to change(SpamLog, :count)
|
||||
end
|
||||
|
||||
it 'does not call SpamAbuseEventsWorker' do
|
||||
expect(::Abuse::SpamAbuseEventsWorker).not_to receive(:perform_async)
|
||||
|
||||
subject
|
||||
end
|
||||
|
||||
it 'clears spam flags' do
|
||||
expect(target).to receive(:clear_spam_flags!)
|
||||
|
||||
|
|
@ -316,6 +369,12 @@ RSpec.describe Spam::SpamActionService, feature_category: :instance_resiliency d
|
|||
expect { subject }.not_to change(SpamLog, :count)
|
||||
end
|
||||
|
||||
it 'does not call SpamAbuseEventsWorker' do
|
||||
expect(::Abuse::SpamAbuseEventsWorker).not_to receive(:perform_async)
|
||||
|
||||
subject
|
||||
end
|
||||
|
||||
it 'clears spam flags' do
|
||||
expect(target).to receive(:clear_spam_flags!)
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ RSpec.shared_context 'project integration activation' do
|
|||
include_context 'with integration activation'
|
||||
|
||||
let_it_be(:project) { create(:project) }
|
||||
let_it_be(:user) { create(:user, :no_super_sidebar) }
|
||||
let_it_be(:user) { create(:user) }
|
||||
|
||||
before do
|
||||
project.add_maintainer(user)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,85 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Abuse::SpamAbuseEventsWorker, :clean_gitlab_redis_shared_state, feature_category: :instance_resiliency do
|
||||
let(:worker) { described_class.new }
|
||||
let_it_be(:user) { create(:user) }
|
||||
|
||||
let(:params) do
|
||||
{
|
||||
user_id: user.id,
|
||||
title: 'Test title',
|
||||
description: 'Test description',
|
||||
source_ip: '1.2.3.4',
|
||||
user_agent: 'fake-user-agent',
|
||||
noteable_type: 'Issue',
|
||||
verdict: 'BLOCK_USER'
|
||||
}
|
||||
end
|
||||
|
||||
shared_examples 'creates an abuse event with the correct data' do
|
||||
it do
|
||||
expect { worker.perform(params) }.to change { Abuse::Event.count }.from(0).to(1)
|
||||
expect(Abuse::Event.last.attributes).to include({
|
||||
abuse_report_id: report_id,
|
||||
category: "spam",
|
||||
metadata: params.except(:user_id),
|
||||
source: "spamcheck",
|
||||
user_id: params[:user_id]
|
||||
}.deep_stringify_keys)
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'an idempotent worker' do
|
||||
let(:job_args) { [params] }
|
||||
end
|
||||
|
||||
context "when the user does not exist" do
|
||||
let(:log_payload) { { 'message' => 'User not found.', 'user_id' => user.id } }
|
||||
|
||||
before do
|
||||
allow(User).to receive(:find_by_id).with(user.id).and_return(nil)
|
||||
end
|
||||
|
||||
it 'logs an error' do
|
||||
expect(Sidekiq.logger).to receive(:info).with(hash_including(log_payload))
|
||||
|
||||
expect { worker.perform(params) }.not_to raise_exception
|
||||
end
|
||||
|
||||
it 'does not report the user' do
|
||||
expect(described_class).not_to receive(:report_user).with(user.id)
|
||||
|
||||
worker.perform(params)
|
||||
end
|
||||
end
|
||||
|
||||
context "when the user exists" do
|
||||
context 'and there is an existing abuse report' do
|
||||
let_it_be(:abuse_report) do
|
||||
create(:abuse_report, user: user, reporter: Users::Internal.security_bot, message: 'Test report')
|
||||
end
|
||||
|
||||
it_behaves_like 'creates an abuse event with the correct data' do
|
||||
let(:report_id) { abuse_report.id }
|
||||
end
|
||||
end
|
||||
|
||||
context 'and there is no existing abuse report' do
|
||||
it 'creates an abuse report with the correct data' do
|
||||
expect { worker.perform(params) }.to change { AbuseReport.count }.from(0).to(1)
|
||||
expect(AbuseReport.last.attributes).to include({
|
||||
reporter_id: Users::Internal.security_bot.id,
|
||||
user_id: user.id,
|
||||
category: "spam",
|
||||
message: "User reported for abuse based on spam verdict"
|
||||
}.stringify_keys)
|
||||
end
|
||||
|
||||
it_behaves_like 'creates an abuse event with the correct data' do
|
||||
let(:report_id) { AbuseReport.last.attributes["id"] }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Reference in New Issue