diff options
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" |