summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--GITLAB_WORKHORSE_VERSION2
-rw-r--r--app/assets/javascripts/design_management/components/design_overlay.vue42
-rw-r--r--app/assets/javascripts/diffs/components/app.vue57
-rw-r--r--app/assets/javascripts/pdf/page/index.vue10
-rw-r--r--app/assets/javascripts/performance_bar/services/performance_bar_service.js3
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/nav_controls.vue13
-rw-r--r--app/assets/javascripts/snippets/components/edit.vue4
-rw-r--r--app/assets/javascripts/snippets/components/snippet_header.vue4
-rw-r--r--app/assets/stylesheets/components/design_management/design.scss4
-rw-r--r--app/controllers/admin/users_controller.rb10
-rw-r--r--app/controllers/profiles/two_factor_auths_controller.rb8
-rw-r--r--app/helpers/emails_helper.rb21
-rw-r--r--app/helpers/icons_helper.rb2
-rw-r--r--app/mailers/emails/profile.rb10
-rw-r--r--app/models/concerns/issuable.rb8
-rw-r--r--app/policies/user_policy.rb1
-rw-r--r--app/serializers/issuable_sidebar_basic_entity.rb2
-rw-r--r--app/services/event_create_service.rb6
-rw-r--r--app/services/notification_service.rb6
-rw-r--r--app/services/two_factor/base_service.rb14
-rw-r--r--app/services/two_factor/destroy_service.rb24
-rw-r--r--app/views/admin/health_check/show.html.haml2
-rw-r--r--app/views/notify/disabled_two_factor_email.html.haml6
-rw-r--r--app/views/notify/disabled_two_factor_email.text.erb5
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml11
-rw-r--r--changelogs/unreleased/220509-new-snippet-link.yml5
-rw-r--r--changelogs/unreleased/225242-use-pointer-crosshair-when-hovering-on-the-design-view.yml5
-rw-r--r--changelogs/unreleased/229970-time-tracking-incident.yml5
-rw-r--r--changelogs/unreleased/238485-generic-alert-environment.yml5
-rw-r--r--changelogs/unreleased/26149-email-notifications-not-being-sent-on-important-security-event-like.yml5
-rw-r--r--changelogs/unreleased/33909-show-peek-ajax-api-requests.yml5
-rw-r--r--changelogs/unreleased/maintenance-improve-file-by-file.yml5
-rw-r--r--changelogs/unreleased/mw-replace-circle-icon.yml5
-rw-r--r--doc/development/telemetry/usage_ping.md6
-rw-r--r--doc/topics/autodevops/upgrading_chart.md6
-rw-r--r--doc/user/profile/notifications.md1
-rw-r--r--doc/user/project/integrations/generic_alerts.md1
-rw-r--r--lib/gitlab/alert_management/alert_params.rb3
-rw-r--r--lib/gitlab/alerting/notification_payload_parser.rb13
-rw-r--r--lib/gitlab/usage_data.rb14
-rw-r--r--lib/gitlab/usage_data_counters/track_unique_events.rb (renamed from lib/gitlab/usage_data_counters/track_unique_actions.rb)4
-rw-r--r--locale/gitlab.pot31
-rw-r--r--package.json2
-rw-r--r--spec/controllers/admin/users_controller_spec.rb46
-rw-r--r--spec/controllers/profiles/two_factor_auths_controller_spec.rb42
-rw-r--r--spec/features/file_uploads/ci_artifact_spec.rb29
-rw-r--r--spec/features/file_uploads/git_lfs_spec.rb37
-rw-r--r--spec/features/file_uploads/graphql_add_design_spec.rb54
-rw-r--r--spec/features/file_uploads/group_import_spec.rb32
-rw-r--r--spec/features/file_uploads/maven_package_spec.rb29
-rw-r--r--spec/features/file_uploads/nuget_package_spec.rb35
-rw-r--r--spec/features/file_uploads/project_import_spec.rb31
-rw-r--r--spec/features/file_uploads/user_avatar_spec.rb33
-rw-r--r--spec/features/merge_request/user_views_auto_expanding_diff_spec.rb3
-rw-r--r--spec/features/merge_request/user_views_diffs_file_by_file_spec.rb2
-rw-r--r--spec/features/projects/artifacts/raw_spec.rb2
-rw-r--r--spec/fixtures/api/schemas/entities/issue_sidebar.json3
-rw-r--r--spec/fixtures/api/schemas/entities/merge_request_sidebar.json3
-rw-r--r--spec/frontend/design_management/components/design_overlay_spec.js9
-rw-r--r--spec/frontend/design_management/components/design_presentation_spec.js2
-rw-r--r--spec/frontend/diffs/components/app_spec.js40
-rw-r--r--spec/frontend/monitoring/components/charts/stacked_column_spec.js10
-rw-r--r--spec/frontend/monitoring/graph_data.js13
-rw-r--r--spec/frontend/monitoring/mock_data.js45
-rw-r--r--spec/frontend/performance_bar/services/performance_bar_service_spec.js30
-rw-r--r--spec/frontend/snippets/components/edit_spec.js8
-rw-r--r--spec/frontend/snippets/components/snippet_header_spec.js5
-rw-r--r--spec/helpers/emails_helper_spec.rb37
-rw-r--r--spec/lib/gitlab/alert_management/alert_params_spec.rb3
-rw-r--r--spec/lib/gitlab/alerting/notification_payload_parser_spec.rb12
-rw-r--r--spec/lib/gitlab/usage_data_counters/track_unique_events_spec.rb (renamed from spec/lib/gitlab/usage_data_counters/track_unique_actions_spec.rb)4
-rw-r--r--spec/lib/gitlab/usage_data_spec.rb2
-rw-r--r--spec/mailers/emails/profile_spec.rb22
-rw-r--r--spec/models/concerns/issuable_spec.rb36
-rw-r--r--spec/policies/user_policy_spec.rb20
-rw-r--r--spec/serializers/issue_serializer_spec.rb5
-rw-r--r--spec/services/event_create_service_spec.rb20
-rw-r--r--spec/services/notification_service_spec.rb10
-rw-r--r--spec/services/projects/alerting/notify_service_spec.rb10
-rw-r--r--spec/services/two_factor/destroy_service_spec.rb97
-rw-r--r--spec/support/helpers/test_env.rb1
-rw-r--r--spec/support/shared_contexts/features/file_uploads_shared_context.rb7
-rw-r--r--spec/support/shared_examples/features/file_uploads_shared_examples.rb7
-rw-r--r--yarn.lock8
85 files changed, 1012 insertions, 230 deletions
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 4d339815e2f..2df7975119c 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-78d2b0cdb08b0e45de5324e2ac992282b7ecf691
+0fe0cfaccc979592610cbf65807f19b307957750
diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION
index aaccb7fe689..c6959d4d950 100644
--- a/GITLAB_WORKHORSE_VERSION
+++ b/GITLAB_WORKHORSE_VERSION
@@ -1 +1 @@
-8.39.0
+8.41.0
diff --git a/app/assets/javascripts/design_management/components/design_overlay.vue b/app/assets/javascripts/design_management/components/design_overlay.vue
index 926e7c74802..3f214ff54b4 100644
--- a/app/assets/javascripts/design_management/components/design_overlay.vue
+++ b/app/assets/javascripts/design_management/components/design_overlay.vue
@@ -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"
diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue
index 5062006424e..3680bebfdd0 100644
--- a/app/assets/javascripts/diffs/components/app.vue
+++ b/app/assets/javascripts/diffs/components/app.vue
@@ -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" />
diff --git a/app/assets/javascripts/pdf/page/index.vue b/app/assets/javascripts/pdf/page/index.vue
index 65f84e75e86..843c50cf9bc 100644
--- a/app/assets/javascripts/pdf/page/index.vue
+++ b/app/assets/javascripts/pdf/page/index.vue
@@ -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>
diff --git a/app/assets/javascripts/performance_bar/services/performance_bar_service.js b/app/assets/javascripts/performance_bar/services/performance_bar_service.js
index 61b35b4b8f5..164f1f8dff7 100644
--- a/app/assets/javascripts/performance_bar/services/performance_bar_service.js
+++ b/app/assets/javascripts/performance_bar/services/performance_bar_service.js
@@ -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];
}
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/nav_controls.vue b/app/assets/javascripts/pipelines/components/pipelines_list/nav_controls.vue
index a66bbb7e5ba..799da610370 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/nav_controls.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/nav_controls.vue
@@ -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>
diff --git a/app/assets/javascripts/snippets/components/edit.vue b/app/assets/javascripts/snippets/components/edit.vue
index 6e3a670dc38..2c067a36f75 100644
--- a/app/assets/javascripts/snippets/components/edit.vue
+++ b/app/assets/javascripts/snippets/components/edit.vue
@@ -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;
},
diff --git a/app/assets/javascripts/snippets/components/snippet_header.vue b/app/assets/javascripts/snippets/components/snippet_header.vue
index ed087dcfaf9..057756f7d64 100644
--- a/app/assets/javascripts/snippets/components/snippet_header.vue
+++ b/app/assets/javascripts/snippets/components/snippet_header.vue
@@ -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();
diff --git a/app/assets/stylesheets/components/design_management/design.scss b/app/assets/stylesheets/components/design_management/design.scss
index 80421598966..21133316291 100644
--- a/app/assets/stylesheets/components/design_management/design.scss
+++ b/app/assets/stylesheets/components/design_management/design.scss
@@ -34,6 +34,10 @@
background-color: $gray-500;
}
}
+
+ .design-detail-overlay-add-comment {
+ cursor: crosshair;
+ }
}
.design-presentation-wrapper {
diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb
index fc0acd8f99a..c3ea7f28530 100644
--- a/app/controllers/admin/users_controller.rb
+++ b/app/controllers/admin/users_controller.rb
@@ -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
diff --git a/app/controllers/profiles/two_factor_auths_controller.rb b/app/controllers/profiles/two_factor_auths_controller.rb
index 95b9344c551..a88c5ca4fa1 100644
--- a/app/controllers/profiles/two_factor_auths_controller.rb
+++ b/app/controllers/profiles/two_factor_auths_controller.rb
@@ -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
diff --git a/app/helpers/emails_helper.rb b/app/helpers/emails_helper.rb
index ba2330dfc9a..9a44b66002a 100644
--- a/app/helpers/emails_helper.rb
+++ b/app/helpers/emails_helper.rb
@@ -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?
diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb
index 9957d5c6330..0352b0ddf28 100644
--- a/app/helpers/icons_helper.rb
+++ b/app/helpers/icons_helper.rb
@@ -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
diff --git a/app/mailers/emails/profile.rb b/app/mailers/emails/profile.rb
index b45755788b8..96cf3571968 100644
--- a/app/mailers/emails/profile.rb
+++ b/app/mailers/emails/profile.rb
@@ -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
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index dd5aedbb760..46cd9649c2d 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -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
diff --git a/app/policies/user_policy.rb b/app/policies/user_policy.rb
index 6ebafca9885..c9dfa98b285 100644
--- a/app/policies/user_policy.rb
+++ b/app/policies/user_policy.rb
@@ -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')
diff --git a/app/serializers/issuable_sidebar_basic_entity.rb b/app/serializers/issuable_sidebar_basic_entity.rb
index bbec107544e..7e4164fecbc 100644
--- a/app/serializers/issuable_sidebar_basic_entity.rb
+++ b/app/serializers/issuable_sidebar_basic_entity.rb
@@ -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
diff --git a/app/services/event_create_service.rb b/app/services/event_create_service.rb
index 3921dbefd06..c7be0a1f686 100644
--- a/app/services/event_create_service.rb
+++ b/app/services/event_create_service.rb
@@ -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)
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index 909a0033d12..731d72c41d4 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -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
#
diff --git a/app/services/two_factor/base_service.rb b/app/services/two_factor/base_service.rb
new file mode 100644
index 00000000000..7d3f63f3442
--- /dev/null
+++ b/app/services/two_factor/base_service.rb
@@ -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
diff --git a/app/services/two_factor/destroy_service.rb b/app/services/two_factor/destroy_service.rb
new file mode 100644
index 00000000000..b8bbe215d6e
--- /dev/null
+++ b/app/services/two_factor/destroy_service.rb
@@ -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
diff --git a/app/views/admin/health_check/show.html.haml b/app/views/admin/health_check/show.html.haml
index fbe37f6c509..65d3c78ec11 100644
--- a/app/views/admin/health_check/show.html.haml
+++ b/app/views/admin/health_check/show.html.haml
@@ -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')
diff --git a/app/views/notify/disabled_two_factor_email.html.haml b/app/views/notify/disabled_two_factor_email.html.haml
new file mode 100644
index 00000000000..8c64a43fc07
--- /dev/null
+++ b/app/views/notify/disabled_two_factor_email.html.haml
@@ -0,0 +1,6 @@
+%p
+ = say_hi(@user)
+%p
+ = two_factor_authentication_disabled_text
+%p
+ = re_enable_two_factor_authentication_text(format: :html)
diff --git a/app/views/notify/disabled_two_factor_email.text.erb b/app/views/notify/disabled_two_factor_email.text.erb
new file mode 100644
index 00000000000..46eeab4414f
--- /dev/null
+++ b/app/views/notify/disabled_two_factor_email.text.erb
@@ -0,0 +1,5 @@
+<%= say_hi(@user) %>
+
+<%= two_factor_authentication_disabled_text %>
+
+<%= re_enable_two_factor_authentication_text %>
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 6f31d7290b7..7cdebdb646d 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -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]) }
diff --git a/changelogs/unreleased/220509-new-snippet-link.yml b/changelogs/unreleased/220509-new-snippet-link.yml
new file mode 100644
index 00000000000..6bf21189d9f
--- /dev/null
+++ b/changelogs/unreleased/220509-new-snippet-link.yml
@@ -0,0 +1,5 @@
+---
+title: Take relative_url_path into account when building URLs in snippets
+merge_request: 39960
+author:
+type: fixed
diff --git a/changelogs/unreleased/225242-use-pointer-crosshair-when-hovering-on-the-design-view.yml b/changelogs/unreleased/225242-use-pointer-crosshair-when-hovering-on-the-design-view.yml
new file mode 100644
index 00000000000..899202dbda6
--- /dev/null
+++ b/changelogs/unreleased/225242-use-pointer-crosshair-when-hovering-on-the-design-view.yml
@@ -0,0 +1,5 @@
+---
+title: Use pointer:crosshair when hovering on the design view
+merge_request: 39671
+author:
+type: changed
diff --git a/changelogs/unreleased/229970-time-tracking-incident.yml b/changelogs/unreleased/229970-time-tracking-incident.yml
new file mode 100644
index 00000000000..9ee1ea84e8d
--- /dev/null
+++ b/changelogs/unreleased/229970-time-tracking-incident.yml
@@ -0,0 +1,5 @@
+---
+title: Remove time tracking from incidents sidebar
+merge_request: 39837
+author:
+type: changed
diff --git a/changelogs/unreleased/238485-generic-alert-environment.yml b/changelogs/unreleased/238485-generic-alert-environment.yml
new file mode 100644
index 00000000000..79a73bdbd94
--- /dev/null
+++ b/changelogs/unreleased/238485-generic-alert-environment.yml
@@ -0,0 +1,5 @@
+---
+title: Add ability to associate Environment with Alert with gitlab_environment_name payload key
+merge_request: 39785
+author:
+type: added
diff --git a/changelogs/unreleased/26149-email-notifications-not-being-sent-on-important-security-event-like.yml b/changelogs/unreleased/26149-email-notifications-not-being-sent-on-important-security-event-like.yml
new file mode 100644
index 00000000000..94563db669c
--- /dev/null
+++ b/changelogs/unreleased/26149-email-notifications-not-being-sent-on-important-security-event-like.yml
@@ -0,0 +1,5 @@
+---
+title: Send email notification on disabling 2FA
+merge_request: 39572
+author:
+type: added
diff --git a/changelogs/unreleased/33909-show-peek-ajax-api-requests.yml b/changelogs/unreleased/33909-show-peek-ajax-api-requests.yml
new file mode 100644
index 00000000000..aadf7bc1e7a
--- /dev/null
+++ b/changelogs/unreleased/33909-show-peek-ajax-api-requests.yml
@@ -0,0 +1,5 @@
+---
+title: Automatically add AJAX API requests to the performance bar
+merge_request: 39069
+author:
+type: added
diff --git a/changelogs/unreleased/maintenance-improve-file-by-file.yml b/changelogs/unreleased/maintenance-improve-file-by-file.yml
new file mode 100644
index 00000000000..56b1668f0b1
--- /dev/null
+++ b/changelogs/unreleased/maintenance-improve-file-by-file.yml
@@ -0,0 +1,5 @@
+---
+title: Tweak file-by-file display and add file current/total display
+merge_request: 39719
+author:
+type: changed
diff --git a/changelogs/unreleased/mw-replace-circle-icon.yml b/changelogs/unreleased/mw-replace-circle-icon.yml
new file mode 100644
index 00000000000..ae64a425aea
--- /dev/null
+++ b/changelogs/unreleased/mw-replace-circle-icon.yml
@@ -0,0 +1,5 @@
+---
+title: Replace fa-circle icon instances with GitLab SVG check icon
+merge_request: 39745
+author:
+type: changed
diff --git a/doc/development/telemetry/usage_ping.md b/doc/development/telemetry/usage_ping.md
index ea5eb6c389f..be599898214 100644
--- a/doc/development/telemetry/usage_ping.md
+++ b/doc/development/telemetry/usage_ping.md
@@ -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
)
diff --git a/doc/topics/autodevops/upgrading_chart.md b/doc/topics/autodevops/upgrading_chart.md
index e4dacdfcf5b..ffa485f6d2c 100644
--- a/doc/topics/autodevops/upgrading_chart.md
+++ b/doc/topics/autodevops/upgrading_chart.md
@@ -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`.
diff --git a/doc/user/profile/notifications.md b/doc/user/profile/notifications.md
index 336c1b8f254..1ec09639694 100644
--- a/doc/user/profile/notifications.md
+++ b/doc/user/profile/notifications.md
@@ -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 |
diff --git a/doc/user/project/integrations/generic_alerts.md b/doc/user/project/integrations/generic_alerts.md
index dc6aa40ea82..42f33e8d670 100644
--- a/doc/user/project/integrations/generic_alerts.md
+++ b/doc/user/project/integrations/generic_alerts.md
@@ -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
diff --git a/lib/gitlab/alert_management/alert_params.rb b/lib/gitlab/alert_management/alert_params.rb
index 84a75e62ecf..0edc77efa10 100644
--- a/lib/gitlab/alert_management/alert_params.rb
+++ b/lib/gitlab/alert_management/alert_params.rb
@@ -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
diff --git a/lib/gitlab/alerting/notification_payload_parser.rb b/lib/gitlab/alerting/notification_payload_parser.rb
index f285dcf507f..ce04205a1ba 100644
--- a/lib/gitlab/alerting/notification_payload_parser.rb
+++ b/lib/gitlab/alerting/notification_payload_parser.rb
@@ -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
diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb
index 70efe86143e..f4d3186657f 100644
--- a/lib/gitlab/usage_data.rb
+++ b/lib/gitlab/usage_data.rb
@@ -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
)
diff --git a/lib/gitlab/usage_data_counters/track_unique_actions.rb b/lib/gitlab/usage_data_counters/track_unique_events.rb
index 0df982572a4..db18200f059 100644
--- a/lib/gitlab/usage_data_counters/track_unique_actions.rb
+++ b/lib/gitlab/usage_data_counters/track_unique_events.rb
@@ -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)
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 050b08dc32f..2cfa87b9436 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -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 ""
diff --git a/package.json b/package.json
index 99dd3a819b7..97055523636 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb
index 08a1d7c9fa9..30fa991190a 100644
--- a/spec/controllers/admin/users_controller_spec.rb
+++ b/spec/controllers/admin/users_controller_spec.rb
@@ -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
- end
+ context 'for a user that has 2FA enabled' do
+ let(:user) { create(:user, :two_factor) }
- it 'redirects back' do
- go
+ it 'disables 2FA for the user' do
+ subject
- expect(response).to redirect_to(admin_user_path(user))
- end
+ 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 an alert' do
- go
+ it 'displays a notice on success' do
+ subject
- expect(flash[:notice])
- .to eq _('Two-factor Authentication has been disabled for this user')
+ expect(flash[:notice])
+ .to eq _('Two-factor authentication has been disabled for this user')
+ end
end
- def go
- patch :disable_two_factor, params: { id: user.to_param }
+ 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
+
+ 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
diff --git a/spec/controllers/profiles/two_factor_auths_controller_spec.rb b/spec/controllers/profiles/two_factor_auths_controller_spec.rb
index f645081219a..e6839f54c5d 100644
--- a/spec/controllers/profiles/two_factor_auths_controller_spec.rb
+++ b/spec/controllers/profiles/two_factor_auths_controller_spec.rb
@@ -107,18 +107,46 @@ RSpec.describe Profiles::TwoFactorAuthsController do
end
describe 'DELETE destroy' do
- let(:user) { create(:user, :two_factor) }
+ subject { delete :destroy }
+
+ context 'for a user that has 2FA enabled' do
+ let(:user) { create(:user, :two_factor) }
+
+ 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 'disables two factor' do
- expect(user).to receive(:disable_two_factor!)
+ it 'displays a notice on success' do
+ subject
- delete :destroy
+ 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
diff --git a/spec/features/file_uploads/ci_artifact_spec.rb b/spec/features/file_uploads/ci_artifact_spec.rb
new file mode 100644
index 00000000000..4f3b6c90ad4
--- /dev/null
+++ b/spec/features/file_uploads/ci_artifact_spec.rb
@@ -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
diff --git a/spec/features/file_uploads/git_lfs_spec.rb b/spec/features/file_uploads/git_lfs_spec.rb
new file mode 100644
index 00000000000..b902d7ab702
--- /dev/null
+++ b/spec/features/file_uploads/git_lfs_spec.rb
@@ -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
diff --git a/spec/features/file_uploads/graphql_add_design_spec.rb b/spec/features/file_uploads/graphql_add_design_spec.rb
new file mode 100644
index 00000000000..f805ea86b4c
--- /dev/null
+++ b/spec/features/file_uploads/graphql_add_design_spec.rb
@@ -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
diff --git a/spec/features/file_uploads/group_import_spec.rb b/spec/features/file_uploads/group_import_spec.rb
new file mode 100644
index 00000000000..0f9d05c3975
--- /dev/null
+++ b/spec/features/file_uploads/group_import_spec.rb
@@ -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
diff --git a/spec/features/file_uploads/maven_package_spec.rb b/spec/features/file_uploads/maven_package_spec.rb
new file mode 100644
index 00000000000..c873a0e9a36
--- /dev/null
+++ b/spec/features/file_uploads/maven_package_spec.rb
@@ -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
diff --git a/spec/features/file_uploads/nuget_package_spec.rb b/spec/features/file_uploads/nuget_package_spec.rb
new file mode 100644
index 00000000000..fb1e0a54744
--- /dev/null
+++ b/spec/features/file_uploads/nuget_package_spec.rb
@@ -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
diff --git a/spec/features/file_uploads/project_import_spec.rb b/spec/features/file_uploads/project_import_spec.rb
new file mode 100644
index 00000000000..1bf16f46c63
--- /dev/null
+++ b/spec/features/file_uploads/project_import_spec.rb
@@ -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
diff --git a/spec/features/file_uploads/user_avatar_spec.rb b/spec/features/file_uploads/user_avatar_spec.rb
new file mode 100644
index 00000000000..043115be61a
--- /dev/null
+++ b/spec/features/file_uploads/user_avatar_spec.rb
@@ -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
diff --git a/spec/features/merge_request/user_views_auto_expanding_diff_spec.rb b/spec/features/merge_request/user_views_auto_expanding_diff_spec.rb
index 20a5910e66d..585a389157e 100644
--- a/spec/features/merge_request/user_views_auto_expanding_diff_spec.rb
+++ b/spec/features/merge_request/user_views_auto_expanding_diff_spec.rb
@@ -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
diff --git a/spec/features/merge_request/user_views_diffs_file_by_file_spec.rb b/spec/features/merge_request/user_views_diffs_file_by_file_spec.rb
index abb313cb529..bb4bf0864c9 100644
--- a/spec/features/merge_request/user_views_diffs_file_by_file_spec.rb
+++ b/spec/features/merge_request/user_views_diffs_file_by_file_spec.rb
@@ -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')
diff --git a/spec/features/projects/artifacts/raw_spec.rb b/spec/features/projects/artifacts/raw_spec.rb
index d72a35fddf8..d580262d48b 100644
--- a/spec/features/projects/artifacts/raw_spec.rb
+++ b/spec/features/projects/artifacts/raw_spec.rb
@@ -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) }
diff --git a/spec/fixtures/api/schemas/entities/issue_sidebar.json b/spec/fixtures/api/schemas/entities/issue_sidebar.json
index 93adb493d1b..9161c992a97 100644
--- a/spec/fixtures/api/schemas/entities/issue_sidebar.json
+++ b/spec/fixtures/api/schemas/entities/issue_sidebar.json
@@ -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" }
}
}
diff --git a/spec/fixtures/api/schemas/entities/merge_request_sidebar.json b/spec/fixtures/api/schemas/entities/merge_request_sidebar.json
index 9945de8a856..c20d07e99f7 100644
--- a/spec/fixtures/api/schemas/entities/merge_request_sidebar.json
+++ b/spec/fixtures/api/schemas/entities/merge_request_sidebar.json
@@ -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" }
}
}
diff --git a/spec/frontend/design_management/components/design_overlay_spec.js b/spec/frontend/design_management/components/design_overlay_spec.js
index f243323b162..bbd0fbee81f 100644
--- a/spec/frontend/design_management/components/design_overlay_spec.js
+++ b/spec/frontend/design_management/components/design_overlay_spec.js
@@ -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',
diff --git a/spec/frontend/design_management/components/design_presentation_spec.js b/spec/frontend/design_management/components/design_presentation_spec.js
index 7e513182589..d633d00f2ed 100644
--- a/spec/frontend/design_management/components/design_presentation_spec.js
+++ b/spec/frontend/design_management/components/design_presentation_spec.js
@@ -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
diff --git a/spec/frontend/diffs/components/app_spec.js b/spec/frontend/diffs/components/app_spec.js
index ac046ddc203..1f274456ded 100644
--- a/spec/frontend/diffs/components/app_spec.js
+++ b/spec/frontend/diffs/components/app_spec.js
@@ -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);
},
);
});
diff --git a/spec/frontend/monitoring/components/charts/stacked_column_spec.js b/spec/frontend/monitoring/components/charts/stacked_column_spec.js
index bb2fbc68eaa..24a2af87eb8 100644
--- a/spec/frontend/monitoring/components/charts/stacked_column_spec.js
+++ b/spec/frontend/monitoring/components/charts/stacked_column_spec.js
@@ -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',
]);
});
diff --git a/spec/frontend/monitoring/graph_data.js b/spec/frontend/monitoring/graph_data.js
index f85351e55d7..5c1de8491ea 100644
--- a/spec/frontend/monitoring/graph_data.js
+++ b/spec/frontend/monitoring/graph_data.js
@@ -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,
+ };
+};
diff --git a/spec/frontend/monitoring/mock_data.js b/spec/frontend/monitoring/mock_data.js
index 28a7dd1af4f..aea8815fb10 100644
--- a/spec/frontend/monitoring/mock_data.js
+++ b/spec/frontend/monitoring/mock_data.js
@@ -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',
diff --git a/spec/frontend/performance_bar/services/performance_bar_service_spec.js b/spec/frontend/performance_bar/services/performance_bar_service_spec.js
index cfec4b779e4..5e7a52ba734 100644
--- a/spec/frontend/performance_bar/services/performance_bar_service_spec.js
+++ b/spec/frontend/performance_bar/services/performance_bar_service_spec.js
@@ -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,
+ );
});
});
diff --git a/spec/frontend/snippets/components/edit_spec.js b/spec/frontend/snippets/components/edit_spec.js
index 980855a0615..57142da0557 100644
--- a/spec/frontend/snippets/components/edit_spec.js
+++ b/spec/frontend/snippets/components/edit_spec.js
@@ -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}
`(
diff --git a/spec/frontend/snippets/components/snippet_header_spec.js b/spec/frontend/snippets/components/snippet_header_spec.js
index da8cb2e6a8d..a997b337047 100644
--- a/spec/frontend/snippets/components/snippet_header_spec.js
+++ b/spec/frontend/snippets/components/snippet_header_spec.js
@@ -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`);
});
});
diff --git a/spec/helpers/emails_helper_spec.rb b/spec/helpers/emails_helper_spec.rb
index bc5fe05ab52..4af81cf83ac 100644
--- a/spec/helpers/emails_helper_spec.rb
+++ b/spec/helpers/emails_helper_spec.rb
@@ -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
diff --git a/spec/lib/gitlab/alert_management/alert_params_spec.rb b/spec/lib/gitlab/alert_management/alert_params_spec.rb
index 1fe27365c83..8bab79f5a6c 100644
--- a/spec/lib/gitlab/alert_management/alert_params_spec.rb
+++ b/spec/lib/gitlab/alert_management/alert_params_spec.rb
@@ -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
diff --git a/spec/lib/gitlab/alerting/notification_payload_parser_spec.rb b/spec/lib/gitlab/alerting/notification_payload_parser_spec.rb
index 0489108b159..c3d4fab221c 100644
--- a/spec/lib/gitlab/alerting/notification_payload_parser_spec.rb
+++ b/spec/lib/gitlab/alerting/notification_payload_parser_spec.rb
@@ -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
{
diff --git a/spec/lib/gitlab/usage_data_counters/track_unique_actions_spec.rb b/spec/lib/gitlab/usage_data_counters/track_unique_events_spec.rb
index bd348666729..b9cc4a3c4f8 100644
--- a/spec/lib/gitlab/usage_data_counters/track_unique_actions_spec.rb
+++ b/spec/lib/gitlab/usage_data_counters/track_unique_events_spec.rb
@@ -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
diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb
index 3be8a770b2b..589c11f79a7 100644
--- a/spec/lib/gitlab/usage_data_spec.rb
+++ b/spec/lib/gitlab/usage_data_spec.rb
@@ -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]
diff --git a/spec/mailers/emails/profile_spec.rb b/spec/mailers/emails/profile_spec.rb
index fbbdef5feee..fdff2d837f8 100644
--- a/spec/mailers/emails/profile_spec.rb
+++ b/spec/mailers/emails/profile_spec.rb
@@ -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
diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb
index 0824b5c7834..46fe942fec1 100644
--- a/spec/models/concerns/issuable_spec.rb
+++ b/spec/models/concerns/issuable_spec.rb
@@ -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
diff --git a/spec/policies/user_policy_spec.rb b/spec/policies/user_policy_spec.rb
index d7338622c86..38641558b6b 100644
--- a/spec/policies/user_policy_spec.rb
+++ b/spec/policies/user_policy_spec.rb
@@ -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
diff --git a/spec/serializers/issue_serializer_spec.rb b/spec/serializers/issue_serializer_spec.rb
index a51297d6d80..491e2f0835b 100644
--- a/spec/serializers/issue_serializer_spec.rb
+++ b/spec/serializers/issue_serializer_spec.rb
@@ -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)
diff --git a/spec/services/event_create_service_spec.rb b/spec/services/event_create_service_spec.rb
index a91519a710f..039aa12265f 100644
--- a/spec/services/event_create_service_spec.rb
+++ b/spec/services/event_create_service_spec.rb
@@ -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
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index 8186bc40bc0..78e918f9c05 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -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) }
diff --git a/spec/services/projects/alerting/notify_service_spec.rb b/spec/services/projects/alerting/notify_service_spec.rb
index 3e74a15c3c0..5dd85758de8 100644
--- a/spec/services/projects/alerting/notify_service_spec.rb
+++ b/spec/services/projects/alerting/notify_service_spec.rb
@@ -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
diff --git a/spec/services/two_factor/destroy_service_spec.rb b/spec/services/two_factor/destroy_service_spec.rb
new file mode 100644
index 00000000000..3df4d1593c6
--- /dev/null
+++ b/spec/services/two_factor/destroy_service_spec.rb
@@ -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
diff --git a/spec/support/helpers/test_env.rb b/spec/support/helpers/test_env.rb
index 7dae960410d..a64871ca75b 100644
--- a/spec/support/helpers/test_env.rb
+++ b/spec/support/helpers/test_env.rb
@@ -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,
diff --git a/spec/support/shared_contexts/features/file_uploads_shared_context.rb b/spec/support/shared_contexts/features/file_uploads_shared_context.rb
new file mode 100644
index 00000000000..972d25e81d2
--- /dev/null
+++ b/spec/support/shared_contexts/features/file_uploads_shared_context.rb
@@ -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
diff --git a/spec/support/shared_examples/features/file_uploads_shared_examples.rb b/spec/support/shared_examples/features/file_uploads_shared_examples.rb
new file mode 100644
index 00000000000..d586bf03b59
--- /dev/null
+++ b/spec/support/shared_examples/features/file_uploads_shared_examples.rb
@@ -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
diff --git a/yarn.lock b/yarn.lock
index 068513341c2..4686d870740 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -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"