Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-08-20 15:10:18 +00:00
parent 3b963d6919
commit 520f317866
85 changed files with 1013 additions and 231 deletions

View File

@ -1 +1 @@
78d2b0cdb08b0e45de5324e2ac992282b7ecf691
0fe0cfaccc979592610cbf65807f19b307957750

View File

@ -1 +1 @@
8.39.0
8.41.0

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -34,6 +34,10 @@
background-color: $gray-500;
}
}
.design-detail-overlay-add-comment {
cursor: crosshair;
}
}
.design-presentation-wrapper {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,6 @@
%p
= say_hi(@user)
%p
= two_factor_authentication_disabled_text
%p
= re_enable_two_factor_authentication_text(format: :html)

View File

@ -0,0 +1,5 @@
<%= say_hi(@user) %>
<%= two_factor_authentication_disabled_text %>
<%= re_enable_two_factor_authentication_text %>

View File

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

View File

@ -0,0 +1,5 @@
---
title: Take relative_url_path into account when building URLs in snippets
merge_request: 39960
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Use pointer:crosshair when hovering on the design view
merge_request: 39671
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Remove time tracking from incidents sidebar
merge_request: 39837
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Add ability to associate Environment with Alert with gitlab_environment_name payload key
merge_request: 39785
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Send email notification on disabling 2FA
merge_request: 39572
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Automatically add AJAX API requests to the performance bar
merge_request: 39069
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Tweak file-by-file display and add file current/total display
merge_request: 39719
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Replace fa-circle icon instances with GitLab SVG check icon
merge_request: 39745
author:
type: changed

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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}
`(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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