Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-04-15 09:09:03 +00:00
parent 17e561ffb8
commit 73ff43129b
75 changed files with 1147 additions and 231 deletions

View File

@ -135,7 +135,6 @@ linters:
- Style/NegatedIf
- Style/NestedTernaryOperator
- Style/ParenthesesAroundCondition
- Style/RedundantParentheses
- Style/SelfAssignment
- Style/TernaryParentheses
- Style/TrailingCommaInHashLiteral

View File

@ -2,7 +2,7 @@
source 'https://rubygems.org'
gem 'rails', '~> 6.0.3.1'
gem 'rails', '~> 6.0.3.6'
gem 'bootsnap', '~> 1.4.6'

View File

@ -17,59 +17,59 @@ GEM
abstract_type (0.0.7)
acme-client (2.0.6)
faraday (>= 0.17, < 2.0.0)
actioncable (6.0.3.4)
actionpack (= 6.0.3.4)
actioncable (6.0.3.6)
actionpack (= 6.0.3.6)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
actionmailbox (6.0.3.4)
actionpack (= 6.0.3.4)
activejob (= 6.0.3.4)
activerecord (= 6.0.3.4)
activestorage (= 6.0.3.4)
activesupport (= 6.0.3.4)
actionmailbox (6.0.3.6)
actionpack (= 6.0.3.6)
activejob (= 6.0.3.6)
activerecord (= 6.0.3.6)
activestorage (= 6.0.3.6)
activesupport (= 6.0.3.6)
mail (>= 2.7.1)
actionmailer (6.0.3.4)
actionpack (= 6.0.3.4)
actionview (= 6.0.3.4)
activejob (= 6.0.3.4)
actionmailer (6.0.3.6)
actionpack (= 6.0.3.6)
actionview (= 6.0.3.6)
activejob (= 6.0.3.6)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0)
actionpack (6.0.3.4)
actionview (= 6.0.3.4)
activesupport (= 6.0.3.4)
actionpack (6.0.3.6)
actionview (= 6.0.3.6)
activesupport (= 6.0.3.6)
rack (~> 2.0, >= 2.0.8)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0)
actiontext (6.0.3.4)
actionpack (= 6.0.3.4)
activerecord (= 6.0.3.4)
activestorage (= 6.0.3.4)
activesupport (= 6.0.3.4)
actiontext (6.0.3.6)
actionpack (= 6.0.3.6)
activerecord (= 6.0.3.6)
activestorage (= 6.0.3.6)
activesupport (= 6.0.3.6)
nokogiri (>= 1.8.5)
actionview (6.0.3.4)
activesupport (= 6.0.3.4)
actionview (6.0.3.6)
activesupport (= 6.0.3.6)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.1, >= 1.2.0)
activejob (6.0.3.4)
activesupport (= 6.0.3.4)
activejob (6.0.3.6)
activesupport (= 6.0.3.6)
globalid (>= 0.3.6)
activemodel (6.0.3.4)
activesupport (= 6.0.3.4)
activerecord (6.0.3.4)
activemodel (= 6.0.3.4)
activesupport (= 6.0.3.4)
activemodel (6.0.3.6)
activesupport (= 6.0.3.6)
activerecord (6.0.3.6)
activemodel (= 6.0.3.6)
activesupport (= 6.0.3.6)
activerecord-explain-analyze (0.1.0)
activerecord (>= 4)
pg
activestorage (6.0.3.4)
actionpack (= 6.0.3.4)
activejob (= 6.0.3.4)
activerecord (= 6.0.3.4)
marcel (~> 0.3.1)
activesupport (6.0.3.4)
activestorage (6.0.3.6)
actionpack (= 6.0.3.6)
activejob (= 6.0.3.6)
activerecord (= 6.0.3.6)
marcel (~> 1.0.0)
activesupport (6.0.3.6)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 0.7, < 2)
minitest (~> 5.1)
@ -728,8 +728,7 @@ GEM
lumberjack (1.2.7)
mail (2.7.1)
mini_mime (>= 0.1.1)
marcel (0.3.3)
mimemagic (~> 0.3.2)
marcel (1.0.1)
marginalia (1.10.0)
actionpack (>= 2.3)
activerecord (>= 2.3)
@ -961,20 +960,20 @@ GEM
rack-test (1.1.0)
rack (>= 1.0, < 3)
rack-timeout (0.5.2)
rails (6.0.3.4)
actioncable (= 6.0.3.4)
actionmailbox (= 6.0.3.4)
actionmailer (= 6.0.3.4)
actionpack (= 6.0.3.4)
actiontext (= 6.0.3.4)
actionview (= 6.0.3.4)
activejob (= 6.0.3.4)
activemodel (= 6.0.3.4)
activerecord (= 6.0.3.4)
activestorage (= 6.0.3.4)
activesupport (= 6.0.3.4)
rails (6.0.3.6)
actioncable (= 6.0.3.6)
actionmailbox (= 6.0.3.6)
actionmailer (= 6.0.3.6)
actionpack (= 6.0.3.6)
actiontext (= 6.0.3.6)
actionview (= 6.0.3.6)
activejob (= 6.0.3.6)
activemodel (= 6.0.3.6)
activerecord (= 6.0.3.6)
activestorage (= 6.0.3.6)
activesupport (= 6.0.3.6)
bundler (>= 1.3.0)
railties (= 6.0.3.4)
railties (= 6.0.3.6)
sprockets-rails (>= 2.0.0)
rails-controller-testing (1.0.5)
actionpack (>= 5.0.1.rc1)
@ -988,9 +987,9 @@ GEM
rails-i18n (6.0.0)
i18n (>= 0.7, < 2)
railties (>= 6.0.0, < 7)
railties (6.0.3.4)
actionpack (= 6.0.3.4)
activesupport (= 6.0.3.4)
railties (6.0.3.6)
actionpack (= 6.0.3.6)
activesupport (= 6.0.3.6)
method_source
rake (>= 0.8.7)
thor (>= 0.20.3, < 2.0)
@ -1548,7 +1547,7 @@ DEPENDENCIES
rack-oauth2 (~> 1.16.0)
rack-proxy (~> 0.6.0)
rack-timeout (~> 0.5.1)
rails (~> 6.0.3.1)
rails (~> 6.0.3.6)
rails-controller-testing
rails-i18n (~> 6.0)
rainbow (~> 3.0)

View File

@ -163,15 +163,9 @@ export default () => {
eventHub.$off('initialBoardLoad', this.initialBoardLoad);
},
mounted() {
if (!gon.features?.boardsFilteredSearch) {
this.filterManager = new FilteredSearchBoards(
boardsStore.filter,
true,
boardsStore.cantEdit,
);
this.filterManager = new FilteredSearchBoards(boardsStore.filter, true, boardsStore.cantEdit);
this.filterManager.setup();
}
this.filterManager.setup();
this.performSearch();

View File

@ -42,6 +42,11 @@ export default {
required: false,
default: false,
},
oncallSchedules: {
type: Object,
required: false,
default: () => {},
},
},
computed: {
...mapState({
@ -52,6 +57,9 @@ export default {
computedMemberPath() {
return this.memberPath.replace(':id', this.memberId);
},
stringifiedSchedules() {
return JSON.stringify(this.oncallSchedules);
},
},
};
</script>
@ -69,6 +77,7 @@ export default {
:data-is-access-request="isAccessRequest"
:data-is-invite="isInvite"
:data-message="message"
:data-oncall-schedules="stringifiedSchedules"
data-qa-selector="delete_member_button"
/>
</template>

View File

@ -33,7 +33,7 @@ export default {
if (user) {
return sprintf(
s__('Members|Are you sure you want to remove %{usersName} from "%{source}"'),
s__('Members|Are you sure you want to remove %{usersName} from "%{source}"?'),
{
usersName: user.name,
source: source.fullName,
@ -42,12 +42,16 @@ export default {
}
return sprintf(
s__('Members|Are you sure you want to remove this orphaned member from "%{source}"'),
s__('Members|Are you sure you want to remove this orphaned member from "%{source}"?'),
{
source: source.fullName,
},
);
},
oncallScheduleUserData() {
const { user: { name, oncallSchedules: schedules } = {} } = this.member;
return { name, schedules };
},
},
};
</script>
@ -60,6 +64,7 @@ export default {
v-else
:member-id="member.id"
:member-type="member.type"
:oncall-schedules="oncallScheduleUserData"
:message="message"
:title="s__('Member|Remove member')"
/>

View File

@ -3,6 +3,7 @@ import { GlModal, GlForm, GlSprintf, GlTooltipDirective } from '@gitlab/ui';
import { mapState } from 'vuex';
import csrf from '~/lib/utils/csrf';
import { __, s__, sprintf } from '~/locale';
import OncallSchedulesList from '~/vue_shared/components/oncall_schedules_list.vue';
import { LEAVE_MODAL_ID } from '../../constants';
export default {
@ -19,7 +20,7 @@ export default {
csrf,
modalId: LEAVE_MODAL_ID,
modalContent: s__('Members|Are you sure you want to leave "%{source}"?'),
components: { GlModal, GlForm, GlSprintf },
components: { GlModal, GlForm, GlSprintf, OncallSchedulesList },
directives: {
GlTooltip: GlTooltipDirective,
},
@ -42,6 +43,12 @@ export default {
modalTitle() {
return sprintf(s__('Members|Leave "%{source}"'), { source: this.member.source.fullName });
},
schedules() {
return this.member.user?.oncallSchedules;
},
isPartOfOnCallSchedules() {
return this.schedules?.length;
},
},
methods: {
handlePrimary() {
@ -58,7 +65,6 @@ export default {
:title="modalTitle"
:action-primary="$options.actionPrimary"
:action-cancel="$options.actionCancel"
size="sm"
@primary="handlePrimary"
>
<gl-form ref="form" :action="leavePath" method="post">
@ -68,6 +74,12 @@ export default {
</gl-sprintf>
</p>
<oncall-schedules-list
v-if="isPartOfOnCallSchedules"
:schedules="schedules"
:is-current-user="true"
/>
<input type="hidden" name="_method" value="delete" />
<input :value="$options.csrf.token" type="hidden" name="authenticity_token" />
</gl-form>

View File

@ -514,7 +514,7 @@ export default {
>
{{
s__(
'mrWidget|Fork merge requests do not create merge request pipelines which validate a post merge result',
'mrWidget|If the last pipeline ran in the fork project, it may be inaccurate. Before merge, we advise running a pipeline in this project.',
)
}}
</mr-widget-alert-message>

View File

@ -1,16 +1,17 @@
<script>
/* eslint-disable vue/no-v-html */
import { GlIcon } from '@gitlab/ui';
import EditorLite from '~/vue_shared/components/editor_lite.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { HIGHLIGHT_CLASS_NAME } from './constants';
import ViewerMixin from './mixins';
export default {
components: {
GlIcon,
EditorLite,
EditorLite: () =>
import(/* webpackChunkName: 'EditorLite' */ '~/vue_shared/components/editor_lite.vue'),
},
mixins: [ViewerMixin],
mixins: [ViewerMixin, glFeatureFlagsMixin()],
inject: ['blobHash'],
data() {
return {
@ -21,6 +22,9 @@ export default {
lineNumbers() {
return this.content.split('\n').length;
},
refactorBlobViewerEnabled() {
return this.glFeatures.refactorBlobViewer;
},
},
mounted() {
const { hash } = window.location;
@ -49,7 +53,7 @@ export default {
<template>
<div>
<editor-lite
v-if="isRawContent"
v-if="isRawContent && refactorBlobViewerEnabled"
:value="content"
:file-name="fileName"
:editor-options="{ readOnly: true }"

View File

@ -147,6 +147,7 @@ export default {
</gl-button>
<apply-suggestion
v-if="isLoggedIn"
v-gl-tooltip.viewport="tooltipMessage"
:disabled="isDisableButton"
:default-commit-message="defaultCommitMessage"
class="gl-ml-3"

View File

@ -0,0 +1,70 @@
<script>
import { GlSprintf, GlLink } from '@gitlab/ui';
import { sprintf, s__ } from '~/locale';
export default {
components: {
GlSprintf,
GlLink,
},
props: {
schedules: {
type: Array,
required: true,
},
userName: {
type: String,
required: false,
default: null,
},
isCurrentUser: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
title() {
return this.isCurrentUser
? s__('OnCallSchedules|You are currently a part of:')
: sprintf(s__('OnCallSchedules|User %{name} is currently part of:'), {
name: this.userName,
});
},
footer() {
return this.isCurrentUser
? s__(
'OnCallSchedules|Removing yourself may put your on-call team at risk of missing a notification.',
)
: s__(
'OnCallSchedules|Removing this user may put their on-call team at risk of missing a notification.',
);
},
},
};
</script>
<template>
<div>
<p data-testid="title">{{ title }}</p>
<ul data-testid="schedules-list">
<li v-for="(schedule, index) in schedules" :key="`${schedule.name}-${index}`">
<gl-sprintf
:message="s__('OnCallSchedules|On-call schedule %{schedule} in Project %{project}')"
>
<template #schedule>
<gl-link :href="schedule.scheduleUrl" target="_blank">{{ schedule.name }}</gl-link>
</template>
<template #project>
<gl-link :href="schedule.projectUrl" target="_blank">{{
schedule.projectName
}}</gl-link>
</template>
</gl-sprintf>
</li>
</ul>
<p data-testid="footer">{{ footer }}</p>
</div>
</template>

View File

@ -1,8 +1,10 @@
<script>
import { GlFormCheckbox, GlModal } from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import { parseBoolean } from '~/lib/utils/common_utils';
import csrf from '~/lib/utils/csrf';
import { s__, __ } from '~/locale';
import OncallSchedulesList from '~/vue_shared/components/oncall_schedules_list.vue';
export default {
actionCancel: {
@ -12,6 +14,7 @@ export default {
components: {
GlFormCheckbox,
GlModal,
OncallSchedulesList,
},
data() {
return {
@ -48,6 +51,18 @@ export default {
showUnassignIssuablesCheckbox() {
return !this.isAccessRequest && !this.isInvite;
},
isPartOfOncallSchedules() {
return !this.isAccessRequest && this.oncallSchedules.schedules?.length;
},
oncallSchedules() {
let schedules = {};
try {
schedules = JSON.parse(this.modalData.oncallSchedules);
} catch (e) {
Sentry.captureException(e);
}
return schedules;
},
},
mounted() {
document.addEventListener('click', this.handleClick);
@ -83,6 +98,12 @@ export default {
<form ref="form" :action="modalData.memberPath" method="post">
<p data-testid="modal-message">{{ modalData.message }}</p>
<oncall-schedules-list
v-if="isPartOfOncallSchedules"
:schedules="oncallSchedules.schedules"
:user-name="oncallSchedules.name"
/>
<input ref="method" type="hidden" name="_method" value="delete" />
<input :value="$options.csrf.token" type="hidden" name="authenticity_token" />
<gl-form-checkbox v-if="isGroupMember" name="remove_sub_memberships">

View File

@ -26,6 +26,13 @@ gl-emoji {
height: 30px;
// Create a width that fits 9 emojis per row
width: 100 / 9 * 1%;
transition: transform 0.15s cubic-bezier(0.3, 0, 0.2, 2) !important;
will-change: transform;
&:hover,
&:focus {
transform: scale(1.3);
}
}
.emoji-picker .gl-new-dropdown .dropdown-menu {

View File

@ -9,7 +9,6 @@ class Groups::BoardsController < Groups::ApplicationController
before_action :assign_endpoint_vars
before_action do
push_frontend_feature_flag(:graphql_board_lists, group, default_enabled: false)
push_frontend_feature_flag(:boards_filtered_search, group)
push_frontend_feature_flag(:swimlanes_buffered_rendering, group, default_enabled: :yaml)
end

View File

@ -346,6 +346,10 @@ class Group < Namespace
members_with_parents.owners.exists?(user_id: user)
end
def blocked_owners
members.blocked.where(access_level: Gitlab::Access::OWNER)
end
def has_maintainer?(user)
return false unless user
@ -358,14 +362,29 @@ class Group < Namespace
# Check if user is a last owner of the group.
def last_owner?(user)
has_owner?(user) && members_with_parents.owners.size == 1
has_owner?(user) && single_owner?
end
def last_blocked_owner?(user)
def member_last_owner?(member)
return member.last_owner unless member.last_owner.nil?
last_owner?(member.user)
end
def single_owner?
members_with_parents.owners.size == 1
end
def single_blocked_owner?
blocked_owners.size == 1
end
def member_last_blocked_owner?(member)
return member.last_blocked_owner unless member.last_blocked_owner.nil?
return false if members_with_parents.owners.any?
blocked_owners = members.blocked.where(access_level: Gitlab::Access::OWNER)
blocked_owners.size == 1 && blocked_owners.exists?(user_id: user)
single_blocked_owner? && blocked_owners.exists?(user_id: member.user)
end
def ldap_synced?

View File

@ -26,6 +26,8 @@ class GroupMember < Member
after_create :update_two_factor_requirement, unless: :invite?
after_destroy :update_two_factor_requirement, unless: :invite?
attr_accessor :last_owner, :last_blocked_owner
def self.access_level_roles
Gitlab::Access.options_with_owner
end

View File

@ -0,0 +1,46 @@
# frozen_string_literal: true
module Members
class LastGroupOwnerAssigner
def initialize(group, members)
@group = group
@members = members
end
def execute
@last_blocked_owner = no_owners_in_heirarchy? && group.single_blocked_owner?
@group_single_owner = owners.size == 1
members.each { |member| set_last_owner(member) }
end
private
attr_reader :group, :members, :last_blocked_owner, :group_single_owner
def no_owners_in_heirarchy?
owners.empty?
end
def set_last_owner(member)
member.last_owner = member.id.in?(owner_ids) && group_single_owner
member.last_blocked_owner = member.id.in?(blocked_owner_ids) && last_blocked_owner
end
def owner_ids
@owner_ids ||= owners.where(id: member_ids).ids
end
def blocked_owner_ids
@blocked_owner_ids ||= group.blocked_owners.where(id: member_ids).ids
end
def member_ids
@members_ids ||= members.pluck(:id)
end
def owners
@owners ||= group.members_with_parents.owners.load
end
end
end

View File

@ -1,6 +1,8 @@
# frozen_string_literal: true
class Packages::Debian::FileMetadatum < ApplicationRecord
self.primary_key = :package_file_id
belongs_to :package_file, inverse_of: :debian_file_metadatum
validates :package_file, presence: true

View File

@ -19,6 +19,7 @@ class Packages::Maven::Metadatum < ApplicationRecord
validate :maven_package_type
scope :for_package_ids, -> (package_ids) { where(package_id: package_ids) }
scope :with_path, ->(path) { where(path: path) }
scope :order_created, -> { reorder('created_at ASC') }
def self.pluck_app_name

View File

@ -4,7 +4,7 @@ class GroupMemberPolicy < BasePolicy
delegate :group
with_scope :subject
condition(:last_owner) { @subject.group.last_owner?(@subject.user) || @subject.group.last_blocked_owner?(@subject.user) }
condition(:last_owner) { @subject.group.member_last_owner?(@subject) || @subject.group.member_last_blocked_owner?(@subject) }
desc "Membership is users' own"
with_score 0

View File

@ -2,4 +2,10 @@
class MemberSerializer < BaseSerializer
entity MemberEntity
def represent(members, opts = {})
Members::LastGroupOwnerAssigner.new(opts[:group], members).execute unless opts[:source].is_a?(Project)
super(members, opts)
end
end

View File

@ -1,4 +1,4 @@
= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-account-settings'), html: { class: 'fieldset-form' } do |f|
= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-account-settings'), html: { class: 'fieldset-form', id: 'account-settings' } do |f|
= form_errors(@application_setting)
%fieldset

View File

@ -1,4 +1,4 @@
= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-merge-request-settings'), html: { class: 'fieldset-form' } do |f|
= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-merge-request-settings'), html: { class: 'fieldset-form', id: 'merge-request-settings' } do |f|
= form_errors(@application_setting)
%fieldset

View File

@ -9,7 +9,7 @@
= _('Amazon EKS integration allows you to provision EKS clusters from GitLab.')
.settings-content
= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-eks-settings'), html: { class: 'fieldset-form' } do |f|
= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-eks-settings'), html: { class: 'fieldset-form', id: 'eks-settings' } do |f|
= form_errors(@application_setting)
%fieldset

View File

@ -8,7 +8,7 @@
= _('External Classification Policy Authorization')
.settings-content
= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-external-auth-settings'), html: { class: 'fieldset-form' } do |f|
= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-external-auth-settings'), html: { class: 'fieldset-form', id: 'external-auth-settings' } do |f|
= form_errors(@application_setting)
%fieldset

View File

@ -12,7 +12,7 @@
.settings-content
= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-gitpod-settings'), html: { class: 'fieldset-form' } do |f|
= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-gitpod-settings'), html: { class: 'fieldset-form', id: 'gitpod-settings' } do |f|
= form_errors(@application_setting)
%fieldset

View File

@ -8,7 +8,7 @@
%p
= _('Allow rendering of diagrams in AsciiDoc and Markdown documents using %{link}.').html_safe % { link: link_to('Kroki', 'https://kroki.io', target: '_blank') }
.settings-content
= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-kroki-settings'), html: { class: 'fieldset-form' } do |f|
= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-kroki-settings'), html: { class: 'fieldset-form', id: 'kroki-settings' } do |f|
= form_errors(@application_setting) if expanded
%fieldset

View File

@ -8,7 +8,7 @@
%p
= _('Allow rendering of PlantUML diagrams in Asciidoc documents.')
.settings-content
= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-plantuml-settings'), html: { class: 'fieldset-form' } do |f|
= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-plantuml-settings'), html: { class: 'fieldset-form', id: 'plantuml-settings' } do |f|
= form_errors(@application_setting) if expanded
%fieldset

View File

@ -1,4 +1,4 @@
= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-signin-settings'), html: { class: 'fieldset-form' } do |f|
= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-signin-settings'), html: { class: 'fieldset-form', id: 'signin-settings' } do |f|
= form_errors(@application_setting)
%fieldset

View File

@ -8,7 +8,7 @@
%p
= _('Configure the %{link} integration.').html_safe % { link: link_to('Snowplow', 'https://snowplowanalytics.com/', target: '_blank') }
.settings-content
= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-snowplow-settings'), html: { class: 'fieldset-form' } do |f|
= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-snowplow-settings'), html: { class: 'fieldset-form', id: 'snowplow-settings' } do |f|
= form_errors(@application_setting) if expanded
%fieldset

View File

@ -16,7 +16,7 @@
.settings-content
= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-sourcegraph-settings'), html: { class: 'fieldset-form' } do |f|
= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-sourcegraph-settings'), html: { class: 'fieldset-form', id: 'sourcegraph-settings' } do |f|
= form_errors(@application_setting)
%fieldset

View File

@ -1,4 +1,4 @@
= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-terminal-settings'), html: { class: 'fieldset-form' } do |f|
= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-terminal-settings'), html: { class: 'fieldset-form', id: 'terminal-settings' } do |f|
= form_errors(@application_setting)
%fieldset

View File

@ -1,4 +1,4 @@
= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-terms-settings'), html: { class: 'fieldset-form' } do |f|
= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-terms-settings'), html: { class: 'fieldset-form', id: 'terms-settings' } do |f|
= form_errors(@application_setting)
%fieldset

View File

@ -8,7 +8,7 @@
%p
= _('Control the display of third party offers.')
.settings-content
= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-third-party-offers-settings'), html: { class: 'fieldset-form' } do |f|
= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-third-party-offers-settings'), html: { class: 'fieldset-form', id: 'third-party-offers-settings' } do |f|
= form_errors(@application_setting) if expanded
%fieldset

View File

@ -1,4 +1,4 @@
= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-visibility-settings'), html: { class: 'fieldset-form' } do |f|
= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-visibility-settings'), html: { class: 'fieldset-form', id: 'visibility-settings' } do |f|
= form_errors(@application_setting)
%fieldset

View File

@ -90,7 +90,7 @@
%p
= _('Manage Web IDE features')
.settings-content
= form_for @application_setting, url: general_admin_application_settings_path(anchor: "#js-web-ide-settings"), html: { class: 'fieldset-form' } do |f|
= form_for @application_setting, url: general_admin_application_settings_path(anchor: "#js-web-ide-settings"), html: { class: 'fieldset-form', id: 'web-ide-settings' } do |f|
= form_errors(@application_setting)
%fieldset

View File

@ -9,7 +9,7 @@
.awards.js-awards-block{ class: ("hidden" if !inline && grouped_emojis.empty?), data: { award_url: toggle_award_url(awardable) } }
- awards_sort(grouped_emojis).each do |emoji, awards|
%button.gl-button.btn.btn-default.award-control.js-emoji-btn.has-tooltip{ type: "button",
class: [(award_state_class(awardable, awards, current_user))],
class: [award_state_class(awardable, awards, current_user)],
data: { title: award_user_list(awards, current_user) } }
= emoji_icon(emoji)
%span.award-control-text.js-counter

View File

@ -5,7 +5,7 @@
= render 'shared/namespaces/cascading_settings/lock_popovers'
%section.settings.gs-general.no-animate#js-general-settings{ class: ('expanded') }
%section.settings.gs-general.no-animate.expanded#js-general-settings
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only{ role: 'button' }
= _('Naming, visibility')

View File

@ -32,7 +32,7 @@
%tbody
- @user_map.each do |id, user|
%tr
%td= (id)
%td= id
%td= text_field_tag "users[#{id}][name]", user[:name], class: 'form-control'
%td= text_field_tag "users[#{id}][email]", user[:email], class: 'form-control'
%td

View File

@ -2,6 +2,6 @@
- if message
.flash-container.flash-container-page
.flash-notice
%div{ class: (container_class) }
%div{ class: container_class }
%span
= message

View File

@ -20,7 +20,7 @@
= f.select :role, ::User.roles.keys.map { |role| [role.titleize, role] }, {}, class: 'form-control js-user-role-dropdown', autofocus: true
- if Feature.enabled?(:user_other_role_details)
.row
.form-group.col-sm-12.js-other-role-group{ class: ("hidden") }
.form-group.col-sm-12.js-other-role-group.hidden
= f.label :other_role, _('What is your job title? (optional)'), class: 'form-check-label gl-mb-3'
= f.text_field :other_role, class: 'form-control'
= render_if_exists "registrations/welcome/setup_for_company", f: f

View File

@ -5,5 +5,5 @@
- scopes.each do |scope|
%fieldset.form-group.form-check
= check_box_tag "#{prefix}[scopes][]", scope, token.scopes.include?(scope), id: "#{prefix}_scopes_#{scope}", class: "form-check-input qa-#{scope}-radio"
= label_tag ("#{prefix}_scopes_#{scope}"), scope, class: 'label-bold form-check-label'
= label_tag "#{prefix}_scopes_#{scope}", scope, class: 'label-bold form-check-label'
.text-secondary= t scope, scope: scope_description(prefix)

View File

@ -0,0 +1,5 @@
---
title: Resolve group_member policy n+1
merge_request: 58668
author:
type: performance

View File

@ -0,0 +1,5 @@
---
title: "When removing a user, warn Admin user is part of an on-call schedule"
merge_request: 57397
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Add index for the path column on the packages_maven_metadata table
merge_request: 59241
author:
type: performance

View File

@ -0,0 +1,5 @@
---
title: Fix tooltip not rendering
merge_request: 59202
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Allow a Global ID to be used when filtering issue by iterationId in GraphQL
merge_request: 57620
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Update to Rails v6.0.3.6
merge_request: 59328
author:
type: security

View File

@ -0,0 +1,8 @@
---
name: check_maven_path_first
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/59241
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/327487
milestone: '13.11'
type: development
group: group::package
default_enabled: false

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
class AddIndexToPackagesMavenMetadataPath < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
INDEX_NAME = 'index_packages_maven_metadata_on_path'
def up
add_concurrent_index :packages_maven_metadata, :path, name: INDEX_NAME
end
def down
remove_concurrent_index :packages_maven_metadata, :path, name: INDEX_NAME
end
end

View File

@ -0,0 +1 @@
2da634fa920e3989d9b8e53ddc1ba005e5bc0f4701426e3841d90a42bd2e908f

View File

@ -23360,6 +23360,8 @@ CREATE INDEX index_packages_events_on_package_id ON packages_events USING btree
CREATE INDEX index_packages_maven_metadata_on_package_id_and_path ON packages_maven_metadata USING btree (package_id, path);
CREATE INDEX index_packages_maven_metadata_on_path ON packages_maven_metadata USING btree (path);
CREATE INDEX index_packages_nuget_dl_metadata_on_dependency_link_id ON packages_nuget_dependency_link_metadata USING btree (dependency_link_id);
CREATE UNIQUE INDEX index_packages_on_project_id_name_version_unique_when_generic ON packages_packages USING btree (project_id, name, version) WHERE (package_type = 7);

View File

@ -181,6 +181,7 @@ successfully, you must replicate their data using some other means.
| [Personal snippets](../../../user/snippets.md) | **Yes** (10.2) | **Yes** (10.2) | No | |
| [Project snippets](../../../user/snippets.md) | **Yes** (10.2) | **Yes** (10.2) | No | |
| [CI job artifacts (other than Job Logs)](../../../ci/pipelines/job_artifacts.md) | **Yes** (10.4) | [No](https://gitlab.com/gitlab-org/gitlab/-/issues/8923) | Via Object Storage provider if supported. Native Geo support (Beta) . | Verified only manually using [Integrity Check Rake Task](../../raketasks/check.md) on both sites and comparing the output between them |
| [CI Pipeline Artifacts](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/models/ci/pipeline_artifact.rb) | [**Yes** (13.11)](https://gitlab.com/gitlab-org/gitlab/-/issues/238464) | [**Yes** (13.11)](https://gitlab.com/gitlab-org/gitlab/-/issues/238464) | Via Object Storage provider if supported. Native Geo support (Beta). | Persists additional artifacts after a pipeline completes |
| [Job logs](../../job_logs.md) | **Yes** (10.4) | [No](https://gitlab.com/gitlab-org/gitlab/-/issues/8923) | Via Object Storage provider if supported. Native Geo support (Beta). | Verified only on transfer or manually using [Integrity Check Rake Task](../../raketasks/check.md) on both sites and comparing the output between them |
| [Object pools for forked project deduplication](../../../development/git_object_deduplication.md) | **Yes** | No | No | |
| [Container Registry](../../packages/container_registry.md) | **Yes** (12.3) | No | No | Disabled by default. See [instructions](docker_registry.md) to enable. |
@ -199,6 +200,5 @@ successfully, you must replicate their data using some other means.
| [Server-side Git hooks](../../server_hooks.md) | [No](https://gitlab.com/groups/gitlab-org/-/epics/1867) | No | No | |
| [Elasticsearch integration](../../../integration/elasticsearch.md) | [No](https://gitlab.com/gitlab-org/gitlab/-/issues/1186) | No | No | |
| [GitLab Pages](../../pages/index.md) | [No](https://gitlab.com/groups/gitlab-org/-/epics/589) | No | No | |
| [CI Pipeline Artifacts](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/models/ci/pipeline_artifact.rb) | [No](https://gitlab.com/gitlab-org/gitlab/-/issues/238464) | No | Via Object Storage provider if supported. Native Geo support (Beta). | Persists additional artifacts after a pipeline completes |
| [Dependency proxy images](../../../user/packages/dependency_proxy/index.md) | [No](https://gitlab.com/gitlab-org/gitlab/-/issues/259694) | No | No | Blocked on [Geo: Secondary Mimicry](https://gitlab.com/groups/gitlab-org/-/epics/1528). Note that replication of this cache is not needed for Disaster Recovery purposes because it can be recreated from external sources. |
| [Vulnerability Export](../../../user/application_security/vulnerability_report/#export-vulnerability-details) | [Not planned](https://gitlab.com/groups/gitlab-org/-/epics/3111) | No | Via Object Storage provider if supported. Native Geo support (Beta). | Not planned because they are ephemeral and sensitive. They can be regenerated on demand. |

View File

@ -1,14 +1,13 @@
---
stage: Verify
group: Continuous Integration
group: Pipeline Authoring
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
type: reference
---
# Multi-project pipelines
# Multi-project pipelines **(FREE)**
> - [Introduced](https://about.gitlab.com/releases/2015/08/22/gitlab-7-14-released/#build-triggers-api-gitlab-ci) in GitLab 7.14, as Build Triggers.
> - [Made available](https://gitlab.com/gitlab-org/gitlab/-/issues/199224) in all tiers in GitLab 12.8.
> [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/199224) to GitLab Free in 12.8.
You can set up [GitLab CI/CD](README.md) across multiple projects, so that a pipeline
in one project can trigger a pipeline in another project.
@ -42,8 +41,6 @@ With Multi-Project Pipelines you can visualize the entire pipeline, including al
## Multi-project pipeline visualization **(PREMIUM)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/2121) in [GitLab Premium 9.3](https://about.gitlab.com/releases/2017/06/22/gitlab-9-3-released/#multi-project-pipeline-graphs).
When you configure GitLab CI/CD for your project, you can visualize the stages of your
[jobs](pipelines/index.md#configure-a-pipeline) on a [pipeline graph](pipelines/index.md#visualize-pipelines).
@ -56,8 +53,7 @@ and when hovering or tapping (on touchscreen devices) they expand and are shown
## Triggering multi-project pipelines through API
> - Use of `CI_JOB_TOKEN` for multi-project pipelines was [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/2017) in [GitLab Premium](https://about.gitlab.com/pricing/) 9.3.
> - Use of `CI_JOB_TOKEN` for multi-project pipelines was [made available](https://gitlab.com/gitlab-org/gitlab/-/issues/31573) in all tiers in GitLab 12.4.
> [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/31573) to GitLab Free in 12.4.
When you use the [`CI_JOB_TOKEN` to trigger pipelines](triggers/README.md#ci-job-token), GitLab
recognizes the source of the job token, and thus internally ties these pipelines
@ -76,8 +72,7 @@ When using:
## Creating multi-project pipelines from `.gitlab-ci.yml`
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/8997) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.8.
> - [Made available](https://gitlab.com/gitlab-org/gitlab/-/issues/199224) in all tiers in 12.8.
> [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/199224) to GitLab Free in 12.8.
### Triggering a downstream pipeline using a bridge job
@ -260,7 +255,7 @@ test:
### Mirroring status from triggered pipeline
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/11238) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.3.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/11238) in GitLab Premium 12.3.
> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/199224) to GitLab Free in 12.8.
You can mirror the pipeline status from the triggered pipeline to the source
@ -309,7 +304,7 @@ Some features are not implemented yet. For example, support for environments.
## Trigger a pipeline when an upstream project is rebuilt **(PREMIUM)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/9045) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.8.
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/9045) in GitLab Premium 12.8.
You can trigger a pipeline in your project whenever a pipeline finishes for a new
tag in a different project:

View File

@ -23,6 +23,15 @@ module API
helpers ::API::Helpers::PackagesHelpers
helpers do
def path_exists?(path)
# return true when FF disabled so that processing the request is not stopped
return true unless Feature.enabled?(:check_maven_path_first)
return false if path.blank?
Packages::Maven::Metadatum.with_path(path)
.exists?
end
def extract_format(file_name)
name, _, format = file_name.rpartition('.')
@ -104,6 +113,9 @@ module API
end
route_setting :authentication, job_token_allowed: true, deploy_token_allowed: true
get 'packages/maven/*path/:file_name', requirements: MAVEN_ENDPOINT_REQUIREMENTS do
# return a similar failure to authorize_read_package!(project)
forbidden! unless path_exists?(params[:path])
file_name, format = extract_format(params[:file_name])
# To avoid name collision we require project path and project package be the same.
@ -142,6 +154,9 @@ module API
end
route_setting :authentication, job_token_allowed: true, deploy_token_allowed: true
get ':id/-/packages/maven/*path/:file_name', requirements: MAVEN_ENDPOINT_REQUIREMENTS do
# return a similar failure to group = find_group(params[:id])
not_found!('Group') unless path_exists?(params[:path])
file_name, format = extract_format(params[:file_name])
group = find_group(params[:id])
@ -181,6 +196,9 @@ module API
end
route_setting :authentication, job_token_allowed: true, deploy_token_allowed: true
get ':id/packages/maven/*path/:file_name', requirements: MAVEN_ENDPOINT_REQUIREMENTS do
# return a similar failure to user_project
not_found!('Project') unless path_exists?(params[:path])
authorize_read_package!(user_project)
file_name, format = extract_format(params[:file_name])

View File

@ -19640,10 +19640,10 @@ msgstr ""
msgid "Members|Are you sure you want to remove \"%{groupName}\"?"
msgstr ""
msgid "Members|Are you sure you want to remove %{usersName} from \"%{source}\""
msgid "Members|Are you sure you want to remove %{usersName} from \"%{source}\"?"
msgstr ""
msgid "Members|Are you sure you want to remove this orphaned member from \"%{source}\""
msgid "Members|Are you sure you want to remove this orphaned member from \"%{source}\"?"
msgstr ""
msgid "Members|Are you sure you want to revoke the invitation for %{inviteEmail} to join \"%{source}\""
@ -22011,12 +22011,21 @@ msgstr ""
msgid "OnCallSchedules|For this rotation, on-call will be:"
msgstr ""
msgid "OnCallSchedules|On-call schedule %{schedule} in Project %{project}"
msgstr ""
msgid "OnCallSchedules|On-call schedules"
msgstr ""
msgid "OnCallSchedules|Please note, rotations with shifts that are less than four hours are currently not supported in the weekly view."
msgstr ""
msgid "OnCallSchedules|Removing this user may put their on-call team at risk of missing a notification."
msgstr ""
msgid "OnCallSchedules|Removing yourself may put your on-call team at risk of missing a notification."
msgstr ""
msgid "OnCallSchedules|Restrict to time intervals"
msgstr ""
@ -22071,12 +22080,18 @@ msgstr ""
msgid "OnCallSchedules|Try adding a rotation"
msgstr ""
msgid "OnCallSchedules|User %{name} is currently part of:"
msgstr ""
msgid "OnCallSchedules|View next timeframe"
msgstr ""
msgid "OnCallSchedules|View previous timeframe"
msgstr ""
msgid "OnCallSchedules|You are currently a part of:"
msgstr ""
msgid "OnCallSchedules|Your schedule has been successfully created and all alerts from this project will now be routed to this schedule. Currently, only one schedule can be created per project. More coming soon! To add individual users to this schedule, use the add a rotation button."
msgstr ""
@ -37323,15 +37338,15 @@ msgstr ""
msgid "mrWidget|Fast-forward merge is not possible. To merge this request, first rebase locally."
msgstr ""
msgid "mrWidget|Fork merge requests do not create merge request pipelines which validate a post merge result"
msgstr ""
msgid "mrWidget|Fork project merge requests do not create merge request pipelines that validate a post merge result unless invoked by a project member."
msgstr ""
msgid "mrWidget|If the %{missingBranchName} branch exists in your local repository, you can merge this merge request manually using the command line"
msgstr ""
msgid "mrWidget|If the last pipeline ran in the fork project, it may be inaccurate. Before merge, we advise running a pipeline in this project."
msgstr ""
msgid "mrWidget|Jump to first unresolved thread"
msgstr ""

View File

@ -55,11 +55,11 @@ module DeprecationToolkitEnv
# one by one
def self.allowed_kwarg_warning_paths
%w[
activerecord-6.0.3.4/lib/active_record/migration.rb
activesupport-6.0.3.4/lib/active_support/cache.rb
activerecord-6.0.3.6/lib/active_record/migration.rb
activesupport-6.0.3.6/lib/active_support/cache.rb
batch-loader-1.4.0/lib/batch_loader/graphql.rb
carrierwave-1.3.1/lib/carrierwave/sanitized_file.rb
activerecord-6.0.3.4/lib/active_record/relation.rb
activerecord-6.0.3.6/lib/active_record/relation.rb
selenium-webdriver-3.142.7/lib/selenium/webdriver/firefox/driver.rb
asciidoctor-2.0.12/lib/asciidoctor/extensions.rb
]

View File

@ -15,7 +15,7 @@ RSpec.describe 'Projects > Settings > For a forked project', :js do
end
shared_examples 'project settings for a forked projects' do
it 'allows deleting the link to the forked project' do
it 'allows deleting the link to the forked project', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/327817' do
visit edit_project_path(forked_project)
click_button 'Remove fork relationship'

View File

@ -183,7 +183,7 @@ RSpec.describe 'Project' do
visit edit_project_path(project)
end
it 'removes fork' do
it 'removes fork', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/327817' do
expect(page).to have_content 'Remove fork relationship'
remove_with_confirm('Remove fork relationship', project.path)

View File

@ -16,8 +16,6 @@ RSpec.describe Projects::IssuesController, '(JavaScript fixtures)', type: :contr
end
before do
stub_feature_flags(boards_filtered_search: false)
project.add_maintainer(user)
sign_in(user)
end

View File

@ -38,6 +38,7 @@ describe('RemoveMemberButton', () => {
title: 'Remove member',
isAccessRequest: true,
isInvite: true,
oncallSchedules: { name: 'user', schedules: [] },
...propsData,
},
directives: {
@ -59,6 +60,7 @@ describe('RemoveMemberButton', () => {
'data-message': 'Are you sure you want to remove John Smith?',
'data-is-access-request': 'true',
'data-is-invite': 'true',
'data-oncall-schedules': '{"name":"user","schedules":[]}',
'aria-label': 'Remove member',
title: 'Remove member',
icon: 'remove',

View File

@ -40,11 +40,15 @@ describe('UserActionButtons', () => {
expect(findRemoveMemberButton().props()).toEqual({
memberId: member.id,
memberType: 'GroupMember',
message: `Are you sure you want to remove ${member.user.name} from "${member.source.fullName}"`,
message: `Are you sure you want to remove ${member.user.name} from "${member.source.fullName}"?`,
title: 'Remove member',
isAccessRequest: false,
isInvite: false,
icon: 'remove',
oncallSchedules: {
name: member.user.name,
schedules: member.user.oncallSchedules,
},
});
});
@ -58,7 +62,7 @@ describe('UserActionButtons', () => {
});
expect(findRemoveMemberButton().props('message')).toBe(
`Are you sure you want to remove this orphaned member from "${orphanedMember.source.fullName}"`,
`Are you sure you want to remove this orphaned member from "${orphanedMember.source.fullName}"?`,
);
});
});

View File

@ -1,10 +1,12 @@
import { GlModal, GlForm } from '@gitlab/ui';
import { within } from '@testing-library/dom';
import { mount, createLocalVue, createWrapper } from '@vue/test-utils';
import { cloneDeep } from 'lodash';
import { nextTick } from 'vue';
import Vuex from 'vuex';
import LeaveModal from '~/members/components/modals/leave_modal.vue';
import { LEAVE_MODAL_ID, MEMBER_TYPES } from '~/members/constants';
import OncallSchedulesList from '~/vue_shared/components/oncall_schedules_list.vue';
import { member } from '../../mock_data';
jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' }));
@ -47,9 +49,9 @@ describe('LeaveModal', () => {
});
};
const findModal = () => wrapper.find(GlModal);
const findForm = () => findModal().find(GlForm);
const findModal = () => wrapper.findComponent(GlModal);
const findForm = () => findModal().findComponent(GlForm);
const findOncallSchedulesList = () => findModal().findComponent(OncallSchedulesList);
const getByText = (text, options) =>
createWrapper(within(findModal().element).getByText(text, options));
@ -87,6 +89,24 @@ describe('LeaveModal', () => {
);
});
describe('On-call schedules list', () => {
it("displays oncall schedules list when member's user is part of on-call schedules ", () => {
const schedulesList = findOncallSchedulesList();
expect(schedulesList.exists()).toBe(true);
expect(schedulesList.props()).toMatchObject({
isCurrentUser: true,
schedules: member.user.oncallSchedules,
});
});
it("does NOT display oncall schedules list when member's user is NOT a part of on-call schedules ", () => {
const memberWithoutOncallSchedules = cloneDeep(member);
delete (memberWithoutOncallSchedules, 'user.oncallSchedules');
createComponent({ member: memberWithoutOncallSchedules });
expect(findOncallSchedulesList().exists()).toBe(false);
});
});
it('submits the form when "Leave" button is clicked', () => {
const submitSpy = jest.spyOn(findForm().element, 'submit');

View File

@ -20,6 +20,7 @@ export const member = {
avatarUrl: 'https://www.gravatar.com/avatar/4816142ef496f956a277bedf1a40607b?s=80&d=identicon',
blocked: false,
twoFactorEnabled: false,
oncallSchedules: [{ name: 'schedule 1' }],
},
id: 238,
createdAt: '2020-07-17T16:22:46.923Z',

View File

@ -1,4 +1,5 @@
import { shallowMount } from '@vue/test-utils';
import waitForPromises from 'helpers/wait_for_promises';
import { HIGHLIGHT_CLASS_NAME } from '~/vue_shared/components/blob_viewers/constants';
import SimpleViewer from '~/vue_shared/components/blob_viewers/simple_viewer.vue';
import EditorLite from '~/vue_shared/components/editor_lite.vue';
@ -8,10 +9,17 @@ describe('Blob Simple Viewer component', () => {
const contentMock = `<span id="LC1">First</span>\n<span id="LC2">Second</span>\n<span id="LC3">Third</span>`;
const blobHash = 'foo-bar';
function createComponent(content = contentMock, isRawContent = false) {
function createComponent(
content = contentMock,
isRawContent = false,
isRefactorFlagEnabled = false,
) {
wrapper = shallowMount(SimpleViewer, {
provide: {
blobHash,
glFeatures: {
refactorBlobViewer: isRefactorFlagEnabled,
},
},
propsData: {
content,
@ -87,17 +95,31 @@ describe('Blob Simple Viewer component', () => {
});
});
describe('raw content', () => {
describe('Vue refactoring to use Source Editor', () => {
const findEditorLite = () => wrapper.find(EditorLite);
const isRawContent = true;
it('uses the Editor Lite component in readonly mode when viewing raw content', () => {
createComponent('raw content', isRawContent);
it.each`
doesRender | condition | isRawContent | isRefactorFlagEnabled
${'Does not'} | ${'rawContent is not specified'} | ${false} | ${true}
${'Does not'} | ${'feature flag is disabled is not specified'} | ${true} | ${false}
${'Does not'} | ${'both, the FF and rawContent are not specified'} | ${false} | ${false}
${'Does'} | ${'both, the FF and rawContent are specified'} | ${true} | ${true}
`(
'$doesRender render Editor Lite component in readonly mode when $condition',
async ({ isRawContent, isRefactorFlagEnabled } = {}) => {
createComponent('raw content', isRawContent, isRefactorFlagEnabled);
await waitForPromises();
expect(findEditorLite().exists()).toBe(true);
expect(findEditorLite().props('value')).toBe('raw content');
expect(findEditorLite().props('fileName')).toBe('test.js');
expect(findEditorLite().props('editorOptions')).toEqual({ readOnly: true });
});
if (isRawContent && isRefactorFlagEnabled) {
expect(findEditorLite().exists()).toBe(true);
expect(findEditorLite().props('value')).toBe('raw content');
expect(findEditorLite().props('fileName')).toBe('test.js');
expect(findEditorLite().props('editorOptions')).toEqual({ readOnly: true });
} else {
expect(findEditorLite().exists()).toBe(false);
}
},
);
});
});

View File

@ -1,5 +1,6 @@
import { GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import ApplySuggestion from '~/vue_shared/components/markdown/apply_suggestion.vue';
import SuggestionDiffHeader from '~/vue_shared/components/markdown/suggestion_diff_header.vue';
@ -22,6 +23,9 @@ describe('Suggestion Diff component', () => {
...DEFAULT_PROPS,
...props,
},
directives: {
GlTooltip: createMockDirective(),
},
});
};
@ -218,15 +222,23 @@ describe('Suggestion Diff component', () => {
});
describe('tooltip message for apply button', () => {
const findTooltip = () => getBinding(findApplyButton().element, 'gl-tooltip');
it('renders correct tooltip message when button is applicable', () => {
createComponent();
expect(wrapper.vm.tooltipMessage).toBe('This also resolves this thread');
const tooltip = findTooltip();
expect(tooltip.modifiers.viewport).toBe(true);
expect(tooltip.value).toBe('This also resolves this thread');
});
it('renders the inapplicable reason in the tooltip when button is not applicable', () => {
const inapplicableReason = 'lorem';
createComponent({ canApply: false, inapplicableReason });
expect(wrapper.vm.tooltipMessage).toBe(inapplicableReason);
const tooltip = findTooltip();
expect(tooltip.modifiers.viewport).toBe(true);
expect(tooltip.value).toBe(inapplicableReason);
});
});
});

View File

@ -1,13 +1,25 @@
import { GlModal } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import OncallSchedulesList from '~/vue_shared/components/oncall_schedules_list.vue';
import RemoveMemberModal from '~/vue_shared/components/remove_member_modal.vue';
const mockSchedules = JSON.stringify({
schedules: [
{
id: 1,
name: 'Schedule 1',
},
],
name: 'User1',
});
describe('RemoveMemberModal', () => {
const memberPath = '/gitlab-org/gitlab-test/-/project_members/90';
let wrapper;
const findForm = () => wrapper.find({ ref: 'form' });
const findGlModal = () => wrapper.find(GlModal);
const findGlModal = () => wrapper.findComponent(GlModal);
const findOnCallSchedulesList = () => wrapper.findComponent(OncallSchedulesList);
afterEach(() => {
wrapper.destroy();
@ -15,11 +27,11 @@ describe('RemoveMemberModal', () => {
});
describe.each`
state | memberType | isAccessRequest | isInvite | actionText | removeSubMembershipsCheckboxExpected | unassignIssuablesCheckboxExpected | message
${'removing a group member'} | ${'GroupMember'} | ${'false'} | ${'false'} | ${'Remove member'} | ${true} | ${true} | ${'Are you sure you want to remove Jane Doe from the Gitlab Org / Gitlab Test project?'}
${'removing a project member'} | ${'ProjectMember'} | ${'false'} | ${'false'} | ${'Remove member'} | ${false} | ${true} | ${'Are you sure you want to remove Jane Doe from the Gitlab Org / Gitlab Test project?'}
${'denying an access request'} | ${'ProjectMember'} | ${'true'} | ${'false'} | ${'Deny access request'} | ${false} | ${false} | ${"Are you sure you want to deny Jane Doe's request to join the Gitlab Org / Gitlab Test project?"}
${'revoking invite'} | ${'ProjectMember'} | ${'false'} | ${'true'} | ${'Revoke invite'} | ${false} | ${false} | ${'Are you sure you want to revoke the invitation for foo@bar.com to join the Gitlab Org / Gitlab Test project?'}
state | memberType | isAccessRequest | isInvite | actionText | removeSubMembershipsCheckboxExpected | unassignIssuablesCheckboxExpected | message | onCallSchedules
${'removing a group member'} | ${'GroupMember'} | ${false} | ${'false'} | ${'Remove member'} | ${true} | ${true} | ${'Are you sure you want to remove Jane Doe from the Gitlab Org / Gitlab Test project?'} | ${`{}`}
${'removing a project member'} | ${'ProjectMember'} | ${false} | ${'false'} | ${'Remove member'} | ${false} | ${true} | ${'Are you sure you want to remove Jane Doe from the Gitlab Org / Gitlab Test project?'} | ${mockSchedules}
${'denying an access request'} | ${'ProjectMember'} | ${true} | ${'false'} | ${'Deny access request'} | ${false} | ${false} | ${"Are you sure you want to deny Jane Doe's request to join the Gitlab Org / Gitlab Test project?"} | ${`{}`}
${'revoking invite'} | ${'ProjectMember'} | ${false} | ${'true'} | ${'Revoke invite'} | ${false} | ${false} | ${'Are you sure you want to revoke the invitation for foo@bar.com to join the Gitlab Org / Gitlab Test project?'} | ${mockSchedules}
`(
'when $state',
({
@ -30,6 +42,7 @@ describe('RemoveMemberModal', () => {
message,
removeSubMembershipsCheckboxExpected,
unassignIssuablesCheckboxExpected,
onCallSchedules,
}) => {
beforeEach(() => {
wrapper = shallowMount(RemoveMemberModal, {
@ -41,12 +54,16 @@ describe('RemoveMemberModal', () => {
message,
memberPath,
memberType,
onCallSchedules,
},
};
},
});
});
const parsedSchedules = JSON.parse(onCallSchedules);
const isPartOfOncallSchedules = Boolean(isAccessRequest && parsedSchedules.schedules?.length);
it(`has the title ${actionText}`, () => {
expect(findGlModal().attributes('title')).toBe(actionText);
});
@ -75,6 +92,10 @@ describe('RemoveMemberModal', () => {
);
});
it(`shows ${isPartOfOncallSchedules ? 'all' : 'no'} related on-call schedules`, () => {
expect(findOnCallSchedulesList().exists()).toBe(isPartOfOncallSchedules);
});
it('submits the form when the modal is submitted', () => {
const spy = jest.spyOn(findForm().element, 'submit');

View File

@ -0,0 +1,87 @@
import { GlLink, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import OncallSchedulesList from '~/vue_shared/components/oncall_schedules_list.vue';
const mockSchedules = [
{
name: 'Schedule 1',
scheduleUrl: 'http://gitlab.com/gitlab-org/gitlab-shell/-/oncall_schedules',
projectName: 'Shell',
projectUrl: 'http://gitlab.com/gitlab-org/gitlab-shell/',
},
{
name: 'Schedule 2',
scheduleUrl: 'http://gitlab.com/gitlab-org/gitlab-ui/-/oncall_schedules',
projectName: 'UI',
projectUrl: 'http://gitlab.com/gitlab-org/gitlab-ui/',
},
];
const userName = 'User 1';
describe('On-call schedules list', () => {
let wrapper;
function createComponent(props) {
wrapper = extendedWrapper(
shallowMount(OncallSchedulesList, {
propsData: {
schedules: mockSchedules,
userName,
...props,
},
stubs: {
GlSprintf,
},
}),
);
}
afterEach(() => {
wrapper.destroy();
});
const findLinks = () => wrapper.findAllComponents(GlLink);
const findTitle = () => wrapper.findByTestId('title');
const findFooter = () => wrapper.findByTestId('footer');
const findSchedules = () => wrapper.findByTestId('schedules-list');
describe.each`
isCurrentUser | titleText | footerText
${true} | ${'You are currently a part of:'} | ${'Removing yourself may put your on-call team at risk of missing a notification.'}
${false} | ${`User ${userName} is currently part of:`} | ${'Removing this user may put their on-call team at risk of missing a notification.'}
`('when current user ', ({ isCurrentUser, titleText, footerText }) => {
it(`${isCurrentUser ? 'is' : 'is not'} a part of on-call schedule`, async () => {
createComponent({
isCurrentUser,
});
expect(findTitle().text()).toBe(titleText);
expect(findFooter().text()).toBe(footerText);
});
});
describe.each(mockSchedules)(
'renders each on-call schedule data',
({ name, scheduleUrl, projectName, projectUrl }) => {
beforeEach(() => {
createComponent({ schedules: [{ name, scheduleUrl, projectName, projectUrl }] });
});
it(`renders schedule ${name}'s name and link`, () => {
const msg = findSchedules().text();
expect(msg).toContain(`On-call schedule ${name}`);
expect(findLinks().at(0).attributes('href')).toBe(scheduleUrl);
});
it(`renders project ${projectName}'s name and link`, () => {
const msg = findSchedules().text();
expect(msg).toContain(`in Project ${projectName}`);
expect(findLinks().at(1).attributes('href')).toBe(projectUrl);
});
},
);
});

View File

@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Gitlab::Pagination::OffsetHeaderBuilder, type: :controller do
controller(ActionController::Base) do
def index
relation = Project.where(archived: params[:archived]).page(params[:page]).per(1)
relation = Project.where(archived: params[:archived]).page(params[:page]).order(:id).per(1)
params_for_pagination = { archived: params[:archived], page: params[:page] }
@ -22,7 +22,7 @@ RSpec.describe Gitlab::Pagination::OffsetHeaderBuilder, type: :controller do
end
end
let_it_be(:projects) { create_list(:project, 2, archived: true) }
let_it_be(:projects) { create_list(:project, 2, archived: true).sort_by(&:id) }
describe 'pagination' do
it 'returns correct result for the first page' do

View File

@ -681,39 +681,178 @@ RSpec.describe Group do
end
end
describe '#last_blocked_owner?' do
let(:blocked_user) { create(:user, :blocked) }
describe '#member_last_blocked_owner?' do
let_it_be(:blocked_user) { create(:user, :blocked) }
let(:member) { blocked_user.group_members.last }
before do
group.add_user(blocked_user, GroupMember::OWNER)
end
it { expect(group.last_blocked_owner?(blocked_user)).to be_truthy }
context 'with another active owner' do
context 'when last_blocked_owner is set' do
before do
group.add_user(create(:user), GroupMember::OWNER)
expect(group).not_to receive(:members_with_parents)
end
it { expect(group.last_blocked_owner?(blocked_user)).to be_falsy }
it 'returns true' do
member.last_blocked_owner = true
expect(group.member_last_blocked_owner?(member)).to be(true)
end
it 'returns false' do
member.last_blocked_owner = false
expect(group.member_last_blocked_owner?(member)).to be(false)
end
end
context 'with 2 blocked owners' do
before do
group.add_user(create(:user, :blocked), GroupMember::OWNER)
context 'when last_blocked_owner is not set' do
it { expect(group.member_last_blocked_owner?(member)).to be(true) }
context 'with another active owner' do
before do
group.add_user(create(:user), GroupMember::OWNER)
end
it { expect(group.member_last_blocked_owner?(member)).to be(false) }
end
it { expect(group.last_blocked_owner?(blocked_user)).to be_falsy }
context 'with 2 blocked owners' do
before do
group.add_user(create(:user, :blocked), GroupMember::OWNER)
end
it { expect(group.member_last_blocked_owner?(member)).to be(false) }
end
context 'with owners from a parent' do
before do
parent_group = create(:group)
create(:group_member, :owner, group: parent_group)
group.update(parent: parent_group)
end
it { expect(group.member_last_blocked_owner?(member)).to be(false) }
end
end
end
context 'when analyzing blocked owners' do
let_it_be(:blocked_user) { create(:user, :blocked) }
describe '#single_blocked_owner?' do
context 'when there is only one blocked owner' do
before do
group.add_user(blocked_user, GroupMember::OWNER)
end
it 'returns true' do
expect(group.single_blocked_owner?).to eq(true)
end
end
context 'when there are multiple blocked owners' do
let_it_be(:blocked_user_2) { create(:user, :blocked) }
before do
group.add_user(blocked_user, GroupMember::OWNER)
group.add_user(blocked_user_2, GroupMember::OWNER)
end
it 'returns true' do
expect(group.single_blocked_owner?).to eq(false)
end
end
context 'when there are no blocked owners' do
it 'returns false' do
expect(group.single_blocked_owner?).to eq(false)
end
end
end
context 'with owners from a parent' do
describe '#blocked_owners' do
let_it_be(:user) { create(:user) }
before do
parent_group = create(:group)
create(:group_member, :owner, group: parent_group)
group.update(parent: parent_group)
group.add_user(blocked_user, GroupMember::OWNER)
group.add_user(user, GroupMember::OWNER)
end
it { expect(group.last_blocked_owner?(blocked_user)).to be_falsy }
it 'has only blocked owners' do
expect(group.blocked_owners.map(&:user)).to match([blocked_user])
end
end
end
describe '#single_owner?' do
let_it_be(:user) { create(:user) }
context 'when there is only one owner' do
before do
group.add_user(user, GroupMember::OWNER)
end
it 'returns true' do
expect(group.single_owner?).to eq(true)
end
end
context 'when there are multiple owners' do
let_it_be(:user_2) { create(:user) }
before do
group.add_user(user, GroupMember::OWNER)
group.add_user(user_2, GroupMember::OWNER)
end
it 'returns true' do
expect(group.single_owner?).to eq(false)
end
end
context 'when there are no owners' do
it 'returns false' do
expect(group.single_owner?).to eq(false)
end
end
end
describe '#member_last_owner?' do
let_it_be(:user) { create(:user) }
let(:member) { group.members.last }
before do
group.add_user(user, GroupMember::OWNER)
end
context 'when last_owner is set' do
before do
expect(group).not_to receive(:last_owner?)
end
it 'returns true' do
member.last_owner = true
expect(group.member_last_owner?(member)).to be(true)
end
it 'returns false' do
member.last_owner = false
expect(group.member_last_owner?(member)).to be(false)
end
end
context 'when last_owner is not set' do
it 'returns true' do
expect(group).to receive(:last_owner?).and_call_original
expect(group.member_last_owner?(member)).to be(true)
end
end
end

View File

@ -0,0 +1,98 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Members::LastGroupOwnerAssigner do
describe "#execute" do
let_it_be(:user, reload: true) { create(:user) }
let_it_be(:group) { create(:group) }
let(:group_member) { user.members.last }
subject(:assigner) { described_class.new(group, [group_member]) }
before do
group.add_owner(user)
end
it "avoids extra database queries utilizing memoization", :aggregate_failures do
control = ActiveRecord::QueryRecorder.new { assigner.execute }
count_queries = control.occurrences_by_line_method.first[1][:occurrences].find_all { |i| i.include?('SELECT COUNT') }
expect(control.count).to be <= 5
expect(count_queries.count).to eq(0)
end
context "when there are unblocked owners" do
context "with one unblocked owner" do
specify do
expect { assigner.execute }.to change(group_member, :last_owner)
.from(nil).to(true)
.and change(group_member, :last_blocked_owner)
.from(nil).to(false)
end
end
context "with multiple unblocked owners" do
let_it_be(:unblocked_owner_member) { create(:group_member, :owner, source: group) }
specify do
expect { assigner.execute }.to change(group_member, :last_owner)
.from(nil).to(false)
.and change(group_member, :last_blocked_owner)
.from(nil).to(false)
end
it "has many members passed" do
assigner = described_class.new(group, [unblocked_owner_member, group_member])
expect { assigner.execute }.to change(group_member, :last_owner)
.from(nil).to(false)
.and change(group_member, :last_blocked_owner)
.from(nil).to(false)
.and change(unblocked_owner_member, :last_owner)
.from(nil).to(false)
.and change(unblocked_owner_member, :last_blocked_owner)
.from(nil).to(false)
end
end
end
context "when there are blocked owners" do
before do
user.block!
end
context "with one blocked owner" do
specify do
expect { assigner.execute }.to change(group_member, :last_owner)
.from(nil).to(false)
.and change(group_member, :last_blocked_owner)
.from(nil).to(true)
end
end
context "with multiple unblocked owners" do
specify do
create_list(:group_member, 2, :owner, source: group)
expect { assigner.execute }.to change(group_member, :last_owner)
.from(nil).to(false)
.and change(group_member, :last_blocked_owner)
.from(nil).to(false)
end
end
context "with multiple blocked owners" do
specify do
create(:group_member, :owner, :blocked, source: group)
expect { assigner.execute }.to change(group_member, :last_owner)
.from(nil).to(false)
.and change(group_member, :last_blocked_owner)
.from(nil).to(false)
end
end
end
end
end

View File

@ -54,7 +54,7 @@ RSpec.describe Packages::Maven::Metadatum, type: :model do
let_it_be(:metadatum3) { create(:maven_metadatum, package: package) }
let_it_be(:metadatum4) { create(:maven_metadatum, package: package) }
subject { Packages::Maven::Metadatum.for_package_ids(package.id).order_created }
subject { described_class.for_package_ids(package.id).order_created }
it { is_expected.to eq([metadatum1, metadatum2, metadatum3, metadatum4]) }
end
@ -64,10 +64,20 @@ RSpec.describe Packages::Maven::Metadatum, type: :model do
let_it_be(:metadatum2) { create(:maven_metadatum, package: package, app_name: 'two') }
let_it_be(:metadatum3) { create(:maven_metadatum, package: package, app_name: 'three') }
subject { Packages::Maven::Metadatum.for_package_ids(package.id).pluck_app_name }
subject { described_class.for_package_ids(package.id).pluck_app_name }
it { is_expected.to match_array([metadatum1, metadatum2, metadatum3].map(&:app_name)) }
end
describe '.with_path' do
let_it_be(:metadatum1) { create(:maven_metadatum, package: package, path: 'one') }
let_it_be(:metadatum2) { create(:maven_metadatum, package: package, path: 'two') }
let_it_be(:metadatum3) { create(:maven_metadatum, package: package, path: 'three') }
subject { described_class.with_path('two') }
it { is_expected.to match_array([metadatum2]) }
end
end
end
end

View File

@ -47,7 +47,21 @@ RSpec.describe API::MavenPackages do
end
end
shared_examples 'processing HEAD requests' do
shared_examples 'rejecting the request for non existing maven path' do |expected_status: :not_found|
before do
if Feature.enabled?(:check_maven_path_first)
expect(::Packages::Maven::PackageFinder).not_to receive(:new)
end
end
it 'rejects the request' do
subject
expect(response).to have_gitlab_http_status(expected_status)
end
end
shared_examples 'processing HEAD requests' do |instance_level: false|
subject { head api(url) }
before do
@ -92,6 +106,12 @@ RSpec.describe API::MavenPackages do
subject
end
context 'with a non existing maven path' do
let(:path) { 'foo/bar/1.2.3' }
it_behaves_like 'rejecting the request for non existing maven path', expected_status: instance_level ? :forbidden : :not_found
end
end
end
@ -99,9 +119,8 @@ RSpec.describe API::MavenPackages do
context 'successful download' do
subject do
download_file(
package_file.file_name,
{},
Gitlab::Auth::AuthFinders::DEPLOY_TOKEN_HEADER => deploy_token.token
file_name: package_file.file_name,
request_headers: { Gitlab::Auth::AuthFinders::DEPLOY_TOKEN_HEADER => deploy_token.token }
)
end
@ -126,7 +145,7 @@ RSpec.describe API::MavenPackages do
shared_examples 'downloads with a job token' do
context 'with a running job' do
it 'allows download with job token' do
download_file(package_file.file_name, job_token: job.token)
download_file(file_name: package_file.file_name, params: { job_token: job.token })
expect(response).to have_gitlab_http_status(:ok)
expect(response.media_type).to eq('application/octet-stream')
@ -139,7 +158,7 @@ RSpec.describe API::MavenPackages do
end
it 'returns unauthorized error' do
download_file(package_file.file_name, job_token: job.token)
download_file(file_name: package_file.file_name, params: { job_token: job.token })
expect(response).to have_gitlab_http_status(:unauthorized)
end
@ -149,7 +168,7 @@ RSpec.describe API::MavenPackages do
describe 'GET /api/v4/packages/maven/*path/:file_name' do
shared_examples 'handling all conditions' do
context 'a public project' do
subject { download_file(package_file.file_name) }
subject { download_file(file_name: package_file.file_name) }
it_behaves_like 'tracking the file download event'
@ -161,12 +180,18 @@ RSpec.describe API::MavenPackages do
end
it 'returns sha1 of the file' do
download_file(package_file.file_name + '.sha1')
download_file(file_name: package_file.file_name + '.sha1')
expect(response).to have_gitlab_http_status(:ok)
expect(response.media_type).to eq('text/plain')
expect(response.body).to eq(package_file.file_sha1)
end
context 'with a non existing maven path' do
subject { download_file(file_name: package_file.file_name, path: 'foo/bar/1.2.3') }
it_behaves_like 'rejecting the request for non existing maven path', expected_status: :forbidden
end
end
context 'internal project' do
@ -175,7 +200,7 @@ RSpec.describe API::MavenPackages do
project.update!(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
end
subject { download_file_with_token(package_file.file_name) }
subject { download_file_with_token(file_name: package_file.file_name) }
it_behaves_like 'tracking the file download event'
@ -187,7 +212,7 @@ RSpec.describe API::MavenPackages do
end
it 'denies download when no private token' do
download_file(package_file.file_name)
download_file(file_name: package_file.file_name)
expect(response).to have_gitlab_http_status(:forbidden)
end
@ -195,10 +220,16 @@ RSpec.describe API::MavenPackages do
it_behaves_like 'downloads with a job token'
it_behaves_like 'downloads with a deploy token'
context 'with a non existing maven path' do
subject { download_file_with_token(file_name: package_file.file_name, path: 'foo/bar/1.2.3') }
it_behaves_like 'rejecting the request for non existing maven path', expected_status: :forbidden
end
end
context 'private project' do
subject { download_file_with_token(package_file.file_name) }
subject { download_file_with_token(file_name: package_file.file_name) }
before do
project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
@ -222,7 +253,7 @@ RSpec.describe API::MavenPackages do
end
it 'denies download when no private token' do
download_file(package_file.file_name)
download_file(file_name: package_file.file_name)
expect(response).to have_gitlab_http_status(:forbidden)
end
@ -241,13 +272,18 @@ RSpec.describe API::MavenPackages do
unauthorized_deploy_token.update!(id: another_user.id)
download_file(
package_file.file_name,
{},
Gitlab::Auth::AuthFinders::DEPLOY_TOKEN_HEADER => unauthorized_deploy_token.token
file_name: package_file.file_name,
request_headers: { Gitlab::Auth::AuthFinders::DEPLOY_TOKEN_HEADER => unauthorized_deploy_token.token }
)
expect(response).to have_gitlab_http_status(:forbidden)
end
context 'with a non existing maven path' do
subject { download_file_with_token(file_name: package_file.file_name, path: 'foo/bar/1.2.3') }
it_behaves_like 'rejecting the request for non existing maven path', expected_status: :forbidden
end
end
context 'project name is different from a package name' do
@ -256,7 +292,7 @@ RSpec.describe API::MavenPackages do
end
it 'rejects request' do
download_file(package_file.file_name)
download_file(file_name: package_file.file_name)
expect(response).to have_gitlab_http_status(:forbidden)
end
@ -279,26 +315,43 @@ RSpec.describe API::MavenPackages do
it_behaves_like 'handling all conditions'
end
def download_file(file_name, params = {}, request_headers = headers)
get api("/packages/maven/#{maven_metadatum.path}/#{file_name}"), params: params, headers: request_headers
context 'with check_maven_path_first enabled' do
before do
stub_feature_flags(check_maven_path_first: true)
end
it_behaves_like 'handling all conditions'
end
def download_file_with_token(file_name, params = {}, request_headers = headers_with_token)
download_file(file_name, params, request_headers)
context 'with check_maven_path_first disabled' do
before do
stub_feature_flags(check_maven_path_first: false)
end
it_behaves_like 'handling all conditions'
end
def download_file(file_name:, params: {}, request_headers: headers, path: maven_metadatum.path)
get api("/packages/maven/#{path}/#{file_name}"), params: params, headers: request_headers
end
def download_file_with_token(file_name:, params: {}, request_headers: headers_with_token, path: maven_metadatum.path)
download_file(file_name: file_name, params: params, request_headers: request_headers, path: path)
end
end
describe 'HEAD /api/v4/packages/maven/*path/:file_name' do
let(:url) { "/packages/maven/#{package.maven_metadatum.path}/#{package_file.file_name}" }
let(:path) { package.maven_metadatum.path }
let(:url) { "/packages/maven/#{path}/#{package_file.file_name}" }
it_behaves_like 'processing HEAD requests'
it_behaves_like 'processing HEAD requests', instance_level: true
context 'with maven_packages_group_level_improvements enabled' do
before do
stub_feature_flags(maven_packages_group_level_improvements: true)
end
it_behaves_like 'processing HEAD requests'
it_behaves_like 'processing HEAD requests', instance_level: true
end
context 'with maven_packages_group_level_improvements disabled' do
@ -306,7 +359,23 @@ RSpec.describe API::MavenPackages do
stub_feature_flags(maven_packages_group_level_improvements: false)
end
it_behaves_like 'processing HEAD requests'
it_behaves_like 'processing HEAD requests', instance_level: true
end
context 'with check_maven_path_first enabled' do
before do
stub_feature_flags(check_maven_path_first: true)
end
it_behaves_like 'processing HEAD requests', instance_level: true
end
context 'with check_maven_path_first disabled' do
before do
stub_feature_flags(check_maven_path_first: false)
end
it_behaves_like 'processing HEAD requests', instance_level: true
end
end
@ -318,7 +387,7 @@ RSpec.describe API::MavenPackages do
shared_examples 'handling all conditions' do
context 'a public project' do
subject { download_file(package_file.file_name) }
subject { download_file(file_name: package_file.file_name) }
it_behaves_like 'tracking the file download event'
@ -330,12 +399,18 @@ RSpec.describe API::MavenPackages do
end
it 'returns sha1 of the file' do
download_file(package_file.file_name + '.sha1')
download_file(file_name: package_file.file_name + '.sha1')
expect(response).to have_gitlab_http_status(:ok)
expect(response.media_type).to eq('text/plain')
expect(response.body).to eq(package_file.file_sha1)
end
context 'with a non existing maven path' do
subject { download_file(file_name: package_file.file_name, path: 'foo/bar/1.2.3') }
it_behaves_like 'rejecting the request for non existing maven path'
end
end
context 'internal project' do
@ -344,7 +419,7 @@ RSpec.describe API::MavenPackages do
project.update!(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
end
subject { download_file_with_token(package_file.file_name) }
subject { download_file_with_token(file_name: package_file.file_name) }
it_behaves_like 'tracking the file download event'
@ -356,7 +431,7 @@ RSpec.describe API::MavenPackages do
end
it 'denies download when no private token' do
download_file(package_file.file_name)
download_file(file_name: package_file.file_name)
expect(response).to have_gitlab_http_status(:not_found)
end
@ -364,6 +439,12 @@ RSpec.describe API::MavenPackages do
it_behaves_like 'downloads with a job token'
it_behaves_like 'downloads with a deploy token'
context 'with a non existing maven path' do
subject { download_file_with_token(file_name: package_file.file_name, path: 'foo/bar/1.2.3') }
it_behaves_like 'rejecting the request for non existing maven path'
end
end
context 'private project' do
@ -371,7 +452,7 @@ RSpec.describe API::MavenPackages do
project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
end
subject { download_file_with_token(package_file.file_name) }
subject { download_file_with_token(file_name: package_file.file_name) }
it_behaves_like 'tracking the file download event'
@ -392,7 +473,7 @@ RSpec.describe API::MavenPackages do
end
it 'denies download when no private token' do
download_file(package_file.file_name)
download_file(file_name: package_file.file_name)
expect(response).to have_gitlab_http_status(:not_found)
end
@ -401,8 +482,14 @@ RSpec.describe API::MavenPackages do
it_behaves_like 'downloads with a deploy token'
context 'with a non existing maven path' do
subject { download_file_with_token(file_name: package_file.file_name, path: 'foo/bar/1.2.3') }
it_behaves_like 'rejecting the request for non existing maven path'
end
context 'with group deploy token' do
subject { download_file_with_token(package_file.file_name, {}, group_deploy_token_headers) }
subject { download_file_with_token(file_name: package_file.file_name, request_headers: group_deploy_token_headers) }
it 'returns the file' do
subject
@ -419,13 +506,19 @@ RSpec.describe API::MavenPackages do
expect(response).to have_gitlab_http_status(:ok)
expect(response.media_type).to eq('application/octet-stream')
end
context 'with a non existing maven path' do
subject { download_file_with_token(file_name: package_file.file_name, path: 'foo/bar/1.2.3', request_headers: group_deploy_token_headers) }
it_behaves_like 'rejecting the request for non existing maven path'
end
end
context 'with a reporter from a subgroup accessing the root group' do
let_it_be(:root_group) { create(:group, :private) }
let_it_be(:group) { create(:group, :private, parent: root_group) }
subject { download_file_with_token(package_file.file_name, {}, headers_with_token, root_group.id) }
subject { download_file_with_token(file_name: package_file.file_name, request_headers: headers_with_token, group_id: root_group.id) }
before do
project.update!(namespace: group)
@ -438,6 +531,12 @@ RSpec.describe API::MavenPackages do
expect(response).to have_gitlab_http_status(:ok)
expect(response.media_type).to eq('application/octet-stream')
end
context 'with a non existing maven path' do
subject { download_file_with_token(file_name: package_file.file_name, path: 'foo/bar/1.2.3', request_headers: headers_with_token, group_id: root_group.id) }
it_behaves_like 'rejecting the request for non existing maven path'
end
end
end
@ -457,7 +556,7 @@ RSpec.describe API::MavenPackages do
let(:maven_metadatum) { package3.maven_metadatum }
subject { download_file_with_token(package_file3.file_name) }
subject { download_file_with_token(file_name: package_file3.file_name) }
before do
sub_group1.add_developer(user)
@ -511,17 +610,34 @@ RSpec.describe API::MavenPackages do
it_behaves_like 'handling all conditions'
end
def download_file(file_name, params = {}, request_headers = headers, group_id = group.id)
get api("/groups/#{group_id}/-/packages/maven/#{maven_metadatum.path}/#{file_name}"), params: params, headers: request_headers
context 'with check_maven_path_first enabled' do
before do
stub_feature_flags(check_maven_path_first: true)
end
it_behaves_like 'handling all conditions'
end
def download_file_with_token(file_name, params = {}, request_headers = headers_with_token, group_id = group.id)
download_file(file_name, params, request_headers, group_id)
context 'with check_maven_path_first disabled' do
before do
stub_feature_flags(check_maven_path_first: false)
end
it_behaves_like 'handling all conditions'
end
def download_file(file_name:, params: {}, request_headers: headers, path: maven_metadatum.path, group_id: group.id)
get api("/groups/#{group_id}/-/packages/maven/#{path}/#{file_name}"), params: params, headers: request_headers
end
def download_file_with_token(file_name:, params: {}, request_headers: headers_with_token, path: maven_metadatum.path, group_id: group.id)
download_file(file_name: file_name, params: params, request_headers: request_headers, path: path, group_id: group_id)
end
end
describe 'HEAD /api/v4/groups/:id/-/packages/maven/*path/:file_name' do
let(:url) { "/groups/#{group.id}/-/packages/maven/#{package.maven_metadatum.path}/#{package_file.file_name}" }
let(:path) { package.maven_metadatum.path }
let(:url) { "/groups/#{group.id}/-/packages/maven/#{path}/#{package_file.file_name}" }
context 'with maven_packages_group_level_improvements enabled' do
before do
@ -538,12 +654,28 @@ RSpec.describe API::MavenPackages do
it_behaves_like 'processing HEAD requests'
end
context 'with check_maven_path_first enabled' do
before do
stub_feature_flags(check_maven_path_first: true)
end
it_behaves_like 'processing HEAD requests'
end
context 'with check_maven_path_first disabled' do
before do
stub_feature_flags(check_maven_path_first: false)
end
it_behaves_like 'processing HEAD requests'
end
end
describe 'GET /api/v4/projects/:id/packages/maven/*path/:file_name' do
shared_examples 'handling all conditions' do
context 'a public project' do
subject { download_file(package_file.file_name) }
subject { download_file(file_name: package_file.file_name) }
it_behaves_like 'tracking the file download event'
@ -555,12 +687,18 @@ RSpec.describe API::MavenPackages do
end
it 'returns sha1 of the file' do
download_file(package_file.file_name + '.sha1')
download_file(file_name: package_file.file_name + '.sha1')
expect(response).to have_gitlab_http_status(:ok)
expect(response.media_type).to eq('text/plain')
expect(response.body).to eq(package_file.file_sha1)
end
context 'with a non existing maven path' do
subject { download_file(file_name: package_file.file_name, path: 'foo/bar/1.2.3') }
it_behaves_like 'rejecting the request for non existing maven path'
end
end
context 'private project' do
@ -568,7 +706,7 @@ RSpec.describe API::MavenPackages do
project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
end
subject { download_file_with_token(package_file.file_name) }
subject { download_file_with_token(file_name: package_file.file_name) }
it_behaves_like 'tracking the file download event'
@ -588,7 +726,7 @@ RSpec.describe API::MavenPackages do
end
it 'denies download when no private token' do
download_file(package_file.file_name)
download_file(file_name: package_file.file_name)
expect(response).to have_gitlab_http_status(:not_found)
end
@ -596,6 +734,12 @@ RSpec.describe API::MavenPackages do
it_behaves_like 'downloads with a job token'
it_behaves_like 'downloads with a deploy token'
context 'with a non existing maven path' do
subject { download_file_with_token(file_name: package_file.file_name, path: 'foo/bar/1.2.3') }
it_behaves_like 'rejecting the request for non existing maven path'
end
end
end
@ -615,18 +759,35 @@ RSpec.describe API::MavenPackages do
it_behaves_like 'handling all conditions'
end
def download_file(file_name, params = {}, request_headers = headers)
get api("/projects/#{project.id}/packages/maven/" \
"#{maven_metadatum.path}/#{file_name}"), params: params, headers: request_headers
context 'with check_maven_path_first enabled' do
before do
stub_feature_flags(check_maven_path_first: true)
end
it_behaves_like 'handling all conditions'
end
def download_file_with_token(file_name, params = {}, request_headers = headers_with_token)
download_file(file_name, params, request_headers)
context 'with check_maven_path_first disabled' do
before do
stub_feature_flags(check_maven_path_first: false)
end
it_behaves_like 'handling all conditions'
end
def download_file(file_name:, params: {}, request_headers: headers, path: maven_metadatum.path)
get api("/projects/#{project.id}/packages/maven/" \
"#{path}/#{file_name}"), params: params, headers: request_headers
end
def download_file_with_token(file_name:, params: {}, request_headers: headers_with_token, path: maven_metadatum.path)
download_file(file_name: file_name, params: params, request_headers: request_headers, path: path)
end
end
describe 'HEAD /api/v4/projects/:id/packages/maven/*path/:file_name' do
let(:url) { "/projects/#{project.id}/packages/maven/#{package.maven_metadatum.path}/#{package_file.file_name}" }
let(:path) { package.maven_metadatum.path }
let(:url) { "/projects/#{project.id}/packages/maven/#{path}/#{package_file.file_name}" }
context 'with maven_packages_group_level_improvements enabled' do
before do
@ -643,11 +804,27 @@ RSpec.describe API::MavenPackages do
it_behaves_like 'processing HEAD requests'
end
context 'with check_maven_path_first enabled' do
before do
stub_feature_flags(check_maven_path_first: true)
end
it_behaves_like 'processing HEAD requests'
end
context 'with check_maven_path_first disabled' do
before do
stub_feature_flags(check_maven_path_first: false)
end
it_behaves_like 'processing HEAD requests'
end
end
describe 'PUT /api/v4/projects/:id/packages/maven/*path/:file_name/authorize' do
it 'rejects a malicious request' do
put api("/projects/#{project.id}/packages/maven/com/example/my-app/#{version}/%2e%2e%2F.ssh%2Fauthorized_keys/authorize"), params: {}, headers: headers_with_token
put api("/projects/#{project.id}/packages/maven/com/example/my-app/#{version}/%2e%2e%2F.ssh%2Fauthorized_keys/authorize"), headers: headers_with_token
expect(response).to have_gitlab_http_status(:bad_request)
end

View File

@ -7,28 +7,77 @@ RSpec.describe MemberSerializer do
let_it_be(:current_user) { create(:user) }
subject { described_class.new.represent(members, { current_user: current_user, group: group, source: source }) }
subject(:representation) do
described_class.new.represent(members, { current_user: current_user, group: group, source: source }).to_json
end
shared_examples 'members.json' do
it 'matches json schema' do
expect(subject.to_json).to match_schema('members')
end
it { is_expected.to match_schema('members') }
end
context 'group member' do
let(:group) { create(:group) }
let_it_be(:group) { create(:group) }
let_it_be(:members) { present_members(create_list(:group_member, 1, group: group)) }
let(:source) { group }
let(:members) { present_members(create_list(:group_member, 1, group: group)) }
it_behaves_like 'members.json'
it 'handles last group owner assignment' do
group_member = members.last
expect { representation }.to change(group_member, :last_owner)
.from(nil).to(true)
.and change(group_member, :last_blocked_owner).from(nil).to(false)
end
context "with LastGroupOwnerAssigner query improvements" do
it "avoids N+1 database queries for last group owner assignment in MembersPresenter" do
group_member = create(:group_member, group: group)
control_count = ActiveRecord::QueryRecorder.new { member_last_owner_with_preload([group_member]) }.count
group_members = create_list(:group_member, 3, group: group)
expect { member_last_owner_with_preload(group_members) }.not_to exceed_query_limit(control_count)
end
it "avoids N+1 database queries for last blocked owner assignment in MembersPresenter" do
group_member = create(:group_member, group: group)
control_count = ActiveRecord::QueryRecorder.new { member_last_blocked_owner_with_preload([group_member]) }.count
group_members = create_list(:group_member, 3, group: group)
expect { member_last_blocked_owner_with_preload(group_members) }.not_to exceed_query_limit(control_count)
end
def member_last_owner_with_preload(members)
assigner_with_preload(members)
members.map { |m| group.member_last_owner?(m) }
end
def member_last_blocked_owner_with_preload(members)
assigner_with_preload(members)
members.map { |m| group.member_last_blocked_owner?(m) }
end
def assigner_with_preload(members)
MembersPreloader.new(members).preload_all
Members::LastGroupOwnerAssigner.new(group, members).execute
end
end
end
context 'project member' do
let(:project) { create(:project) }
let_it_be(:project) { create(:project) }
let_it_be(:members) { present_members(create_list(:project_member, 1, project: project)) }
let(:source) { project }
let(:group) { project.group }
let(:members) { present_members(create_list(:project_member, 1, project: project)) }
it_behaves_like 'members.json'
it 'does not invoke group owner assignment' do
expect(Members::LastGroupOwnerAssigner).not_to receive(:new)
representation
end
end
end

View File

@ -266,12 +266,6 @@ RSpec.configure do |config|
stub_feature_flags(unified_diff_components: false)
# Disable this feature flag as we iterate and
# refactor filtered search to use gitlab ui
# components to meet feature parody. More details found
# https://gitlab.com/groups/gitlab-org/-/epics/5501
stub_feature_flags(boards_filtered_search: false)
# The following `vue_issues_list` stub can be removed once the
# Vue issues page has feature parity with the current Haml page
stub_feature_flags(vue_issues_list: false)