Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-12-03 00:11:20 +00:00
parent 515f39456f
commit 498ba9dc41
49 changed files with 693 additions and 509 deletions

View file

@ -2,9 +2,14 @@
import { GlSearchBoxByType, GlOutsideDirective as Outside } from '@gitlab/ui';
import { mapState, mapActions, mapGetters } from 'vuex';
import { visitUrl } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import { s__, sprintf } from '~/locale';
import DropdownKeyboardNavigation from '~/vue_shared/components/dropdown_keyboard_navigation.vue';
import { FIRST_DROPDOWN_INDEX, SEARCH_BOX_INDEX } from '../constants';
import {
FIRST_DROPDOWN_INDEX,
SEARCH_BOX_INDEX,
SEARCH_INPUT_DESCRIPTION,
SEARCH_RESULTS_DESCRIPTION,
} from '../constants';
import HeaderSearchAutocompleteItems from './header_search_autocomplete_items.vue';
import HeaderSearchDefaultItems from './header_search_default_items.vue';
import HeaderSearchScopedItems from './header_search_scoped_items.vue';
@ -12,7 +17,21 @@ import HeaderSearchScopedItems from './header_search_scoped_items.vue';
export default {
name: 'HeaderSearchApp',
i18n: {
searchPlaceholder: __('Search or jump to...'),
searchPlaceholder: s__('GlobalSearch|Search or jump to...'),
searchAria: s__('GlobalSearch|Search GitLab'),
searchInputDescribeByNoDropdown: s__(
'GlobalSearch|Type and press the enter key to submit search.',
),
searchInputDescribeByWithDropdown: s__(
'GlobalSearch|Type for new suggestions to appear below.',
),
searchDescribedByDefault: s__(
'GlobalSearch|%{count} default results provided. Use the up and down arrow keys to navigate search results list.',
),
searchDescribedByUpdated: s__(
'GlobalSearch|Results updated. %{count} results available. Use the up and down arrow keys to navigate search results list, or ENTER to submit.',
),
searchResultsLoading: s__('GlobalSearch|Search results are loading'),
},
directives: { Outside },
components: {
@ -29,7 +48,7 @@ export default {
};
},
computed: {
...mapState(['search']),
...mapState(['search', 'loading']),
...mapGetters(['searchQuery', 'searchOptions']),
searchText: {
get() {
@ -42,6 +61,9 @@ export default {
currentFocusedOption() {
return this.searchOptions[this.currentFocusIndex];
},
currentFocusedId() {
return this.currentFocusedOption?.html_id;
},
isLoggedIn() {
return gon?.current_username;
},
@ -58,6 +80,30 @@ export default {
return FIRST_DROPDOWN_INDEX;
},
searchInputDescribeBy() {
if (this.isLoggedIn) {
return this.$options.i18n.searchInputDescribeByWithDropdown;
}
return this.$options.i18n.searchInputDescribeByNoDropdown;
},
dropdownResultsDescription() {
if (!this.showSearchDropdown) {
return ''; // This allows aria-live to see register an update when the dropdown is shown
}
if (this.showDefaultItems) {
return sprintf(this.$options.i18n.searchDescribedByDefault, {
count: this.searchOptions.length,
});
}
return this.loading
? this.$options.i18n.searchResultsLoading
: sprintf(this.$options.i18n.searchDescribedByUpdated, {
count: this.searchOptions.length,
});
},
},
methods: {
...mapActions(['setSearch', 'fetchAutocompleteOptions']),
@ -79,22 +125,44 @@ export default {
},
},
SEARCH_BOX_INDEX,
SEARCH_INPUT_DESCRIPTION,
SEARCH_RESULTS_DESCRIPTION,
};
</script>
<template>
<section v-outside="closeDropdown" class="header-search gl-relative">
<form
v-outside="closeDropdown"
role="search"
:aria-label="$options.i18n.searchAria"
class="header-search gl-relative"
>
<gl-search-box-by-type
v-model="searchText"
role="searchbox"
class="gl-z-index-1"
:debounce="500"
autocomplete="off"
:placeholder="$options.i18n.searchPlaceholder"
:aria-activedescendant="currentFocusedId"
:aria-describedby="$options.SEARCH_INPUT_DESCRIPTION"
@focus="openDropdown"
@click="openDropdown"
@input="getAutocompleteOptions"
@keydown.enter.stop.prevent="submitSearch"
/>
<span :id="$options.SEARCH_INPUT_DESCRIPTION" role="region" class="gl-sr-only">{{
searchInputDescribeBy
}}</span>
<span
role="region"
:data-testid="$options.SEARCH_RESULTS_DESCRIPTION"
class="gl-sr-only"
aria-live="polite"
aria-atomic="true"
>
{{ dropdownResultsDescription }}
</span>
<div
v-if="showSearchDropdown"
data-testid="header-search-dropdown-menu"
@ -118,5 +186,5 @@ export default {
</template>
</div>
</div>
</section>
</form>
</template>

View file

@ -69,13 +69,16 @@ export default {
<gl-dropdown-section-header>{{ option.category }}</gl-dropdown-section-header>
<gl-dropdown-item
v-for="data in option.data"
:id="data.html_id"
:ref="data.html_id"
:key="data.html_id"
:class="{ 'gl-bg-gray-50': isOptionFocused(data) }"
:aria-selected="isOptionFocused(data)"
:aria-label="data.label"
tabindex="-1"
:href="data.url"
>
<div class="gl-display-flex gl-align-items-center">
<div class="gl-display-flex gl-align-items-center" aria-hidden="true">
<gl-avatar
v-if="data.avatar_url !== undefined"
:src="data.avatar_url"

View file

@ -43,13 +43,16 @@ export default {
<gl-dropdown-section-header>{{ sectionHeader }}</gl-dropdown-section-header>
<gl-dropdown-item
v-for="option in defaultSearchOptions"
:id="option.html_id"
:ref="option.html_id"
:key="option.html_id"
:class="{ 'gl-bg-gray-50': isOptionFocused(option) }"
:aria-selected="isOptionFocused(option)"
:aria-label="option.title"
tabindex="-1"
:href="option.url"
>
{{ option.title }}
<span aria-hidden="true">{{ option.title }}</span>
</gl-dropdown-item>
</div>
</template>

View file

@ -1,6 +1,7 @@
<script>
import { GlDropdownItem } from '@gitlab/ui';
import { mapState, mapGetters } from 'vuex';
import { __, sprintf } from '~/locale';
export default {
name: 'HeaderSearchScopedItems',
@ -22,6 +23,13 @@ export default {
isOptionFocused(option) {
return this.currentFocusedOption?.html_id === option.html_id;
},
ariaLabel(option) {
return sprintf(__('%{search} %{description} %{scope}'), {
search: this.search,
description: option.description,
scope: option.scope || '',
});
},
},
};
</script>
@ -30,15 +38,20 @@ export default {
<div>
<gl-dropdown-item
v-for="option in scopedSearchOptions"
:id="option.html_id"
:ref="option.html_id"
:key="option.html_id"
:class="{ 'gl-bg-gray-50': isOptionFocused(option) }"
:aria-selected="isOptionFocused(option)"
:aria-label="ariaLabel(option)"
tabindex="-1"
:href="option.url"
>
"<span class="gl-font-weight-bold">{{ search }}</span
>" {{ option.description }}
<span v-if="option.scope" class="gl-font-style-italic">{{ option.scope }}</span>
<span aria-hidden="true">
"<span class="gl-font-weight-bold">{{ search }}</span
>" {{ option.description }}
<span v-if="option.scope" class="gl-font-style-italic">{{ option.scope }}</span>
</span>
</gl-dropdown-item>
</div>
</template>

View file

@ -1,20 +1,20 @@
import { __ } from '~/locale';
import { s__ } from '~/locale';
export const MSG_ISSUES_ASSIGNED_TO_ME = __('Issues assigned to me');
export const MSG_ISSUES_ASSIGNED_TO_ME = s__('GlobalSearch|Issues assigned to me');
export const MSG_ISSUES_IVE_CREATED = __("Issues I've created");
export const MSG_ISSUES_IVE_CREATED = s__("GlobalSearch|Issues I've created");
export const MSG_MR_ASSIGNED_TO_ME = __('Merge requests assigned to me');
export const MSG_MR_ASSIGNED_TO_ME = s__('GlobalSearch|Merge requests assigned to me');
export const MSG_MR_IM_REVIEWER = __("Merge requests that I'm a reviewer");
export const MSG_MR_IM_REVIEWER = s__("GlobalSearch|Merge requests that I'm a reviewer");
export const MSG_MR_IVE_CREATED = __("Merge requests I've created");
export const MSG_MR_IVE_CREATED = s__("GlobalSearch|Merge requests I've created");
export const MSG_IN_ALL_GITLAB = __('in all GitLab');
export const MSG_IN_ALL_GITLAB = s__('GlobalSearch|in all GitLab');
export const MSG_IN_GROUP = __('in group');
export const MSG_IN_GROUP = s__('GlobalSearch|in group');
export const MSG_IN_PROJECT = __('in project');
export const MSG_IN_PROJECT = s__('GlobalSearch|in project');
export const GROUPS_CATEGORY = 'Groups';
@ -27,3 +27,7 @@ export const SMALL_AVATAR_PX = 16;
export const FIRST_DROPDOWN_INDEX = 0;
export const SEARCH_BOX_INDEX = -1;
export const SEARCH_INPUT_DESCRIPTION = 'search-input-description';
export const SEARCH_RESULTS_DESCRIPTION = 'search-results-description';

View file

@ -23,7 +23,6 @@ import {
INVITE_MEMBERS_IN_COMMENT,
GROUP_FILTERS,
USERS_FILTER_ALL,
MEMBER_AREAS_OF_FOCUS,
INVITE_MEMBERS_FOR_TASK,
MODAL_LABELS,
LEARN_GITLAB,
@ -101,14 +100,6 @@ export default {
type: String,
required: true,
},
areasOfFocusOptions: {
type: Array,
required: true,
},
noSelectionAreasOfFocus: {
type: Array,
required: true,
},
tasksToBeDoneOptions: {
type: Array,
required: true,
@ -126,7 +117,6 @@ export default {
inviteeType: 'members',
newUsersToInvite: [],
selectedDate: undefined,
selectedAreasOfFocus: [],
selectedTasksToBeDone: [],
selectedTaskProject: this.projects[0],
groupToBeSharedWith: {},
@ -182,16 +172,6 @@ export default {
this.newUsersToInvite.length === 0 && Object.keys(this.groupToBeSharedWith).length === 0
);
},
areasOfFocusEnabled() {
return !this.tasksToBeDoneEnabled && this.areasOfFocusOptions.length !== 0;
},
areasOfFocusForPost() {
if (this.selectedAreasOfFocus.length === 0 && this.areasOfFocusEnabled) {
return this.noSelectionAreasOfFocus;
}
return this.selectedAreasOfFocus;
},
errorFieldDescription() {
if (this.inviteeType === 'group') {
return '';
@ -232,8 +212,6 @@ export default {
this.openModal(options);
if (this.isOnLearnGitlab) {
this.trackEvent(INVITE_MEMBERS_FOR_TASK.name, this.source);
} else {
this.trackEvent(MEMBER_AREAS_OF_FOCUS.name, MEMBER_AREAS_OF_FOCUS.view);
}
});
@ -280,8 +258,6 @@ export default {
if (this.source === INVITE_MEMBERS_IN_COMMENT) {
this.trackEvent(INVITE_MEMBERS_IN_COMMENT, 'comment_invite_success');
}
this.trackEvent(MEMBER_AREAS_OF_FOCUS.name, MEMBER_AREAS_OF_FOCUS.submit);
},
trackinviteMembersForTask() {
const label = 'selected_tasks_to_be_done';
@ -296,7 +272,6 @@ export default {
this.newUsersToInvite = [];
this.groupToBeSharedWith = {};
this.invalidFeedbackMessage = '';
this.selectedAreasOfFocus = [];
this.selectedTasksToBeDone = [];
[this.selectedTaskProject] = this.projects;
},
@ -350,7 +325,6 @@ export default {
email: usersToInviteByEmail,
access_level: this.selectedAccessLevel,
invite_source: this.source,
areas_of_focus: this.areasOfFocusForPost,
tasks_to_be_done: this.tasksToBeDoneForPost,
tasks_project_id: this.tasksProjectForPost,
};
@ -361,7 +335,6 @@ export default {
user_id: usersToAddById,
access_level: this.selectedAccessLevel,
invite_source: this.source,
areas_of_focus: this.areasOfFocusForPost,
tasks_to_be_done: this.tasksToBeDoneForPost,
tasks_project_id: this.tasksProjectForPost,
};
@ -517,16 +490,6 @@ export default {
</template>
</gl-datepicker>
</div>
<div v-if="areasOfFocusEnabled">
<label class="gl-mt-5">
{{ $options.labels.areasOfFocusLabel }}
</label>
<gl-form-checkbox-group
v-model="selectedAreasOfFocus"
:options="areasOfFocusOptions"
data-testid="area-of-focus-checks"
/>
</div>
<div v-if="showTasksToBeDone" data-testid="invite-members-modal-tasks-to-be-done">
<label class="gl-mt-5">
{{ $options.labels.members.tasksToBeDone.title }}

View file

@ -3,11 +3,6 @@ import { __, s__ } from '~/locale';
export const SEARCH_DELAY = 200;
export const INVITE_MEMBERS_IN_COMMENT = 'invite_members_in_comment';
export const MEMBER_AREAS_OF_FOCUS = {
name: 'member_areas_of_focus',
view: 'view',
submit: 'submit',
};
export const INVITE_MEMBERS_FOR_TASK = {
minimum_access_level: 30,
name: 'invite_members_for_task',
@ -77,9 +72,6 @@ export const READ_MORE_TEXT = s__(
export const INVITE_BUTTON_TEXT = s__('InviteMembersModal|Invite');
export const CANCEL_BUTTON_TEXT = s__('InviteMembersModal|Cancel');
export const HEADER_CLOSE_LABEL = s__('InviteMembersModal|Close invite team members');
export const AREAS_OF_FOCUS_LABEL = s__(
'InviteMembersModal|What would you like new member(s) to focus on? (optional)',
);
export const MODAL_LABELS = {
members: {
@ -142,7 +134,6 @@ export const MODAL_LABELS = {
inviteButtonText: INVITE_BUTTON_TEXT,
cancelButtonText: CANCEL_BUTTON_TEXT,
headerCloseLabel: HEADER_CLOSE_LABEL,
areasOfFocusLabel: AREAS_OF_FOCUS_LABEL,
};
export const LEARN_GITLAB = 'learn_gitlab';

View file

@ -40,10 +40,8 @@ export default function initInviteMembersModal() {
defaultAccessLevel: parseInt(el.dataset.defaultAccessLevel, 10),
groupSelectFilter: el.dataset.groupsFilter,
groupSelectParentId: parseInt(el.dataset.parentId, 10),
areasOfFocusOptions: JSON.parse(el.dataset.areasOfFocusOptions),
tasksToBeDoneOptions: JSON.parse(el.dataset.tasksToBeDoneOptions || '[]'),
projects: JSON.parse(el.dataset.projects || '[]'),
noSelectionAreasOfFocus: JSON.parse(el.dataset.noSelectionAreasOfFocus),
usersFilter: el.dataset.usersFilter,
filterId: parseInt(el.dataset.filterId, 10),
},

View file

@ -8,6 +8,7 @@ import AutoDevOpsAlert from './auto_dev_ops_alert.vue';
import AutoDevOpsEnabledAlert from './auto_dev_ops_enabled_alert.vue';
import { AUTO_DEVOPS_ENABLED_ALERT_DISMISSED_STORAGE_KEY } from './constants';
import FeatureCard from './feature_card.vue';
import TrainingProviderList from './training_provider_list.vue';
import SectionLayout from './section_layout.vue';
import UpgradeBanner from './upgrade_banner.vue';
@ -28,8 +29,28 @@ export const i18n = {
securityTraining: s__('SecurityConfiguration|Security training'),
};
// This will be removed and replaced with GraphQL query:
// https://gitlab.com/gitlab-org/gitlab/-/issues/346480
export const TRAINING_PROVIDERS = [
{
id: 101,
name: __('Kontra'),
description: __('Interactive developer security education.'),
url: 'https://application.security/',
isEnabled: false,
},
{
id: 102,
name: __('SecureCodeWarrior'),
description: __('Security training with guide and learning pathways.'),
url: 'https://www.securecodewarrior.com/',
isEnabled: true,
},
];
export default {
i18n,
TRAINING_PROVIDERS,
components: {
AutoDevOpsAlert,
AutoDevOpsEnabledAlert,
@ -43,6 +64,7 @@ export default {
SectionLayout,
UpgradeBanner,
UserCalloutDismisser,
TrainingProviderList,
},
mixins: [glFeatureFlagsMixin()],
inject: ['projectPath'],
@ -240,7 +262,11 @@ export default {
data-testid="vulnerability-management-tab"
:title="$options.i18n.vulnerabilityManagement"
>
<section-layout :heading="$options.i18n.securityTraining" />
<section-layout :heading="$options.i18n.securityTraining">
<template #features>
<training-provider-list :providers="$options.TRAINING_PROVIDERS" />
</template>
</section-layout>
</gl-tab>
</gl-tabs>
</article>

View file

@ -0,0 +1,36 @@
<script>
import { GlCard, GlToggle, GlLink } from '@gitlab/ui';
export default {
components: {
GlCard,
GlToggle,
GlLink,
},
props: {
providers: {
type: Array,
required: true,
},
},
};
</script>
<template>
<ul class="gl-list-style-none gl-m-0 gl-p-0">
<li v-for="{ id, isEnabled, name, description, url } in providers" :key="id" class="gl-mb-6">
<gl-card>
<div class="gl-display-flex">
<gl-toggle :value="isEnabled" :label="__('Training mode')" label-position="hidden" />
<div class="gl-ml-5">
<h3 class="gl-font-lg gl-m-0 gl-mb-2">{{ name }}</h3>
<p>
{{ description }}
<gl-link :href="url" target="_blank">{{ __('Learn more.') }}</gl-link>
</p>
</div>
</div>
</gl-card>
</li>
</ul>
</template>

View file

@ -3,6 +3,7 @@
class ConfirmationsController < Devise::ConfirmationsController
include AcceptsPendingInvitations
include GitlabRecaptcha
include OneTrustCSP
prepend_before_action :check_recaptcha, only: :create
before_action :load_recaptcha, only: :new

View file

@ -8,12 +8,9 @@ module Repositories
attr_reader :authentication_result, :redirected_path
delegate :actor, :authentication_abilities, to: :authentication_result, allow_nil: true
delegate :authentication_abilities, to: :authentication_result, allow_nil: true
delegate :type, to: :authentication_result, allow_nil: true, prefix: :auth_result
alias_method :user, :actor
alias_method :authenticated_user, :actor
# Git clients will not know what authenticity token to send along
skip_around_action :set_session_storage
skip_before_action :verify_authenticity_token
@ -22,8 +19,16 @@ module Repositories
feature_category :source_code_management
def authenticated_user
authentication_result&.user || authentication_result&.deploy_token
end
private
def user
authenticated_user
end
def download_request?
raise NotImplementedError
end

View file

@ -35,13 +35,6 @@ module InviteMembersHelper
default_access_level: Gitlab::Access::GUEST
}
experiment(:member_areas_of_focus, user: current_user) do |e|
e.publish_to_database
e.control { dataset.merge!(areas_of_focus_options: [], no_selection_areas_of_focus: []) }
e.candidate { dataset.merge!(areas_of_focus_options: member_areas_of_focus_options.to_json, no_selection_areas_of_focus: ['no_selection']) }
end
if show_invite_members_for_task?(source)
dataset.merge!(
tasks_to_be_done_options: tasks_to_be_done_options.to_json,
@ -55,26 +48,6 @@ module InviteMembersHelper
private
def member_areas_of_focus_options
[
{
value: 'Contribute to the codebase', text: s_('InviteMembersModal|Contribute to the codebase')
},
{
value: 'Collaborate on open issues and merge requests', text: s_('InviteMembersModal|Collaborate on open issues and merge requests')
},
{
value: 'Configure CI/CD', text: s_('InviteMembersModal|Configure CI/CD')
},
{
value: 'Configure security features', text: s_('InviteMembersModal|Configure security features')
},
{
value: 'Other', text: s_('InviteMembersModal|Other')
}
]
end
# Overridden in EE
def users_filter_data(group)
{}

View file

@ -9,7 +9,7 @@ module Ci
#
if build.enqueue
build.tap do |build|
build.update(user: current_user, job_variables_attributes: job_variables_attributes || [])
build.update!(user: current_user, job_variables_attributes: job_variables_attributes || [])
AfterRequeueJobService.new(project, current_user).execute(build)
end

View file

@ -92,7 +92,6 @@ module Members
super
track_invite_source(member)
track_areas_of_focus(member)
end
def track_invite_source(member)
@ -110,12 +109,6 @@ module Members
member.invite? ? 'net_new_user' : 'existing_user'
end
def track_areas_of_focus(member)
areas_of_focus.each do |area_of_focus|
Gitlab::Tracking.event(self.class.name, 'area_of_focus', label: area_of_focus, property: member.id.to_s)
end
end
def create_tasks_to_be_done
return if params[:tasks_to_be_done].blank? || params[:tasks_project_id].blank?
@ -128,10 +121,6 @@ module Members
TasksToBeDone::CreateWorker.perform_async(member_task.id, current_user.id, valid_members.map(&:user_id))
end
def areas_of_focus
params[:areas_of_focus] || []
end
def user_limit
limit = params.fetch(:limit, DEFAULT_INVITE_LIMIT)

View file

@ -1,6 +1,8 @@
- user_email = "(#{params[:email]})" if params[:email].present?
- request_link_start = '<a href="%{new_user_confirmation_path}">'.html_safe % { new_user_confirmation_path: new_user_confirmation_path }
- request_link_end = '</a>'.html_safe
- content_for :page_specific_javascripts do
= render "layouts/one_trust"
.well-confirmation.gl-text-center.gl-mb-6
%h1.gl-mt-0

View file

@ -1,8 +0,0 @@
---
name: member_areas_of_focus
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/65273
rollout_issue_url: https://gitlab.com/gitlab-org/growth/team-tasks/-/issues/406
milestone: '14.2'
type: experiment
group: group::expansion
default_enabled: false

View file

@ -0,0 +1,159 @@
# frozen_string_literal: true
require 'wikicloth'
require 'wikicloth/wiki_buffer/var'
# Adds patch for changes in this PR: https://github.com/nricciar/wikicloth/pull/112/files
#
# That fix has already been merged, but the maintainers are not releasing new versions, so we
# need to patch it here.
#
# If they ever do release a version, then we can remove this file.
#
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/334056#note_745336618
# Guard to ensure we remember to delete this patch if they ever release a new version of wikicloth
raise 'New version of WikiCloth detected, please remove this patch' unless Gem::Version.new(WikiCloth::VERSION) == Gem::Version.new('0.8.1')
# rubocop:disable Style/ClassAndModuleChildren
# rubocop:disable Layout/SpaceAroundEqualsInParameterDefault
# rubocop:disable Style/HashSyntax
# rubocop:disable Layout/SpaceAfterComma
# rubocop:disable Style/RescueStandardError
# rubocop:disable Rails/Output
# rubocop:disable Style/MethodCallWithoutArgsParentheses
# rubocop:disable Layout/EmptyLinesAroundClassBody
# rubocop:disable Metrics/AbcSize
# rubocop:disable Metrics/CyclomaticComplexity
# rubocop:disable Metrics/PerceivedComplexity
# rubocop:disable Cop/LineBreakAroundConditionalBlock
# rubocop:disable Layout/EmptyLineAfterGuardClause
# rubocop:disable Performance/ReverseEach
# rubocop:disable Style/BlockDelimiters
# rubocop:disable Cop/LineBreakAroundConditionalBlock
# rubocop:disable Layout/MultilineBlockLayout
# rubocop:disable Layout/BlockEndNewline
module WikiCloth
class WikiCloth
def render(opt={})
self.options = { :noedit => false, :locale => I18n.default_locale, :fast => true, :output => :html, :link_handler => self.link_handler,
:params => self.params, :sections => self.sections }.merge(self.options).merge(opt)
self.options[:link_handler].params = options[:params]
I18n.locale = self.options[:locale]
data = self.sections.collect { |s| s.render(self.options) }.join
# This is the first patched line from:
# https://github.com/nricciar/wikicloth/pull/112/files#diff-eed3de11b953105f9181a6859d58f52af8912d28525fd2a289f8be184e66f531R69
data.gsub!(/<!--.*?-->/m,"")
data << "\n" if data.last(1) != "\n"
data << "garbage"
buffer = WikiBuffer.new("",options)
begin
if self.options[:fast]
until data.empty?
case data
when /\A\w+/
data = $'
@current_row += $&.length
buffer.add_word($&)
when /\A[^\w]+(\w|)/m
data = $'
$&.each_char { |c| add_current_char(buffer,c) }
end
end
else
data.each_char { |c| add_current_char(buffer,c) }
end
rescue => err
debug_tree = buffer.buffers.collect { |b| b.debug }.join("-->")
puts I18n.t("unknown error on line", :line => @current_line, :row => @current_row, :tree => debug_tree)
raise err
end
buffer.eof()
buffer.send("to_#{self.options[:output]}")
end
end
class WikiBuffer::Var < WikiBuffer
def to_html
return "" if will_not_be_rendered
if self.is_function?
if Extension.function_exists?(function_name)
return Extension.functions[function_name][:klass].new(@options).instance_exec( params.collect { |p| p.strip }, &Extension.functions[function_name][:block] ).to_s
end
ret = default_functions(function_name,params.collect { |p| p.strip })
ret ||= @options[:link_handler].function(function_name, params.collect { |p| p.strip })
ret.to_s
elsif self.is_param?
ret = nil
@options[:buffer].buffers.reverse.each do |b|
ret = b.get_param(params[0],params[1]) if b.instance_of?(WikiBuffer::HTMLElement) && b.element_name == "template"
break unless ret.nil?
end
ret.to_s
else
# put template at beginning of buffer
template_stack = @options[:buffer].buffers.collect { |b| b.get_param("__name") if b.instance_of?(WikiBuffer::HTMLElement) &&
b.element_name == "template" }.compact
if template_stack.last == params[0]
debug_tree = @options[:buffer].buffers.collect { |b| b.debug }.join("-->")
"<span class=\"error\">#{I18n.t('template loop detected', :tree => debug_tree)}</span>"
else
key = params[0].to_s.strip
key_options = params[1..-1].collect { |p| p.is_a?(Hash) ? { :name => p[:name].strip, :value => p[:value].strip } : p.strip }
key_options ||= []
key_digest = Digest::MD5.hexdigest(key_options.to_a.sort {|x,y| (x.is_a?(Hash) ? x[:name] : x) <=> (y.is_a?(Hash) ? y[:name] : y) }.inspect)
return @options[:params][key] if @options[:params].has_key?(key)
# if we have a valid cache fragment use it
return @options[:cache][key][key_digest] unless @options[:cache].nil? || @options[:cache][key].nil? || @options[:cache][key][key_digest].nil?
ret = @options[:link_handler].include_resource(key,key_options).to_s
# This is the second patched line from:
# https://github.com/nricciar/wikicloth/pull/112/files#diff-f262faf4fadb222cca87185be0fb65b3f49659abc840794cc83a736d41310fb1R83
ret.gsub!(/<!--.*?-->/m,"") unless ret.frozen?
count = 0
tag_attr = key_options.collect { |p|
if p.instance_of?(Hash)
"#{p[:name]}=\"#{p[:value].gsub(/"/,'&quot;')}\""
else
count += 1
"#{count}=\"#{p.gsub(/"/,'&quot;')}\""
end
}.join(" ")
self.data = ret.blank? ? "" : "<template __name=\"#{key}\" __hash=\"#{key_digest}\" #{tag_attr}>#{ret}</template>"
""
end
end
end
end
end
# rubocop:enable Style/ClassAndModuleChildren
# rubocop:enable Layout/SpaceAroundEqualsInParameterDefault
# rubocop:enable Style/HashSyntax
# rubocop:enable Layout/SpaceAfterComma
# rubocop:enable Style/RescueStandardError
# rubocop:enable Rails/Output
# rubocop:enable Style/MethodCallWithoutArgsParentheses
# rubocop:enable Layout/EmptyLinesAroundClassBody
# rubocop:enable Metrics/AbcSize
# rubocop:enable Metrics/CyclomaticComplexity
# rubocop:enable Metrics/PerceivedComplexity
# rubocop:enable Cop/LineBreakAroundConditionalBlock
# rubocop:enable Layout/EmptyLineAfterGuardClause
# rubocop:enable Performance/ReverseEach
# rubocop:enable Style/BlockDelimiters
# rubocop:enable Cop/LineBreakAroundConditionalBlock
# rubocop:enable Layout/MultilineBlockLayout
# rubocop:enable Layout/BlockEndNewline

View file

@ -0,0 +1,7 @@
# frozen_string_literal: true
class AddMigratedToNewStructureColumnToVulnerabilityOccurrences < Gitlab::Database::Migration[1.0]
def change
add_column :vulnerability_occurrences, :migrated_to_new_structure, :boolean, default: false, null: false
end
end

View file

@ -0,0 +1,15 @@
# frozen_string_literal: true
class AddIndexOnVulnerabilityOccurrencesMigratedToNewStructureColumn < Gitlab::Database::Migration[1.0]
INDEX_NAME = 'index_vulnerability_occurrences_on_migrated_to_new_structure'
disable_ddl_transaction!
def up
add_concurrent_index :vulnerability_occurrences, [:migrated_to_new_structure, :id], name: INDEX_NAME
end
def down
remove_concurrent_index_by_name :vulnerability_occurrences, INDEX_NAME
end
end

View file

@ -0,0 +1,15 @@
# frozen_string_literal: true
class DropClustersApplicationsRunnersCiRunnersFk < Gitlab::Database::Migration[1.0]
disable_ddl_transaction!
def up
with_lock_retries do
remove_foreign_key_if_exists(:clusters_applications_runners, :ci_runners, name: 'fk_02de2ded36')
end
end
def down
add_concurrent_foreign_key(:clusters_applications_runners, :ci_runners, name: 'fk_02de2ded36', column: :runner_id, target_column: :id, on_delete: 'set null')
end
end

View file

@ -0,0 +1 @@
c1ba97f01fca6330628090010abb54220c0d057514386c6bb867c1b6f13f252c

View file

@ -0,0 +1 @@
c6d257f635049f88cd6efba903c9384a0a1af23b3c8fe6fa7f0842dcdf9f7e39

View file

@ -0,0 +1 @@
277cfcd1002e32c6cd664d6c0b6a7cbdf2ed7e5242e46dbddc4f99b0e8422361

View file

@ -20828,6 +20828,7 @@ CREATE TABLE vulnerability_occurrences (
cve text,
location jsonb,
detection_method smallint DEFAULT 0 NOT NULL,
migrated_to_new_structure boolean DEFAULT false NOT NULL,
CONSTRAINT check_4a3a60f2ba CHECK ((char_length(solution) <= 7000)),
CONSTRAINT check_ade261da6b CHECK ((char_length(description) <= 15000)),
CONSTRAINT check_df6dd20219 CHECK ((char_length(message) <= 3000)),
@ -27780,6 +27781,8 @@ CREATE INDEX index_vulnerability_occurrences_on_location_cluster_id ON vulnerabi
CREATE INDEX index_vulnerability_occurrences_on_location_image ON vulnerability_occurrences USING gin (((location -> 'image'::text))) WHERE (report_type = ANY (ARRAY[2, 7]));
CREATE INDEX index_vulnerability_occurrences_on_migrated_to_new_structure ON vulnerability_occurrences USING btree (migrated_to_new_structure, id);
CREATE INDEX index_vulnerability_occurrences_on_primary_identifier_id ON vulnerability_occurrences USING btree (primary_identifier_id);
CREATE INDEX index_vulnerability_occurrences_on_project_fingerprint ON vulnerability_occurrences USING btree (project_fingerprint);
@ -28891,9 +28894,6 @@ ALTER TABLE ONLY deployments
ALTER TABLE ONLY epics
ADD CONSTRAINT fk_013c9f36ca FOREIGN KEY (due_date_sourcing_epic_id) REFERENCES epics(id) ON DELETE SET NULL;
ALTER TABLE ONLY clusters_applications_runners
ADD CONSTRAINT fk_02de2ded36 FOREIGN KEY (runner_id) REFERENCES ci_runners(id) ON DELETE SET NULL;
ALTER TABLE ONLY incident_management_escalation_rules
ADD CONSTRAINT fk_0314ee86eb FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;

View file

@ -6,7 +6,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Geo Nodes API **(PREMIUM SELF)**
To interact with Geo node endpoints, you need to authenticate yourself as an
To interact with Geo node endpoints, you must authenticate yourself as an
administrator.
## Create a new Geo node
@ -26,7 +26,7 @@ curl --header "PRIVATE-TOKEN: <your_access_token>" "https://primary.example.com/
| Attribute | Type | Required | Description |
| ----------------------------| ------- | -------- | -----------------------------------------------------------------|
| `primary` | boolean | no | Specifying whether this node will be primary. Defaults to false. |
| `primary` | boolean | no | Specifying whether this node should be primary. Defaults to false. |
| `enabled` | boolean | no | Flag indicating if the Geo node is enabled. Defaults to true. |
| `name` | string | yes | The unique identifier for the Geo node. Must match `geo_node_name` if it is set in `gitlab.rb`, otherwise it must match `external_url` |
| `url` | string | yes | The user-facing URL for the Geo node. |
@ -35,11 +35,11 @@ curl --header "PRIVATE-TOKEN: <your_access_token>" "https://primary.example.com/
| `repos_max_capacity` | integer | no | Control the maximum concurrency of repository backfill for this secondary node. Defaults to 25. |
| `verification_max_capacity` | integer | no | Control the maximum concurrency of repository verification for this node. Defaults to 100. |
| `container_repositories_max_capacity` | integer | no | Control the maximum concurrency of container repository sync for this node. Defaults to 10. |
| `sync_object_storage` | boolean | no | Flag indicating if the secondary Geo node will replicate blobs in Object Storage. Defaults to false. |
| `sync_object_storage` | boolean | no | Flag indicating if the secondary Geo node should replicate blobs in Object Storage. Defaults to false. |
| `selective_sync_type` | string | no | Limit syncing to only specific groups or shards. Valid values: `"namespaces"`, `"shards"`, or `null`. |
| `selective_sync_shards` | array | no | The repository storage for the projects synced if `selective_sync_type` == `shards`. |
| `selective_sync_namespace_ids` | array | no | The IDs of groups that should be synced, if `selective_sync_type` == `namespaces`. |
| `minimum_reverification_interval` | integer | no | The interval (in days) in which the repository verification is valid. Once expired, it will be reverified. This has no effect when set on a secondary node. |
| `minimum_reverification_interval` | integer | no | The interval (in days) in which the repository verification is valid. Once expired, it is reverified. This has no effect when set on a secondary node. |
Example response:
@ -199,11 +199,11 @@ PUT /geo_nodes/:id
| `repos_max_capacity` | integer | no | Control the maximum concurrency of repository backfill for this secondary node. |
| `verification_max_capacity` | integer | no | Control the maximum concurrency of verification for this node. |
| `container_repositories_max_capacity` | integer | no | Control the maximum concurrency of container repository sync for this node. |
| `sync_object_storage` | boolean | no | Flag indicating if the secondary Geo node will replicate blobs in Object Storage. |
| `sync_object_storage` | boolean | no | Flag indicating if the secondary Geo node should replicate blobs in Object Storage. |
| `selective_sync_type` | string | no | Limit syncing to only specific groups or shards. Valid values: `"namespaces"`, `"shards"`, or `null`. |
| `selective_sync_shards` | array | no | The repository storage for the projects synced if `selective_sync_type` == `shards`. |
| `selective_sync_namespace_ids` | array | no | The IDs of groups that should be synced, if `selective_sync_type` == `namespaces`. |
| `minimum_reverification_interval` | integer | no | The interval (in days) in which the repository verification is valid. Once expired, it will be reverified. This has no effect when set on a secondary node. |
| `minimum_reverification_interval` | integer | no | The interval (in days) in which the repository verification is valid. Once expired, it is reverified. This has no effect when set on a secondary node. |
Example response:
@ -241,7 +241,7 @@ Example response:
Removes the Geo node.
NOTE:
Only a Geo primary node will accept this request.
Only a Geo primary node accepts this request.
```plaintext
DELETE /geo_nodes/:id

View file

@ -42,7 +42,6 @@ POST /projects/:id/invitations
| `access_level` | integer | yes | A valid access level |
| `expires_at` | string | no | A date string in the format YEAR-MONTH-DAY |
| `invite_source` | string | no | The source of the invitation that starts the member creation process. See [this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/327120). |
| `areas_of_focus` | string | no | Areas the inviter wants the member to focus upon. |
| `tasks_to_be_done` | array of strings | no | Tasks the inviter wants the member to focus on. The tasks are added as issues to a specified project. The possible values are: `ci`, `code` and `issues`. If specified, requires `tasks_project_id`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/69299) in GitLab 14.6 |
| `tasks_project_id` | integer | no | The project ID in which to create the task issues. If specified, requires `tasks_to_be_done`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/69299) in GitLab 14.6 |

View file

@ -429,7 +429,6 @@ POST /projects/:id/members
| `access_level` | integer | yes | A valid access level |
| `expires_at` | string | no | A date string in the format `YEAR-MONTH-DAY` |
| `invite_source` | string | no | The source of the invitation that starts the member creation process. See [this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/327120). |
| `areas_of_focus` | string | no | Areas the inviter wants the member to focus upon. |
| `tasks_to_be_done` | array of strings | no | Tasks the inviter wants the member to focus on. The tasks are added as issues to a specified project. The possible values are: `ci`, `code` and `issues`. If specified, requires `tasks_project_id`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/69299) in GitLab 14.5 [with a flag](../administration/feature_flags.md) named `invite_members_for_task`. Disabled by default. |
| `tasks_project_id` | integer | no | The project ID in which to create the task issues. If specified, requires `tasks_to_be_done`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/69299) in GitLab 14.5 [with a flag](../administration/feature_flags.md) named `invite_members_for_task`. Disabled by default. |

View file

@ -313,12 +313,8 @@ As in other list types, click the trash icon to remove a list.
### Iteration lists **(PREMIUM)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/250479) in GitLab 13.11 [with a flag](../../administration/feature_flags.md) named `iteration_board_lists`. Enabled by default.
FLAG:
On self-managed GitLab, by default this feature is available. To hide the feature, ask an
administrator to [disable the `iteration_board_lists` flag](../../administration/feature_flags.md).
On GitLab.com, this feature is available.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/250479) in GitLab 13.11 [with a flag](../../administration/feature_flags.md) named `iteration_board_lists`. Enabled by default.
> - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/75404) in GitLab 14.6. Feature flag `iteration_board_lists` removed.
You're also able to create lists of an iteration.
These lists filter issues by the assigned iteration.

View file

@ -24,7 +24,6 @@ module API
requires :access_level, type: Integer, values: Gitlab::Access.all_values, desc: 'A valid access level (defaults: `30`, developer access level)'
optional :expires_at, type: DateTime, desc: 'Date string in the format YEAR-MONTH-DAY'
optional :invite_source, type: String, desc: 'Source that triggered the member creation process', default: 'invitations-api'
optional :areas_of_focus, type: Array[String], coerce_with: Validations::Types::CommaSeparatedToArray.coerce, desc: 'Areas the inviter wants the member to focus upon'
optional :tasks_to_be_done, type: Array[String], coerce_with: Validations::Types::CommaSeparatedToArray.coerce, desc: 'Tasks the inviter wants the member to do'
optional :tasks_project_id, type: Integer, desc: 'The project ID in which to create the task issues'
end

View file

@ -95,7 +95,6 @@ module API
requires :user_id, types: [Integer, String], desc: 'The user ID of the new member or multiple IDs separated by commas.'
optional :expires_at, type: DateTime, desc: 'Date string in the format YEAR-MONTH-DAY'
optional :invite_source, type: String, desc: 'Source that triggered the member creation process', default: 'members-api'
optional :areas_of_focus, type: Array[String], coerce_with: Validations::Types::CommaSeparatedToArray.coerce, desc: 'Areas the inviter wants the member to focus upon'
optional :tasks_to_be_done, type: Array[String], coerce_with: Validations::Types::CommaSeparatedToArray.coerce, desc: 'Tasks the inviter wants the member to do'
optional :tasks_project_id, type: Integer, desc: 'The project ID in which to create the task issues'
end

View file

@ -873,6 +873,9 @@ msgstr ""
msgid "%{scope} results for term '%{term}'"
msgstr ""
msgid "%{search} %{description} %{scope}"
msgstr ""
msgid "%{seconds}s"
msgstr ""
@ -16077,6 +16080,51 @@ msgstr ""
msgid "Global notification settings"
msgstr ""
msgid "GlobalSearch|%{count} default results provided. Use the up and down arrow keys to navigate search results list."
msgstr ""
msgid "GlobalSearch|Issues I've created"
msgstr ""
msgid "GlobalSearch|Issues assigned to me"
msgstr ""
msgid "GlobalSearch|Merge requests I've created"
msgstr ""
msgid "GlobalSearch|Merge requests assigned to me"
msgstr ""
msgid "GlobalSearch|Merge requests that I'm a reviewer"
msgstr ""
msgid "GlobalSearch|Results updated. %{count} results available. Use the up and down arrow keys to navigate search results list, or ENTER to submit."
msgstr ""
msgid "GlobalSearch|Search GitLab"
msgstr ""
msgid "GlobalSearch|Search or jump to..."
msgstr ""
msgid "GlobalSearch|Search results are loading"
msgstr ""
msgid "GlobalSearch|Type and press the enter key to submit search."
msgstr ""
msgid "GlobalSearch|Type for new suggestions to appear below."
msgstr ""
msgid "GlobalSearch|in all GitLab"
msgstr ""
msgid "GlobalSearch|in group"
msgstr ""
msgid "GlobalSearch|in project"
msgstr ""
msgid "Go Back"
msgstr ""
@ -18843,6 +18891,9 @@ msgstr ""
msgid "Integrations|can't exceed %{recipients_limit}"
msgstr ""
msgid "Interactive developer security education."
msgstr ""
msgid "Interactive mode"
msgstr ""
@ -19071,21 +19122,9 @@ msgstr ""
msgid "InviteMembersModal|Close invite team members"
msgstr ""
msgid "InviteMembersModal|Collaborate on open issues and merge requests"
msgstr ""
msgid "InviteMembersModal|Configure CI/CD"
msgstr ""
msgid "InviteMembersModal|Configure security features"
msgstr ""
msgid "InviteMembersModal|Congratulations on creating your project, you're almost there!"
msgstr ""
msgid "InviteMembersModal|Contribute to the codebase"
msgstr ""
msgid "InviteMembersModal|Create issues for your new team member to work on (optional)"
msgstr ""
@ -19110,9 +19149,6 @@ msgstr ""
msgid "InviteMembersModal|Members were successfully added"
msgstr ""
msgid "InviteMembersModal|Other"
msgstr ""
msgid "InviteMembersModal|Search for a group to invite"
msgstr ""
@ -19131,9 +19167,6 @@ msgstr ""
msgid "InviteMembersModal|To assign issues to a new team member, you need a project for the issues. %{linkStart}Create a project to get started.%{linkEnd}"
msgstr ""
msgid "InviteMembersModal|What would you like new member(s) to focus on? (optional)"
msgstr ""
msgid "InviteMembersModal|You're inviting a group to the %{strongStart}%{name}%{strongEnd} group."
msgstr ""
@ -19455,9 +19488,6 @@ msgstr ""
msgid "Issues"
msgstr ""
msgid "Issues I've created"
msgstr ""
msgid "Issues Rate Limits"
msgstr ""
@ -19467,9 +19497,6 @@ msgstr ""
msgid "Issues are being rebalanced at the moment, so manual reordering is disabled."
msgstr ""
msgid "Issues assigned to me"
msgstr ""
msgid "Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable."
msgstr ""
@ -20196,6 +20223,9 @@ msgstr ""
msgid "Ki"
msgstr ""
msgid "Kontra"
msgstr ""
msgid "Kroki"
msgstr ""
@ -21859,18 +21889,9 @@ msgstr ""
msgid "Merge requests"
msgstr ""
msgid "Merge requests I've created"
msgstr ""
msgid "Merge requests are a place to propose changes you've made to a project and discuss those changes with others"
msgstr ""
msgid "Merge requests assigned to me"
msgstr ""
msgid "Merge requests that I'm a reviewer"
msgstr ""
msgid "Merge the branch and fix any conflicts that come up"
msgstr ""
@ -30671,6 +30692,9 @@ msgstr ""
msgid "Secure token that identifies an external storage request."
msgstr ""
msgid "SecureCodeWarrior"
msgstr ""
msgid "Security"
msgstr ""
@ -30695,6 +30719,9 @@ msgstr ""
msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
msgstr ""
msgid "Security training with guide and learning pathways."
msgstr ""
msgid "SecurityApprovals|A merge request approval is required when a security report contains a new vulnerability."
msgstr ""
@ -36606,6 +36633,9 @@ msgstr ""
msgid "Track your GitLab projects with GitLab for Slack."
msgstr ""
msgid "Training mode"
msgstr ""
msgid "Transfer"
msgstr ""
@ -41231,18 +41261,9 @@ msgstr ""
msgid "in"
msgstr ""
msgid "in all GitLab"
msgstr ""
msgid "in group"
msgstr ""
msgid "in group %{link_to_group}"
msgstr ""
msgid "in project"
msgstr ""
msgid "in project %{link_to_project}"
msgstr ""

View file

@ -90,6 +90,14 @@ RSpec.describe Repositories::GitHttpController do
end
end
end
context 'when the user is a deploy token' do
it_behaves_like Repositories::GitHttpController do
let(:container) { project }
let(:user) { create(:deploy_token, :project, projects: [project]) }
let(:access_checker_class) { Gitlab::GitAccess }
end
end
end
context 'when repository container is a project wiki' do

View file

@ -85,33 +85,6 @@ RSpec.describe 'Groups > Members > Manage members' do
property: 'existing_user',
user: user1
)
expect_no_snowplow_event(
category: 'Members::CreateService',
action: 'area_of_focus'
)
end
it 'adds a user to group with area_of_focus', :js, :snowplow, :aggregate_failures do
stub_experiments(member_areas_of_focus: :candidate)
group.add_owner(user1)
visit group_group_members_path(group)
invite_member(user2.name, role: 'Reporter', area_of_focus: true)
wait_for_requests
expect_snowplow_event(
category: 'Members::CreateService',
action: 'area_of_focus',
label: 'Contribute to the codebase',
property: group.members.last.id.to_s
)
expect_snowplow_event(
category: 'Members::CreateService',
action: 'area_of_focus',
label: 'Collaborate on open issues and merge requests',
property: group.members.last.id.to_s
)
end
it 'do not disclose email addresses', :js do
@ -221,36 +194,9 @@ RSpec.describe 'Groups > Members > Manage members' do
property: 'net_new_user',
user: user1
)
expect_no_snowplow_event(
category: 'Members::CreateService',
action: 'area_of_focus'
)
end
end
it 'invite user to group with area_of_focus', :js, :snowplow, :aggregate_failures do
stub_experiments(member_areas_of_focus: :candidate)
group.add_owner(user1)
visit group_group_members_path(group)
invite_member('test@example.com', role: 'Reporter', area_of_focus: true)
wait_for_requests
expect_snowplow_event(
category: 'Members::InviteService',
action: 'area_of_focus',
label: 'Contribute to the codebase',
property: group.members.last.id.to_s
)
expect_snowplow_event(
category: 'Members::InviteService',
action: 'area_of_focus',
label: 'Collaborate on open issues and merge requests',
property: group.members.last.id.to_s
)
end
context 'when user is a guest' do
before do
group.add_guest(user1)

View file

@ -0,0 +1,23 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'OneTrust' do
context 'almost there page' do
context 'when OneTrust is enabled' do
let_it_be(:onetrust_url) { 'https://*.onetrust.com' }
let_it_be(:one_trust_id) { SecureRandom.uuid }
before do
stub_config(extra: { one_trust_id: one_trust_id })
stub_feature_flags(ecomm_instrumentation: true)
visit users_almost_there_path
end
it 'has the OneTrust CSP settings', :aggregate_failures do
expect(response_headers['Content-Security-Policy']).to include("#{onetrust_url}")
expect(page.html).to include("https://cdn.cookielaw.org/consent/#{one_trust_id}/OtAutoBlock.js")
end
end
end
end

View file

@ -6,6 +6,7 @@ import HeaderSearchApp from '~/header_search/components/app.vue';
import HeaderSearchAutocompleteItems from '~/header_search/components/header_search_autocomplete_items.vue';
import HeaderSearchDefaultItems from '~/header_search/components/header_search_default_items.vue';
import HeaderSearchScopedItems from '~/header_search/components/header_search_scoped_items.vue';
import { SEARCH_INPUT_DESCRIPTION, SEARCH_RESULTS_DESCRIPTION } from '~/header_search/constants';
import DropdownKeyboardNavigation from '~/vue_shared/components/dropdown_keyboard_navigation.vue';
import { ENTER_KEY } from '~/lib/utils/keys';
import { visitUrl } from '~/lib/utils/url_utility';
@ -14,6 +15,7 @@ import {
MOCK_SEARCH_QUERY,
MOCK_USERNAME,
MOCK_DEFAULT_SEARCH_OPTIONS,
MOCK_SCOPED_SEARCH_OPTIONS,
} from '../mock_data';
Vue.use(Vuex);
@ -59,11 +61,26 @@ describe('HeaderSearchApp', () => {
const findHeaderSearchAutocompleteItems = () =>
wrapper.findComponent(HeaderSearchAutocompleteItems);
const findDropdownKeyboardNavigation = () => wrapper.findComponent(DropdownKeyboardNavigation);
const findSearchInputDescription = () => wrapper.find(`#${SEARCH_INPUT_DESCRIPTION}`);
const findSearchResultsDescription = () => wrapper.findByTestId(SEARCH_RESULTS_DESCRIPTION);
describe('template', () => {
it('always renders Header Search Input', () => {
createComponent();
expect(findHeaderSearchInput().exists()).toBe(true);
describe('always renders', () => {
beforeEach(() => {
createComponent();
});
it('Header Search Input', () => {
expect(findHeaderSearchInput().exists()).toBe(true);
});
it('Search Input Description', () => {
expect(findSearchInputDescription().exists()).toBe(true);
});
it('Search Results Description', () => {
expect(findSearchResultsDescription().exists()).toBe(true);
});
});
describe.each`
@ -77,7 +94,7 @@ describe('HeaderSearchApp', () => {
beforeEach(() => {
window.gon.current_username = username;
createComponent();
wrapper.setData({ showDropdown });
findHeaderSearchInput().vm.$emit(showDropdown ? 'click' : '');
});
it(`should${showSearchDropdown ? '' : ' not'} render`, () => {
@ -123,6 +140,53 @@ describe('HeaderSearchApp', () => {
});
},
);
describe.each`
username | showDropdown | expectedDesc
${null} | ${false} | ${HeaderSearchApp.i18n.searchInputDescribeByNoDropdown}
${null} | ${true} | ${HeaderSearchApp.i18n.searchInputDescribeByNoDropdown}
${MOCK_USERNAME} | ${false} | ${HeaderSearchApp.i18n.searchInputDescribeByWithDropdown}
${MOCK_USERNAME} | ${true} | ${HeaderSearchApp.i18n.searchInputDescribeByWithDropdown}
`('Search Input Description', ({ username, showDropdown, expectedDesc }) => {
describe(`current_username is ${username} and showDropdown is ${showDropdown}`, () => {
beforeEach(() => {
window.gon.current_username = username;
createComponent();
findHeaderSearchInput().vm.$emit(showDropdown ? 'click' : '');
});
it(`sets description to ${expectedDesc}`, () => {
expect(findSearchInputDescription().text()).toBe(expectedDesc);
});
});
});
describe.each`
username | showDropdown | search | loading | searchOptions | expectedDesc
${null} | ${true} | ${''} | ${false} | ${[]} | ${''}
${MOCK_USERNAME} | ${false} | ${''} | ${false} | ${[]} | ${''}
${MOCK_USERNAME} | ${true} | ${''} | ${false} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${`${MOCK_DEFAULT_SEARCH_OPTIONS.length} default results provided. Use the up and down arrow keys to navigate search results list.`}
${MOCK_USERNAME} | ${true} | ${''} | ${true} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${`${MOCK_DEFAULT_SEARCH_OPTIONS.length} default results provided. Use the up and down arrow keys to navigate search results list.`}
${MOCK_USERNAME} | ${true} | ${MOCK_SEARCH} | ${false} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${`Results updated. ${MOCK_SCOPED_SEARCH_OPTIONS.length} results available. Use the up and down arrow keys to navigate search results list, or ENTER to submit.`}
${MOCK_USERNAME} | ${true} | ${MOCK_SEARCH} | ${true} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${HeaderSearchApp.i18n.searchResultsLoading}
`(
'Search Results Description',
({ username, showDropdown, search, loading, searchOptions, expectedDesc }) => {
describe(`search is ${search}, loading is ${loading}, and showSearchDropdown is ${
Boolean(username) && showDropdown
}`, () => {
beforeEach(() => {
window.gon.current_username = username;
createComponent({ search, loading }, { searchOptions: () => searchOptions });
findHeaderSearchInput().vm.$emit(showDropdown ? 'click' : '');
});
it(`sets description to ${expectedDesc}`, () => {
expect(findSearchResultsDescription().text()).toBe(expectedDesc);
});
});
},
);
});
describe('events', () => {

View file

@ -110,11 +110,11 @@ describe('HeaderSearchAutocompleteItems', () => {
});
describe.each`
currentFocusedOption | isFocused
${null} | ${false}
${{ html_id: 'not-a-match' }} | ${false}
${MOCK_SORTED_AUTOCOMPLETE_OPTIONS[0]} | ${true}
`('isOptionFocused', ({ currentFocusedOption, isFocused }) => {
currentFocusedOption | isFocused | ariaSelected
${null} | ${false} | ${undefined}
${{ html_id: 'not-a-match' }} | ${false} | ${undefined}
${MOCK_SORTED_AUTOCOMPLETE_OPTIONS[0]} | ${true} | ${'true'}
`('isOptionFocused', ({ currentFocusedOption, isFocused, ariaSelected }) => {
describe(`when currentFocusedOption.html_id is ${currentFocusedOption?.html_id}`, () => {
beforeEach(() => {
createComponent({}, {}, { currentFocusedOption });
@ -123,6 +123,10 @@ describe('HeaderSearchAutocompleteItems', () => {
it(`should${isFocused ? '' : ' not'} have gl-bg-gray-50 applied`, () => {
expect(findFirstDropdownItem().classes('gl-bg-gray-50')).toBe(isFocused);
});
it(`sets "aria-selected to ${ariaSelected}`, () => {
expect(findFirstDropdownItem().attributes('aria-selected')).toBe(ariaSelected);
});
});
});
});

View file

@ -83,11 +83,11 @@ describe('HeaderSearchDefaultItems', () => {
});
describe.each`
currentFocusedOption | isFocused
${null} | ${false}
${{ html_id: 'not-a-match' }} | ${false}
${MOCK_DEFAULT_SEARCH_OPTIONS[0]} | ${true}
`('isOptionFocused', ({ currentFocusedOption, isFocused }) => {
currentFocusedOption | isFocused | ariaSelected
${null} | ${false} | ${undefined}
${{ html_id: 'not-a-match' }} | ${false} | ${undefined}
${MOCK_DEFAULT_SEARCH_OPTIONS[0]} | ${true} | ${'true'}
`('isOptionFocused', ({ currentFocusedOption, isFocused, ariaSelected }) => {
describe(`when currentFocusedOption.html_id is ${currentFocusedOption?.html_id}`, () => {
beforeEach(() => {
createComponent({}, { currentFocusedOption });
@ -96,6 +96,10 @@ describe('HeaderSearchDefaultItems', () => {
it(`should${isFocused ? '' : ' not'} have gl-bg-gray-50 applied`, () => {
expect(findFirstDropdownItem().classes('gl-bg-gray-50')).toBe(isFocused);
});
it(`sets "aria-selected to ${ariaSelected}`, () => {
expect(findFirstDropdownItem().attributes('aria-selected')).toBe(ariaSelected);
});
});
});
});

View file

@ -37,6 +37,8 @@ describe('HeaderSearchScopedItems', () => {
const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
const findFirstDropdownItem = () => findDropdownItems().at(0);
const findDropdownItemTitles = () => findDropdownItems().wrappers.map((w) => trimText(w.text()));
const findDropdownItemAriaLabels = () =>
findDropdownItems().wrappers.map((w) => trimText(w.attributes('aria-label')));
const findDropdownItemLinks = () => findDropdownItems().wrappers.map((w) => w.attributes('href'));
describe('template', () => {
@ -56,6 +58,13 @@ describe('HeaderSearchScopedItems', () => {
expect(findDropdownItemTitles()).toStrictEqual(expectedTitles);
});
it('renders aria-labels correctly', () => {
const expectedLabels = MOCK_SCOPED_SEARCH_OPTIONS.map((o) =>
trimText(`${MOCK_SEARCH} ${o.description} ${o.scope || ''}`),
);
expect(findDropdownItemAriaLabels()).toStrictEqual(expectedLabels);
});
it('renders links correctly', () => {
const expectedLinks = MOCK_SCOPED_SEARCH_OPTIONS.map((o) => o.url);
expect(findDropdownItemLinks()).toStrictEqual(expectedLinks);
@ -63,11 +72,11 @@ describe('HeaderSearchScopedItems', () => {
});
describe.each`
currentFocusedOption | isFocused
${null} | ${false}
${{ html_id: 'not-a-match' }} | ${false}
${MOCK_SCOPED_SEARCH_OPTIONS[0]} | ${true}
`('isOptionFocused', ({ currentFocusedOption, isFocused }) => {
currentFocusedOption | isFocused | ariaSelected
${null} | ${false} | ${undefined}
${{ html_id: 'not-a-match' }} | ${false} | ${undefined}
${MOCK_SCOPED_SEARCH_OPTIONS[0]} | ${true} | ${'true'}
`('isOptionFocused', ({ currentFocusedOption, isFocused, ariaSelected }) => {
describe(`when currentFocusedOption.html_id is ${currentFocusedOption?.html_id}`, () => {
beforeEach(() => {
createComponent({}, { currentFocusedOption });
@ -76,6 +85,10 @@ describe('HeaderSearchScopedItems', () => {
it(`should${isFocused ? '' : ' not'} have gl-bg-gray-50 applied`, () => {
expect(findFirstDropdownItem().classes('gl-bg-gray-50')).toBe(isFocused);
});
it(`sets "aria-selected to ${ariaSelected}`, () => {
expect(findFirstDropdownItem().attributes('aria-selected')).toBe(ariaSelected);
});
});
});
});

View file

@ -6,7 +6,6 @@ import {
GlSprintf,
GlLink,
GlModal,
GlFormCheckboxGroup,
} from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
import { stubComponent } from 'helpers/stub_component';
@ -19,7 +18,6 @@ import ModalConfetti from '~/invite_members/components/confetti.vue';
import MembersTokenSelect from '~/invite_members/components/members_token_select.vue';
import {
INVITE_MEMBERS_IN_COMMENT,
MEMBER_AREAS_OF_FOCUS,
INVITE_MEMBERS_FOR_TASK,
CANCEL_BUTTON_TEXT,
INVITE_BUTTON_TEXT,
@ -52,12 +50,7 @@ const inviteeType = 'members';
const accessLevels = { Guest: 10, Reporter: 20, Developer: 30, Maintainer: 40, Owner: 50 };
const defaultAccessLevel = 10;
const inviteSource = 'unknown';
const noSelectionAreasOfFocus = ['no_selection'];
const helpLink = 'https://example.com';
const areasOfFocusOptions = [
{ text: 'area1', value: 'area1' },
{ text: 'area2', value: 'area2' },
];
const tasksToBeDoneOptions = [
{ text: 'First task', value: 'first' },
{ text: 'Second task', value: 'second' },
@ -96,9 +89,7 @@ const createComponent = (data = {}, props = {}) => {
isProject,
inviteeType,
accessLevels,
areasOfFocusOptions,
defaultAccessLevel,
noSelectionAreasOfFocus,
tasksToBeDoneOptions,
projects,
helpLink,
@ -164,7 +155,6 @@ describe('InviteMembersModal', () => {
const membersFormGroupInvalidFeedback = () => findMembersFormGroup().props('invalidFeedback');
const membersFormGroupDescription = () => findMembersFormGroup().props('description');
const findMembersSelect = () => wrapper.findComponent(MembersTokenSelect);
const findAreaofFocusCheckBoxGroup = () => wrapper.findComponent(GlFormCheckboxGroup);
const findTasksToBeDone = () => wrapper.findByTestId('invite-members-modal-tasks-to-be-done');
const findTasks = () => wrapper.findByTestId('invite-members-modal-tasks');
const findProjectSelect = () => wrapper.findByTestId('invite-members-modal-project-select');
@ -215,21 +205,6 @@ describe('InviteMembersModal', () => {
});
});
describe('rendering the areas_of_focus', () => {
it('renders the areas_of_focus checkboxes', () => {
createComponent();
expect(findAreaofFocusCheckBoxGroup().props('options')).toBe(areasOfFocusOptions);
expect(findAreaofFocusCheckBoxGroup().exists()).toBe(true);
});
it('does not render the areas_of_focus checkboxes', () => {
createComponent({}, { areasOfFocusOptions: [] });
expect(findAreaofFocusCheckBoxGroup().exists()).toBe(false);
});
});
describe('rendering the tasks to be done', () => {
const setupComponent = (
extraData = {},
@ -442,20 +417,6 @@ describe('InviteMembersModal', () => {
"The member's email address is not allowed for this project. Go to the Admin area > Sign-up restrictions, and check Allowed domains for sign-ups.";
const expectedSyntaxError = 'email contains an invalid email address';
it('calls the API with the expected focus data when an areas_of_focus checkbox is clicked', () => {
const spy = jest.spyOn(Api, 'addGroupMembersByUserId');
const expectedFocus = [areasOfFocusOptions[0].value];
createComponent({ newUsersToInvite: [user1] });
findAreaofFocusCheckBoxGroup().vm.$emit('input', expectedFocus);
clickInviteButton();
expect(spy).toHaveBeenCalledWith(
user1.id.toString(),
expect.objectContaining({ areas_of_focus: expectedFocus }),
);
});
describe('when inviting an existing user to group by user ID', () => {
const postData = {
user_id: '1,2',
@ -463,7 +424,6 @@ describe('InviteMembersModal', () => {
expires_at: undefined,
invite_source: inviteSource,
format: 'json',
areas_of_focus: noSelectionAreasOfFocus,
tasks_to_be_done: [],
tasks_project_id: '',
};
@ -476,16 +436,6 @@ describe('InviteMembersModal', () => {
jest.spyOn(Api, 'addGroupMembersByUserId').mockResolvedValue({ data: postData });
});
it('includes the non-default selected areas of focus', () => {
const focus = ['abc'];
const updatedPostData = { ...postData, areas_of_focus: focus };
wrapper.setData({ selectedAreasOfFocus: focus });
clickInviteButton();
expect(Api.addGroupMembersByUserId).toHaveBeenCalledWith(id, updatedPostData);
});
describe('when triggered from regular mounting', () => {
beforeEach(() => {
clickInviteButton();
@ -661,7 +611,6 @@ describe('InviteMembersModal', () => {
expires_at: undefined,
email: 'email@example.com',
invite_source: inviteSource,
areas_of_focus: noSelectionAreasOfFocus,
tasks_to_be_done: [],
tasks_project_id: '',
format: 'json',
@ -675,16 +624,6 @@ describe('InviteMembersModal', () => {
jest.spyOn(Api, 'inviteGroupMembersByEmail').mockResolvedValue({ data: postData });
});
it('includes the non-default selected areas of focus', () => {
const focus = ['abc'];
const updatedPostData = { ...postData, areas_of_focus: focus };
wrapper.setData({ selectedAreasOfFocus: focus });
clickInviteButton();
expect(Api.inviteGroupMembersByEmail).toHaveBeenCalledWith(id, updatedPostData);
});
describe('when triggered from regular mounting', () => {
beforeEach(() => {
clickInviteButton();
@ -792,7 +731,6 @@ describe('InviteMembersModal', () => {
access_level: defaultAccessLevel,
expires_at: undefined,
invite_source: inviteSource,
areas_of_focus: noSelectionAreasOfFocus,
format: 'json',
tasks_to_be_done: [],
tasks_project_id: '',
@ -951,30 +889,12 @@ describe('InviteMembersModal', () => {
expect(ExperimentTracking).not.toHaveBeenCalledWith(INVITE_MEMBERS_IN_COMMENT);
});
it('tracks the view for areas_of_focus', () => {
eventHub.$emit('openModal', { inviteeType: 'members' });
expect(ExperimentTracking).toHaveBeenCalledWith(MEMBER_AREAS_OF_FOCUS.name);
expect(ExperimentTracking.prototype.event).toHaveBeenCalledWith(MEMBER_AREAS_OF_FOCUS.view);
});
it('tracks the view for learn_gitlab source', () => {
eventHub.$emit('openModal', { inviteeType: 'members', source: LEARN_GITLAB });
expect(ExperimentTracking).toHaveBeenCalledWith(INVITE_MEMBERS_FOR_TASK.name);
expect(ExperimentTracking.prototype.event).toHaveBeenCalledWith(LEARN_GITLAB);
});
it('tracks the invite for areas_of_focus', () => {
eventHub.$emit('openModal', { inviteeType: 'members' });
clickInviteButton();
expect(ExperimentTracking).toHaveBeenCalledWith(MEMBER_AREAS_OF_FOCUS.name);
expect(ExperimentTracking.prototype.event).toHaveBeenCalledWith(
MEMBER_AREAS_OF_FOCUS.submit,
);
});
});
});
});

View file

@ -5,7 +5,10 @@ import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import { makeMockUserCalloutDismisser } from 'helpers/mock_user_callout_dismisser';
import stubChildren from 'helpers/stub_children';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import SecurityConfigurationApp, { i18n } from '~/security_configuration/components/app.vue';
import SecurityConfigurationApp, {
i18n,
TRAINING_PROVIDERS,
} from '~/security_configuration/components/app.vue';
import AutoDevopsAlert from '~/security_configuration/components/auto_dev_ops_alert.vue';
import AutoDevopsEnabledAlert from '~/security_configuration/components/auto_dev_ops_enabled_alert.vue';
import {
@ -20,6 +23,7 @@ import {
AUTO_DEVOPS_ENABLED_ALERT_DISMISSED_STORAGE_KEY,
} from '~/security_configuration/components/constants';
import FeatureCard from '~/security_configuration/components/feature_card.vue';
import TrainingProviderList from '~/security_configuration/components/training_provider_list.vue';
import UpgradeBanner from '~/security_configuration/components/upgrade_banner.vue';
import {
@ -78,6 +82,7 @@ describe('App component', () => {
const findTabs = () => wrapper.findAllComponents(GlTab);
const findByTestId = (id) => wrapper.findByTestId(id);
const findFeatureCards = () => wrapper.findAllComponents(FeatureCard);
const findTrainingProviderList = () => wrapper.findComponent(TrainingProviderList);
const findManageViaMRErrorAlert = () => wrapper.findByTestId('manage-via-mr-error-alert');
const findLink = ({ href, text, container = wrapper }) => {
const selector = `a[href="${href}"]`;
@ -180,6 +185,10 @@ describe('App component', () => {
expect(findComplianceViewHistoryLink().exists()).toBe(false);
expect(findSecurityViewHistoryLink().exists()).toBe(false);
});
it('renders training provider list with correct props', () => {
expect(findTrainingProviderList().props('providers')).toEqual(TRAINING_PROVIDERS);
});
});
describe('Manage via MR Error Alert', () => {

View file

@ -0,0 +1,60 @@
import { GlLink, GlToggle, GlCard } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import TrainingProviderList from '~/security_configuration/components/training_provider_list.vue';
import { TRAINING_PROVIDERS } from '~/security_configuration/components/app.vue';
const DEFAULT_PROPS = {
providers: TRAINING_PROVIDERS,
};
describe('TrainingProviderList component', () => {
let wrapper;
const createComponent = (props = {}) => {
wrapper = shallowMount(TrainingProviderList, {
propsData: {
...DEFAULT_PROPS,
...props,
},
});
};
const findCards = () => wrapper.findAllComponents(GlCard);
const findLinks = () => wrapper.findAllComponents(GlLink);
const findToggles = () => wrapper.findAllComponents(GlToggle);
afterEach(() => {
wrapper.destroy();
});
describe('basic structure', () => {
beforeEach(() => {
createComponent();
});
it('renders correct amount of cards', () => {
expect(findCards()).toHaveLength(DEFAULT_PROPS.providers.length);
});
DEFAULT_PROPS.providers.forEach(({ name, description, url, isEnabled }, index) => {
it(`shows the name for card ${index}`, () => {
expect(findCards().at(index).text()).toContain(name);
});
it(`shows the description for card ${index}`, () => {
expect(findCards().at(index).text()).toContain(description);
});
it(`shows the learn more link for card ${index}`, () => {
expect(findLinks().at(index).attributes()).toEqual({
target: '_blank',
href: url,
});
});
it(`shows the toggle with the correct value for card ${index}`, () => {
expect(findToggles().at(index).props('value')).toEqual(isEnabled);
});
});
});
});

View file

@ -16,52 +16,14 @@ RSpec.describe InviteMembersHelper do
end
describe '#common_invite_modal_dataset' do
context 'when member_areas_of_focus is enabled', :experiment do
context 'with control experience' do
before do
stub_experiments(member_areas_of_focus: :control)
end
it 'has expected common attributes' do
attributes = {
id: project.id,
name: project.name,
default_access_level: Gitlab::Access::GUEST
}
it 'has expected attributes' do
attributes = {
areas_of_focus_options: [],
no_selection_areas_of_focus: []
}
expect(helper.common_invite_modal_dataset(project)).to include(attributes)
end
end
context 'with candidate experience' do
before do
stub_experiments(member_areas_of_focus: :candidate)
end
it 'has expected attributes', :aggregate_failures do
output = helper.common_invite_modal_dataset(project)
expect(output[:no_selection_areas_of_focus]).to eq ['no_selection']
expect(Gitlab::Json.parse(output[:areas_of_focus_options]).first['value']).to eq 'Contribute to the codebase'
end
end
end
context 'when member_areas_of_focus is disabled' do
before do
stub_feature_flags(member_areas_of_focus: false)
end
it 'has expected attributes' do
attributes = {
id: project.id,
name: project.name,
default_access_level: Gitlab::Access::GUEST,
areas_of_focus_options: [],
no_selection_areas_of_focus: []
}
expect(helper.common_invite_modal_dataset(project)).to include(attributes)
end
expect(helper.common_invite_modal_dataset(project)).to include(attributes)
end
context 'tasks_to_be_done' do

View file

@ -152,20 +152,6 @@ RSpec.describe API::Invitations do
end
end
context 'with areas_of_focus', :snowplow do
it 'tracks the areas_of_focus from params' do
post invitations_url(source, maintainer),
params: { email: email, access_level: Member::DEVELOPER, areas_of_focus: 'Other' }
expect_snowplow_event(
category: 'Members::InviteService',
action: 'area_of_focus',
label: 'Other',
property: source.members.last.id.to_s
)
end
end
context 'with tasks_to_be_done and tasks_project_id in the params' do
let(:project_id) { source_type == 'project' ? source.id : create(:project, namespace: source).id }

View file

@ -387,33 +387,6 @@ RSpec.describe API::Members do
end
end
context 'with areas_of_focus considerations', :snowplow do
let(:user_id) { stranger.id }
context 'when areas_of_focus is present in params' do
it 'tracks the areas_of_focus' do
post api("/#{source_type.pluralize}/#{source.id}/members", maintainer),
params: { user_id: user_id, access_level: Member::DEVELOPER, areas_of_focus: 'Other' }
expect_snowplow_event(
category: 'Members::CreateService',
action: 'area_of_focus',
label: 'Other',
property: source.members.last.id.to_s
)
end
end
context 'when areas_of_focus is not present in params' do
it 'does not track the areas_of_focus' do
post api("/#{source_type.pluralize}/#{source.id}/members", maintainer),
params: { user_id: user_id, access_level: Member::DEVELOPER }
expect_no_snowplow_event(category: 'Members::CreateService', action: 'area_of_focus')
end
end
end
context 'with tasks_to_be_done and tasks_project_id in the params' do
let(:project_id) { source_type == 'project' ? source.id : create(:project, namespace: source).id }

View file

@ -79,12 +79,22 @@ RSpec.describe Ci::PlayBuildService, '#execute' do
{ key: 'second', secret_value: 'second' }]
end
subject { service.execute(build, job_variables) }
it 'assigns the variables to the build' do
service.execute(build, job_variables)
subject
expect(build.reload.job_variables.map(&:key)).to contain_exactly('first', 'second')
end
context 'when variables are invalid' do
let(:job_variables) { [{}] }
it 'raises an error' do
expect { subject }.to raise_error(ActiveRecord::RecordInvalid)
end
end
context 'when user defined variables are restricted' do
before do
project.update!(restrict_user_defined_variables: true)
@ -96,7 +106,7 @@ RSpec.describe Ci::PlayBuildService, '#execute' do
end
it 'assigns the variables to the build' do
service.execute(build, job_variables)
subject
expect(build.reload.job_variables.map(&:key)).to contain_exactly('first', 'second')
end
@ -104,8 +114,7 @@ RSpec.describe Ci::PlayBuildService, '#execute' do
context 'when user is developer' do
it 'raises an error' do
expect { service.execute(build, job_variables) }
.to raise_error Gitlab::Access::AccessDeniedError
expect { subject }.to raise_error Gitlab::Access::AccessDeniedError
end
end
end

View file

@ -127,76 +127,6 @@ RSpec.describe Members::CreateService, :aggregate_failures, :clean_gitlab_redis_
end
end
context 'when tracking the areas of focus', :snowplow do
context 'when areas_of_focus is not passed' do
it 'does not track' do
execute_service
expect_no_snowplow_event(category: described_class.name, action: 'area_of_focus')
end
end
context 'when 1 areas_of_focus is passed' do
let(:additional_params) { { invite_source: '_invite_source_', areas_of_focus: ['no_selection'] } }
it 'tracks the areas_of_focus from params' do
execute_service
expect_snowplow_event(
category: described_class.name,
action: 'area_of_focus',
label: 'no_selection',
property: source.members.last.id.to_s
)
end
context 'when passing many user ids' do
let(:another_user) { create(:user) }
let(:user_ids) { [member.id, another_user.id].join(',') }
it 'tracks the areas_of_focus from params' do
execute_service
members = source.members.last(2)
expect_snowplow_event(
category: described_class.name,
action: 'area_of_focus',
label: 'no_selection',
property: members.first.id.to_s
)
expect_snowplow_event(
category: described_class.name,
action: 'area_of_focus',
label: 'no_selection',
property: members.last.id.to_s
)
end
end
end
context 'when multiple areas_of_focus are passed' do
let(:additional_params) { { invite_source: '_invite_source_', areas_of_focus: %w[no_selection Other] } }
it 'tracks the areas_of_focus from params' do
execute_service
expect_snowplow_event(
category: described_class.name,
action: 'area_of_focus',
label: 'no_selection',
property: source.members.last.id.to_s
)
expect_snowplow_event(
category: described_class.name,
action: 'area_of_focus',
label: 'Other',
property: source.members.last.id.to_s
)
end
end
end
context 'when assigning tasks to be done' do
let(:additional_params) do
{ invite_source: '_invite_source_', tasks_to_be_done: %w(ci code), tasks_project_id: source.id }

View file

@ -5,7 +5,7 @@ module Spec
module Helpers
module Features
module InviteMembersModalHelper
def invite_member(name, role: 'Guest', expires_at: nil, area_of_focus: false)
def invite_member(name, role: 'Guest', expires_at: nil)
click_on 'Invite members'
page.within '[data-testid="invite-members-modal"]' do
@ -14,7 +14,6 @@ module Spec
wait_for_requests
click_button name
choose_options(role, expires_at)
choose_area_of_focus if area_of_focus
click_button 'Invite'
@ -44,13 +43,6 @@ module Spec
fill_in 'YYYY-MM-DD', with: expires_at.strftime('%Y-%m-%d') if expires_at
end
def choose_area_of_focus
page.within '[data-testid="area-of-focus-checks"]' do
check 'Contribute to the codebase'
check 'Collaborate on open issues and merge requests'
end
end
end
end
end

View file

@ -50,7 +50,8 @@ RSpec.shared_examples Repositories::GitHttpController do
context 'with authorized user' do
before do
request.headers.merge! auth_env(user.username, user.password, nil)
password = user.try(:password) || user.try(:token)
request.headers.merge! auth_env(user.username, password, nil)
end
it 'returns 200' do
@ -71,9 +72,10 @@ RSpec.shared_examples Repositories::GitHttpController do
it 'adds user info to the logs' do
get :info_refs, params: params
expect(log_data).to include('username' => user.username,
'user_id' => user.id,
'meta.user' => user.username)
user_log_data = { 'username' => user.username, 'user_id' => user.id }
user_log_data['meta.user'] = user.username if user.is_a?(User)
expect(log_data).to include(user_log_data)
end
end
end