Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
3b963d6919
commit
520f317866
|
|
@ -1 +1 @@
|
|||
78d2b0cdb08b0e45de5324e2ac992282b7ecf691
|
||||
0fe0cfaccc979592610cbf65807f19b307957750
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
8.39.0
|
||||
8.41.0
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
<script>
|
||||
import { __ } from '~/locale';
|
||||
import activeDiscussionQuery from '../graphql/queries/active_discussion.query.graphql';
|
||||
import updateActiveDiscussionMutation from '../graphql/mutations/update_active_discussion.mutation.graphql';
|
||||
import DesignNotePin from './design_note_pin.vue';
|
||||
|
|
@ -242,12 +243,15 @@ export default {
|
|||
return { inactive: this.isNoteInactive(note), resolved: note.resolved };
|
||||
},
|
||||
},
|
||||
i18n: {
|
||||
newCommentButtonLabel: __('Add comment to design'),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="position-absolute image-diff-overlay frame"
|
||||
class="gl-absolute gl-top-0 gl-left-0 frame"
|
||||
:style="overlayStyle"
|
||||
@mousemove="onOverlayMousemove"
|
||||
@mouseleave="onNoteMouseup"
|
||||
|
|
@ -255,26 +259,28 @@ export default {
|
|||
<button
|
||||
v-show="!disableCommenting"
|
||||
type="button"
|
||||
class="btn-transparent position-absolute image-diff-overlay-add-comment w-100 h-100 js-add-image-diff-note-button"
|
||||
role="button"
|
||||
:aria-label="$options.i18n.newCommentButtonLabel"
|
||||
class="gl-absolute gl-w-full gl-h-full gl-p-0 gl-top-0 gl-left-0 gl-outline-0! btn-transparent design-detail-overlay-add-comment"
|
||||
data-qa-selector="design_image_button"
|
||||
@mouseup="onAddCommentMouseup"
|
||||
></button>
|
||||
<template v-for="note in notes">
|
||||
<design-note-pin
|
||||
v-if="resolvedDiscussionsExpanded || !note.resolved"
|
||||
:key="note.id"
|
||||
:label="note.index"
|
||||
:repositioning="isMovingNote(note.id)"
|
||||
:position="
|
||||
isMovingNote(note.id) && movingNoteNewPosition
|
||||
? getNotePositionStyle(movingNoteNewPosition)
|
||||
: getNotePositionStyle(note.position)
|
||||
"
|
||||
:class="designPinClass(note)"
|
||||
@mousedown.stop="onNoteMousedown($event, note)"
|
||||
@mouseup.stop="onNoteMouseup(note)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<design-note-pin
|
||||
v-for="note in notes"
|
||||
v-if="resolvedDiscussionsExpanded || !note.resolved"
|
||||
:key="note.id"
|
||||
:label="note.index"
|
||||
:repositioning="isMovingNote(note.id)"
|
||||
:position="
|
||||
isMovingNote(note.id) && movingNoteNewPosition
|
||||
? getNotePositionStyle(movingNoteNewPosition)
|
||||
: getNotePositionStyle(note.position)
|
||||
"
|
||||
:class="designPinClass(note)"
|
||||
@mousedown.stop="onNoteMousedown($event, note)"
|
||||
@mouseup.stop="onNoteMouseup(note)"
|
||||
/>
|
||||
|
||||
<design-note-pin
|
||||
v-if="currentCommentForm"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
import { mapState, mapGetters, mapActions } from 'vuex';
|
||||
import { GlLoadingIcon, GlButtonGroup, GlButton, GlAlert } from '@gitlab/ui';
|
||||
import { GlLoadingIcon, GlButton, GlAlert, GlPagination, GlSprintf } from '@gitlab/ui';
|
||||
import Mousetrap from 'mousetrap';
|
||||
import { __ } from '~/locale';
|
||||
import { getParameterByName, parseBoolean } from '~/lib/utils/common_utils';
|
||||
|
|
@ -37,9 +37,10 @@ export default {
|
|||
TreeList,
|
||||
GlLoadingIcon,
|
||||
PanelResizer,
|
||||
GlButtonGroup,
|
||||
GlPagination,
|
||||
GlButton,
|
||||
GlAlert,
|
||||
GlSprintf,
|
||||
},
|
||||
mixins: [glFeatureFlagsMixin()],
|
||||
props: {
|
||||
|
|
@ -169,6 +170,22 @@ export default {
|
|||
isDiffHead() {
|
||||
return parseBoolean(getParameterByName('diff_head'));
|
||||
},
|
||||
showFileByFileNavigation() {
|
||||
return this.diffFiles.length > 1 && this.viewDiffsFileByFile;
|
||||
},
|
||||
currentFileNumber() {
|
||||
return this.currentDiffIndex + 1;
|
||||
},
|
||||
previousFileNumber() {
|
||||
const { currentDiffIndex } = this;
|
||||
|
||||
return currentDiffIndex >= 1 ? currentDiffIndex : null;
|
||||
},
|
||||
nextFileNumber() {
|
||||
const { currentFileNumber, diffFiles } = this;
|
||||
|
||||
return currentFileNumber < diffFiles.length ? currentFileNumber + 1 : null;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
commit(newCommit, oldCommit) {
|
||||
|
|
@ -274,6 +291,9 @@ export default {
|
|||
'toggleShowTreeList',
|
||||
'navigateToDiffFileIndex',
|
||||
]),
|
||||
navigateToDiffFileNumber(number) {
|
||||
this.navigateToDiffFileIndex(number - 1);
|
||||
},
|
||||
refetchDiffData() {
|
||||
this.fetchData(false);
|
||||
},
|
||||
|
|
@ -509,23 +529,22 @@ export default {
|
|||
:can-current-user-fork="canCurrentUserFork"
|
||||
:view-diffs-file-by-file="viewDiffsFileByFile"
|
||||
/>
|
||||
<div v-if="viewDiffsFileByFile" class="d-flex gl-justify-content-center">
|
||||
<gl-button-group>
|
||||
<gl-button
|
||||
:disabled="currentDiffIndex === 0"
|
||||
data-testid="singleFilePrevious"
|
||||
@click="navigateToDiffFileIndex(currentDiffIndex - 1)"
|
||||
>
|
||||
{{ __('Prev') }}
|
||||
</gl-button>
|
||||
<gl-button
|
||||
:disabled="currentDiffIndex === diffFiles.length - 1"
|
||||
data-testid="singleFileNext"
|
||||
@click="navigateToDiffFileIndex(currentDiffIndex + 1)"
|
||||
>
|
||||
{{ __('Next') }}
|
||||
</gl-button>
|
||||
</gl-button-group>
|
||||
<div
|
||||
v-if="showFileByFileNavigation"
|
||||
data-testid="file-by-file-navigation"
|
||||
class="gl-display-grid gl-text-center"
|
||||
>
|
||||
<gl-pagination
|
||||
class="gl-mx-auto"
|
||||
:value="currentFileNumber"
|
||||
:prev-page="previousFileNumber"
|
||||
:next-page="nextFileNumber"
|
||||
@input="navigateToDiffFileNumber"
|
||||
/>
|
||||
<gl-sprintf :message="__('File %{current} of %{total}')">
|
||||
<template #current>{{ currentFileNumber }}</template>
|
||||
<template #total>{{ diffFiles.length }}</template>
|
||||
</gl-sprintf>
|
||||
</div>
|
||||
</template>
|
||||
<no-changes v-else :changes-empty-state-illustration="changesEmptyStateIllustration" />
|
||||
|
|
|
|||
|
|
@ -52,19 +52,19 @@ export default {
|
|||
|
||||
<style>
|
||||
.pdf-page {
|
||||
margin: 8px auto 0 auto;
|
||||
margin: 8px auto 0;
|
||||
border-top: 1px #ddd solid;
|
||||
border-bottom: 1px #ddd solid;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.pdf-page:first-child {
|
||||
margin-top: 0px;
|
||||
border-top: 0px;
|
||||
margin-top: 0;
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
.pdf-page:last-child {
|
||||
margin-bottom: 0px;
|
||||
border-bottom: 0px;
|
||||
margin-bottom: 0;
|
||||
border-bottom: 0;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -32,10 +32,9 @@ export default class PerformanceBarService {
|
|||
// Get the request URL from response.config for Axios, and response for
|
||||
// Vue Resource.
|
||||
const requestUrl = (response.config || response).url;
|
||||
const apiRequest = requestUrl && requestUrl.match(/^\/api\//);
|
||||
const cachedResponse =
|
||||
response.headers && parseBoolean(response.headers['x-gitlab-from-cache']);
|
||||
const fireCallback = requestUrl !== peekUrl && requestId && !apiRequest && !cachedResponse;
|
||||
const fireCallback = requestUrl !== peekUrl && Boolean(requestId) && !cachedResponse;
|
||||
|
||||
return [fireCallback, requestId, requestUrl];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
<script>
|
||||
import { GlDeprecatedButton } from '@gitlab/ui';
|
||||
import { GlButton } from '@gitlab/ui';
|
||||
import LoadingButton from '~/vue_shared/components/loading_button.vue';
|
||||
|
||||
export default {
|
||||
name: 'PipelineNavControls',
|
||||
components: {
|
||||
LoadingButton,
|
||||
GlDeprecatedButton,
|
||||
GlButton,
|
||||
},
|
||||
props: {
|
||||
newPipelinePath: {
|
||||
|
|
@ -42,14 +42,15 @@ export default {
|
|||
</script>
|
||||
<template>
|
||||
<div class="nav-controls">
|
||||
<gl-deprecated-button
|
||||
<gl-button
|
||||
v-if="newPipelinePath"
|
||||
:href="newPipelinePath"
|
||||
variant="success"
|
||||
category="primary"
|
||||
class="js-run-pipeline"
|
||||
>
|
||||
{{ s__('Pipelines|Run Pipeline') }}
|
||||
</gl-deprecated-button>
|
||||
</gl-button>
|
||||
|
||||
<loading-button
|
||||
v-if="resetCachePath"
|
||||
|
|
@ -59,8 +60,8 @@ export default {
|
|||
@click="onClickResetCache"
|
||||
/>
|
||||
|
||||
<gl-deprecated-button v-if="ciLintPath" :href="ciLintPath" class="js-ci-lint">
|
||||
<gl-button v-if="ciLintPath" :href="ciLintPath" class="js-ci-lint">
|
||||
{{ s__('Pipelines|CI Lint') }}
|
||||
</gl-deprecated-button>
|
||||
</gl-button>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -88,7 +88,9 @@ export default {
|
|||
},
|
||||
cancelButtonHref() {
|
||||
if (this.newSnippet) {
|
||||
return this.projectPath ? `/${this.projectPath}/-/snippets` : `/-/snippets`;
|
||||
return this.projectPath
|
||||
? `${gon.relative_url_root}${this.projectPath}/-/snippets`
|
||||
: `${gon.relative_url_root}-/snippets`;
|
||||
}
|
||||
return this.snippet.webUrl;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -97,7 +97,7 @@ export default {
|
|||
text: __('New snippet'),
|
||||
href: this.snippet.project
|
||||
? `${this.snippet.project.webUrl}/-/snippets/new`
|
||||
: '/-/snippets/new',
|
||||
: `${gon.relative_url_root}-/snippets/new`,
|
||||
variant: 'success',
|
||||
category: 'secondary',
|
||||
cssClass: 'ml-2',
|
||||
|
|
@ -137,7 +137,7 @@ export default {
|
|||
redirectToSnippets() {
|
||||
window.location.pathname = this.snippet.project
|
||||
? `${this.snippet.project.fullPath}/-/snippets`
|
||||
: 'dashboard/snippets';
|
||||
: `${gon.relative_url_root}dashboard/snippets`;
|
||||
},
|
||||
closeDeleteModal() {
|
||||
this.$refs.deleteModal.hide();
|
||||
|
|
|
|||
|
|
@ -34,6 +34,10 @@
|
|||
background-color: $gray-500;
|
||||
}
|
||||
}
|
||||
|
||||
.design-detail-overlay-add-comment {
|
||||
cursor: crosshair;
|
||||
}
|
||||
}
|
||||
|
||||
.design-presentation-wrapper {
|
||||
|
|
|
|||
|
|
@ -111,10 +111,14 @@ class Admin::UsersController < Admin::ApplicationController
|
|||
end
|
||||
|
||||
def disable_two_factor
|
||||
update_user { |user| user.disable_two_factor! }
|
||||
result = TwoFactor::DestroyService.new(current_user, user: user).execute
|
||||
|
||||
redirect_to admin_user_path(user),
|
||||
notice: _('Two-factor Authentication has been disabled for this user')
|
||||
if result[:status] == :success
|
||||
redirect_to admin_user_path(user),
|
||||
notice: _('Two-factor authentication has been disabled for this user')
|
||||
else
|
||||
redirect_to admin_user_path(user), alert: result[:message]
|
||||
end
|
||||
end
|
||||
|
||||
def create
|
||||
|
|
|
|||
|
|
@ -73,9 +73,13 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
|
|||
end
|
||||
|
||||
def destroy
|
||||
current_user.disable_two_factor!
|
||||
result = TwoFactor::DestroyService.new(current_user, user: current_user).execute
|
||||
|
||||
redirect_to profile_account_path, status: :found
|
||||
if result[:status] == :success
|
||||
redirect_to profile_account_path, status: :found, notice: s_('Two-factor authentication has been disabled successfully!')
|
||||
else
|
||||
redirect_to profile_account_path, status: :found, alert: result[:message]
|
||||
end
|
||||
end
|
||||
|
||||
def skip
|
||||
|
|
|
|||
|
|
@ -177,6 +177,27 @@ module EmailsHelper
|
|||
strip_tags(render_message(:footer_message, style: ''))
|
||||
end
|
||||
|
||||
def say_hi(user)
|
||||
_('Hi %{username}!') % { username: sanitize_name(user.name) }
|
||||
end
|
||||
|
||||
def two_factor_authentication_disabled_text
|
||||
_('Two-factor authentication has been disabled for your GitLab account.')
|
||||
end
|
||||
|
||||
def re_enable_two_factor_authentication_text(format: nil)
|
||||
url = profile_two_factor_auth_url
|
||||
|
||||
case format
|
||||
when :html
|
||||
settings_link_to = link_to(_('two-factor authentication settings'), url, target: :_blank, rel: 'noopener noreferrer').html_safe
|
||||
_("If you want to re-enable two-factor authentication, visit the %{settings_link_to} page.").html_safe % { settings_link_to: settings_link_to }
|
||||
else
|
||||
_('If you want to re-enable two-factor authentication, visit %{two_factor_link}') %
|
||||
{ two_factor_link: url }
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def show_footer?
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ module IconsHelper
|
|||
|
||||
def boolean_to_icon(value)
|
||||
if value
|
||||
icon('circle', class: 'cgreen')
|
||||
sprite_icon('check', css_class: 'cgreen')
|
||||
else
|
||||
sprite_icon('power', css_class: 'clgray')
|
||||
end
|
||||
|
|
|
|||
|
|
@ -72,6 +72,16 @@ module Emails
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
def disabled_two_factor_email(user)
|
||||
return unless user
|
||||
|
||||
@user = user
|
||||
|
||||
Gitlab::I18n.with_locale(@user.preferred_language) do
|
||||
mail(to: @user.notification_email, subject: subject(_("Two-factor authentication disabled")))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -181,6 +181,14 @@ module Issuable
|
|||
false
|
||||
end
|
||||
|
||||
def supports_time_tracking?
|
||||
is_a?(TimeTrackable) && !incident?
|
||||
end
|
||||
|
||||
def incident?
|
||||
is_a?(Issue) && super
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def description_max_length_for_new_records_is_valid
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ class UserPolicy < BasePolicy
|
|||
|
||||
rule { default }.enable :read_user_profile
|
||||
rule { (private_profile | blocked_user) & ~(user_is_self | admin) }.prevent :read_user_profile
|
||||
rule { user_is_self | admin }.enable :disable_two_factor
|
||||
end
|
||||
|
||||
UserPolicy.prepend_if_ee('EE::UserPolicy')
|
||||
|
|
|
|||
|
|
@ -103,6 +103,8 @@ class IssuableSidebarBasicEntity < Grape::Entity
|
|||
issuable.project.emails_disabled?
|
||||
end
|
||||
|
||||
expose :supports_time_tracking?, as: :supports_time_tracking
|
||||
|
||||
private
|
||||
|
||||
def current_user
|
||||
|
|
|
|||
|
|
@ -109,7 +109,7 @@ class EventCreateService
|
|||
def wiki_event(wiki_page_meta, author, action, fingerprint)
|
||||
raise IllegalActionError, action unless Event::WIKI_ACTIONS.include?(action)
|
||||
|
||||
Gitlab::UsageDataCounters::TrackUniqueActions.track_event(event_action: action, event_target: wiki_page_meta.class, author_id: author.id)
|
||||
Gitlab::UsageDataCounters::TrackUniqueEvents.track_event(event_action: action, event_target: wiki_page_meta.class, author_id: author.id)
|
||||
|
||||
duplicate = Event.for_wiki_meta(wiki_page_meta).for_fingerprint(fingerprint).first
|
||||
return duplicate if duplicate.present?
|
||||
|
|
@ -154,7 +154,7 @@ class EventCreateService
|
|||
result = Event.insert_all(attribute_sets, returning: %w[id])
|
||||
|
||||
tuples.each do |record, status, _|
|
||||
Gitlab::UsageDataCounters::TrackUniqueActions.track_event(event_action: status, event_target: record.class, author_id: current_user.id)
|
||||
Gitlab::UsageDataCounters::TrackUniqueEvents.track_event(event_action: status, event_target: record.class, author_id: current_user.id)
|
||||
end
|
||||
|
||||
result
|
||||
|
|
@ -172,7 +172,7 @@ class EventCreateService
|
|||
new_event
|
||||
end
|
||||
|
||||
Gitlab::UsageDataCounters::TrackUniqueActions.track_event(event_action: :pushed, event_target: Project, author_id: current_user.id)
|
||||
Gitlab::UsageDataCounters::TrackUniqueEvents.track_event(event_action: :pushed, event_target: Project, author_id: current_user.id)
|
||||
|
||||
Users::LastPushEventService.new(current_user)
|
||||
.cache_last_push_event(event)
|
||||
|
|
|
|||
|
|
@ -35,6 +35,12 @@ class NotificationService
|
|||
@async ||= Async.new(self)
|
||||
end
|
||||
|
||||
def disabled_two_factor(user)
|
||||
return unless user.can?(:receive_notifications)
|
||||
|
||||
mailer.disabled_two_factor_email(user).deliver_later
|
||||
end
|
||||
|
||||
# Always notify user about ssh key added
|
||||
# only if ssh key is not deploy key
|
||||
#
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module TwoFactor
|
||||
class BaseService
|
||||
include BaseServiceUtility
|
||||
|
||||
attr_reader :current_user, :params, :user
|
||||
|
||||
def initialize(current_user, params = {})
|
||||
@current_user, @params = current_user, params
|
||||
@user = params.delete(:user)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module TwoFactor
|
||||
class DestroyService < ::TwoFactor::BaseService
|
||||
def execute
|
||||
return error(_('You are not authorized to perform this action')) unless can?(current_user, :disable_two_factor, user)
|
||||
return error(_('Two-factor authentication is not enabled for this user')) unless user.two_factor_enabled?
|
||||
|
||||
result = disable_two_factor
|
||||
|
||||
notification_service.disabled_two_factor(user) if result[:status] == :success
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def disable_two_factor
|
||||
::Users::UpdateService.new(current_user, user: user).execute do |user|
|
||||
user.disable_two_factor!
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -27,7 +27,7 @@
|
|||
.card-header
|
||||
Current Status:
|
||||
- if no_errors
|
||||
= icon('circle', class: 'cgreen')
|
||||
= sprite_icon('check', css_class: 'cgreen')
|
||||
#{ s_('HealthCheck|Healthy') }
|
||||
- else
|
||||
= icon('warning', class: 'cred')
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
%p
|
||||
= say_hi(@user)
|
||||
%p
|
||||
= two_factor_authentication_disabled_text
|
||||
%p
|
||||
= re_enable_two_factor_authentication_text(format: :html)
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<%= say_hi(@user) %>
|
||||
|
||||
<%= two_factor_authentication_disabled_text %>
|
||||
|
||||
<%= re_enable_two_factor_authentication_text %>
|
||||
|
|
@ -55,12 +55,13 @@
|
|||
= dropdown_tag('Milestone', options: { title: _('Assign milestone'), toggle_class: 'js-milestone-select js-extra-options', filter: true, dropdown_class: 'dropdown-menu-selectable', placeholder: _('Search milestones'), data: { show_no: true, field_name: "#{issuable_type}[milestone_id]", project_id: issuable_sidebar[:project_id], issuable_id: issuable_sidebar[:id], milestones: issuable_sidebar[:project_milestones_path], ability_name: issuable_type, issue_update: issuable_sidebar[:issuable_json_path], use_id: true, default_no: true, selected: milestone[:title], null_default: true, display: 'static' }})
|
||||
- if @project.group.present?
|
||||
= render_if_exists 'shared/issuable/iteration_select', { can_edit: can_edit_issuable, group_path: @project.group.full_path, project_path: issuable_sidebar[:project_full_path], issue_iid: issuable_sidebar[:iid], issuable_type: issuable_type }
|
||||
#issuable-time-tracker.block
|
||||
// Fallback while content is loading
|
||||
.title.hide-collapsed
|
||||
= _('Time tracking')
|
||||
= icon('spinner spin', 'aria-hidden': 'true')
|
||||
|
||||
- if issuable_sidebar[:supports_time_tracking]
|
||||
#issuable-time-tracker.block
|
||||
// Fallback while content is loading
|
||||
.title.hide-collapsed
|
||||
= _('Time tracking')
|
||||
= icon('spinner spin', 'aria-hidden': 'true')
|
||||
- if issuable_sidebar.has_key?(:due_date)
|
||||
.block.due_date
|
||||
.sidebar-collapsed-icon.has-tooltip{ data: { placement: 'left', container: 'body', html: 'true', boundary: 'viewport' }, title: sidebar_due_date_tooltip_label(issuable_sidebar[:due_date]) }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Take relative_url_path into account when building URLs in snippets
|
||||
merge_request: 39960
|
||||
author:
|
||||
type: fixed
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Use pointer:crosshair when hovering on the design view
|
||||
merge_request: 39671
|
||||
author:
|
||||
type: changed
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Remove time tracking from incidents sidebar
|
||||
merge_request: 39837
|
||||
author:
|
||||
type: changed
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add ability to associate Environment with Alert with gitlab_environment_name payload key
|
||||
merge_request: 39785
|
||||
author:
|
||||
type: added
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Send email notification on disabling 2FA
|
||||
merge_request: 39572
|
||||
author:
|
||||
type: added
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Automatically add AJAX API requests to the performance bar
|
||||
merge_request: 39069
|
||||
author:
|
||||
type: added
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Tweak file-by-file display and add file current/total display
|
||||
merge_request: 39719
|
||||
author:
|
||||
type: changed
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Replace fa-circle icon instances with GitLab SVG check icon
|
||||
merge_request: 39745
|
||||
author:
|
||||
type: changed
|
||||
|
|
@ -236,7 +236,7 @@ Recommendations:
|
|||
|
||||
Examples of implementation:
|
||||
|
||||
- [`Gitlab::UsageDataCounters::TrackUniqueActions`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/usage_data_counters/track_unique_actions.rb)
|
||||
- [`Gitlab::UsageDataCounters::TrackUniqueEvents`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/usage_data_counters/track_unique_actions.rb)
|
||||
- [`Gitlab::Analytics::UniqueVisits`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/analytics/unique_visits.rb)
|
||||
|
||||
Example of usage:
|
||||
|
|
@ -247,10 +247,10 @@ redis_usage_data(Gitlab::UsageDataCounters::WikiPageCounter)
|
|||
redis_usage_data { ::Gitlab::UsageCounters::PodLogs.usage_totals[:total] }
|
||||
|
||||
# Redis HLL counter
|
||||
counter = Gitlab::UsageDataCounters::TrackUniqueActions
|
||||
counter = Gitlab::UsageDataCounters::TrackUniqueEvents
|
||||
redis_usage_data do
|
||||
counter.count_unique_events(
|
||||
event_action: Gitlab::UsageDataCounters::TrackUniqueActions::PUSH_ACTION,
|
||||
event_action: Gitlab::UsageDataCounters::TrackUniqueEvents::PUSH_ACTION,
|
||||
date_from: time_period[:created_at].first,
|
||||
date_to: time_period[:created_at].last
|
||||
)
|
||||
|
|
|
|||
|
|
@ -62,11 +62,11 @@ include:
|
|||
image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v0.17.0"
|
||||
```
|
||||
|
||||
### Ignore warning and continue deploying
|
||||
#### Ignore warning and continue deploying
|
||||
|
||||
If you are certain that the new chart version is safe to be deployed,
|
||||
you can add the `AUTO_DEVOPS_ALLOW_TO_FORCE_DEPLOY_V<N>` [environment variable](customize.md#build-and-deployment)
|
||||
you can add the `AUTO_DEVOPS_FORCE_DEPLOY_V<N>` [environment variable](customize.md#build-and-deployment)
|
||||
to force the deployment to continue, where `<N>` is the major version.
|
||||
|
||||
For example, if you want to deploy the v2.0.0 chart on a deployment that previously
|
||||
used the v0.17.0 chart, add `AUTO_DEVOPS_ALLOW_TO_FORCE_DEPLOY_V2`.
|
||||
used the v0.17.0 chart, add `AUTO_DEVOPS_FORCE_DEPLOY_V2`.
|
||||
|
|
|
|||
|
|
@ -144,6 +144,7 @@ Users will be notified of the following events:
|
|||
| New email added | User | Security email, always sent. |
|
||||
| Email changed | User | Security email, always sent. |
|
||||
| Password changed | User | Security email, always sent. |
|
||||
| Two-factor authentication disabled | User | Security email, always sent. |
|
||||
| New user created | User | Sent on user creation, except for OmniAuth (LDAP)|
|
||||
| User added to project | User | Sent when user is added to project |
|
||||
| Project access level changed | User | Sent when user project access level is changed |
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ You can customize the payload by sending the following parameters. All fields ot
|
|||
| `hosts` | String or Array | One or more hosts, as to where this incident occurred. |
|
||||
| `severity` | String | The severity of the alert. Must be one of `critical`, `high`, `medium`, `low`, `info`, `unknown`. Default is `critical`. |
|
||||
| `fingerprint` | String or Array | The unique identifier of the alert. This can be used to group occurrences of the same alert. |
|
||||
| `gitlab_environment_name` | String | The name of the associated GitLab [environment](../../../ci/environments/index.md). This can be used to associate your alert to your environment. |
|
||||
|
||||
You can also add custom fields to the alert's payload. The values of extra parameters
|
||||
are not limited to primitive types, such as strings or numbers, but can be a nested
|
||||
|
|
|
|||
|
|
@ -21,7 +21,8 @@ module Gitlab
|
|||
payload: payload,
|
||||
started_at: parsed_payload['startsAt'],
|
||||
severity: annotations[:severity],
|
||||
fingerprint: annotations[:fingerprint]
|
||||
fingerprint: annotations[:fingerprint],
|
||||
environment: annotations[:environment]
|
||||
}
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -55,7 +55,8 @@ module Gitlab
|
|||
'service' => payload[:service],
|
||||
'hosts' => hosts.presence,
|
||||
'severity' => severity,
|
||||
'fingerprint' => fingerprint
|
||||
'fingerprint' => fingerprint,
|
||||
'environment' => environment
|
||||
}
|
||||
end
|
||||
|
||||
|
|
@ -73,6 +74,16 @@ module Gitlab
|
|||
current_time
|
||||
end
|
||||
|
||||
def environment
|
||||
environment_name = payload[:gitlab_environment_name]
|
||||
|
||||
return unless environment_name
|
||||
|
||||
EnvironmentsFinder.new(project, nil, { name: environment_name })
|
||||
.find
|
||||
&.first
|
||||
end
|
||||
|
||||
def secondary_params
|
||||
payload.except(:start_time)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -604,27 +604,27 @@ module Gitlab
|
|||
end
|
||||
|
||||
def action_monthly_active_users(time_period)
|
||||
counter = Gitlab::UsageDataCounters::TrackUniqueActions
|
||||
counter = Gitlab::UsageDataCounters::TrackUniqueEvents
|
||||
|
||||
project_count = redis_usage_data do
|
||||
counter.count_unique(
|
||||
event_action: Gitlab::UsageDataCounters::TrackUniqueActions::PUSH_ACTION,
|
||||
counter.count_unique_events(
|
||||
event_action: Gitlab::UsageDataCounters::TrackUniqueEvents::PUSH_ACTION,
|
||||
date_from: time_period[:created_at].first,
|
||||
date_to: time_period[:created_at].last
|
||||
)
|
||||
end
|
||||
|
||||
design_count = redis_usage_data do
|
||||
counter.count_unique(
|
||||
event_action: Gitlab::UsageDataCounters::TrackUniqueActions::DESIGN_ACTION,
|
||||
counter.count_unique_events(
|
||||
event_action: Gitlab::UsageDataCounters::TrackUniqueEvents::DESIGN_ACTION,
|
||||
date_from: time_period[:created_at].first,
|
||||
date_to: time_period[:created_at].last
|
||||
)
|
||||
end
|
||||
|
||||
wiki_count = redis_usage_data do
|
||||
counter.count_unique(
|
||||
event_action: Gitlab::UsageDataCounters::TrackUniqueActions::WIKI_ACTION,
|
||||
counter.count_unique_events(
|
||||
event_action: Gitlab::UsageDataCounters::TrackUniqueEvents::WIKI_ACTION,
|
||||
date_from: time_period[:created_at].first,
|
||||
date_to: time_period[:created_at].last
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
module Gitlab
|
||||
module UsageDataCounters
|
||||
module TrackUniqueActions
|
||||
module TrackUniqueEvents
|
||||
KEY_EXPIRY_LENGTH = 29.days
|
||||
|
||||
WIKI_ACTION = :wiki_action
|
||||
|
|
@ -38,7 +38,7 @@ module Gitlab
|
|||
Gitlab::Redis::HLL.add(key: target_key, value: author_id, expiry: KEY_EXPIRY_LENGTH)
|
||||
end
|
||||
|
||||
def count_unique(event_action:, date_from:, date_to:)
|
||||
def count_unique_events(event_action:, date_from:, date_to:)
|
||||
keys = (date_from.to_date..date_to.to_date).map { |date| key(event_action, date) }
|
||||
|
||||
Gitlab::Redis::HLL.count(keys: keys)
|
||||
|
|
@ -1524,6 +1524,9 @@ msgstr ""
|
|||
msgid "Add comment now"
|
||||
msgstr ""
|
||||
|
||||
msgid "Add comment to design"
|
||||
msgstr ""
|
||||
|
||||
msgid "Add deploy freeze"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -10660,6 +10663,9 @@ msgstr ""
|
|||
msgid "File"
|
||||
msgstr ""
|
||||
|
||||
msgid "File %{current} of %{total}"
|
||||
msgstr ""
|
||||
|
||||
msgid "File Hooks"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -12743,6 +12749,12 @@ msgstr ""
|
|||
msgid "If you remove this license, GitLab will fall back on the previous license, if any."
|
||||
msgstr ""
|
||||
|
||||
msgid "If you want to re-enable two-factor authentication, visit %{two_factor_link}"
|
||||
msgstr ""
|
||||
|
||||
msgid "If you want to re-enable two-factor authentication, visit the %{settings_link_to} page."
|
||||
msgstr ""
|
||||
|
||||
msgid "If your HTTP repository is not publicly accessible, add your credentials."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -26073,10 +26085,22 @@ msgstr ""
|
|||
msgid "Two-factor Authentication Recovery codes"
|
||||
msgstr ""
|
||||
|
||||
msgid "Two-factor Authentication has been disabled for this user"
|
||||
msgid "Two-factor authentication"
|
||||
msgstr ""
|
||||
|
||||
msgid "Two-factor authentication"
|
||||
msgid "Two-factor authentication disabled"
|
||||
msgstr ""
|
||||
|
||||
msgid "Two-factor authentication has been disabled for this user"
|
||||
msgstr ""
|
||||
|
||||
msgid "Two-factor authentication has been disabled for your GitLab account."
|
||||
msgstr ""
|
||||
|
||||
msgid "Two-factor authentication has been disabled successfully!"
|
||||
msgstr ""
|
||||
|
||||
msgid "Two-factor authentication is not enabled for this user"
|
||||
msgstr ""
|
||||
|
||||
msgid "Type"
|
||||
|
|
@ -29907,6 +29931,9 @@ msgstr ""
|
|||
msgid "triggered"
|
||||
msgstr ""
|
||||
|
||||
msgid "two-factor authentication settings"
|
||||
msgstr ""
|
||||
|
||||
msgid "unicode domains should use IDNA encoding"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@
|
|||
"@babel/preset-env": "^7.10.1",
|
||||
"@gitlab/at.js": "1.5.5",
|
||||
"@gitlab/svgs": "1.158.0",
|
||||
"@gitlab/ui": "20.3.1",
|
||||
"@gitlab/ui": "20.4.0",
|
||||
"@gitlab/visual-review-tools": "1.6.1",
|
||||
"@rails/actioncable": "^6.0.3-1",
|
||||
"@sentry/browser": "^5.10.2",
|
||||
|
|
|
|||
|
|
@ -218,28 +218,44 @@ RSpec.describe Admin::UsersController do
|
|||
end
|
||||
|
||||
describe 'PATCH disable_two_factor' do
|
||||
it 'disables 2FA for the user' do
|
||||
expect(user).to receive(:disable_two_factor!)
|
||||
allow(subject).to receive(:user).and_return(user)
|
||||
subject { patch :disable_two_factor, params: { id: user.to_param } }
|
||||
|
||||
go
|
||||
context 'for a user that has 2FA enabled' do
|
||||
let(:user) { create(:user, :two_factor) }
|
||||
|
||||
it 'disables 2FA for the user' do
|
||||
subject
|
||||
|
||||
expect(user.reload.two_factor_enabled?).to eq(false)
|
||||
end
|
||||
|
||||
it 'redirects back' do
|
||||
subject
|
||||
|
||||
expect(response).to redirect_to(admin_user_path(user))
|
||||
end
|
||||
|
||||
it 'displays a notice on success' do
|
||||
subject
|
||||
|
||||
expect(flash[:notice])
|
||||
.to eq _('Two-factor authentication has been disabled for this user')
|
||||
end
|
||||
end
|
||||
|
||||
it 'redirects back' do
|
||||
go
|
||||
context 'for a user that does not have 2FA enabled' do
|
||||
it 'redirects back' do
|
||||
subject
|
||||
|
||||
expect(response).to redirect_to(admin_user_path(user))
|
||||
end
|
||||
expect(response).to redirect_to(admin_user_path(user))
|
||||
end
|
||||
|
||||
it 'displays an alert' do
|
||||
go
|
||||
it 'displays an alert on failure' do
|
||||
subject
|
||||
|
||||
expect(flash[:notice])
|
||||
.to eq _('Two-factor Authentication has been disabled for this user')
|
||||
end
|
||||
|
||||
def go
|
||||
patch :disable_two_factor, params: { id: user.to_param }
|
||||
expect(flash[:alert])
|
||||
.to eq _('Two-factor authentication is not enabled for this user')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -107,18 +107,46 @@ RSpec.describe Profiles::TwoFactorAuthsController do
|
|||
end
|
||||
|
||||
describe 'DELETE destroy' do
|
||||
let(:user) { create(:user, :two_factor) }
|
||||
subject { delete :destroy }
|
||||
|
||||
it 'disables two factor' do
|
||||
expect(user).to receive(:disable_two_factor!)
|
||||
context 'for a user that has 2FA enabled' do
|
||||
let(:user) { create(:user, :two_factor) }
|
||||
|
||||
delete :destroy
|
||||
it 'disables two factor' do
|
||||
subject
|
||||
|
||||
expect(user.reload.two_factor_enabled?).to eq(false)
|
||||
end
|
||||
|
||||
it 'redirects to profile_account_path' do
|
||||
subject
|
||||
|
||||
expect(response).to redirect_to(profile_account_path)
|
||||
end
|
||||
|
||||
it 'displays a notice on success' do
|
||||
subject
|
||||
|
||||
expect(flash[:notice])
|
||||
.to eq _('Two-factor authentication has been disabled successfully!')
|
||||
end
|
||||
end
|
||||
|
||||
it 'redirects to profile_account_path' do
|
||||
delete :destroy
|
||||
context 'for a user that does not have 2FA enabled' do
|
||||
let(:user) { create(:user) }
|
||||
|
||||
expect(response).to redirect_to(profile_account_path)
|
||||
it 'redirects to profile_account_path' do
|
||||
subject
|
||||
|
||||
expect(response).to redirect_to(profile_account_path)
|
||||
end
|
||||
|
||||
it 'displays an alert on failure' do
|
||||
subject
|
||||
|
||||
expect(flash[:alert])
|
||||
.to eq _('Two-factor authentication is not enabled for this user')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'Upload ci artifact', :api, :js do
|
||||
include_context 'file upload requests helpers'
|
||||
|
||||
let_it_be(:user) { create(:user, :admin) }
|
||||
let_it_be(:project) { create(:project) }
|
||||
let_it_be(:pipeline) { create(:ci_pipeline, project: project, ref: 'master') }
|
||||
let_it_be(:runner) { create(:ci_runner, :project, projects: [project]) }
|
||||
let_it_be(:job) { create(:ci_build, :running, user: user, project: project, pipeline: pipeline, runner_id: runner.id) }
|
||||
|
||||
let(:api_path) { "/jobs/#{job.id}/artifacts?token=#{job.token}" }
|
||||
let(:url) { capybara_url(api(api_path)) }
|
||||
let(:file) { fixture_file_upload('spec/fixtures/ci_build_artifacts.zip') }
|
||||
|
||||
subject do
|
||||
HTTParty.post(url, body: { file: file })
|
||||
end
|
||||
|
||||
RSpec.shared_examples 'for ci artifact' do
|
||||
it { expect { subject }.to change { ::Ci::JobArtifact.count }.by(2) }
|
||||
|
||||
it { expect(subject.code).to eq(201) }
|
||||
end
|
||||
|
||||
it_behaves_like 'handling file uploads', 'for ci artifact'
|
||||
end
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'Upload a git lfs object', :js do
|
||||
include_context 'file upload requests helpers'
|
||||
|
||||
let_it_be(:project) { create(:project) }
|
||||
let_it_be(:user) { create(:user, :admin) }
|
||||
let_it_be(:personal_access_token) { create(:personal_access_token, user: user) }
|
||||
|
||||
let(:file) { fixture_file_upload('spec/fixtures/banana_sample.gif') }
|
||||
let(:oid) { Digest::SHA256.hexdigest(File.read(file.path)) }
|
||||
let(:size) { file.size }
|
||||
let(:url) { capybara_url("/#{project.namespace.path}/#{project.path}.git/gitlab-lfs/objects/#{oid}/#{size}") }
|
||||
let(:headers) { { 'Content-Type' => 'application/octet-stream' } }
|
||||
|
||||
subject do
|
||||
HTTParty.put(
|
||||
url,
|
||||
headers: headers,
|
||||
basic_auth: { user: user.username, password: personal_access_token.token },
|
||||
body: file.read
|
||||
)
|
||||
end
|
||||
|
||||
before do
|
||||
stub_lfs_setting(enabled: true)
|
||||
end
|
||||
|
||||
RSpec.shared_examples 'for a git lfs object' do
|
||||
it { expect { subject }.to change { LfsObject.count }.by(1) }
|
||||
it { expect(subject.code).to eq(200) }
|
||||
end
|
||||
|
||||
it_behaves_like 'handling file uploads', 'for a git lfs object'
|
||||
end
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'Upload a design through graphQL', :js do
|
||||
include_context 'file upload requests helpers'
|
||||
|
||||
let_it_be(:query) do
|
||||
"
|
||||
mutation uploadDesign($files: [Upload!]!, $projectPath: ID!, $iid: ID!) {
|
||||
designManagementUpload(input: { projectPath: $projectPath, iid: $iid, files: $files}) {
|
||||
clientMutationId,
|
||||
errors
|
||||
}
|
||||
}
|
||||
"
|
||||
end
|
||||
|
||||
let_it_be(:user) { create(:user, :admin) }
|
||||
let_it_be(:personal_access_token) { create(:personal_access_token, user: user) }
|
||||
let_it_be(:design) { create(:design) }
|
||||
let_it_be(:operations) { { "operationName": "uploadDesign", "variables": { "files": [], "projectPath": design.project.full_path, "iid": design.issue.iid }, "query": query }.to_json }
|
||||
let_it_be(:map) { { "1": ["variables.files.0"] }.to_json }
|
||||
|
||||
let(:url) { capybara_url("/api/graphql?private_token=#{personal_access_token.token}") }
|
||||
let(:file) { fixture_file_upload('spec/fixtures/dk.png') }
|
||||
|
||||
subject do
|
||||
HTTParty.post(
|
||||
url,
|
||||
body: {
|
||||
operations: operations,
|
||||
map: map,
|
||||
"1": file
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
before do
|
||||
stub_lfs_setting(enabled: true)
|
||||
end
|
||||
|
||||
RSpec.shared_examples 'for a design upload through graphQL' do
|
||||
it 'creates proper objects' do
|
||||
expect { subject }
|
||||
.to change { ::DesignManagement::Design.count }.by(1)
|
||||
.and change { ::LfsObject.count }.by(1)
|
||||
end
|
||||
|
||||
it { expect(subject.code).to eq(200) }
|
||||
end
|
||||
|
||||
it_behaves_like 'handling file uploads', 'for a design upload through graphQL'
|
||||
end
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'Upload a group export archive', :api, :js do
|
||||
include_context 'file upload requests helpers'
|
||||
|
||||
let_it_be(:user) { create(:user, :admin) }
|
||||
let_it_be(:personal_access_token) { create(:personal_access_token, user: user) }
|
||||
let(:api_path) { '/groups/import' }
|
||||
let(:url) { capybara_url(api(api_path, personal_access_token: personal_access_token)) }
|
||||
let(:file) { fixture_file_upload('spec/fixtures/group_export.tar.gz') }
|
||||
|
||||
subject do
|
||||
HTTParty.post(
|
||||
url,
|
||||
body: {
|
||||
path: 'test-import-group',
|
||||
name: 'test-import-group',
|
||||
file: file
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
RSpec.shared_examples 'for a group export archive' do
|
||||
it { expect { subject }.to change { Group.count }.by(1) }
|
||||
|
||||
it { expect(subject.code).to eq(202) }
|
||||
end
|
||||
|
||||
it_behaves_like 'handling file uploads', 'for a group export archive'
|
||||
end
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'Upload a maven package', :api, :js do
|
||||
include_context 'file upload requests helpers'
|
||||
|
||||
let_it_be(:project) { create(:project) }
|
||||
let_it_be(:user) { create(:user, :admin) }
|
||||
let_it_be(:personal_access_token) { create(:personal_access_token, user: user) }
|
||||
|
||||
let(:api_path) { "/projects/#{project.id}/packages/maven/com/example/my-app/1.0/my-app-1.0-20180724.124855-1.jar" }
|
||||
let(:url) { capybara_url(api(api_path, personal_access_token: personal_access_token)) }
|
||||
let(:file) { fixture_file_upload('spec/fixtures/dk.png') }
|
||||
|
||||
subject { HTTParty.put(url, body: file.read) }
|
||||
|
||||
RSpec.shared_examples 'for a maven package' do
|
||||
it 'creates package files' do
|
||||
expect { subject }
|
||||
.to change { Packages::Package.maven.count }.by(1)
|
||||
.and change { Packages::PackageFile.count }.by(1)
|
||||
end
|
||||
|
||||
it { expect(subject.code).to eq(200) }
|
||||
end
|
||||
|
||||
it_behaves_like 'handling file uploads', 'for a maven package'
|
||||
end
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'Upload a nuget package', :api, :js do
|
||||
include_context 'file upload requests helpers'
|
||||
|
||||
let_it_be(:project) { create(:project) }
|
||||
let_it_be(:user) { create(:user, :admin) }
|
||||
let_it_be(:personal_access_token) { create(:personal_access_token, user: user) }
|
||||
|
||||
let(:api_path) { "/projects/#{project.id}/packages/nuget/" }
|
||||
let(:url) { capybara_url(api(api_path)) }
|
||||
let(:file) { fixture_file_upload('spec/fixtures/dk.png') }
|
||||
|
||||
subject do
|
||||
HTTParty.put(
|
||||
url,
|
||||
basic_auth: { user: user.username, password: personal_access_token.token },
|
||||
body: { package: file }
|
||||
)
|
||||
end
|
||||
|
||||
RSpec.shared_examples 'for a nuget package' do
|
||||
it 'creates package files' do
|
||||
expect { subject }
|
||||
.to change { Packages::Package.nuget.count }.by(1)
|
||||
.and change { Packages::PackageFile.count }.by(1)
|
||||
end
|
||||
|
||||
it { expect(subject.code).to eq(201) }
|
||||
end
|
||||
|
||||
it_behaves_like 'handling file uploads', 'for a nuget package'
|
||||
end
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'Upload a project export archive', :api, :js do
|
||||
include_context 'file upload requests helpers'
|
||||
|
||||
let_it_be(:user) { create(:user, :admin) }
|
||||
let_it_be(:personal_access_token) { create(:personal_access_token, user: user) }
|
||||
let(:api_path) { '/projects/import' }
|
||||
let(:url) { capybara_url(api(api_path, personal_access_token: personal_access_token)) }
|
||||
let(:file) { fixture_file_upload('spec/features/projects/import_export/test_project_export.tar.gz') }
|
||||
|
||||
subject do
|
||||
HTTParty.post(
|
||||
url,
|
||||
body: {
|
||||
path: 'test-import',
|
||||
file: file
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
RSpec.shared_examples 'for a project export archive' do
|
||||
it { expect { subject }.to change { Project.count }.by(1) }
|
||||
|
||||
it { expect(subject.code).to eq(201) }
|
||||
end
|
||||
|
||||
it_behaves_like 'handling file uploads', 'for a project export archive'
|
||||
end
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'Upload a user avatar', :js do
|
||||
let_it_be(:user, reload: true) { create(:user) }
|
||||
let(:file) { fixture_file_upload('spec/fixtures/banana_sample.gif') }
|
||||
|
||||
before do
|
||||
sign_in(user)
|
||||
visit(profile_path)
|
||||
attach_file('user_avatar-trigger', file.path, make_visible: true)
|
||||
click_button 'Set new profile picture'
|
||||
end
|
||||
|
||||
subject do
|
||||
click_button 'Update profile settings'
|
||||
end
|
||||
|
||||
RSpec.shared_examples 'for a user avatar' do
|
||||
it 'uploads successfully' do
|
||||
expect(user.avatar.file).to eq nil
|
||||
subject
|
||||
|
||||
expect(page).to have_content 'Profile was successfully updated'
|
||||
expect(user.reload.avatar.file).to be_present
|
||||
expect(user.avatar).to be_instance_of AvatarUploader
|
||||
expect(current_path).to eq(profile_path)
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'handling file uploads', 'for a user avatar'
|
||||
end
|
||||
|
|
@ -27,9 +27,10 @@ RSpec.describe 'User views diffs file-by-file', :js do
|
|||
page.within('#diffs') do
|
||||
expect(page).not_to have_content('This diff is collapsed')
|
||||
|
||||
click_button 'Next'
|
||||
find('.page-link.next-page-item').click
|
||||
|
||||
expect(page).not_to have_content('This diff is collapsed')
|
||||
expect(page).to have_selector('.diff-file .file-title', text: 'large_diff_renamed.md')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ RSpec.describe 'User views diffs file-by-file', :js do
|
|||
expect(page).to have_selector('.file-holder', count: 1)
|
||||
expect(page).to have_selector('.diff-file .file-title', text: '.DS_Store')
|
||||
|
||||
click_button 'Next'
|
||||
find('.page-link.next-page-item').click
|
||||
|
||||
expect(page).to have_selector('.file-holder', count: 1)
|
||||
expect(page).to have_selector('.diff-file .file-title', text: '.gitignore')
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'Raw artifact', :js do
|
||||
RSpec.describe 'Raw artifact' do
|
||||
let(:project) { create(:project, :public) }
|
||||
let(:pipeline) { create(:ci_empty_pipeline, project: project) }
|
||||
let(:job) { create(:ci_build, :artifacts, pipeline: pipeline) }
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@
|
|||
"project_labels_path": { "type": "string" },
|
||||
"toggle_subscription_path": { "type": "string" },
|
||||
"move_issue_path": { "type": "string" },
|
||||
"projects_autocomplete_path": { "type": "string" }
|
||||
"projects_autocomplete_path": { "type": "string" },
|
||||
"supports_time_tracking": { "type": "boolean" }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@
|
|||
"project_labels_path": { "type": "string" },
|
||||
"toggle_subscription_path": { "type": "string" },
|
||||
"move_issue_path": { "type": "string" },
|
||||
"projects_autocomplete_path": { "type": "string" }
|
||||
"projects_autocomplete_path": { "type": "string" },
|
||||
"supports_time_tracking": { "type": "boolean" }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ describe('Design overlay component', () => {
|
|||
|
||||
const mockDimensions = { width: 100, height: 100 };
|
||||
|
||||
const findOverlay = () => wrapper.find('.image-diff-overlay');
|
||||
const findAllNotes = () => wrapper.findAll('.js-image-badge');
|
||||
const findCommentBadge = () => wrapper.find('.comment-indicator');
|
||||
const findFirstBadge = () => findAllNotes().at(0);
|
||||
|
|
@ -56,9 +55,7 @@ describe('Design overlay component', () => {
|
|||
it('should have correct inline style', () => {
|
||||
createComponent();
|
||||
|
||||
expect(wrapper.find('.image-diff-overlay').attributes().style).toBe(
|
||||
'width: 100px; height: 100px; top: 0px; left: 0px;',
|
||||
);
|
||||
expect(wrapper.attributes().style).toBe('width: 100px; height: 100px; top: 0px; left: 0px;');
|
||||
});
|
||||
|
||||
it('should emit `openCommentForm` when clicking on overlay', () => {
|
||||
|
|
@ -69,7 +66,7 @@ describe('Design overlay component', () => {
|
|||
};
|
||||
|
||||
wrapper
|
||||
.find('.image-diff-overlay-add-comment')
|
||||
.find('[data-qa-selector="design_image_button"]')
|
||||
.trigger('mouseup', { offsetX: newCoordinates.x, offsetY: newCoordinates.y });
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(wrapper.emitted('openCommentForm')).toEqual([
|
||||
|
|
@ -309,7 +306,7 @@ describe('Design overlay component', () => {
|
|||
|
||||
it.each`
|
||||
element | getElementFunc | event
|
||||
${'overlay'} | ${findOverlay} | ${'mouseleave'}
|
||||
${'overlay'} | ${() => wrapper} | ${'mouseleave'}
|
||||
${'comment badge'} | ${findCommentBadge} | ${'mouseup'}
|
||||
`(
|
||||
'should emit `openCommentForm` event when $event fired on $element element',
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ describe('Design management design presentation component', () => {
|
|||
wrapper.element.scrollTo = jest.fn();
|
||||
}
|
||||
|
||||
const findOverlayCommentButton = () => wrapper.find('.image-diff-overlay-add-comment');
|
||||
const findOverlayCommentButton = () => wrapper.find('[data-qa-selector="design_image_button"]');
|
||||
|
||||
/**
|
||||
* Spy on $refs and mock given values
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import Vuex from 'vuex';
|
||||
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||
import { GlLoadingIcon } from '@gitlab/ui';
|
||||
import { GlLoadingIcon, GlPagination } from '@gitlab/ui';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { TEST_HOST } from 'spec/test_constants';
|
||||
import Mousetrap from 'mousetrap';
|
||||
|
|
@ -843,13 +843,16 @@ describe('diffs/components/app', () => {
|
|||
});
|
||||
|
||||
describe('pagination', () => {
|
||||
const fileByFileNav = () => wrapper.find('[data-testid="file-by-file-navigation"]');
|
||||
const paginator = () => fileByFileNav().find(GlPagination);
|
||||
|
||||
it('sets previous button as disabled', () => {
|
||||
createComponent({ viewDiffsFileByFile: true }, ({ state }) => {
|
||||
state.diffs.diffFiles.push({ file_hash: '123' }, { file_hash: '312' });
|
||||
});
|
||||
|
||||
expect(wrapper.find('[data-testid="singleFilePrevious"]').props('disabled')).toBe(true);
|
||||
expect(wrapper.find('[data-testid="singleFileNext"]').props('disabled')).toBe(false);
|
||||
expect(paginator().attributes('prevpage')).toBe(undefined);
|
||||
expect(paginator().attributes('nextpage')).toBe('2');
|
||||
});
|
||||
|
||||
it('sets next button as disabled', () => {
|
||||
|
|
@ -858,17 +861,26 @@ describe('diffs/components/app', () => {
|
|||
state.diffs.currentDiffFileId = '312';
|
||||
});
|
||||
|
||||
expect(wrapper.find('[data-testid="singleFilePrevious"]').props('disabled')).toBe(false);
|
||||
expect(wrapper.find('[data-testid="singleFileNext"]').props('disabled')).toBe(true);
|
||||
expect(paginator().attributes('prevpage')).toBe('1');
|
||||
expect(paginator().attributes('nextpage')).toBe(undefined);
|
||||
});
|
||||
|
||||
it("doesn't display when there's fewer than 2 files", () => {
|
||||
createComponent({ viewDiffsFileByFile: true }, ({ state }) => {
|
||||
state.diffs.diffFiles.push({ file_hash: '123' });
|
||||
state.diffs.currentDiffFileId = '123';
|
||||
});
|
||||
|
||||
expect(fileByFileNav().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it.each`
|
||||
currentDiffFileId | button | index
|
||||
${'123'} | ${'singleFileNext'} | ${1}
|
||||
${'312'} | ${'singleFilePrevious'} | ${0}
|
||||
currentDiffFileId | targetFile
|
||||
${'123'} | ${2}
|
||||
${'312'} | ${1}
|
||||
`(
|
||||
'it calls navigateToDiffFileIndex with $index when $button is clicked',
|
||||
({ currentDiffFileId, button, index }) => {
|
||||
'it calls navigateToDiffFileIndex with $index when $link is clicked',
|
||||
async ({ currentDiffFileId, targetFile }) => {
|
||||
createComponent({ viewDiffsFileByFile: true }, ({ state }) => {
|
||||
state.diffs.diffFiles.push({ file_hash: '123' }, { file_hash: '312' });
|
||||
state.diffs.currentDiffFileId = currentDiffFileId;
|
||||
|
|
@ -876,11 +888,11 @@ describe('diffs/components/app', () => {
|
|||
|
||||
jest.spyOn(wrapper.vm, 'navigateToDiffFileIndex');
|
||||
|
||||
wrapper.find(`[data-testid="${button}"]`).vm.$emit('click');
|
||||
paginator().vm.$emit('input', targetFile);
|
||||
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(wrapper.vm.navigateToDiffFileIndex).toHaveBeenCalledWith(index);
|
||||
});
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.vm.navigateToDiffFileIndex).toHaveBeenCalledWith(targetFile - 1);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,13 +3,15 @@ import timezoneMock from 'timezone-mock';
|
|||
import { cloneDeep } from 'lodash';
|
||||
import { GlStackedColumnChart, GlChartLegend } from '@gitlab/ui/dist/charts';
|
||||
import StackedColumnChart from '~/monitoring/components/charts/stacked_column.vue';
|
||||
import { stackedColumnMockedData } from '../../mock_data';
|
||||
import { stackedColumnGraphData } from '../../graph_data';
|
||||
|
||||
jest.mock('~/lib/utils/icon_utils', () => ({
|
||||
getSvgIconPathContent: jest.fn().mockImplementation(icon => Promise.resolve(`${icon}-content`)),
|
||||
}));
|
||||
|
||||
describe('Stacked column chart component', () => {
|
||||
const stackedColumnMockedData = stackedColumnGraphData();
|
||||
|
||||
let wrapper;
|
||||
|
||||
const findChart = () => wrapper.find(GlStackedColumnChart);
|
||||
|
|
@ -63,9 +65,9 @@ describe('Stacked column chart component', () => {
|
|||
const groupBy = findChart().props('groupBy');
|
||||
|
||||
expect(groupBy).toEqual([
|
||||
'2020-01-30T12:00:00.000Z',
|
||||
'2020-01-30T12:01:00.000Z',
|
||||
'2020-01-30T12:02:00.000Z',
|
||||
'2015-07-01T20:10:50.000Z',
|
||||
'2015-07-01T20:12:50.000Z',
|
||||
'2015-07-01T20:14:50.000Z',
|
||||
]);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -246,3 +246,16 @@ export const gaugeChartGraphData = (panelOptions = {}) => {
|
|||
],
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates stacked mock graph data according to options
|
||||
*
|
||||
* @param {Object} panelOptions - Panel options as in YML.
|
||||
* @param {Object} dataOptions
|
||||
*/
|
||||
export const stackedColumnGraphData = (panelOptions = {}, dataOptions = {}) => {
|
||||
return {
|
||||
...timeSeriesGraphData(panelOptions, dataOptions),
|
||||
type: panelTypes.STACKED_COLUMN,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -245,51 +245,6 @@ export const metricsResult = [
|
|||
},
|
||||
];
|
||||
|
||||
export const stackedColumnMockedData = {
|
||||
title: 'memories',
|
||||
type: 'stacked-column',
|
||||
x_label: 'x label',
|
||||
y_label: 'y label',
|
||||
metrics: [
|
||||
{
|
||||
label: 'memory_1024',
|
||||
unit: 'count',
|
||||
series_name: 'group 1',
|
||||
prometheus_endpoint_path:
|
||||
'/root/autodevops-deploy-6/-/environments/24/prometheus/api/v1/query_range?query=avg%28sum%28container_memory_usage_bytes%7Bcontainer_name%21%3D%22POD%22%2Cpod_name%3D~%22%5E%25%7Bci_environment_slug%7D-%28%5B%5Ec%5D.%2A%7Cc%28%5B%5Ea%5D%7Ca%28%5B%5En%5D%7Cn%28%5B%5Ea%5D%7Ca%28%5B%5Er%5D%7Cr%5B%5Ey%5D%29%29%29%29.%2A%7C%29-%28.%2A%29%22%2Cnamespace%3D%22%25%7Bkube_namespace%7D%22%7D%29+by+%28job%29%29+without+%28job%29+%2F+count%28avg%28container_memory_usage_bytes%7Bcontainer_name%21%3D%22POD%22%2Cpod_name%3D~%22%5E%25%7Bci_environment_slug%7D-%28%5B%5Ec%5D.%2A%7Cc%28%5B%5Ea%5D%7Ca%28%5B%5En%5D%7Cn%28%5B%5Ea%5D%7Ca%28%5B%5Er%5D%7Cr%5B%5Ey%5D%29%29%29%29.%2A%7C%29-%28.%2A%29%22%2Cnamespace%3D%22%25%7Bkube_namespace%7D%22%7D%29+without+%28job%29%29+%2F1024%2F1024',
|
||||
metricId: 'NO_DB_metric_of_ages_1024',
|
||||
result: [
|
||||
{
|
||||
metric: {},
|
||||
values: [
|
||||
['2020-01-30T12:00:00.000Z', '5'],
|
||||
['2020-01-30T12:01:00.000Z', '10'],
|
||||
['2020-01-30T12:02:00.000Z', '15'],
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'memory_1000',
|
||||
unit: 'count',
|
||||
series_name: 'group 2',
|
||||
prometheus_endpoint_path:
|
||||
'/root/autodevops-deploy-6/-/environments/24/prometheus/api/v1/query_range?query=avg%28sum%28container_memory_usage_bytes%7Bcontainer_name%21%3D%22POD%22%2Cpod_name%3D~%22%5E%25%7Bci_environment_slug%7D-%28%5B%5Ec%5D.%2A%7Cc%28%5B%5Ea%5D%7Ca%28%5B%5En%5D%7Cn%28%5B%5Ea%5D%7Ca%28%5B%5Er%5D%7Cr%5B%5Ey%5D%29%29%29%29.%2A%7C%29-%28.%2A%29%22%2Cnamespace%3D%22%25%7Bkube_namespace%7D%22%7D%29+by+%28job%29%29+without+%28job%29+%2F+count%28avg%28container_memory_usage_bytes%7Bcontainer_name%21%3D%22POD%22%2Cpod_name%3D~%22%5E%25%7Bci_environment_slug%7D-%28%5B%5Ec%5D.%2A%7Cc%28%5B%5Ea%5D%7Ca%28%5B%5En%5D%7Cn%28%5B%5Ea%5D%7Ca%28%5B%5Er%5D%7Cr%5B%5Ey%5D%29%29%29%29.%2A%7C%29-%28.%2A%29%22%2Cnamespace%3D%22%25%7Bkube_namespace%7D%22%7D%29+without+%28job%29%29+%2F1024%2F1024',
|
||||
metricId: 'NO_DB_metric_of_ages_1000',
|
||||
result: [
|
||||
{
|
||||
metric: {},
|
||||
values: [
|
||||
['2020-01-30T12:00:00.000Z', '20'],
|
||||
['2020-01-30T12:01:00.000Z', '25'],
|
||||
['2020-01-30T12:02:00.000Z', '30'],
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const barMockData = {
|
||||
title: 'SLA Trends - Primary Services',
|
||||
type: 'bar',
|
||||
|
|
|
|||
|
|
@ -8,19 +8,13 @@ describe('PerformanceBarService', () => {
|
|||
}
|
||||
|
||||
it('returns false when the request URL is the peek URL', () => {
|
||||
expect(
|
||||
fireCallback({ headers: { 'x-request-id': '123' }, url: '/peek' }, '/peek'),
|
||||
).toBeFalsy();
|
||||
expect(fireCallback({ headers: { 'x-request-id': '123' }, url: '/peek' }, '/peek')).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('returns false when there is no request ID', () => {
|
||||
expect(fireCallback({ headers: {}, url: '/request' }, '/peek')).toBeFalsy();
|
||||
});
|
||||
|
||||
it('returns false when the request is an API request', () => {
|
||||
expect(
|
||||
fireCallback({ headers: { 'x-request-id': '123' }, url: '/api/' }, '/peek'),
|
||||
).toBeFalsy();
|
||||
expect(fireCallback({ headers: {}, url: '/request' }, '/peek')).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when the response is from the cache', () => {
|
||||
|
|
@ -29,13 +23,19 @@ describe('PerformanceBarService', () => {
|
|||
{ headers: { 'x-request-id': '123', 'x-gitlab-from-cache': 'true' }, url: '/request' },
|
||||
'/peek',
|
||||
),
|
||||
).toBeFalsy();
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true when all conditions are met', () => {
|
||||
expect(
|
||||
fireCallback({ headers: { 'x-request-id': '123' }, url: '/request' }, '/peek'),
|
||||
).toBeTruthy();
|
||||
it('returns true when the request is an API request', () => {
|
||||
expect(fireCallback({ headers: { 'x-request-id': '123' }, url: '/api/' }, '/peek')).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it('returns true for all other requests', () => {
|
||||
expect(fireCallback({ headers: { 'x-request-id': '123' }, url: '/request' }, '/peek')).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -47,6 +47,8 @@ const createTestSnippet = () => ({
|
|||
|
||||
describe('Snippet Edit app', () => {
|
||||
let wrapper;
|
||||
const relativeUrlRoot = '/foo/';
|
||||
const originalRelativeUrlRoot = gon.relative_url_root;
|
||||
|
||||
const mutationTypes = {
|
||||
RESOLVE: jest.fn().mockResolvedValue({
|
||||
|
|
@ -104,12 +106,14 @@ describe('Snippet Edit app', () => {
|
|||
}
|
||||
|
||||
beforeEach(() => {
|
||||
gon.relative_url_root = relativeUrlRoot;
|
||||
jest.spyOn(urlUtils, 'redirectTo').mockImplementation();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
wrapper = null;
|
||||
gon.relative_url_root = originalRelativeUrlRoot;
|
||||
});
|
||||
|
||||
const findBlobActions = () => wrapper.find(SnippetBlobActionsEdit);
|
||||
|
|
@ -196,8 +200,8 @@ describe('Snippet Edit app', () => {
|
|||
|
||||
it.each`
|
||||
projectPath | snippetArg | expectation
|
||||
${''} | ${[]} | ${'/-/snippets'}
|
||||
${'project/path'} | ${[]} | ${'/project/path/-/snippets'}
|
||||
${''} | ${[]} | ${`${relativeUrlRoot}-/snippets`}
|
||||
${'project/path'} | ${[]} | ${`${relativeUrlRoot}project/path/-/snippets`}
|
||||
${''} | ${[createTestSnippet()]} | ${TEST_WEB_URL}
|
||||
${'project/path'} | ${[createTestSnippet()]} | ${TEST_WEB_URL}
|
||||
`(
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ describe('Snippet header component', () => {
|
|||
|
||||
let errorMsg;
|
||||
let err;
|
||||
const originalRelativeUrlRoot = gon.relative_url_root;
|
||||
|
||||
function createComponent({
|
||||
loading = false,
|
||||
|
|
@ -50,6 +51,7 @@ describe('Snippet header component', () => {
|
|||
}
|
||||
|
||||
beforeEach(() => {
|
||||
gon.relative_url_root = '/foo/';
|
||||
snippet = {
|
||||
id: 'gid://gitlab/PersonalSnippet/50',
|
||||
title: 'The property of Thor',
|
||||
|
|
@ -86,6 +88,7 @@ describe('Snippet header component', () => {
|
|||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
gon.relative_url_root = originalRelativeUrlRoot;
|
||||
});
|
||||
|
||||
it('renders itself', () => {
|
||||
|
|
@ -213,7 +216,7 @@ describe('Snippet header component', () => {
|
|||
it('redirects to dashboard/snippets for personal snippet', () => {
|
||||
return createDeleteSnippet().then(() => {
|
||||
expect(wrapper.vm.closeDeleteModal).toHaveBeenCalled();
|
||||
expect(window.location.pathname).toBe('dashboard/snippets');
|
||||
expect(window.location.pathname).toBe(`${gon.relative_url_root}dashboard/snippets`);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ RSpec.describe EmailsHelper do
|
|||
|
||||
context "and format is unknown" do
|
||||
it "returns plain text" do
|
||||
expect(helper.closure_reason_text(merge_request, format: :text)).to eq("via merge request #{merge_request.to_reference} (#{merge_request_presenter.web_url})")
|
||||
expect(helper.closure_reason_text(merge_request, format: 'unknown')).to eq("via merge request #{merge_request.to_reference} (#{merge_request_presenter.web_url})")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -110,6 +110,41 @@ RSpec.describe EmailsHelper do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#say_hi' do
|
||||
let(:user) { create(:user, name: 'John') }
|
||||
|
||||
it 'returns the greeting message for the given user' do
|
||||
expect(say_hi(user)).to eq('Hi John!')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#two_factor_authentication_disabled_text' do
|
||||
it 'returns the message that 2FA is disabled' do
|
||||
expect(two_factor_authentication_disabled_text).to eq(
|
||||
_('Two-factor authentication has been disabled for your GitLab account.')
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#re_enable_two_factor_authentication_text' do
|
||||
context 'format is html' do
|
||||
it 'returns HTML' do
|
||||
expect(re_enable_two_factor_authentication_text(format: :html)).to eq(
|
||||
"If you want to re-enable two-factor authentication, visit the " \
|
||||
"#{link_to('two-factor authentication settings', profile_two_factor_auth_url, target: :_blank, rel: 'noopener noreferrer')} page."
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'format is not specified' do
|
||||
it 'returns text' do
|
||||
expect(re_enable_two_factor_authentication_text).to eq(
|
||||
"If you want to re-enable two-factor authentication, visit #{profile_two_factor_auth_url}"
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'password_reset_token_valid_time' do
|
||||
def validate_time_string(time_limit, expected_string)
|
||||
Devise.reset_password_within = time_limit
|
||||
|
|
|
|||
|
|
@ -34,7 +34,8 @@ RSpec.describe Gitlab::AlertManagement::AlertParams do
|
|||
hosts: ['gitlab.com'],
|
||||
payload: payload,
|
||||
started_at: started_at,
|
||||
fingerprint: nil
|
||||
fingerprint: nil,
|
||||
environment: nil
|
||||
)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -124,6 +124,18 @@ RSpec.describe Gitlab::Alerting::NotificationPayloadParser do
|
|||
end
|
||||
end
|
||||
|
||||
context 'with environment' do
|
||||
let(:environment) { create(:environment, project: project) }
|
||||
|
||||
before do
|
||||
payload[:gitlab_environment_name] = environment.name
|
||||
end
|
||||
|
||||
it 'sets the environment ' do
|
||||
expect(subject.dig('annotations', 'environment')).to eq(environment)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when payload attributes have blank lines' do
|
||||
let(:payload) do
|
||||
{
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::UsageDataCounters::TrackUniqueActions, :clean_gitlab_redis_shared_state do
|
||||
RSpec.describe Gitlab::UsageDataCounters::TrackUniqueEvents, :clean_gitlab_redis_shared_state do
|
||||
subject(:track_unique_events) { described_class }
|
||||
|
||||
let(:time) { Time.zone.now }
|
||||
|
|
@ -12,7 +12,7 @@ RSpec.describe Gitlab::UsageDataCounters::TrackUniqueActions, :clean_gitlab_redi
|
|||
end
|
||||
|
||||
def count_unique(params)
|
||||
track_unique_events.count_unique(params)
|
||||
track_unique_events.count_unique_events(params)
|
||||
end
|
||||
|
||||
context 'tracking an event' do
|
||||
|
|
@ -912,7 +912,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
|
|||
let(:time) { Time.zone.now }
|
||||
|
||||
before do
|
||||
counter = Gitlab::UsageDataCounters::TrackUniqueActions
|
||||
counter = Gitlab::UsageDataCounters::TrackUniqueEvents
|
||||
project = Event::TARGET_TYPES[:project]
|
||||
wiki = Event::TARGET_TYPES[:wiki]
|
||||
design = Event::TARGET_TYPES[:design]
|
||||
|
|
|
|||
|
|
@ -256,4 +256,26 @@ RSpec.describe Emails::Profile do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'disabled two-factor authentication email' do
|
||||
let_it_be(:user) { create(:user) }
|
||||
|
||||
subject { Notify.disabled_two_factor_email(user) }
|
||||
|
||||
it_behaves_like 'an email sent from GitLab'
|
||||
it_behaves_like 'it should not have Gmail Actions links'
|
||||
it_behaves_like 'a user cannot unsubscribe through footer link'
|
||||
|
||||
it 'is sent to the user' do
|
||||
is_expected.to deliver_to user.email
|
||||
end
|
||||
|
||||
it 'has the correct subject' do
|
||||
is_expected.to have_subject /^Two-factor authentication disabled$/i
|
||||
end
|
||||
|
||||
it 'includes a link to two-factor authentication settings page' do
|
||||
is_expected.to have_body_text /#{profile_two_factor_auth_path}/
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -824,4 +824,40 @@ RSpec.describe Issuable do
|
|||
it_behaves_like 'matches_cross_reference_regex? fails fast'
|
||||
end
|
||||
end
|
||||
|
||||
describe '#supports_time_tracking?' do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
where(:issuable_type, :supports_time_tracking) do
|
||||
:issue | true
|
||||
:incident | false
|
||||
:merge_request | true
|
||||
end
|
||||
|
||||
with_them do
|
||||
let(:issuable) { build_stubbed(issuable_type) }
|
||||
|
||||
subject { issuable.supports_time_tracking? }
|
||||
|
||||
it { is_expected.to eq(supports_time_tracking) }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#incident?' do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
where(:issuable_type, :incident) do
|
||||
:issue | false
|
||||
:incident | true
|
||||
:merge_request | false
|
||||
end
|
||||
|
||||
with_them do
|
||||
let(:issuable) { build_stubbed(issuable_type) }
|
||||
|
||||
subject { issuable.incident? }
|
||||
|
||||
it { is_expected.to eq(incident) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -82,4 +82,24 @@ RSpec.describe UserPolicy do
|
|||
describe "updating a user" do
|
||||
it_behaves_like 'changing a user', :update_user
|
||||
end
|
||||
|
||||
describe 'disabling two-factor authentication' do
|
||||
context 'disabling their own two-factor authentication' do
|
||||
let(:user) { current_user }
|
||||
|
||||
it { is_expected.to be_allowed(:disable_two_factor) }
|
||||
end
|
||||
|
||||
context 'disabling the two-factor authentication of another user' do
|
||||
context 'when the executor is an admin', :enable_admin_mode do
|
||||
let(:current_user) { create(:user, :admin) }
|
||||
|
||||
it { is_expected.to be_allowed(:disable_two_factor) }
|
||||
end
|
||||
|
||||
context 'when the executor is not an admin' do
|
||||
it { is_expected.not_to be_allowed(:disable_two_factor) }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -3,8 +3,9 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe IssueSerializer do
|
||||
let(:resource) { create(:issue) }
|
||||
let(:user) { create(:user) }
|
||||
let_it_be(:resource) { create(:issue) }
|
||||
let_it_be(:user) { create(:user) }
|
||||
|
||||
let(:json_entity) do
|
||||
described_class.new(current_user: user)
|
||||
.represent(resource, serializer: serializer)
|
||||
|
|
|
|||
|
|
@ -202,11 +202,11 @@ RSpec.describe EventCreateService do
|
|||
end
|
||||
|
||||
it 'records the event in the event counter' do
|
||||
counter_class = Gitlab::UsageDataCounters::TrackUniqueActions
|
||||
counter_class = Gitlab::UsageDataCounters::TrackUniqueEvents
|
||||
tracking_params = { event_action: counter_class::WIKI_ACTION, date_from: Date.yesterday, date_to: Date.today }
|
||||
|
||||
expect { create_event }
|
||||
.to change { counter_class.count_unique(tracking_params) }
|
||||
.to change { counter_class.count_unique_events(tracking_params) }
|
||||
.by(1)
|
||||
end
|
||||
end
|
||||
|
|
@ -243,11 +243,11 @@ RSpec.describe EventCreateService do
|
|||
it_behaves_like 'service for creating a push event', PushEventPayloadService
|
||||
|
||||
it 'records the event in the event counter' do
|
||||
counter_class = Gitlab::UsageDataCounters::TrackUniqueActions
|
||||
counter_class = Gitlab::UsageDataCounters::TrackUniqueEvents
|
||||
tracking_params = { event_action: counter_class::PUSH_ACTION, date_from: Date.yesterday, date_to: Date.today }
|
||||
|
||||
expect { subject }
|
||||
.to change { counter_class.count_unique(tracking_params) }
|
||||
.to change { counter_class.count_unique_events(tracking_params) }
|
||||
.from(0).to(1)
|
||||
end
|
||||
end
|
||||
|
|
@ -266,11 +266,11 @@ RSpec.describe EventCreateService do
|
|||
it_behaves_like 'service for creating a push event', BulkPushEventPayloadService
|
||||
|
||||
it 'records the event in the event counter' do
|
||||
counter_class = Gitlab::UsageDataCounters::TrackUniqueActions
|
||||
counter_class = Gitlab::UsageDataCounters::TrackUniqueEvents
|
||||
tracking_params = { event_action: counter_class::PUSH_ACTION, date_from: Date.yesterday, date_to: Date.today }
|
||||
|
||||
expect { subject }
|
||||
.to change { counter_class.count_unique(tracking_params) }
|
||||
.to change { counter_class.count_unique_events(tracking_params) }
|
||||
.from(0).to(1)
|
||||
end
|
||||
end
|
||||
|
|
@ -320,11 +320,11 @@ RSpec.describe EventCreateService do
|
|||
end
|
||||
|
||||
it 'records the event in the event counter' do
|
||||
counter_class = Gitlab::UsageDataCounters::TrackUniqueActions
|
||||
counter_class = Gitlab::UsageDataCounters::TrackUniqueEvents
|
||||
tracking_params = { event_action: counter_class::DESIGN_ACTION, date_from: Date.yesterday, date_to: Date.today }
|
||||
|
||||
expect { result }
|
||||
.to change { counter_class.count_unique(tracking_params) }
|
||||
.to change { counter_class.count_unique_events(tracking_params) }
|
||||
.from(0).to(1)
|
||||
end
|
||||
end
|
||||
|
|
@ -347,11 +347,11 @@ RSpec.describe EventCreateService do
|
|||
end
|
||||
|
||||
it 'records the event in the event counter' do
|
||||
counter_class = Gitlab::UsageDataCounters::TrackUniqueActions
|
||||
counter_class = Gitlab::UsageDataCounters::TrackUniqueEvents
|
||||
tracking_params = { event_action: counter_class::DESIGN_ACTION, date_from: Date.yesterday, date_to: Date.today }
|
||||
|
||||
expect { result }
|
||||
.to change { counter_class.count_unique(tracking_params) }
|
||||
.to change { counter_class.count_unique_events(tracking_params) }
|
||||
.from(0).to(1)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -272,6 +272,16 @@ RSpec.describe NotificationService, :mailer do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#disabled_two_factor' do
|
||||
let_it_be(:user) { create(:user) }
|
||||
|
||||
subject { notification.disabled_two_factor(user) }
|
||||
|
||||
it 'sends email to the user' do
|
||||
expect { subject }.to have_enqueued_email(user, mail: 'disabled_two_factor_email')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'Notes' do
|
||||
context 'issue note' do
|
||||
let(:project) { create(:project, :private) }
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Projects::Alerting::NotifyService do
|
||||
let_it_be(:project, reload: true) { create(:project) }
|
||||
let_it_be(:project, reload: true) { create(:project, :repository) }
|
||||
|
||||
before do
|
||||
# We use `let_it_be(:project)` so we make sure to clear caches
|
||||
|
|
@ -54,6 +54,7 @@ RSpec.describe Projects::Alerting::NotifyService do
|
|||
let(:starts_at) { Time.current.change(usec: 0) }
|
||||
let(:fingerprint) { 'testing' }
|
||||
let(:service) { described_class.new(project, nil, payload) }
|
||||
let(:environment) { create(:environment, project: project) }
|
||||
let(:payload_raw) do
|
||||
{
|
||||
title: 'alert title',
|
||||
|
|
@ -63,7 +64,8 @@ RSpec.describe Projects::Alerting::NotifyService do
|
|||
service: 'GitLab Test Suite',
|
||||
description: 'Very detailed description',
|
||||
hosts: ['1.1.1.1', '2.2.2.2'],
|
||||
fingerprint: fingerprint
|
||||
fingerprint: fingerprint,
|
||||
gitlab_environment_name: environment.name
|
||||
}.with_indifferent_access
|
||||
end
|
||||
|
||||
|
|
@ -105,9 +107,9 @@ RSpec.describe Projects::Alerting::NotifyService do
|
|||
monitoring_tool: payload_raw.fetch(:monitoring_tool),
|
||||
service: payload_raw.fetch(:service),
|
||||
fingerprint: Digest::SHA1.hexdigest(fingerprint),
|
||||
environment_id: environment.id,
|
||||
ended_at: nil,
|
||||
prometheus_alert_id: nil,
|
||||
environment_id: nil
|
||||
prometheus_alert_id: nil
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,97 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe TwoFactor::DestroyService do
|
||||
let_it_be(:current_user) { create(:user) }
|
||||
|
||||
subject { described_class.new(current_user, user: user).execute }
|
||||
|
||||
context 'disabling two-factor authentication' do
|
||||
shared_examples_for 'does not send notification email' do
|
||||
context 'notification', :mailer do
|
||||
it 'does not send a notification' do
|
||||
perform_enqueued_jobs do
|
||||
subject
|
||||
end
|
||||
|
||||
should_not_email(user)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the user does not have two-factor authentication enabled' do
|
||||
let(:user) { current_user }
|
||||
|
||||
it 'returns error' do
|
||||
expect(subject).to eq(
|
||||
{
|
||||
status: :error,
|
||||
message: 'Two-factor authentication is not enabled for this user'
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
it_behaves_like 'does not send notification email'
|
||||
end
|
||||
|
||||
context 'when the user has two-factor authentication enabled' do
|
||||
context 'when the executor is not authorized to disable two-factor authentication' do
|
||||
context 'disabling the two-factor authentication of another user' do
|
||||
let(:user) { create(:user, :two_factor) }
|
||||
|
||||
it 'returns error' do
|
||||
expect(subject).to eq(
|
||||
{
|
||||
status: :error,
|
||||
message: 'You are not authorized to perform this action'
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
it 'does not disable two-factor authentication' do
|
||||
expect { subject }.not_to change { user.reload.two_factor_enabled? }.from(true)
|
||||
end
|
||||
|
||||
it_behaves_like 'does not send notification email'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the executor is authorized to disable two-factor authentication' do
|
||||
shared_examples_for 'disables two-factor authentication' do
|
||||
it 'returns success' do
|
||||
expect(subject).to eq({ status: :success })
|
||||
end
|
||||
|
||||
it 'disables the two-factor authentication of the user' do
|
||||
expect { subject }.to change { user.reload.two_factor_enabled? }.from(true).to(false)
|
||||
end
|
||||
|
||||
context 'notification', :mailer do
|
||||
it 'sends a notification' do
|
||||
perform_enqueued_jobs do
|
||||
subject
|
||||
end
|
||||
|
||||
should_email(user)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'disabling their own two-factor authentication' do
|
||||
let(:current_user) { create(:user, :two_factor) }
|
||||
let(:user) { current_user }
|
||||
|
||||
it_behaves_like 'disables two-factor authentication'
|
||||
end
|
||||
|
||||
context 'admin disables the two-factor authentication of another user' do
|
||||
let(:current_user) { create(:admin) }
|
||||
let(:user) { create(:user, :two_factor) }
|
||||
|
||||
it_behaves_like 'disables two-factor authentication'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -260,6 +260,7 @@ module TestEnv
|
|||
listen_addr = [host, port].join(':')
|
||||
|
||||
workhorse_pid = spawn(
|
||||
{ 'PATH' => "#{ENV['PATH']}:#{workhorse_dir}" },
|
||||
File.join(workhorse_dir, 'gitlab-workhorse'),
|
||||
'-authSocket', upstream,
|
||||
'-documentRoot', Rails.root.join('public').to_s,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.shared_context 'file upload requests helpers' do
|
||||
def capybara_url(path)
|
||||
"http://#{Capybara.current_session.server.host}:#{Capybara.current_session.server.port}#{path}"
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.shared_examples 'handling file uploads' do |shared_examples_name|
|
||||
context 'with object storage disabled' do
|
||||
it_behaves_like shared_examples_name
|
||||
end
|
||||
end
|
||||
|
|
@ -848,10 +848,10 @@
|
|||
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.158.0.tgz#300d416184a2b0e05f15a96547f726e1825b08a1"
|
||||
integrity sha512-5OJl+7TsXN9PJhY6/uwi+mTwmDZa9n/6119rf77orQ/joFYUypaYhBmy/1TcKVPsy5Zs6KCxE1kmGsfoXc1TYA==
|
||||
|
||||
"@gitlab/ui@20.3.1":
|
||||
version "20.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-20.3.1.tgz#4f29f9c16b34303074228081264415c3cd1e04de"
|
||||
integrity sha512-CwxTKzvyVU4s25RCcfa4NBSxnRqQ/zHrYsAyBOJdK7uTDcuoPh6UqvXw4U0ghyIExRtTsF9GCWQJNYxcRT6p/g==
|
||||
"@gitlab/ui@20.4.0":
|
||||
version "20.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-20.4.0.tgz#ea5195b181f56312ede55e89c444805594adedbb"
|
||||
integrity sha512-QLxj0a2iRDuSvAdvgZf8KtpUg8Bt8jSQbupCdiiohSp73LidRB4aZv0b/TTb6sxpmhKRaKSx9uqHrpHXtymGyw==
|
||||
dependencies:
|
||||
"@babel/standalone" "^7.0.0"
|
||||
"@gitlab/vue-toasted" "^1.3.0"
|
||||
|
|
|
|||
Loading…
Reference in New Issue