Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
		
							parent
							
								
									3d38e524f1
								
							
						
					
					
						commit
						791054a0a5
					
				| 
						 | 
				
			
			@ -209,7 +209,6 @@ export default {
 | 
			
		|||
        />
 | 
			
		||||
        <sidebar-labels-widget
 | 
			
		||||
          class="block labels"
 | 
			
		||||
          data-testid="sidebar-labels"
 | 
			
		||||
          :iid="activeBoardItem.iid"
 | 
			
		||||
          :full-path="projectPathForActiveIssue"
 | 
			
		||||
          :allow-label-remove="allowLabelEdit"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -16,6 +16,7 @@ export default class CreateLabelDropdown {
 | 
			
		|||
    this.$colorPreview = $('.js-dropdown-label-color-preview', this.$el);
 | 
			
		||||
    this.$addList = $('.js-add-list', this.$el);
 | 
			
		||||
    this.$newLabelError = $('.js-label-error', this.$el);
 | 
			
		||||
    this.$newLabelErrorContent = $('.gl-alert-content', this.$newLabelError);
 | 
			
		||||
    this.$newLabelCreateButton = $('.js-new-label-btn', this.$el);
 | 
			
		||||
    this.$colorSuggestions = $('.suggest-colors-dropdown a', this.$el);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -119,7 +120,8 @@ export default class CreateLabelDropdown {
 | 
			
		|||
              .join('<br/>');
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          this.$newLabelError.html(errors).show();
 | 
			
		||||
          this.$newLabelErrorContent.html(errors);
 | 
			
		||||
          this.$newLabelError.show();
 | 
			
		||||
        } else {
 | 
			
		||||
          const addNewList = this.$addList.is(':checked');
 | 
			
		||||
          this.$dropdownBack.trigger('click');
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,12 @@
 | 
			
		|||
<script>
 | 
			
		||||
import { GlTooltipDirective, GlButton, GlFormInput, GlLink, GlLoadingIcon } from '@gitlab/ui';
 | 
			
		||||
import {
 | 
			
		||||
  GlAlert,
 | 
			
		||||
  GlTooltipDirective,
 | 
			
		||||
  GlButton,
 | 
			
		||||
  GlFormInput,
 | 
			
		||||
  GlLink,
 | 
			
		||||
  GlLoadingIcon,
 | 
			
		||||
} from '@gitlab/ui';
 | 
			
		||||
import produce from 'immer';
 | 
			
		||||
import createFlash from '~/flash';
 | 
			
		||||
import { __ } from '~/locale';
 | 
			
		||||
| 
						 | 
				
			
			@ -11,6 +18,7 @@ const errorMessage = __('Error creating label.');
 | 
			
		|||
 | 
			
		||||
export default {
 | 
			
		||||
  components: {
 | 
			
		||||
    GlAlert,
 | 
			
		||||
    GlButton,
 | 
			
		||||
    GlFormInput,
 | 
			
		||||
    GlLink,
 | 
			
		||||
| 
						 | 
				
			
			@ -42,6 +50,7 @@ export default {
 | 
			
		|||
      labelTitle: '',
 | 
			
		||||
      selectedColor: '',
 | 
			
		||||
      labelCreateInProgress: false,
 | 
			
		||||
      error: undefined,
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
| 
						 | 
				
			
			@ -111,13 +120,14 @@ export default {
 | 
			
		|||
          ) => this.updateLabelsInCache(store, label),
 | 
			
		||||
        });
 | 
			
		||||
        if (labelCreate.errors.length) {
 | 
			
		||||
          createFlash({ message: errorMessage });
 | 
			
		||||
          [this.error] = labelCreate.errors;
 | 
			
		||||
        } else {
 | 
			
		||||
          this.$emit('hideCreateView');
 | 
			
		||||
        }
 | 
			
		||||
      } catch {
 | 
			
		||||
        createFlash({ message: errorMessage });
 | 
			
		||||
      }
 | 
			
		||||
      this.labelCreateInProgress = false;
 | 
			
		||||
      this.$emit('hideCreateView');
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -126,6 +136,9 @@ export default {
 | 
			
		|||
<template>
 | 
			
		||||
  <div class="labels-select-contents-create js-labels-create">
 | 
			
		||||
    <div class="dropdown-input">
 | 
			
		||||
      <gl-alert v-if="error" variant="danger" :dismissible="false" class="gl-mb-3">
 | 
			
		||||
        {{ error }}
 | 
			
		||||
      </gl-alert>
 | 
			
		||||
      <gl-form-input
 | 
			
		||||
        v-model.trim="labelTitle"
 | 
			
		||||
        :placeholder="__('Name new label')"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -289,6 +289,7 @@ export default {
 | 
			
		|||
      'is-standalone': isDropdownVariantStandalone(variant),
 | 
			
		||||
      'is-embedded': isDropdownVariantEmbedded(variant),
 | 
			
		||||
    }"
 | 
			
		||||
    data-testid="sidebar-labels"
 | 
			
		||||
    data-qa-selector="labels_block"
 | 
			
		||||
  >
 | 
			
		||||
    <template v-if="isDropdownVariantSidebar(variant)">
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -68,13 +68,6 @@
 | 
			
		|||
  color: $white;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dropdown-labels-error {
 | 
			
		||||
  padding: 5px 10px;
 | 
			
		||||
  margin-bottom: 10px;
 | 
			
		||||
  background-color: $red-500;
 | 
			
		||||
  color: $white;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.manage-labels-list {
 | 
			
		||||
  padding: 0;
 | 
			
		||||
  margin-bottom: 0;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -158,7 +158,7 @@ class Admin::UsersController < Admin::ApplicationController
 | 
			
		|||
  end
 | 
			
		||||
 | 
			
		||||
  def confirm
 | 
			
		||||
    if update_user { |user| user.confirm }
 | 
			
		||||
    if update_user { |user| user.force_confirm }
 | 
			
		||||
      redirect_back_or_admin_user(notice: _("Successfully confirmed"))
 | 
			
		||||
    else
 | 
			
		||||
      redirect_back_or_admin_user(alert: _("Error occurred. User was not confirmed"))
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -33,17 +33,15 @@ class Groups::DependencyProxyForContainersController < ::Groups::DependencyProxy
 | 
			
		|||
  end
 | 
			
		||||
 | 
			
		||||
  def blob
 | 
			
		||||
    return blob_via_workhorse if Feature.enabled?(:dependency_proxy_workhorse, group, default_enabled: :yaml)
 | 
			
		||||
    blob = @group.dependency_proxy_blobs.find_by_file_name(blob_file_name)
 | 
			
		||||
 | 
			
		||||
    result = DependencyProxy::FindOrCreateBlobService
 | 
			
		||||
      .new(group, image, token, params[:sha]).execute
 | 
			
		||||
 | 
			
		||||
    if result[:status] == :success
 | 
			
		||||
      event_name = tracking_event_name(object_type: :blob, from_cache: result[:from_cache])
 | 
			
		||||
    if blob.present?
 | 
			
		||||
      event_name = tracking_event_name(object_type: :blob, from_cache: true)
 | 
			
		||||
      track_package_event(event_name, :dependency_proxy, namespace: group, user: auth_user)
 | 
			
		||||
      send_upload(result[:blob].file)
 | 
			
		||||
 | 
			
		||||
      send_upload(blob.file)
 | 
			
		||||
    else
 | 
			
		||||
      head result[:http_status]
 | 
			
		||||
      send_dependency(token_header, DependencyProxy::Registry.blob_url(image, params[:sha]), blob_file_name)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -99,19 +97,6 @@ class Groups::DependencyProxyForContainersController < ::Groups::DependencyProxy
 | 
			
		|||
 | 
			
		||||
  private
 | 
			
		||||
 | 
			
		||||
  def blob_via_workhorse
 | 
			
		||||
    blob = @group.dependency_proxy_blobs.find_by_file_name(blob_file_name)
 | 
			
		||||
 | 
			
		||||
    if blob.present?
 | 
			
		||||
      event_name = tracking_event_name(object_type: :blob, from_cache: true)
 | 
			
		||||
      track_package_event(event_name, :dependency_proxy, namespace: group, user: auth_user)
 | 
			
		||||
 | 
			
		||||
      send_upload(blob.file)
 | 
			
		||||
    else
 | 
			
		||||
      send_dependency(token_header, DependencyProxy::Registry.blob_url(image, params[:sha]), blob_file_name)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def send_manifest(manifest, from_cache:)
 | 
			
		||||
    response.headers[DependencyProxy::Manifest::DIGEST_HEADER] = manifest.digest
 | 
			
		||||
    response.headers['Content-Length'] = manifest.size
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,26 @@
 | 
			
		|||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
module ForcedEmailConfirmation
 | 
			
		||||
  extend ActiveSupport::Concern
 | 
			
		||||
 | 
			
		||||
  included do
 | 
			
		||||
    attr_accessor :skip_confirmation_period_expiry_check
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def force_confirm(args = {})
 | 
			
		||||
    self.skip_confirmation_period_expiry_check = true
 | 
			
		||||
    confirm(args)
 | 
			
		||||
  ensure
 | 
			
		||||
    self.skip_confirmation_period_expiry_check = nil
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  protected
 | 
			
		||||
 | 
			
		||||
  # Override, from Devise::Models::Confirmable
 | 
			
		||||
  # Link: https://github.com/heartcombo/devise/blob/main/lib/devise/models/confirmable.rb
 | 
			
		||||
  def confirmation_period_expired?
 | 
			
		||||
    return false if skip_confirmation_period_expiry_check
 | 
			
		||||
 | 
			
		||||
    super
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -19,6 +19,7 @@ class Email < ApplicationRecord
 | 
			
		|||
  # This module adds async behaviour to Devise emails
 | 
			
		||||
  # and should be added after Devise modules are initialized.
 | 
			
		||||
  include AsyncDeviseEmail
 | 
			
		||||
  include ForcedEmailConfirmation
 | 
			
		||||
 | 
			
		||||
  self.reconfirmable = false # currently email can't be changed, no need to reconfirm
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -81,6 +81,7 @@ class User < ApplicationRecord
 | 
			
		|||
  # This module adds async behaviour to Devise emails
 | 
			
		||||
  # and should be added after Devise modules are initialized.
 | 
			
		||||
  include AsyncDeviseEmail
 | 
			
		||||
  include ForcedEmailConfirmation
 | 
			
		||||
 | 
			
		||||
  MINIMUM_INACTIVE_DAYS = 90
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -1974,18 +1975,22 @@ class User < ApplicationRecord
 | 
			
		|||
    ci_job_token_scope.present?
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # override from Devise::Confirmable
 | 
			
		||||
  # override from Devise::Models::Confirmable
 | 
			
		||||
  #
 | 
			
		||||
  # Add the primary email to user.emails (or confirm it if it was already
 | 
			
		||||
  # present) when the primary email is confirmed.
 | 
			
		||||
  def confirm(*args)
 | 
			
		||||
    saved = super(*args)
 | 
			
		||||
  def confirm(args = {})
 | 
			
		||||
    saved = super(args)
 | 
			
		||||
    return false unless saved
 | 
			
		||||
 | 
			
		||||
    email_to_confirm = self.emails.find_by(email: self.email)
 | 
			
		||||
 | 
			
		||||
    if email_to_confirm.present?
 | 
			
		||||
      email_to_confirm.confirm(*args)
 | 
			
		||||
      if skip_confirmation_period_expiry_check
 | 
			
		||||
        email_to_confirm.force_confirm(args)
 | 
			
		||||
      else
 | 
			
		||||
        email_to_confirm.confirm(args)
 | 
			
		||||
      end
 | 
			
		||||
    else
 | 
			
		||||
      add_primary_email_to_emails!
 | 
			
		||||
    end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,38 +0,0 @@
 | 
			
		|||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
module DependencyProxy
 | 
			
		||||
  class DownloadBlobService < DependencyProxy::BaseService
 | 
			
		||||
    def initialize(image, blob_sha, token)
 | 
			
		||||
      @image = image
 | 
			
		||||
      @blob_sha = blob_sha
 | 
			
		||||
      @token = token
 | 
			
		||||
      @temp_file = Tempfile.new
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def execute
 | 
			
		||||
      File.open(@temp_file.path, "wb") do |file|
 | 
			
		||||
        Gitlab::HTTP.get(blob_url, headers: auth_headers, stream_body: true) do |fragment|
 | 
			
		||||
          if [301, 302, 307].include?(fragment.code)
 | 
			
		||||
            # do nothing
 | 
			
		||||
          elsif fragment.code == 200
 | 
			
		||||
            file.write(fragment)
 | 
			
		||||
          else
 | 
			
		||||
            raise DownloadError.new('Non-success response code on downloading blob fragment', fragment.code)
 | 
			
		||||
          end
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      success(file: @temp_file)
 | 
			
		||||
    rescue DownloadError => exception
 | 
			
		||||
      error(exception.message, exception.http_status)
 | 
			
		||||
    rescue Timeout::Error => exception
 | 
			
		||||
      error(exception.message, 599)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    private
 | 
			
		||||
 | 
			
		||||
    def blob_url
 | 
			
		||||
      registry.blob_url(@image, @blob_sha)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -1,48 +0,0 @@
 | 
			
		|||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
module DependencyProxy
 | 
			
		||||
  class FindOrCreateBlobService < DependencyProxy::BaseService
 | 
			
		||||
    def initialize(group, image, token, blob_sha)
 | 
			
		||||
      @group = group
 | 
			
		||||
      @image = image
 | 
			
		||||
      @token = token
 | 
			
		||||
      @blob_sha = blob_sha
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def execute
 | 
			
		||||
      from_cache = true
 | 
			
		||||
      file_name = @blob_sha.sub('sha256:', '') + '.gz'
 | 
			
		||||
      blob = @group.dependency_proxy_blobs.active.find_or_build(file_name)
 | 
			
		||||
 | 
			
		||||
      unless blob.persisted?
 | 
			
		||||
        from_cache = false
 | 
			
		||||
        result = DependencyProxy::DownloadBlobService
 | 
			
		||||
          .new(@image, @blob_sha, @token).execute
 | 
			
		||||
 | 
			
		||||
        if result[:status] == :error
 | 
			
		||||
          log_failure(result)
 | 
			
		||||
 | 
			
		||||
          return error('Failed to download the blob', result[:http_status])
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        blob.file = result[:file]
 | 
			
		||||
        blob.size = result[:file].size
 | 
			
		||||
        blob.save!
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      blob.read! if from_cache
 | 
			
		||||
      success(blob: blob, from_cache: from_cache)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    private
 | 
			
		||||
 | 
			
		||||
    def log_failure(result)
 | 
			
		||||
      log_error(
 | 
			
		||||
        "Dependency proxy: Failed to download the blob." \
 | 
			
		||||
        "Blob sha: #{@blob_sha}." \
 | 
			
		||||
        "Error message: #{result[:message][0, 100]}" \
 | 
			
		||||
        "HTTP status: #{result[:http_status]}"
 | 
			
		||||
      )
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -41,20 +41,51 @@ module Projects
 | 
			
		|||
      remote_mirror.update_start!
 | 
			
		||||
 | 
			
		||||
      # LFS objects must be sent first, or the push has dangling pointers
 | 
			
		||||
      send_lfs_objects!(remote_mirror)
 | 
			
		||||
      lfs_status = send_lfs_objects!(remote_mirror)
 | 
			
		||||
 | 
			
		||||
      response = remote_mirror.update_repository
 | 
			
		||||
      failed, failure_message = failure_status(lfs_status, response, remote_mirror)
 | 
			
		||||
 | 
			
		||||
      if response.divergent_refs.any?
 | 
			
		||||
        message = "Some refs have diverged and have not been updated on the remote:"
 | 
			
		||||
        message += "\n\n#{response.divergent_refs.join("\n")}"
 | 
			
		||||
 | 
			
		||||
        remote_mirror.mark_as_failed!(message)
 | 
			
		||||
      # When the issue https://gitlab.com/gitlab-org/gitlab/-/issues/349262 is closed,
 | 
			
		||||
      # we can move this block within failure_status.
 | 
			
		||||
      if failed
 | 
			
		||||
        remote_mirror.mark_as_failed!(failure_message)
 | 
			
		||||
      else
 | 
			
		||||
        remote_mirror.update_finish!
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def failure_status(lfs_status, response, remote_mirror)
 | 
			
		||||
      message = ''
 | 
			
		||||
      failed = false
 | 
			
		||||
      lfs_sync_failed = false
 | 
			
		||||
 | 
			
		||||
      if lfs_status&.dig(:status) == :error
 | 
			
		||||
        lfs_sync_failed = true
 | 
			
		||||
        message += "Error synchronizing LFS files:"
 | 
			
		||||
        message += "\n\n#{lfs_status[:message]}\n\n"
 | 
			
		||||
 | 
			
		||||
        failed = Feature.enabled?(:remote_mirror_fail_on_lfs, project, default_enabled: :yaml)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      if response.divergent_refs.any?
 | 
			
		||||
        message += "Some refs have diverged and have not been updated on the remote:"
 | 
			
		||||
        message += "\n\n#{response.divergent_refs.join("\n")}"
 | 
			
		||||
        failed = true
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      if message.present?
 | 
			
		||||
        Gitlab::AppJsonLogger.info(message: "Error synching remote mirror",
 | 
			
		||||
          project_id: project.id,
 | 
			
		||||
          project_path: project.full_path,
 | 
			
		||||
          remote_mirror_id: remote_mirror.id,
 | 
			
		||||
          lfs_sync_failed: lfs_sync_failed,
 | 
			
		||||
          divergent_ref_list: response.divergent_refs)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      [failed, message]
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def send_lfs_objects!(remote_mirror)
 | 
			
		||||
      return unless project.lfs_enabled?
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,7 +6,10 @@
 | 
			
		|||
.dropdown-page-two.dropdown-new-label
 | 
			
		||||
  = dropdown_title(create_label_title(subject), options: { back: true, close: show_close })
 | 
			
		||||
  = dropdown_content do
 | 
			
		||||
    .dropdown-labels-error.js-label-error
 | 
			
		||||
    .js-label-error.gl-alert.gl-alert-danger.gl-mb-3
 | 
			
		||||
      .gl-alert-container
 | 
			
		||||
        = sprite_icon('error', size: 16, css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
 | 
			
		||||
      .gl-alert-content
 | 
			
		||||
    %input#new_label_name.default-dropdown-input{ type: "text", placeholder: _('Name new label') }
 | 
			
		||||
    .suggest-colors.suggest-colors-dropdown
 | 
			
		||||
      = render_suggested_colors
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,8 +1,8 @@
 | 
			
		|||
---
 | 
			
		||||
name: dependency_proxy_workhorse
 | 
			
		||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68157
 | 
			
		||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/339639
 | 
			
		||||
milestone: '14.3'
 | 
			
		||||
name: remote_mirror_fail_on_lfs
 | 
			
		||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/77339
 | 
			
		||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/349262
 | 
			
		||||
milestone: '14.7'
 | 
			
		||||
type: development
 | 
			
		||||
group: group::source code
 | 
			
		||||
default_enabled: true
 | 
			
		||||
default_enabled: false
 | 
			
		||||
| 
						 | 
				
			
			@ -421,16 +421,37 @@ RSpec.describe Admin::UsersController do
 | 
			
		|||
  end
 | 
			
		||||
 | 
			
		||||
  describe 'PUT confirm/:id' do
 | 
			
		||||
    let(:user) { create(:user, confirmed_at: nil) }
 | 
			
		||||
    shared_examples_for 'confirms the user' do
 | 
			
		||||
      it 'confirms the user' do
 | 
			
		||||
        put :confirm, params: { id: user.username }
 | 
			
		||||
        user.reload
 | 
			
		||||
        expect(user.confirmed?).to be_truthy
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    let(:expired_confirmation_sent_at) { Date.today - User.confirm_within - 7.days }
 | 
			
		||||
    let(:extant_confirmation_sent_at) { Date.today }
 | 
			
		||||
 | 
			
		||||
    let(:user) do
 | 
			
		||||
      create(:user, :unconfirmed).tap do |user|
 | 
			
		||||
        user.update!(confirmation_sent_at: confirmation_sent_at)
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    before do
 | 
			
		||||
      request.env["HTTP_REFERER"] = "/"
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'confirms user' do
 | 
			
		||||
      put :confirm, params: { id: user.username }
 | 
			
		||||
      user.reload
 | 
			
		||||
      expect(user.confirmed?).to be_truthy
 | 
			
		||||
    context 'when the confirmation period has expired' do
 | 
			
		||||
      let(:confirmation_sent_at) {  expired_confirmation_sent_at }
 | 
			
		||||
 | 
			
		||||
      it_behaves_like 'confirms the user'
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'when the confirmation period has not expired' do
 | 
			
		||||
      let(:confirmation_sent_at) {  extant_confirmation_sent_at }
 | 
			
		||||
 | 
			
		||||
      it_behaves_like 'confirms the user'
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -348,74 +348,6 @@ RSpec.describe Groups::DependencyProxyForContainersController do
 | 
			
		|||
          it_behaves_like 'a successful blob pull'
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      context 'when dependency_proxy_workhorse disabled' do
 | 
			
		||||
        let(:blob_response) { { status: :success, blob: blob, from_cache: false } }
 | 
			
		||||
 | 
			
		||||
        before do
 | 
			
		||||
          stub_feature_flags(dependency_proxy_workhorse: false)
 | 
			
		||||
 | 
			
		||||
          allow_next_instance_of(DependencyProxy::FindOrCreateBlobService) do |instance|
 | 
			
		||||
            allow(instance).to receive(:execute).and_return(blob_response)
 | 
			
		||||
          end
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        context 'remote blob request fails' do
 | 
			
		||||
          let(:blob_response) do
 | 
			
		||||
            {
 | 
			
		||||
              status: :error,
 | 
			
		||||
              http_status: 400,
 | 
			
		||||
              message: ''
 | 
			
		||||
            }
 | 
			
		||||
          end
 | 
			
		||||
 | 
			
		||||
          before do
 | 
			
		||||
            group.add_guest(user)
 | 
			
		||||
          end
 | 
			
		||||
 | 
			
		||||
          it 'proxies status from the remote blob request', :aggregate_failures do
 | 
			
		||||
            subject
 | 
			
		||||
 | 
			
		||||
            expect(response).to have_gitlab_http_status(:bad_request)
 | 
			
		||||
            expect(response.body).to be_empty
 | 
			
		||||
          end
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        context 'a valid user' do
 | 
			
		||||
          before do
 | 
			
		||||
            group.add_guest(user)
 | 
			
		||||
          end
 | 
			
		||||
 | 
			
		||||
          it_behaves_like 'a successful blob pull'
 | 
			
		||||
          it_behaves_like 'a package tracking event', described_class.name, 'pull_blob'
 | 
			
		||||
 | 
			
		||||
          context 'with a cache entry' do
 | 
			
		||||
            let(:blob_response) { { status: :success, blob: blob, from_cache: true } }
 | 
			
		||||
 | 
			
		||||
            it_behaves_like 'returning response status', :success
 | 
			
		||||
            it_behaves_like 'a package tracking event', described_class.name, 'pull_blob_from_cache'
 | 
			
		||||
          end
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        context 'a valid deploy token' do
 | 
			
		||||
          let_it_be(:user) { create(:deploy_token, :group, :dependency_proxy_scopes) }
 | 
			
		||||
          let_it_be(:group_deploy_token) { create(:group_deploy_token, deploy_token: user, group: group) }
 | 
			
		||||
 | 
			
		||||
          it_behaves_like 'a successful blob pull'
 | 
			
		||||
 | 
			
		||||
          context 'pulling from a subgroup' do
 | 
			
		||||
            let_it_be_with_reload(:parent_group) { create(:group) }
 | 
			
		||||
            let_it_be_with_reload(:group) { create(:group, parent: parent_group) }
 | 
			
		||||
 | 
			
		||||
            before do
 | 
			
		||||
              parent_group.create_dependency_proxy_setting!(enabled: true)
 | 
			
		||||
              group_deploy_token.update_column(:group_id, parent_group.id)
 | 
			
		||||
            end
 | 
			
		||||
 | 
			
		||||
            it_behaves_like 'a successful blob pull'
 | 
			
		||||
          end
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it_behaves_like 'not found when disabled'
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,9 +6,11 @@ RSpec.describe 'Project issue boards sidebar', :js do
 | 
			
		|||
  include BoardHelpers
 | 
			
		||||
 | 
			
		||||
  let_it_be(:user)    { create(:user) }
 | 
			
		||||
  let_it_be(:project) { create(:project, :public) }
 | 
			
		||||
  let_it_be(:group)   { create(:group, :public) }
 | 
			
		||||
  let_it_be(:project) { create(:project, :public, namespace: group) }
 | 
			
		||||
  let_it_be(:board)   { create(:board, project: project) }
 | 
			
		||||
  let_it_be(:list)    { create(:list, board: board, position: 0) }
 | 
			
		||||
  let_it_be(:label) { create(:label, project: project, name: 'Label') }
 | 
			
		||||
  let_it_be(:list)    { create(:list, board: board, label: label, position: 0) }
 | 
			
		||||
 | 
			
		||||
  let_it_be(:issue, reload: true) { create(:issue, project: project, relative_position: 1) }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -81,28 +81,11 @@ RSpec.describe 'Group Dependency Proxy for containers', :js do
 | 
			
		|||
      let!(:dependency_proxy_blob) { create(:dependency_proxy_blob, group: group) }
 | 
			
		||||
 | 
			
		||||
      it_behaves_like 'responds with the file'
 | 
			
		||||
 | 
			
		||||
      context 'dependency_proxy_workhorse feature flag disabled' do
 | 
			
		||||
        before do
 | 
			
		||||
          stub_feature_flags({ dependency_proxy_workhorse: false })
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        it_behaves_like 'responds with the file'
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  context 'when the blob must be downloaded' do
 | 
			
		||||
    it_behaves_like 'responds with the file'
 | 
			
		||||
    it_behaves_like 'caches the file'
 | 
			
		||||
 | 
			
		||||
    context 'dependency_proxy_workhorse feature flag disabled' do
 | 
			
		||||
      before do
 | 
			
		||||
        stub_feature_flags({ dependency_proxy_workhorse: false })
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it_behaves_like 'responds with the file'
 | 
			
		||||
      it_behaves_like 'caches the file'
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,10 +8,9 @@ RSpec.describe 'Issue Sidebar' do
 | 
			
		|||
  let_it_be(:group) { create(:group, :nested) }
 | 
			
		||||
  let_it_be(:project) { create(:project, :public, namespace: group) }
 | 
			
		||||
  let_it_be(:user) { create(:user) }
 | 
			
		||||
  let_it_be(:label) { create(:label, project: project, title: 'bug') }
 | 
			
		||||
  let_it_be(:issue) { create(:labeled_issue, project: project, labels: [label]) }
 | 
			
		||||
  let_it_be(:issue) { create(:issue, project: project) }
 | 
			
		||||
  let_it_be(:label) { create(:label, project: project, name: 'Label') }
 | 
			
		||||
  let_it_be(:mock_date) { Date.today.at_beginning_of_month + 2.days }
 | 
			
		||||
  let_it_be(:xss_label) { create(:label, project: project, title: '<script>alert("xss");</script>') }
 | 
			
		||||
 | 
			
		||||
  before do
 | 
			
		||||
    stub_incoming_email_setting(enabled: true, address: "p+%{key}@gl.ab")
 | 
			
		||||
| 
						 | 
				
			
			@ -223,14 +222,6 @@ RSpec.describe 'Issue Sidebar' do
 | 
			
		|||
          restore_window_size
 | 
			
		||||
          open_issue_sidebar
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        it 'escapes XSS when viewing issue labels' do
 | 
			
		||||
          page.within('.block.labels') do
 | 
			
		||||
            click_on 'Edit'
 | 
			
		||||
 | 
			
		||||
            expect(page).to have_content '<script>alert("xss");</script>'
 | 
			
		||||
          end
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      context 'editing issue milestone', :js do
 | 
			
		||||
| 
						 | 
				
			
			@ -242,62 +233,7 @@ RSpec.describe 'Issue Sidebar' do
 | 
			
		|||
      end
 | 
			
		||||
 | 
			
		||||
      context 'editing issue labels', :js do
 | 
			
		||||
        before do
 | 
			
		||||
          issue.update!(labels: [label])
 | 
			
		||||
          page.within('.block.labels') do
 | 
			
		||||
            click_on 'Edit'
 | 
			
		||||
          end
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        it 'shows the current set of labels' do
 | 
			
		||||
          page.within('.issuable-show-labels') do
 | 
			
		||||
            expect(page).to have_content label.title
 | 
			
		||||
          end
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        it 'shows option to create a project label' do
 | 
			
		||||
          page.within('.block.labels') do
 | 
			
		||||
            expect(page).to have_content 'Create project'
 | 
			
		||||
          end
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        context 'creating a project label', :js, quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/27992' do
 | 
			
		||||
          before do
 | 
			
		||||
            page.within('.block.labels') do
 | 
			
		||||
              click_link 'Create project'
 | 
			
		||||
            end
 | 
			
		||||
          end
 | 
			
		||||
 | 
			
		||||
          it 'shows dropdown switches to "create label" section' do
 | 
			
		||||
            page.within('.block.labels') do
 | 
			
		||||
              expect(page).to have_content 'Create project label'
 | 
			
		||||
            end
 | 
			
		||||
          end
 | 
			
		||||
 | 
			
		||||
          it 'adds new label' do
 | 
			
		||||
            page.within('.block.labels') do
 | 
			
		||||
              fill_in 'new_label_name', with: 'wontfix'
 | 
			
		||||
              page.find('.suggest-colors a', match: :first).click
 | 
			
		||||
              page.find('button', text: 'Create').click
 | 
			
		||||
 | 
			
		||||
              page.within('.dropdown-page-one') do
 | 
			
		||||
                expect(page).to have_content 'wontfix'
 | 
			
		||||
              end
 | 
			
		||||
            end
 | 
			
		||||
          end
 | 
			
		||||
 | 
			
		||||
          it 'shows error message if label title is taken' do
 | 
			
		||||
            page.within('.block.labels') do
 | 
			
		||||
              fill_in 'new_label_name', with: label.title
 | 
			
		||||
              page.find('.suggest-colors a', match: :first).click
 | 
			
		||||
              page.find('button', text: 'Create').click
 | 
			
		||||
 | 
			
		||||
              page.within('.dropdown-page-two') do
 | 
			
		||||
                expect(page).to have_content 'Title has already been taken'
 | 
			
		||||
              end
 | 
			
		||||
            end
 | 
			
		||||
          end
 | 
			
		||||
        end
 | 
			
		||||
        it_behaves_like 'labels sidebar widget'
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      context 'interacting with collapsed sidebar', :js do
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,4 @@
 | 
			
		|||
import { GlLoadingIcon, GlLink } from '@gitlab/ui';
 | 
			
		||||
import { GlAlert, GlLoadingIcon, GlLink } from '@gitlab/ui';
 | 
			
		||||
import { shallowMount } from '@vue/test-utils';
 | 
			
		||||
import Vue, { nextTick } from 'vue';
 | 
			
		||||
import VueApollo from 'vue-apollo';
 | 
			
		||||
| 
						 | 
				
			
			@ -9,6 +9,7 @@ import { workspaceLabelsQueries } from '~/sidebar/constants';
 | 
			
		|||
import DropdownContentsCreateView from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue';
 | 
			
		||||
import createLabelMutation from '~/vue_shared/components/sidebar/labels_select_widget/graphql/create_label.mutation.graphql';
 | 
			
		||||
import {
 | 
			
		||||
  mockRegularLabel,
 | 
			
		||||
  mockSuggestedColors,
 | 
			
		||||
  createLabelSuccessfulResponse,
 | 
			
		||||
  workspaceLabelsQueryResponse,
 | 
			
		||||
| 
						 | 
				
			
			@ -25,8 +26,18 @@ const userRecoverableError = {
 | 
			
		|||
  errors: ['Houston, we have a problem'],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const titleTakenError = {
 | 
			
		||||
  data: {
 | 
			
		||||
    labelCreate: {
 | 
			
		||||
      label: mockRegularLabel,
 | 
			
		||||
      errors: ['Title has already been taken'],
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const createLabelSuccessHandler = jest.fn().mockResolvedValue(createLabelSuccessfulResponse);
 | 
			
		||||
const createLabelUserRecoverableErrorHandler = jest.fn().mockResolvedValue(userRecoverableError);
 | 
			
		||||
const createLabelDuplicateErrorHandler = jest.fn().mockResolvedValue(titleTakenError);
 | 
			
		||||
const createLabelErrorHandler = jest.fn().mockRejectedValue('Houston, we have a problem');
 | 
			
		||||
 | 
			
		||||
describe('DropdownContentsCreateView', () => {
 | 
			
		||||
| 
						 | 
				
			
			@ -208,4 +219,17 @@ describe('DropdownContentsCreateView', () => {
 | 
			
		|||
 | 
			
		||||
    expect(createFlash).toHaveBeenCalled();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('displays error in alert if label title is already taken', async () => {
 | 
			
		||||
    createComponent({ mutationHandler: createLabelDuplicateErrorHandler });
 | 
			
		||||
    fillLabelAttributes();
 | 
			
		||||
    await nextTick();
 | 
			
		||||
 | 
			
		||||
    findCreateButton().vm.$emit('click');
 | 
			
		||||
    await waitForPromises();
 | 
			
		||||
 | 
			
		||||
    expect(wrapper.findComponent(GlAlert).text()).toEqual(
 | 
			
		||||
      titleTakenError.data.labelCreate.errors[0],
 | 
			
		||||
    );
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -71,4 +71,84 @@ RSpec.describe Email do
 | 
			
		|||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe '#confirm' do
 | 
			
		||||
    let(:expired_confirmation_sent_at) { Date.today - described_class.confirm_within - 7.days }
 | 
			
		||||
    let(:extant_confirmation_sent_at) { Date.today }
 | 
			
		||||
 | 
			
		||||
    let(:email) do
 | 
			
		||||
      create(:email, email: 'test@gitlab.com').tap do |email|
 | 
			
		||||
        email.update!(confirmation_sent_at: confirmation_sent_at)
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    shared_examples_for 'unconfirmed email' do
 | 
			
		||||
      it 'returns unconfirmed' do
 | 
			
		||||
        expect(email.confirmed?).to be_falsey
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'when the confirmation period has expired' do
 | 
			
		||||
      let(:confirmation_sent_at) {  expired_confirmation_sent_at }
 | 
			
		||||
 | 
			
		||||
      it_behaves_like 'unconfirmed email'
 | 
			
		||||
 | 
			
		||||
      it 'does not confirm the email' do
 | 
			
		||||
        email.confirm
 | 
			
		||||
 | 
			
		||||
        expect(email.confirmed?).to be_falsey
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'when the confirmation period has not expired' do
 | 
			
		||||
      let(:confirmation_sent_at) {  extant_confirmation_sent_at }
 | 
			
		||||
 | 
			
		||||
      it_behaves_like 'unconfirmed email'
 | 
			
		||||
 | 
			
		||||
      it 'confirms the email' do
 | 
			
		||||
        email.confirm
 | 
			
		||||
 | 
			
		||||
        expect(email.confirmed?).to be_truthy
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe '#force_confirm' do
 | 
			
		||||
    let(:expired_confirmation_sent_at) { Date.today - described_class.confirm_within - 7.days }
 | 
			
		||||
    let(:extant_confirmation_sent_at) { Date.today }
 | 
			
		||||
 | 
			
		||||
    let(:email) do
 | 
			
		||||
      create(:email, email: 'test@gitlab.com').tap do |email|
 | 
			
		||||
        email.update!(confirmation_sent_at: confirmation_sent_at)
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    shared_examples_for 'unconfirmed email' do
 | 
			
		||||
      it 'returns unconfirmed' do
 | 
			
		||||
        expect(email.confirmed?).to be_falsey
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    shared_examples_for 'confirms the email on force_confirm' do
 | 
			
		||||
      it 'confirms an email' do
 | 
			
		||||
        email.force_confirm
 | 
			
		||||
 | 
			
		||||
        expect(email.reload.confirmed?).to be_truthy
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'when the confirmation period has expired' do
 | 
			
		||||
      let(:confirmation_sent_at) {  expired_confirmation_sent_at }
 | 
			
		||||
 | 
			
		||||
      it_behaves_like 'unconfirmed email'
 | 
			
		||||
      it_behaves_like 'confirms the email on force_confirm'
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'when the confirmation period has not expired' do
 | 
			
		||||
      let(:confirmation_sent_at) {  extant_confirmation_sent_at }
 | 
			
		||||
 | 
			
		||||
      it_behaves_like 'unconfirmed email'
 | 
			
		||||
      it_behaves_like 'confirms the email on force_confirm'
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1481,27 +1481,176 @@ RSpec.describe User do
 | 
			
		|||
  end
 | 
			
		||||
 | 
			
		||||
  describe '#confirm' do
 | 
			
		||||
    let(:expired_confirmation_sent_at) { Date.today - described_class.confirm_within - 7.days }
 | 
			
		||||
    let(:extant_confirmation_sent_at) { Date.today }
 | 
			
		||||
 | 
			
		||||
    before do
 | 
			
		||||
      allow_any_instance_of(ApplicationSetting).to receive(:send_user_confirmation_email).and_return(true)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    let(:user) { create(:user, :unconfirmed, unconfirmed_email: 'test@gitlab.com') }
 | 
			
		||||
 | 
			
		||||
    it 'returns unconfirmed' do
 | 
			
		||||
      expect(user.confirmed?).to be_falsey
 | 
			
		||||
    let(:user) do
 | 
			
		||||
      create(:user, :unconfirmed, unconfirmed_email: 'test@gitlab.com').tap do |user|
 | 
			
		||||
        user.update!(confirmation_sent_at: confirmation_sent_at)
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'confirms a user' do
 | 
			
		||||
      user.confirm
 | 
			
		||||
      expect(user.confirmed?).to be_truthy
 | 
			
		||||
    shared_examples_for 'unconfirmed user' do
 | 
			
		||||
      it 'returns unconfirmed' do
 | 
			
		||||
        expect(user.confirmed?).to be_falsey
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'adds the confirmed primary email to emails' do
 | 
			
		||||
      expect(user.emails.confirmed.map(&:email)).not_to include(user.email)
 | 
			
		||||
    context 'when the confirmation period has expired' do
 | 
			
		||||
      let(:confirmation_sent_at) {  expired_confirmation_sent_at }
 | 
			
		||||
 | 
			
		||||
      user.confirm
 | 
			
		||||
      it_behaves_like 'unconfirmed user'
 | 
			
		||||
 | 
			
		||||
      expect(user.emails.confirmed.map(&:email)).to include(user.email)
 | 
			
		||||
      it 'does not confirm the user' do
 | 
			
		||||
        user.confirm
 | 
			
		||||
 | 
			
		||||
        expect(user.confirmed?).to be_falsey
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'does not add the confirmed primary email to emails' do
 | 
			
		||||
        user.confirm
 | 
			
		||||
 | 
			
		||||
        expect(user.emails.confirmed.map(&:email)).not_to include(user.email)
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'when the confirmation period has not expired' do
 | 
			
		||||
      let(:confirmation_sent_at) {  extant_confirmation_sent_at }
 | 
			
		||||
 | 
			
		||||
      it_behaves_like 'unconfirmed user'
 | 
			
		||||
 | 
			
		||||
      it 'confirms a user' do
 | 
			
		||||
        user.confirm
 | 
			
		||||
        expect(user.confirmed?).to be_truthy
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'adds the confirmed primary email to emails' do
 | 
			
		||||
        expect(user.emails.confirmed.map(&:email)).not_to include(user.email)
 | 
			
		||||
 | 
			
		||||
        user.confirm
 | 
			
		||||
 | 
			
		||||
        expect(user.emails.confirmed.map(&:email)).to include(user.email)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      context 'when the primary email is already included in user.emails' do
 | 
			
		||||
        let(:expired_confirmation_sent_at_for_email) { Date.today - Email.confirm_within - 7.days }
 | 
			
		||||
        let(:extant_confirmation_sent_at_for_email) { Date.today }
 | 
			
		||||
 | 
			
		||||
        let!(:email) do
 | 
			
		||||
          create(:email, email: user.unconfirmed_email, user: user).tap do |email|
 | 
			
		||||
            email.update!(confirmation_sent_at: confirmation_sent_at_for_email)
 | 
			
		||||
          end
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        context 'when the confirmation period of the email record has expired' do
 | 
			
		||||
          let(:confirmation_sent_at_for_email) { expired_confirmation_sent_at_for_email }
 | 
			
		||||
 | 
			
		||||
          it 'does not confirm the email record' do
 | 
			
		||||
            user.confirm
 | 
			
		||||
 | 
			
		||||
            expect(email.reload.confirmed?).to be_falsey
 | 
			
		||||
          end
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        context 'when the confirmation period of the email record has not expired' do
 | 
			
		||||
          let(:confirmation_sent_at_for_email) { extant_confirmation_sent_at_for_email }
 | 
			
		||||
 | 
			
		||||
          it 'confirms the email record' do
 | 
			
		||||
            user.confirm
 | 
			
		||||
 | 
			
		||||
            expect(email.reload.confirmed?).to be_truthy
 | 
			
		||||
          end
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe '#force_confirm' do
 | 
			
		||||
    let(:expired_confirmation_sent_at) { Date.today - described_class.confirm_within - 7.days }
 | 
			
		||||
    let(:extant_confirmation_sent_at) { Date.today }
 | 
			
		||||
 | 
			
		||||
    let(:user) do
 | 
			
		||||
      create(:user, :unconfirmed, unconfirmed_email: 'test@gitlab.com').tap do |user|
 | 
			
		||||
        user.update!(confirmation_sent_at: confirmation_sent_at)
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    shared_examples_for 'unconfirmed user' do
 | 
			
		||||
      it 'returns unconfirmed' do
 | 
			
		||||
        expect(user.confirmed?).to be_falsey
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    shared_examples_for 'confirms the user on force_confirm' do
 | 
			
		||||
      it 'confirms a user' do
 | 
			
		||||
        user.force_confirm
 | 
			
		||||
        expect(user.confirmed?).to be_truthy
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    shared_examples_for 'adds the confirmed primary email to emails' do
 | 
			
		||||
      it 'adds the confirmed primary email to emails' do
 | 
			
		||||
        expect(user.emails.confirmed.map(&:email)).not_to include(user.email)
 | 
			
		||||
 | 
			
		||||
        user.force_confirm
 | 
			
		||||
 | 
			
		||||
        expect(user.emails.confirmed.map(&:email)).to include(user.email)
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    shared_examples_for 'confirms the email record if the primary email was already present in user.emails' do
 | 
			
		||||
      context 'when the primary email is already included in user.emails' do
 | 
			
		||||
        let(:expired_confirmation_sent_at_for_email) { Date.today - Email.confirm_within - 7.days }
 | 
			
		||||
        let(:extant_confirmation_sent_at_for_email) { Date.today }
 | 
			
		||||
 | 
			
		||||
        let!(:email) do
 | 
			
		||||
          create(:email, email: user.unconfirmed_email, user: user).tap do |email|
 | 
			
		||||
            email.update!(confirmation_sent_at: confirmation_sent_at_for_email)
 | 
			
		||||
          end
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        shared_examples_for 'confirms the email record' do
 | 
			
		||||
          it 'confirms the email record' do
 | 
			
		||||
            user.force_confirm
 | 
			
		||||
 | 
			
		||||
            expect(email.reload.confirmed?).to be_truthy
 | 
			
		||||
          end
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        context 'when the confirmation period of the email record has expired' do
 | 
			
		||||
          let(:confirmation_sent_at_for_email) { expired_confirmation_sent_at_for_email }
 | 
			
		||||
 | 
			
		||||
          it_behaves_like 'confirms the email record'
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        context 'when the confirmation period of the email record has not expired' do
 | 
			
		||||
          let(:confirmation_sent_at_for_email) { extant_confirmation_sent_at_for_email }
 | 
			
		||||
 | 
			
		||||
          it_behaves_like 'confirms the email record'
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'when the confirmation period has expired' do
 | 
			
		||||
      let(:confirmation_sent_at) {  expired_confirmation_sent_at }
 | 
			
		||||
 | 
			
		||||
      it_behaves_like 'unconfirmed user'
 | 
			
		||||
      it_behaves_like 'confirms the user on force_confirm'
 | 
			
		||||
      it_behaves_like 'adds the confirmed primary email to emails'
 | 
			
		||||
      it_behaves_like 'confirms the email record if the primary email was already present in user.emails'
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'when the confirmation period has not expired' do
 | 
			
		||||
      let(:confirmation_sent_at) {  extant_confirmation_sent_at }
 | 
			
		||||
 | 
			
		||||
      it_behaves_like 'unconfirmed user'
 | 
			
		||||
      it_behaves_like 'confirms the user on force_confirm'
 | 
			
		||||
      it_behaves_like 'adds the confirmed primary email to emails'
 | 
			
		||||
      it_behaves_like 'confirms the email record if the primary email was already present in user.emails'
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -533,16 +533,10 @@ RSpec.describe 'Rack Attack global throttles', :use_clean_rails_memory_store_cac
 | 
			
		|||
 | 
			
		||||
    context 'getting a blob' do
 | 
			
		||||
      let_it_be(:blob) { create(:dependency_proxy_blob) }
 | 
			
		||||
      let_it_be(:other_blob) { create(:dependency_proxy_blob) }
 | 
			
		||||
 | 
			
		||||
      let(:path) { "/v2/#{group.path}/dependency_proxy/containers/alpine/blobs/sha256:a0d0a0d46f8b52473982a3c466318f479767577551a53ffc9074c9fa7035982e" }
 | 
			
		||||
      let(:other_path) { "/v2/#{other_group.path}/dependency_proxy/containers/alpine/blobs/sha256:a0d0a0d46f8b52473982a3c466318f479767577551a53ffc9074c9fa7035982e" }
 | 
			
		||||
      let(:blob_response) { { status: :success, blob: blob, from_cache: false } }
 | 
			
		||||
 | 
			
		||||
      before do
 | 
			
		||||
        allow_next_instance_of(DependencyProxy::FindOrCreateBlobService) do |instance|
 | 
			
		||||
          allow(instance).to receive(:execute).and_return(blob_response)
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
      let(:path) { "/v2/#{blob.group.path}/dependency_proxy/containers/alpine/blobs/sha256:a0d0a0d46f8b52473982a3c466318f479767577551a53ffc9074c9fa7035982e" }
 | 
			
		||||
      let(:other_path) { "/v2/#{other_blob.group.path}/dependency_proxy/containers/alpine/blobs/sha256:a0d0a0d46f8b52473982a3c466318f479767577551a53ffc9074c9fa7035982e" }
 | 
			
		||||
 | 
			
		||||
      it_behaves_like 'rate-limited token-authenticated requests'
 | 
			
		||||
    end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,59 +0,0 @@
 | 
			
		|||
# frozen_string_literal: true
 | 
			
		||||
require 'spec_helper'
 | 
			
		||||
 | 
			
		||||
RSpec.describe DependencyProxy::DownloadBlobService do
 | 
			
		||||
  include DependencyProxyHelpers
 | 
			
		||||
 | 
			
		||||
  let(:image) { 'alpine' }
 | 
			
		||||
  let(:token) { Digest::SHA256.hexdigest('123') }
 | 
			
		||||
  let(:blob_sha) { Digest::SHA256.hexdigest('ruby:2.7.0') }
 | 
			
		||||
 | 
			
		||||
  subject(:download_blob) { described_class.new(image, blob_sha, token).execute }
 | 
			
		||||
 | 
			
		||||
  context 'remote request is successful' do
 | 
			
		||||
    before do
 | 
			
		||||
      stub_blob_download(image, blob_sha)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it { expect(subject[:status]).to eq(:success) }
 | 
			
		||||
    it { expect(subject[:file]).to be_a(Tempfile) }
 | 
			
		||||
    it { expect(subject[:file].size).to eq(6) }
 | 
			
		||||
 | 
			
		||||
    it 'streams the download' do
 | 
			
		||||
      expected_options = { headers: anything, stream_body: true }
 | 
			
		||||
 | 
			
		||||
      expect(Gitlab::HTTP).to receive(:perform_request).with(Net::HTTP::Get, anything, expected_options)
 | 
			
		||||
 | 
			
		||||
      download_blob
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'skips read_total_timeout', :aggregate_failures do
 | 
			
		||||
      stub_const('GitLab::HTTP::DEFAULT_READ_TOTAL_TIMEOUT', 0)
 | 
			
		||||
 | 
			
		||||
      expect(Gitlab::Metrics::System).not_to receive(:monotonic_time)
 | 
			
		||||
      expect(download_blob).to include(status: :success)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  context 'remote request is not found' do
 | 
			
		||||
    before do
 | 
			
		||||
      stub_blob_download(image, blob_sha, 404)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it { expect(subject[:status]).to eq(:error) }
 | 
			
		||||
    it { expect(subject[:http_status]).to eq(404) }
 | 
			
		||||
    it { expect(subject[:message]).to eq('Non-success response code on downloading blob fragment') }
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  context 'net timeout exception' do
 | 
			
		||||
    before do
 | 
			
		||||
      blob_url = DependencyProxy::Registry.blob_url(image, blob_sha)
 | 
			
		||||
 | 
			
		||||
      stub_full_request(blob_url).to_timeout
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it { expect(subject[:status]).to eq(:error) }
 | 
			
		||||
    it { expect(subject[:http_status]).to eq(599) }
 | 
			
		||||
    it { expect(subject[:message]).to eq('execution expired') }
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -1,71 +0,0 @@
 | 
			
		|||
# frozen_string_literal: true
 | 
			
		||||
require 'spec_helper'
 | 
			
		||||
 | 
			
		||||
RSpec.describe DependencyProxy::FindOrCreateBlobService do
 | 
			
		||||
  include DependencyProxyHelpers
 | 
			
		||||
 | 
			
		||||
  let_it_be_with_reload(:blob) { create(:dependency_proxy_blob) }
 | 
			
		||||
 | 
			
		||||
  let(:group) { blob.group }
 | 
			
		||||
  let(:image) { 'alpine' }
 | 
			
		||||
  let(:tag)   { '3.9' }
 | 
			
		||||
  let(:token) { Digest::SHA256.hexdigest('123') }
 | 
			
		||||
  let(:blob_sha) { '40bd001563085fc35165329ea1ff5c5ecbdbbeef' }
 | 
			
		||||
 | 
			
		||||
  subject { described_class.new(group, image, token, blob_sha).execute }
 | 
			
		||||
 | 
			
		||||
  before do
 | 
			
		||||
    stub_registry_auth(image, token)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  shared_examples 'downloads the remote blob' do
 | 
			
		||||
    it 'downloads blob from remote registry if there is no cached one' do
 | 
			
		||||
      expect(subject[:status]).to eq(:success)
 | 
			
		||||
      expect(subject[:blob]).to be_a(DependencyProxy::Blob)
 | 
			
		||||
      expect(subject[:blob]).to be_persisted
 | 
			
		||||
      expect(subject[:from_cache]).to eq false
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  context 'no cache' do
 | 
			
		||||
    before do
 | 
			
		||||
      stub_blob_download(image, blob_sha)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it_behaves_like 'downloads the remote blob'
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  context 'cached blob' do
 | 
			
		||||
    let(:blob_sha) { blob.file_name.sub('.gz', '') }
 | 
			
		||||
 | 
			
		||||
    it 'uses cached blob instead of downloading one' do
 | 
			
		||||
      expect { subject }.to change { blob.reload.read_at }
 | 
			
		||||
 | 
			
		||||
      expect(subject[:status]).to eq(:success)
 | 
			
		||||
      expect(subject[:blob]).to be_a(DependencyProxy::Blob)
 | 
			
		||||
      expect(subject[:blob]).to eq(blob)
 | 
			
		||||
      expect(subject[:from_cache]).to eq true
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'when the cached blob is expired' do
 | 
			
		||||
      before do
 | 
			
		||||
        blob.update_column(:status, DependencyProxy::Blob.statuses[:expired])
 | 
			
		||||
        stub_blob_download(image, blob_sha)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it_behaves_like 'downloads the remote blob'
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  context 'no such blob exists remotely' do
 | 
			
		||||
    before do
 | 
			
		||||
      stub_blob_download(image, blob_sha, 404)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'returns error message and http status' do
 | 
			
		||||
      expect(subject[:status]).to eq(:error)
 | 
			
		||||
      expect(subject[:message]).to eq('Failed to download the blob')
 | 
			
		||||
      expect(subject[:http_status]).to eq(404)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -131,32 +131,82 @@ RSpec.describe Projects::UpdateRemoteMirrorService do
 | 
			
		|||
        expect_next_instance_of(Lfs::PushService) do |service|
 | 
			
		||||
          expect(service).to receive(:execute)
 | 
			
		||||
        end
 | 
			
		||||
        expect(Gitlab::AppJsonLogger).not_to receive(:info)
 | 
			
		||||
 | 
			
		||||
        execute!
 | 
			
		||||
 | 
			
		||||
        expect(remote_mirror.update_status).to eq('finished')
 | 
			
		||||
        expect(remote_mirror.last_error).to be_nil
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'does nothing to an SSH repository' do
 | 
			
		||||
        remote_mirror.update!(url: 'ssh://example.com')
 | 
			
		||||
      context 'when LFS objects fail to push' do
 | 
			
		||||
        before do
 | 
			
		||||
          expect_next_instance_of(Lfs::PushService) do |service|
 | 
			
		||||
            expect(service).to receive(:execute).and_return({ status: :error, message: 'unauthorized' })
 | 
			
		||||
          end
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        expect_any_instance_of(Lfs::PushService).not_to receive(:execute)
 | 
			
		||||
        context 'when remote_mirror_fail_on_lfs feature flag enabled' do
 | 
			
		||||
          it 'fails update' do
 | 
			
		||||
            expect(Gitlab::AppJsonLogger).to receive(:info).with(
 | 
			
		||||
              hash_including(message: "Error synching remote mirror")).and_call_original
 | 
			
		||||
 | 
			
		||||
        execute!
 | 
			
		||||
            execute!
 | 
			
		||||
 | 
			
		||||
            expect(remote_mirror.update_status).to eq('failed')
 | 
			
		||||
            expect(remote_mirror.last_error).to eq("Error synchronizing LFS files:\n\nunauthorized\n\n")
 | 
			
		||||
          end
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        context 'when remote_mirror_fail_on_lfs feature flag is disabled' do
 | 
			
		||||
          before do
 | 
			
		||||
            stub_feature_flags(remote_mirror_fail_on_lfs: false)
 | 
			
		||||
          end
 | 
			
		||||
 | 
			
		||||
          it 'does not fail update' do
 | 
			
		||||
            expect(Gitlab::AppJsonLogger).to receive(:info).with(
 | 
			
		||||
              hash_including(message: "Error synching remote mirror")).and_call_original
 | 
			
		||||
 | 
			
		||||
            execute!
 | 
			
		||||
 | 
			
		||||
            expect(remote_mirror.update_status).to eq('finished')
 | 
			
		||||
            expect(remote_mirror.last_error).to be_nil
 | 
			
		||||
          end
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'does nothing if LFS is disabled' do
 | 
			
		||||
        expect(project).to receive(:lfs_enabled?) { false }
 | 
			
		||||
      context 'with SSH repository' do
 | 
			
		||||
        let(:ssh_mirror) { create(:remote_mirror, project: project, enabled: true) }
 | 
			
		||||
 | 
			
		||||
        expect_any_instance_of(Lfs::PushService).not_to receive(:execute)
 | 
			
		||||
        before do
 | 
			
		||||
          allow(ssh_mirror)
 | 
			
		||||
            .to receive(:update_repository)
 | 
			
		||||
            .and_return(double(divergent_refs: []))
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        execute!
 | 
			
		||||
      end
 | 
			
		||||
        it 'does nothing to an SSH repository' do
 | 
			
		||||
          ssh_mirror.update!(url: 'ssh://example.com')
 | 
			
		||||
 | 
			
		||||
      it 'does nothing if non-password auth is specified' do
 | 
			
		||||
        remote_mirror.update!(auth_method: 'ssh_public_key')
 | 
			
		||||
          expect_any_instance_of(Lfs::PushService).not_to receive(:execute)
 | 
			
		||||
 | 
			
		||||
        expect_any_instance_of(Lfs::PushService).not_to receive(:execute)
 | 
			
		||||
          service.execute(ssh_mirror, retries)
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        execute!
 | 
			
		||||
        it 'does nothing if LFS is disabled' do
 | 
			
		||||
          expect(project).to receive(:lfs_enabled?) { false }
 | 
			
		||||
 | 
			
		||||
          expect_any_instance_of(Lfs::PushService).not_to receive(:execute)
 | 
			
		||||
 | 
			
		||||
          service.execute(ssh_mirror, retries)
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        it 'does nothing if non-password auth is specified' do
 | 
			
		||||
          ssh_mirror.update!(auth_method: 'ssh_public_key')
 | 
			
		||||
 | 
			
		||||
          expect_any_instance_of(Lfs::PushService).not_to receive(:execute)
 | 
			
		||||
 | 
			
		||||
          service.execute(ssh_mirror, retries)
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,107 @@
 | 
			
		|||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
RSpec.shared_examples 'labels sidebar widget' do
 | 
			
		||||
  context 'editing labels' do
 | 
			
		||||
    let_it_be(:development) { create(:group_label, group: group, name: 'Development') }
 | 
			
		||||
    let_it_be(:stretch)     { create(:label, project: project, name: 'Stretch') }
 | 
			
		||||
    let_it_be(:xss_label) { create(:label, project: project, title: '<script>alert("xss");</script>') }
 | 
			
		||||
 | 
			
		||||
    let(:labels_widget) { find('[data-testid="sidebar-labels"]') }
 | 
			
		||||
 | 
			
		||||
    before do
 | 
			
		||||
      page.within(labels_widget) do
 | 
			
		||||
        click_on 'Edit'
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      wait_for_all_requests
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'shows labels list in the dropdown' do
 | 
			
		||||
      expect(labels_widget.find('.gl-new-dropdown-contents')).to have_selector('li.gl-new-dropdown-item', count: 4)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'adds a label' do
 | 
			
		||||
      within(labels_widget) do
 | 
			
		||||
        adds_label(stretch)
 | 
			
		||||
 | 
			
		||||
        page.within('[data-testid="value-wrapper"]') do
 | 
			
		||||
          expect(page).to have_content(stretch.name)
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'removes a label' do
 | 
			
		||||
      within(labels_widget) do
 | 
			
		||||
        adds_label(stretch)
 | 
			
		||||
        page.within('[data-testid="value-wrapper"]') do
 | 
			
		||||
          expect(page).to have_content(stretch.name)
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        click_on 'Remove label'
 | 
			
		||||
 | 
			
		||||
        wait_for_requests
 | 
			
		||||
 | 
			
		||||
        page.within('[data-testid="value-wrapper"]') do
 | 
			
		||||
          expect(page).not_to have_content(stretch.name)
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'escapes XSS when viewing issuable labels' do
 | 
			
		||||
      page.within(labels_widget) do
 | 
			
		||||
        expect(page).to have_content '<script>alert("xss");</script>'
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'shows option to create a label' do
 | 
			
		||||
      page.within(labels_widget) do
 | 
			
		||||
        expect(page).to have_content 'Create'
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'creating a label', :js do
 | 
			
		||||
      before do
 | 
			
		||||
        page.within(labels_widget) do
 | 
			
		||||
          page.find('[data-testid="create-label-button"]').click
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'shows dropdown switches to "create label" section' do
 | 
			
		||||
        page.within(labels_widget) do
 | 
			
		||||
          expect(page.find('[data-testid="dropdown-header"]')).to have_content 'Create'
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'creates new label' do
 | 
			
		||||
        page.within(labels_widget) do
 | 
			
		||||
          fill_in 'Name new label', with: 'wontfix'
 | 
			
		||||
          page.find('.suggest-colors a', match: :first).click
 | 
			
		||||
          page.find('button', text: 'Create').click
 | 
			
		||||
          wait_for_requests
 | 
			
		||||
 | 
			
		||||
          expect(page).to have_content 'wontfix'
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'shows error message if label title is taken' do
 | 
			
		||||
        page.within(labels_widget) do
 | 
			
		||||
          fill_in 'Name new label', with: development.title
 | 
			
		||||
          page.find('.suggest-colors a', match: :first).click
 | 
			
		||||
          page.find('button', text: 'Create').click
 | 
			
		||||
          wait_for_requests
 | 
			
		||||
 | 
			
		||||
          page.within('.dropdown-input') do
 | 
			
		||||
            expect(page.find('.gl-alert')).to have_content 'Title'
 | 
			
		||||
          end
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def adds_label(label)
 | 
			
		||||
    click_button label.name
 | 
			
		||||
    click_button 'Close'
 | 
			
		||||
 | 
			
		||||
    wait_for_requests
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -50,6 +50,10 @@ RSpec.shared_examples 'issue boards sidebar' do
 | 
			
		|||
    it_behaves_like 'date sidebar widget'
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  context 'editing issue labels', :js do
 | 
			
		||||
    it_behaves_like 'labels sidebar widget'
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  context 'in notifications subscription' do
 | 
			
		||||
    it 'displays notifications toggle', :aggregate_failures do
 | 
			
		||||
      page.within('[data-testid="sidebar-notifications"]') do
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue