Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
7e5f8d0881
commit
1b9a2ce278
|
@ -1,3 +1,3 @@
|
|||
import Vue from 'vue';
|
||||
import createEventHub from '~/helpers/event_hub_factory';
|
||||
|
||||
export default new Vue();
|
||||
export default createEventHub();
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
import Vue from 'vue';
|
||||
import createEventHub from '~/helpers/event_hub_factory';
|
||||
|
||||
export default new Vue();
|
||||
export default createEventHub();
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
import Vue from 'vue';
|
||||
import createEventHub from '~/helpers/event_hub_factory';
|
||||
|
||||
export default new Vue();
|
||||
export default createEventHub();
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
import Vue from 'vue';
|
||||
import createEventHub from '~/helpers/event_hub_factory';
|
||||
|
||||
export default new Vue();
|
||||
export default createEventHub();
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
import Vue from 'vue';
|
||||
import createEventHub from '~/helpers/event_hub_factory';
|
||||
|
||||
export default new Vue();
|
||||
export default createEventHub();
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
import Vue from 'vue';
|
||||
import createEventHub from '~/helpers/event_hub_factory';
|
||||
|
||||
export default new Vue();
|
||||
export default createEventHub();
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
import Vue from 'vue';
|
||||
import createEventHub from '~/helpers/event_hub_factory';
|
||||
|
||||
export default new Vue();
|
||||
export default createEventHub();
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
import Vue from 'vue';
|
||||
import createEventHub from '~/helpers/event_hub_factory';
|
||||
|
||||
export default new Vue();
|
||||
export default createEventHub();
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
import Vue from 'vue';
|
||||
import createEventHub from '~/helpers/event_hub_factory';
|
||||
|
||||
export default new Vue();
|
||||
export default createEventHub();
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
/* eslint-disable no-new, class-methods-use-this */
|
||||
|
||||
import $ from 'jquery';
|
||||
import Vue from 'vue';
|
||||
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
|
||||
import Cookies from 'js-cookie';
|
||||
import createEventHub from '~/helpers/event_hub_factory';
|
||||
import axios from './lib/utils/axios_utils';
|
||||
import flash from './flash';
|
||||
import BlobForkSuggestion from './blob/blob_fork_suggestion';
|
||||
|
@ -93,7 +93,7 @@ export default class MergeRequestTabs {
|
|||
this.pipelinesLoaded = false;
|
||||
this.commitsLoaded = false;
|
||||
this.fixedLayoutPref = null;
|
||||
this.eventHub = new Vue();
|
||||
this.eventHub = createEventHub();
|
||||
|
||||
this.setUrl = setUrl !== undefined ? setUrl : true;
|
||||
this.setCurrentAction = this.setCurrentAction.bind(this);
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
import Vue from 'vue';
|
||||
import createEventHub from '~/helpers/event_hub_factory';
|
||||
|
||||
export default new Vue();
|
||||
export default createEventHub();
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
import Vue from 'vue';
|
||||
import createEventHub from '~/helpers/event_hub_factory';
|
||||
|
||||
export default new Vue();
|
||||
export default createEventHub();
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
import Vue from 'vue';
|
||||
import createEventHub from '~/helpers/event_hub_factory';
|
||||
|
||||
export default new Vue();
|
||||
export default createEventHub();
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
import Vue from 'vue';
|
||||
import createEventHub from '~/helpers/event_hub_factory';
|
||||
|
||||
export default new Vue();
|
||||
export default createEventHub();
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
import Vue from 'vue';
|
||||
import createEventHub from '~/helpers/event_hub_factory';
|
||||
|
||||
export default new Vue();
|
||||
export default createEventHub();
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import Vue from 'vue';
|
||||
import createEventHub from '~/helpers/event_hub_factory';
|
||||
|
||||
// see recaptcha_tags in app/views/shared/_recaptcha_form.html.haml
|
||||
export const callbackName = 'recaptchaDialogCallback';
|
||||
|
||||
export const eventHub = new Vue();
|
||||
export const eventHub = createEventHub();
|
||||
|
||||
const throwDuplicateCallbackError = () => {
|
||||
throw new Error(`${callbackName} is already defined!`);
|
||||
|
|
|
@ -13,6 +13,7 @@ module AlertManagement
|
|||
|
||||
collection = project.alert_management_alerts
|
||||
collection = by_status(collection)
|
||||
collection = by_search(collection)
|
||||
collection = by_iid(collection)
|
||||
sort(collection)
|
||||
end
|
||||
|
@ -33,6 +34,10 @@ module AlertManagement
|
|||
values.present? ? collection.for_status(values) : collection
|
||||
end
|
||||
|
||||
def by_search(collection)
|
||||
params[:search].present? ? collection.search(params[:search]) : collection
|
||||
end
|
||||
|
||||
def sort(collection)
|
||||
params[:sort] ? collection.sort_by_attribute(params[:sort]) : collection
|
||||
end
|
||||
|
|
|
@ -15,6 +15,10 @@ module Resolvers
|
|||
description: 'Sort alerts by this criteria',
|
||||
required: false
|
||||
|
||||
argument :search, GraphQL::STRING_TYPE,
|
||||
description: 'Search criteria for filtering alerts. This will search on title, description, service, monitoring_tool.',
|
||||
required: false
|
||||
|
||||
type Types::AlertManagement::AlertType, null: true
|
||||
|
||||
def resolve(**args)
|
||||
|
|
|
@ -5,6 +5,7 @@ module AlertManagement
|
|||
include AtomicInternalId
|
||||
include ShaAttribute
|
||||
include Sortable
|
||||
include Gitlab::SQL::Pattern
|
||||
|
||||
STATUSES = {
|
||||
triggered: 0,
|
||||
|
@ -97,6 +98,7 @@ module AlertManagement
|
|||
scope :for_iid, -> (iid) { where(iid: iid) }
|
||||
scope :for_status, -> (status) { where(status: status) }
|
||||
scope :for_fingerprint, -> (project, fingerprint) { where(project: project, fingerprint: fingerprint) }
|
||||
scope :search, -> (query) { fuzzy_search(query, [:title, :description, :monitoring_tool, :service]) }
|
||||
|
||||
scope :order_start_time, -> (sort_order) { order(started_at: sort_order) }
|
||||
scope :order_end_time, -> (sort_order) { order(ended_at: sort_order) }
|
||||
|
|
|
@ -13,5 +13,64 @@ module Ci
|
|||
}
|
||||
|
||||
scope :unprotected, -> { where(protected: false) }
|
||||
after_commit { self.class.touch_redis_cache_timestamp }
|
||||
|
||||
class << self
|
||||
def all_cached
|
||||
cached_data[:all]
|
||||
end
|
||||
|
||||
def unprotected_cached
|
||||
cached_data[:unprotected]
|
||||
end
|
||||
|
||||
def touch_redis_cache_timestamp(time = Time.current.to_f)
|
||||
shared_backend.write(:ci_instance_variable_changed_at, time)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def cached_data
|
||||
fetch_memory_cache(:ci_instance_variable_data) do
|
||||
all_records = unscoped.all.to_a
|
||||
|
||||
{ all: all_records, unprotected: all_records.reject(&:protected?) }
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_memory_cache(key, &payload)
|
||||
cache = process_backend.read(key)
|
||||
|
||||
if cache && !stale_cache?(cache)
|
||||
cache[:data]
|
||||
else
|
||||
store_cache(key, &payload)
|
||||
end
|
||||
end
|
||||
|
||||
def stale_cache?(cache_info)
|
||||
shared_timestamp = shared_backend.read(:ci_instance_variable_changed_at)
|
||||
return true unless shared_timestamp
|
||||
|
||||
shared_timestamp.to_f > cache_info[:cached_at].to_f
|
||||
end
|
||||
|
||||
def store_cache(key)
|
||||
data = yield
|
||||
time = Time.current.to_f
|
||||
|
||||
process_backend.write(key, data: data, cached_at: time)
|
||||
touch_redis_cache_timestamp(time)
|
||||
data
|
||||
end
|
||||
|
||||
def shared_backend
|
||||
Rails.cache
|
||||
end
|
||||
|
||||
def process_backend
|
||||
Gitlab::ProcessMemoryCache.cache_backend
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module AsyncDeviseEmail
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
private
|
||||
|
||||
# Added according to https://github.com/plataformatec/devise/blob/7df57d5081f9884849ca15e4fde179ef164a575f/README.md#activejob-integration
|
||||
def send_devise_notification(notification, *args)
|
||||
return true unless can?(:receive_notifications)
|
||||
|
||||
devise_mailer.__send__(notification, self, *args).deliver_later # rubocop:disable GitlabSecurity/PublicSend
|
||||
end
|
||||
end
|
|
@ -19,6 +19,7 @@ module Ci
|
|||
variables.concat(yaml_variables)
|
||||
variables.concat(user_variables)
|
||||
variables.concat(dependency_variables) if Feature.enabled?(:ci_dependency_variables, project)
|
||||
variables.concat(secret_instance_variables)
|
||||
variables.concat(secret_group_variables)
|
||||
variables.concat(secret_project_variables(environment: environment))
|
||||
variables.concat(trigger_request.user_variables) if trigger_request
|
||||
|
@ -82,6 +83,12 @@ module Ci
|
|||
)
|
||||
end
|
||||
|
||||
def secret_instance_variables
|
||||
return [] unless ::Feature.enabled?(:ci_instance_level_variables, project, default_enabled: true)
|
||||
|
||||
project.ci_instance_variables_for(ref: git_ref)
|
||||
end
|
||||
|
||||
def secret_group_variables
|
||||
return [] unless project.group
|
||||
|
||||
|
|
|
@ -15,9 +15,14 @@ class Email < ApplicationRecord
|
|||
after_commit :update_invalid_gpg_signatures, if: -> { previous_changes.key?('confirmed_at') }
|
||||
|
||||
devise :confirmable
|
||||
|
||||
# This module adds async behaviour to Devise emails
|
||||
# and should be added after Devise modules are initialized.
|
||||
include AsyncDeviseEmail
|
||||
|
||||
self.reconfirmable = false # currently email can't be changed, no need to reconfirm
|
||||
|
||||
delegate :username, to: :user
|
||||
delegate :username, :can?, to: :user
|
||||
|
||||
def email=(value)
|
||||
write_attribute(:email, value.downcase.strip)
|
||||
|
|
|
@ -2018,6 +2018,14 @@ class Project < ApplicationRecord
|
|||
end
|
||||
end
|
||||
|
||||
def ci_instance_variables_for(ref:)
|
||||
if protected_for?(ref)
|
||||
Ci::InstanceVariable.all_cached
|
||||
else
|
||||
Ci::InstanceVariable.unprotected_cached
|
||||
end
|
||||
end
|
||||
|
||||
def protected_for?(ref)
|
||||
raise Repository::AmbiguousRefError if repository.ambiguous_ref?(ref)
|
||||
|
||||
|
|
|
@ -58,6 +58,10 @@ class User < ApplicationRecord
|
|||
devise :lockable, :recoverable, :rememberable, :trackable,
|
||||
:validatable, :omniauthable, :confirmable, :registerable
|
||||
|
||||
# This module adds async behaviour to Devise emails
|
||||
# and should be added after Devise modules are initialized.
|
||||
include AsyncDeviseEmail
|
||||
|
||||
BLOCKED_MESSAGE = "Your account has been blocked. Please contact your GitLab " \
|
||||
"administrator if you think this is an error."
|
||||
LOGIN_FORBIDDEN = "Your account does not have the required permission to login. Please contact your GitLab " \
|
||||
|
@ -1746,13 +1750,6 @@ class User < ApplicationRecord
|
|||
ApplicationSetting.current_without_cache&.usage_stats_set_by_user_id == self.id
|
||||
end
|
||||
|
||||
# Added according to https://github.com/plataformatec/devise/blob/7df57d5081f9884849ca15e4fde179ef164a575f/README.md#activejob-integration
|
||||
def send_devise_notification(notification, *args)
|
||||
return true unless can?(:receive_notifications)
|
||||
|
||||
devise_mailer.__send__(notification, self, *args).deliver_later # rubocop:disable GitlabSecurity/PublicSend
|
||||
end
|
||||
|
||||
def ensure_user_rights_and_limits
|
||||
if external?
|
||||
self.can_create_group = false
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
%ul
|
||||
%li A group is a collection of several projects
|
||||
%li Members of a group may only view projects they have permission to access
|
||||
%li Group project URLs are prefixed with the group namespace
|
||||
%li Existing projects may be moved into a group
|
||||
%li= _('A group is a collection of several projects')
|
||||
%li= _('Members of a group may only view projects they have permission to access')
|
||||
%li= _('Group project URLs are prefixed with the group namespace')
|
||||
%li= _('Existing projects may be moved into a group')
|
||||
|
|
|
@ -8,10 +8,11 @@
|
|||
- data_options = local_assigns.fetch(:data_options, {})
|
||||
- classes = local_assigns.fetch(:classes, [])
|
||||
- selected = local_assigns.fetch(:selected, nil)
|
||||
- dropdown_title = local_assigns.fetch(:dropdown_title, "Filter by label")
|
||||
- dropdown_data = label_dropdown_data(edit_context, labels: labels_filter_path_with_defaults(only_group_labels: edit_context.is_a?(Group)), default_label: "Labels")
|
||||
- dropdown_title = local_assigns.fetch(:dropdown_title, _('Filter by label'))
|
||||
- dropdown_data = label_dropdown_data(edit_context, labels: labels_filter_path_with_defaults(only_group_labels: edit_context.is_a?(Group)), default_label: _('Labels'))
|
||||
|
||||
- dropdown_data.merge!(data_options)
|
||||
- label_name = local_assigns.fetch(:label_name, "Labels")
|
||||
- label_name = local_assigns.fetch(:label_name, _('Labels'))
|
||||
- no_default_styles = local_assigns.fetch(:no_default_styles, false)
|
||||
- classes << 'js-extra-options' if extra_options
|
||||
- classes << 'js-filter-submit' if filter_submit
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
- show_title = local_assigns.fetch(:show_title, true)
|
||||
- show_create = local_assigns.fetch(:show_create, true)
|
||||
- show_footer = local_assigns.fetch(:show_footer, true)
|
||||
- filter_placeholder = local_assigns.fetch(:filter_placeholder, 'Search')
|
||||
- filter_placeholder = local_assigns.fetch(:filter_placeholder, _('Search'))
|
||||
- show_boards_content = local_assigns.fetch(:show_boards_content, false)
|
||||
- subject = @project || @group
|
||||
.dropdown-page-one
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
- if signed_in
|
||||
%span.issuable-header-text.hide-collapsed.float-left
|
||||
= _('To Do')
|
||||
%a.gutter-toggle.float-right.js-sidebar-toggle.has-tooltip{ role: "button", href: "#", "aria-label" => "Toggle sidebar", title: sidebar_gutter_tooltip_text, data: { container: 'body', placement: 'left', boundary: 'viewport' } }
|
||||
%a.gutter-toggle.float-right.js-sidebar-toggle.has-tooltip{ role: "button", href: "#", "aria-label" => _('Toggle sidebar'), title: sidebar_gutter_tooltip_text, data: { container: 'body', placement: 'left', boundary: 'viewport' } }
|
||||
= sidebar_gutter_toggle_icon
|
||||
- if signed_in
|
||||
= render "shared/issuable/sidebar_todo", issuable_sidebar: issuable_sidebar
|
||||
|
@ -65,7 +65,7 @@
|
|||
.sidebar-collapsed-icon.has-tooltip{ data: { placement: 'left', container: 'body', html: 'true', boundary: 'viewport' }, title: sidebar_due_date_tooltip_label(issuable_sidebar[:due_date]) }
|
||||
= icon('calendar', 'aria-hidden': 'true')
|
||||
%span.js-due-date-sidebar-value
|
||||
= issuable_sidebar[:due_date].try(:to_s, :medium) || 'None'
|
||||
= issuable_sidebar[:due_date].try(:to_s, :medium) || _('None')
|
||||
.title.hide-collapsed
|
||||
= _('Due date')
|
||||
= icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
|
||||
|
|
|
@ -25,5 +25,5 @@
|
|||
%span.assignee-icon
|
||||
- assignees.each do |assignee|
|
||||
= link_to polymorphic_path(issuable_type_args, { milestone_title: @milestone.title, assignee_id: assignee.id, state: 'all' }),
|
||||
class: 'has-tooltip', title: "Assigned to #{assignee.name}", data: { container: 'body' } do
|
||||
class: 'has-tooltip', title: _("Assigned to %{assignee_name}") % { assignee_name: assignee.name }, data: { container: 'body' } do
|
||||
- image_tag(avatar_icon_for_user(assignee, 16), class: "avatar s16", alt: '')
|
||||
|
|
|
@ -8,8 +8,8 @@
|
|||
|
||||
.row.prepend-top-default
|
||||
.col-md-4
|
||||
= render 'shared/milestones/issuables', args.merge(title: 'Unstarted Issues (open and unassigned)', issuables: issues.opened.unassigned, id: 'unassigned', show_counter: true)
|
||||
= render 'shared/milestones/issuables', args.merge(title: s_('Milestones|Unstarted Issues (open and unassigned)'), issuables: issues.opened.unassigned, id: 'unassigned', show_counter: true)
|
||||
.col-md-4
|
||||
= render 'shared/milestones/issuables', args.merge(title: 'Ongoing Issues (open and assigned)', issuables: issues.opened.assigned, id: 'ongoing', show_counter: true)
|
||||
= render 'shared/milestones/issuables', args.merge(title: s_('Milestones|Ongoing Issues (open and assigned)'), issuables: issues.opened.assigned, id: 'ongoing', show_counter: true)
|
||||
.col-md-4
|
||||
= render 'shared/milestones/issuables', args.merge(title: 'Completed Issues (closed)', issuables: issues.closed, id: 'closed', show_counter: true)
|
||||
= render 'shared/milestones/issuables', args.merge(title: s_('Milestones|Completed Issues (closed)'), issuables: issues.closed, id: 'closed', show_counter: true)
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Integrate CI instance variables in the build process
|
||||
merge_request: 30186
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add search to Alert Management Alerts GraphQL query
|
||||
merge_request: 32047
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Externalize i18n strings from ./app/views/shared/issuable/_label_*
|
||||
merge_request: 32167
|
||||
author: Gilang Gumilar
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Externalize i18n strings from ./app/views/shared/issuable/_sidebar.html.haml
|
||||
merge_request: 32164
|
||||
author: Gilang Gumilar
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Externalize i18n strings from ./app/views/shared/milestones/_issuable.html.haml
|
||||
merge_request: 32161
|
||||
author: Gilang Gumilar
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Externalize i18n strings from ./app/views/shared/milestones/_issues_tab.html.haml
|
||||
merge_request: 32160
|
||||
author: Gilang Gumilar
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Externalize i18n strings from ./app/views/shared/_group_tips.html.haml
|
||||
merge_request: 32127
|
||||
author: Gilang Gumilar
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Send Devise emails triggered from the 'Email' model asynchronously
|
||||
merge_request: 32286
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Migrate from Vue event hub to Mitt
|
||||
merge_request: 31666
|
||||
author: Arun Kumar Mohan
|
||||
type: changed
|
|
@ -7302,6 +7302,11 @@ type Project {
|
|||
"""
|
||||
iid: String
|
||||
|
||||
"""
|
||||
Search criteria for filtering alerts. This will search on title, description, service, monitoring_tool.
|
||||
"""
|
||||
search: String
|
||||
|
||||
"""
|
||||
Sort alerts by this criteria
|
||||
"""
|
||||
|
@ -7342,6 +7347,11 @@ type Project {
|
|||
"""
|
||||
last: Int
|
||||
|
||||
"""
|
||||
Search criteria for filtering alerts. This will search on title, description, service, monitoring_tool.
|
||||
"""
|
||||
search: String
|
||||
|
||||
"""
|
||||
Sort alerts by this criteria
|
||||
"""
|
||||
|
|
|
@ -21865,6 +21865,16 @@
|
|||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "search",
|
||||
"description": "Search criteria for filtering alerts. This will search on title, description, service, monitoring_tool.",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
}
|
||||
],
|
||||
"type": {
|
||||
|
@ -21917,6 +21927,16 @@
|
|||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "search",
|
||||
"description": "Search criteria for filtering alerts. This will search on title, description, service, monitoring_tool.",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "after",
|
||||
"description": "Returns the elements in the list that come after the specified cursor.",
|
||||
|
|
|
@ -19,6 +19,22 @@ To enable merge request approval rules for an instance:
|
|||
|
||||
GitLab administrators can later override these settings in a project’s settings.
|
||||
|
||||
## Merge request controls **(PREMIUM)**
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/207250) in GitLab 13.0.
|
||||
|
||||
Merge request approval settings, by default, are inherited by all projects in an instance.
|
||||
|
||||
However, organizations with regulated projects may also have unregulated projects
|
||||
that should not inherit these same controls.
|
||||
|
||||
Project-level merge request approval rules can now be edited by administrators.
|
||||
Project owners and maintainers can still view project-level merge request approval rules.
|
||||
|
||||
In upcoming releases, we plan to provide a more holistic experience to scope instance-level merge request settings.
|
||||
For more information, review our plans to provide custom [approval settings for compliance-
|
||||
labeled projects](https://gitlab.com/gitlab-org/gitlab/-/issues/213601).
|
||||
|
||||
## Available rules
|
||||
|
||||
Merge request approval rules that can be set at an instance level are:
|
||||
|
|
|
@ -874,6 +874,9 @@ msgstr ""
|
|||
msgid "A fork is a copy of a project.<br />Forking a repository allows you to make changes without affecting the original project."
|
||||
msgstr ""
|
||||
|
||||
msgid "A group is a collection of several projects"
|
||||
msgstr ""
|
||||
|
||||
msgid "A group represents your organization in GitLab."
|
||||
msgstr ""
|
||||
|
||||
|
@ -2779,6 +2782,9 @@ msgstr ""
|
|||
msgid "Assigned Merge Requests"
|
||||
msgstr ""
|
||||
|
||||
msgid "Assigned to %{assignee_name}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Assigned to me"
|
||||
msgstr ""
|
||||
|
||||
|
@ -8828,6 +8834,9 @@ msgstr ""
|
|||
msgid "Existing members and groups"
|
||||
msgstr ""
|
||||
|
||||
msgid "Existing projects may be moved into a group"
|
||||
msgstr ""
|
||||
|
||||
msgid "Existing projects will be able to use expiration policies. Avoid enabling this if an external Container Registry is being used, as there is a performance risk if many images exist on one project."
|
||||
msgstr ""
|
||||
|
||||
|
@ -9482,6 +9491,9 @@ msgstr ""
|
|||
msgid "Filter by issues that are currently closed."
|
||||
msgstr ""
|
||||
|
||||
msgid "Filter by label"
|
||||
msgstr ""
|
||||
|
||||
msgid "Filter by merge requests that are currently closed and unmerged."
|
||||
msgstr ""
|
||||
|
||||
|
@ -10673,6 +10685,9 @@ msgstr ""
|
|||
msgid "Group pipeline minutes were successfully reset."
|
||||
msgstr ""
|
||||
|
||||
msgid "Group project URLs are prefixed with the group namespace"
|
||||
msgstr ""
|
||||
|
||||
msgid "Group requires separate account"
|
||||
msgstr ""
|
||||
|
||||
|
@ -13214,6 +13229,9 @@ msgstr ""
|
|||
msgid "Members of <strong>%{project_name}</strong>"
|
||||
msgstr ""
|
||||
|
||||
msgid "Members of a group may only view projects they have permission to access"
|
||||
msgstr ""
|
||||
|
||||
msgid "Members with access to %{strong_start}%{group_name}%{strong_end}"
|
||||
msgstr ""
|
||||
|
||||
|
@ -13677,6 +13695,9 @@ msgstr ""
|
|||
msgid "Milestones| You’re about to permanently delete the milestone %{milestoneTitle}. This milestone is not currently used in any issues or merge requests."
|
||||
msgstr ""
|
||||
|
||||
msgid "Milestones|Completed Issues (closed)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Milestones|Delete milestone"
|
||||
msgstr ""
|
||||
|
||||
|
@ -13689,6 +13710,9 @@ msgstr ""
|
|||
msgid "Milestones|Milestone %{milestoneTitle} was not found"
|
||||
msgstr ""
|
||||
|
||||
msgid "Milestones|Ongoing Issues (open and assigned)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Milestones|Promote %{milestoneTitle} to group milestone?"
|
||||
msgstr ""
|
||||
|
||||
|
@ -13701,6 +13725,9 @@ msgstr ""
|
|||
msgid "Milestones|This action cannot be reversed."
|
||||
msgstr ""
|
||||
|
||||
msgid "Milestones|Unstarted Issues (open and unassigned)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Minimum capacity to be available before we schedule more mirrors preemptively."
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -12,6 +12,8 @@ module QA
|
|||
module Git
|
||||
class Repository
|
||||
include Scenario::Actable
|
||||
include Support::Repeater
|
||||
|
||||
RepositoryCommandError = Class.new(StandardError)
|
||||
|
||||
attr_writer :use_lfs, :gpg_key_id
|
||||
|
@ -58,8 +60,8 @@ module QA
|
|||
end
|
||||
|
||||
def clone(opts = '')
|
||||
clone_result = run("git clone #{opts} #{uri} ./")
|
||||
return clone_result.response unless clone_result.success
|
||||
clone_result = run("git clone #{opts} #{uri} ./", max_attempts: 3)
|
||||
return clone_result.response unless clone_result.success?
|
||||
|
||||
enable_lfs_result = enable_lfs if use_lfs?
|
||||
|
||||
|
@ -92,7 +94,7 @@ module QA
|
|||
|
||||
if use_lfs?
|
||||
git_lfs_track_result = run(%Q{git lfs track #{name} --lockable})
|
||||
return git_lfs_track_result.response unless git_lfs_track_result.success
|
||||
return git_lfs_track_result.response unless git_lfs_track_result.success?
|
||||
end
|
||||
|
||||
git_add_result = run(%Q{git add #{name}})
|
||||
|
@ -101,11 +103,11 @@ module QA
|
|||
end
|
||||
|
||||
def delete_tag(tag_name)
|
||||
run(%Q{git push origin --delete #{tag_name}}).to_s
|
||||
run(%Q{git push origin --delete #{tag_name}}, max_attempts: 3).to_s
|
||||
end
|
||||
|
||||
def commit(message)
|
||||
run(%Q{git commit -m "#{message}"}).to_s
|
||||
run(%Q{git commit -m "#{message}"}, max_attempts: 3).to_s
|
||||
end
|
||||
|
||||
def commit_with_gpg(message)
|
||||
|
@ -113,7 +115,7 @@ module QA
|
|||
end
|
||||
|
||||
def push_changes(branch = 'master')
|
||||
run("git push #{uri} #{branch}").to_s
|
||||
run("git push #{uri} #{branch}", max_attempts: 3).to_s
|
||||
end
|
||||
|
||||
def merge(branch)
|
||||
|
@ -164,8 +166,8 @@ module QA
|
|||
def fetch_supported_git_protocol
|
||||
# ls-remote is one command known to respond to Git protocol v2 so we use
|
||||
# it to get output including the version reported via Git tracing
|
||||
output = run("git ls-remote #{uri}", "GIT_TRACE_PACKET=1")
|
||||
output.response[/git< version (\d+)/, 1] || 'unknown'
|
||||
result = run("git ls-remote #{uri}", env: "GIT_TRACE_PACKET=1", max_attempts: 3)
|
||||
result.response[/git< version (\d+)/, 1] || 'unknown'
|
||||
end
|
||||
|
||||
def try_add_credentials_to_netrc
|
||||
|
@ -182,9 +184,12 @@ module QA
|
|||
|
||||
alias_method :use_lfs?, :use_lfs
|
||||
|
||||
Result = Struct.new(:success, :response) do
|
||||
alias_method :success?, :success
|
||||
Result = Struct.new(:command, :exitstatus, :response) do
|
||||
alias_method :to_s, :response
|
||||
|
||||
def success?
|
||||
exitstatus.zero?
|
||||
end
|
||||
end
|
||||
|
||||
def add_credentials?
|
||||
|
@ -209,19 +214,26 @@ module QA
|
|||
touch_gitconfig_result.to_s + git_lfs_install_result.to_s
|
||||
end
|
||||
|
||||
def run(command_str, *extra_env)
|
||||
command = [env_vars, *extra_env, command_str, '2>&1'].compact.join(' ')
|
||||
Runtime::Logger.debug "Git: pwd=[#{Dir.pwd}], command=[#{command}]"
|
||||
def run(command_str, env: [], max_attempts: 1)
|
||||
command = [env_vars, *env, command_str, '2>&1'].compact.join(' ')
|
||||
result = nil
|
||||
|
||||
output, status = Open3.capture2e(command)
|
||||
output.chomp!
|
||||
Runtime::Logger.debug "Git: output=[#{output}], exitstatus=[#{status.exitstatus}]"
|
||||
repeat_until(max_attempts: max_attempts, raise_on_failure: false) do
|
||||
Runtime::Logger.debug "Git: pwd=[#{Dir.pwd}], command=[#{command}]"
|
||||
output, status = Open3.capture2e(command)
|
||||
output.chomp!
|
||||
Runtime::Logger.debug "Git: output=[#{output}], exitstatus=[#{status.exitstatus}]"
|
||||
|
||||
unless status.success?
|
||||
raise RepositoryCommandError, "The command #{command} failed (#{status.exitstatus}) with the following output:\n#{output}"
|
||||
result = Result.new(command, status.exitstatus, output)
|
||||
|
||||
result.success?
|
||||
end
|
||||
|
||||
Result.new(status.exitstatus == 0, output)
|
||||
unless result.success?
|
||||
raise RepositoryCommandError, "The command #{result.command} failed (#{result.exitstatus}) with the following output:\n#{result.response}"
|
||||
end
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
def default_credentials
|
||||
|
|
|
@ -3,55 +3,119 @@
|
|||
describe QA::Git::Repository do
|
||||
include Helpers::StubENV
|
||||
|
||||
shared_context 'git directory' do
|
||||
let(:repository) { described_class.new }
|
||||
shared_context 'unresolvable git directory' do
|
||||
let(:repo_uri) { 'http://foo/bar.git' }
|
||||
let(:repo_uri_with_credentials) { 'http://root@foo/bar.git' }
|
||||
let(:repository) { described_class.new.tap { |r| r.uri = repo_uri } }
|
||||
let(:tmp_git_dir) { Dir.mktmpdir }
|
||||
let(:tmp_netrc_dir) { Dir.mktmpdir }
|
||||
|
||||
before do
|
||||
stub_env('GITLAB_USERNAME', 'root')
|
||||
cd_empty_temp_directory
|
||||
set_bad_uri
|
||||
|
||||
allow(repository).to receive(:tmp_home_dir).and_return(tmp_netrc_dir)
|
||||
end
|
||||
|
||||
around do |example|
|
||||
FileUtils.cd(tmp_git_dir) do
|
||||
example.run
|
||||
end
|
||||
end
|
||||
|
||||
after do
|
||||
# Switch to a safe dir before deleting tmp dirs to avoid dir access errors
|
||||
FileUtils.cd __dir__
|
||||
FileUtils.remove_entry_secure(tmp_git_dir, true)
|
||||
FileUtils.remove_entry_secure(tmp_netrc_dir, true)
|
||||
end
|
||||
end
|
||||
|
||||
def cd_empty_temp_directory
|
||||
FileUtils.cd tmp_git_dir
|
||||
shared_examples 'command with retries' do
|
||||
let(:extra_args) { {} }
|
||||
let(:result_output) { +'Command successful' }
|
||||
let(:result) { described_class::Result.new(any_args, 0, result_output) }
|
||||
let(:command_return) { result_output }
|
||||
|
||||
context 'when command is successful' do
|
||||
it 'returns the #run command Result output' do
|
||||
expect(repository).to receive(:run).with(command, extra_args.merge(max_attempts: 3)).and_return(result)
|
||||
|
||||
expect(call_method).to eq(command_return)
|
||||
end
|
||||
end
|
||||
|
||||
def set_bad_uri
|
||||
repository.uri = 'http://foo/bar.git'
|
||||
context 'when command is not successful the first time' do
|
||||
context 'and retried command is successful' do
|
||||
it 'retries the command twice and returns the successful #run command Result output' do
|
||||
expect(Open3).to receive(:capture2e).and_return([+'', double(exitstatus: 1)]).twice
|
||||
expect(Open3).to receive(:capture2e).and_return([result_output, double(exitstatus: 0)])
|
||||
|
||||
expect(call_method).to eq(command_return)
|
||||
end
|
||||
end
|
||||
|
||||
context 'and retried command is not successful after 3 attempts' do
|
||||
it 'raises a RepositoryCommandError exception' do
|
||||
expect(Open3).to receive(:capture2e).and_return([+'FAILURE', double(exitstatus: 42)]).exactly(3).times
|
||||
|
||||
expect { call_method }.to raise_error(described_class::RepositoryCommandError, /The command .* failed \(42\) with the following output:\nFAILURE/)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with default credentials' do
|
||||
include_context 'git directory' do
|
||||
include_context 'unresolvable git directory' do
|
||||
before do
|
||||
repository.use_default_credentials
|
||||
end
|
||||
end
|
||||
|
||||
describe '#clone' do
|
||||
it 'is unable to resolve host' do
|
||||
expect { repository.clone }.to raise_error(described_class::RepositoryCommandError, /The command .* failed \(128\) with the following output/)
|
||||
let(:opts) { '' }
|
||||
let(:call_method) { repository.clone }
|
||||
let(:command) { "git clone #{opts} #{repo_uri_with_credentials} ./" }
|
||||
|
||||
context 'when no opts is given' do
|
||||
it_behaves_like 'command with retries'
|
||||
end
|
||||
|
||||
context 'when opts is given' do
|
||||
let(:opts) { '--depth 1' }
|
||||
|
||||
it_behaves_like 'command with retries' do
|
||||
let(:call_method) { repository.clone(opts) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#shallow_clone' do
|
||||
it_behaves_like 'command with retries' do
|
||||
let(:call_method) { repository.shallow_clone }
|
||||
let(:command) { "git clone --depth 1 #{repo_uri_with_credentials} ./" }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#delete_tag' do
|
||||
it_behaves_like 'command with retries' do
|
||||
let(:tag_name) { 'v1.0' }
|
||||
let(:call_method) { repository.delete_tag(tag_name) }
|
||||
let(:command) { "git push origin --delete #{tag_name}" }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#push_changes' do
|
||||
before do
|
||||
`git init` # need a repo to push from
|
||||
let(:branch) { 'master' }
|
||||
let(:call_method) { repository.push_changes }
|
||||
let(:command) { "git push #{repo_uri_with_credentials} #{branch}" }
|
||||
|
||||
context 'when no branch is given' do
|
||||
it_behaves_like 'command with retries'
|
||||
end
|
||||
|
||||
it 'fails to push changes' do
|
||||
expect { repository.push_changes }.to raise_error(described_class::RepositoryCommandError, /The command .* failed \(1\) with the following output/)
|
||||
context 'when branch is given' do
|
||||
let(:branch) { 'my-branch' }
|
||||
|
||||
it_behaves_like 'command with retries' do
|
||||
let(:call_method) { repository.push_changes(branch) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -59,6 +123,7 @@ describe QA::Git::Repository do
|
|||
[0, 1, 2].each do |version|
|
||||
it "configures git to use protocol version #{version}" do
|
||||
expect(repository).to receive(:run).with("git config protocol.version #{version}")
|
||||
|
||||
repository.git_protocol = version
|
||||
end
|
||||
end
|
||||
|
@ -69,21 +134,31 @@ describe QA::Git::Repository do
|
|||
end
|
||||
|
||||
describe '#fetch_supported_git_protocol' do
|
||||
result = Struct.new(:response)
|
||||
let(:call_method) { repository.fetch_supported_git_protocol }
|
||||
|
||||
it_behaves_like 'command with retries' do
|
||||
let(:command) { "git ls-remote #{repo_uri_with_credentials}" }
|
||||
let(:result_output) { +'packet: git< version 2' }
|
||||
let(:command_return) { '2' }
|
||||
let(:extra_args) { { env: "GIT_TRACE_PACKET=1" } }
|
||||
end
|
||||
|
||||
it "reports the detected version" do
|
||||
expect(repository).to receive(:run).and_return(result.new("packet: git< version 2"))
|
||||
expect(repository.fetch_supported_git_protocol).to eq('2')
|
||||
expect(repository).to receive(:run).and_return(described_class::Result.new(any_args, 0, "packet: git< version 2"))
|
||||
|
||||
expect(call_method).to eq('2')
|
||||
end
|
||||
|
||||
it 'reports unknown if version is unknown' do
|
||||
expect(repository).to receive(:run).and_return(result.new("packet: git< version -1"))
|
||||
expect(repository.fetch_supported_git_protocol).to eq('unknown')
|
||||
expect(repository).to receive(:run).and_return(described_class::Result.new(any_args, 0, "packet: git< version -1"))
|
||||
|
||||
expect(call_method).to eq('unknown')
|
||||
end
|
||||
|
||||
it 'reports unknown if content does not identify a version' do
|
||||
expect(repository).to receive(:run).and_return(result.new("foo"))
|
||||
expect(repository.fetch_supported_git_protocol).to eq('unknown')
|
||||
expect(repository).to receive(:run).and_return(described_class::Result.new(any_args, 0, "foo"))
|
||||
|
||||
expect(call_method).to eq('unknown')
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -96,7 +171,7 @@ describe QA::Git::Repository do
|
|||
end
|
||||
|
||||
context 'with specific credentials' do
|
||||
include_context 'git directory'
|
||||
include_context 'unresolvable git directory'
|
||||
|
||||
context 'before setting credentials' do
|
||||
it 'does not add credentials to .netrc' do
|
||||
|
|
|
@ -9,6 +9,12 @@ describe Profiles::EmailsController do
|
|||
sign_in(user)
|
||||
end
|
||||
|
||||
around do |example|
|
||||
perform_enqueued_jobs do
|
||||
example.run
|
||||
end
|
||||
end
|
||||
|
||||
describe '#create' do
|
||||
context 'when email address is valid' do
|
||||
let(:email_params) { { email: "add_email@example.com" } }
|
||||
|
|
|
@ -24,6 +24,10 @@ FactoryBot.define do
|
|||
monitoring_tool { FFaker::AWS.product_description }
|
||||
end
|
||||
|
||||
trait :with_description do
|
||||
description { FFaker::Lorem.sentence }
|
||||
end
|
||||
|
||||
trait :with_host do
|
||||
hosts { [FFaker::Internet.ip_v4_address] }
|
||||
end
|
||||
|
@ -70,6 +74,7 @@ FactoryBot.define do
|
|||
with_service
|
||||
with_monitoring_tool
|
||||
with_host
|
||||
with_description
|
||||
low_severity
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,10 +3,10 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe 'Dashboard Todos' do
|
||||
let(:user) { create(:user, username: 'john') }
|
||||
let(:author) { create(:user) }
|
||||
let(:project) { create(:project, :public) }
|
||||
let(:issue) { create(:issue, due_date: Date.today, title: "Fix bug") }
|
||||
let_it_be(:user) { create(:user, username: 'john') }
|
||||
let_it_be(:author) { create(:user) }
|
||||
let_it_be(:project) { create(:project, :public) }
|
||||
let_it_be(:issue) { create(:issue, due_date: Date.today, title: "Fix bug") }
|
||||
|
||||
context 'User does not have todos' do
|
||||
before do
|
||||
|
@ -357,4 +357,38 @@ describe 'Dashboard Todos' do
|
|||
expect(page).to have_link "merge request #{todo.target.to_reference}", href: href
|
||||
end
|
||||
end
|
||||
|
||||
context 'User has a todo regarding a design' do
|
||||
let_it_be(:target) { create(:design, issue: issue, project: project) }
|
||||
let_it_be(:note) { create(:note, project: project, note: 'I am note, hear me roar') }
|
||||
let_it_be(:todo) do
|
||||
create(:todo, :mentioned,
|
||||
user: user,
|
||||
project: project,
|
||||
target: target,
|
||||
author: author,
|
||||
note: note)
|
||||
end
|
||||
|
||||
before do
|
||||
project.add_developer(user)
|
||||
sign_in(user)
|
||||
|
||||
visit dashboard_todos_path
|
||||
end
|
||||
|
||||
it 'has todo present' do
|
||||
expect(page).to have_selector('.todos-list .todo', count: 1)
|
||||
end
|
||||
|
||||
it 'has a link that will take me to the design page' do
|
||||
click_link "design #{target.to_reference}"
|
||||
|
||||
expectation = Gitlab::Routing.url_helpers.designs_project_issue_path(
|
||||
target.project, target.issue, target.filename
|
||||
)
|
||||
|
||||
expect(current_path).to eq(expectation)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -67,7 +67,7 @@ describe 'Profile > Emails' do
|
|||
email = user.emails.create(email: 'my@email.com')
|
||||
visit profile_emails_path
|
||||
|
||||
expect { click_link("Resend confirmation email") }.to change { ActionMailer::Base.deliveries.size }
|
||||
expect { click_link("Resend confirmation email") }.to have_enqueued_job.on_queue('mailers')
|
||||
expect(page).to have_content("Confirmation email sent to #{email.email}")
|
||||
end
|
||||
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe 'Projects > Activity > User sees design comment', :js do
|
||||
include DesignManagementTestHelpers
|
||||
|
||||
let_it_be(:project) { create(:project, :repository, :public) }
|
||||
let_it_be(:user) { project.creator }
|
||||
let_it_be(:commenter) { create(:user) }
|
||||
let_it_be(:issue) { create(:closed_issue, project: project) }
|
||||
let_it_be(:design) { create(:design, issue: issue) }
|
||||
|
||||
let(:design_activity) do
|
||||
"#{commenter.name} #{commenter.to_reference} commented on design"
|
||||
end
|
||||
|
||||
let(:issue_activity) do
|
||||
"#{user.name} #{user.to_reference} closed issue #{issue.to_reference}"
|
||||
end
|
||||
|
||||
before_all do
|
||||
project.add_developer(commenter)
|
||||
create(:event, :for_design, project: project, author: commenter, design: design)
|
||||
create(:closed_issue_event, project: project, author: user, target: issue)
|
||||
end
|
||||
|
||||
before do
|
||||
enable_design_management
|
||||
end
|
||||
|
||||
it 'shows the design comment action in the activity page' do
|
||||
visit activity_project_path(project)
|
||||
|
||||
expect(page).to have_content(design_activity)
|
||||
end
|
||||
|
||||
it 'allows to filter out the design event with the "event_filter=issue" URL param', :aggregate_failures do
|
||||
visit activity_project_path(project, event_filter: EventFilter::ISSUE)
|
||||
|
||||
expect(page).not_to have_content(design_activity)
|
||||
expect(page).to have_content(issue_activity)
|
||||
end
|
||||
|
||||
it 'allows to filter in the event with the "event_filter=comments" URL param', :aggregate_failures do
|
||||
visit activity_project_path(project, event_filter: EventFilter::COMMENTS)
|
||||
|
||||
expect(page).to have_content(design_activity)
|
||||
expect(page).not_to have_content(issue_activity)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,40 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe 'User paginates issue designs', :js do
|
||||
include DesignManagementTestHelpers
|
||||
|
||||
let(:project) { create(:project_empty_repo, :public) }
|
||||
let(:issue) { create(:issue, project: project) }
|
||||
|
||||
before do
|
||||
enable_design_management
|
||||
|
||||
create_list(:design, 2, :with_file, issue: issue)
|
||||
|
||||
visit project_issue_path(project, issue)
|
||||
|
||||
click_link 'Designs'
|
||||
|
||||
wait_for_requests
|
||||
|
||||
find('.js-design-list-item', match: :first).click
|
||||
end
|
||||
|
||||
it 'paginates to next design' do
|
||||
expect(find('.js-previous-design')[:disabled]).to eq('true')
|
||||
|
||||
page.within(find('.js-design-header')) do
|
||||
expect(page).to have_content('1 of 2')
|
||||
end
|
||||
|
||||
find('.js-next-design').click
|
||||
|
||||
expect(find('.js-previous-design')[:disabled]).not_to eq('true')
|
||||
|
||||
page.within(find('.js-design-header')) do
|
||||
expect(page).to have_content('2 of 2')
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,24 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe 'User design permissions', :js do
|
||||
include DesignManagementTestHelpers
|
||||
|
||||
let(:project) { create(:project_empty_repo, :public) }
|
||||
let(:issue) { create(:issue, project: project) }
|
||||
|
||||
before do
|
||||
enable_design_management
|
||||
|
||||
visit project_issue_path(project, issue)
|
||||
|
||||
click_link 'Designs'
|
||||
|
||||
wait_for_requests
|
||||
end
|
||||
|
||||
it 'user does not have permissions to upload design' do
|
||||
expect(page).not_to have_field('design_file')
|
||||
end
|
||||
end
|
|
@ -0,0 +1,63 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe 'User uploads new design', :js do
|
||||
include DesignManagementTestHelpers
|
||||
|
||||
let_it_be(:project) { create(:project_empty_repo, :public) }
|
||||
let_it_be(:user) { project.owner }
|
||||
let_it_be(:issue) { create(:issue, project: project) }
|
||||
|
||||
before do
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
context "when the feature is available" do
|
||||
before do
|
||||
enable_design_management
|
||||
|
||||
visit project_issue_path(project, issue)
|
||||
|
||||
click_link 'Designs'
|
||||
|
||||
wait_for_requests
|
||||
end
|
||||
|
||||
it 'uploads designs' do
|
||||
attach_file(:design_file, logo_fixture, make_visible: true)
|
||||
|
||||
expect(page).to have_selector('.js-design-list-item', count: 1)
|
||||
|
||||
within first('#designs-tab .js-design-list-item') do
|
||||
expect(page).to have_content('dk.png')
|
||||
end
|
||||
|
||||
attach_file(:design_file, gif_fixture, make_visible: true)
|
||||
|
||||
expect(page).to have_selector('.js-design-list-item', count: 2)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the feature is not available' do
|
||||
before do
|
||||
visit project_issue_path(project, issue)
|
||||
|
||||
click_link 'Designs'
|
||||
|
||||
wait_for_requests
|
||||
end
|
||||
|
||||
it 'shows the message about requirements' do
|
||||
expect(page).to have_content("To enable design management, you'll need to meet the requirements.")
|
||||
end
|
||||
end
|
||||
|
||||
def logo_fixture
|
||||
Rails.root.join('spec', 'fixtures', 'dk.png')
|
||||
end
|
||||
|
||||
def gif_fixture
|
||||
Rails.root.join('spec', 'fixtures', 'banana_sample.gif')
|
||||
end
|
||||
end
|
|
@ -0,0 +1,41 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe 'Users views raw design image files' do
|
||||
include DesignManagementTestHelpers
|
||||
|
||||
let_it_be(:project) { create(:project, :public) }
|
||||
let_it_be(:issue) { create(:issue, project: project) }
|
||||
let_it_be(:design) { create(:design, :with_file, issue: issue, versions_count: 2) }
|
||||
let(:newest_version) { design.versions.ordered.first }
|
||||
let(:oldest_version) { design.versions.ordered.last }
|
||||
|
||||
before do
|
||||
enable_design_management
|
||||
end
|
||||
|
||||
it 'serves the latest design version when no ref is given' do
|
||||
visit project_design_management_designs_raw_image_path(design.project, design)
|
||||
|
||||
expect(response_headers[Gitlab::Workhorse::SEND_DATA_HEADER]).to eq(
|
||||
workhorse_data_header_for_version(oldest_version.sha)
|
||||
)
|
||||
end
|
||||
|
||||
it 'serves the correct design version when a ref is given' do
|
||||
visit project_design_management_designs_raw_image_path(design.project, design, oldest_version.sha)
|
||||
|
||||
expect(response_headers[Gitlab::Workhorse::SEND_DATA_HEADER]).to eq(
|
||||
workhorse_data_header_for_version(oldest_version.sha)
|
||||
)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def workhorse_data_header_for_version(ref)
|
||||
blob = project.design_repository.blob_at(ref, design.full_path)
|
||||
|
||||
Gitlab::Workhorse.send_git_blob(project.design_repository, blob).last
|
||||
end
|
||||
end
|
|
@ -0,0 +1,29 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe 'User views issue designs', :js do
|
||||
include DesignManagementTestHelpers
|
||||
|
||||
let_it_be(:project) { create(:project_empty_repo, :public) }
|
||||
let_it_be(:issue) { create(:issue, project: project) }
|
||||
let_it_be(:design) { create(:design, :with_file, issue: issue) }
|
||||
|
||||
before do
|
||||
enable_design_management
|
||||
|
||||
visit project_issue_path(project, issue)
|
||||
|
||||
click_link 'Designs'
|
||||
end
|
||||
|
||||
it 'opens design detail' do
|
||||
click_link design.filename
|
||||
|
||||
page.within(find('.js-design-header')) do
|
||||
expect(page).to have_content(design.filename)
|
||||
end
|
||||
|
||||
expect(page).to have_selector('.js-design-image')
|
||||
end
|
||||
end
|
|
@ -0,0 +1,47 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe 'User views issue designs', :js do
|
||||
include DesignManagementTestHelpers
|
||||
|
||||
let_it_be(:project) { create(:project_empty_repo, :public) }
|
||||
let_it_be(:issue) { create(:issue, project: project) }
|
||||
let_it_be(:design) { create(:design, :with_file, issue: issue) }
|
||||
|
||||
before do
|
||||
enable_design_management
|
||||
end
|
||||
|
||||
context 'navigates from the issue view' do
|
||||
before do
|
||||
visit project_issue_path(project, issue)
|
||||
click_link 'Designs'
|
||||
wait_for_requests
|
||||
end
|
||||
|
||||
it 'fetches list of designs' do
|
||||
expect(page).to have_selector('.js-design-list-item', count: 1)
|
||||
end
|
||||
end
|
||||
|
||||
context 'navigates directly to the design collection view' do
|
||||
before do
|
||||
visit designs_project_issue_path(project, issue)
|
||||
end
|
||||
|
||||
it 'expands the sidebar' do
|
||||
expect(page).to have_selector('.layout-page.right-sidebar-expanded')
|
||||
end
|
||||
end
|
||||
|
||||
context 'navigates directly to the individual design view' do
|
||||
before do
|
||||
visit designs_project_issue_path(project, issue, vueroute: design.filename)
|
||||
end
|
||||
|
||||
it 'sees the design' do
|
||||
expect(page).to have_selector('.js-design-detail')
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,55 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe 'User views an SVG design that contains XSS', :js do
|
||||
include DesignManagementTestHelpers
|
||||
|
||||
let(:project) { create(:project_empty_repo, :public) }
|
||||
let(:issue) { create(:issue, project: project) }
|
||||
let(:file) { Rails.root.join('spec', 'fixtures', 'logo_sample.svg') }
|
||||
let(:design) { create(:design, :with_file, filename: 'xss.svg', file: file, issue: issue) }
|
||||
|
||||
before do
|
||||
enable_design_management
|
||||
|
||||
visit designs_project_issue_path(
|
||||
project,
|
||||
issue,
|
||||
{ vueroute: design.filename }
|
||||
)
|
||||
|
||||
wait_for_requests
|
||||
end
|
||||
|
||||
it 'has XSS within the SVG file' do
|
||||
file_content = File.read(file)
|
||||
|
||||
expect(file_content).to include("<script>alert('FAIL')</script>")
|
||||
end
|
||||
|
||||
it 'displays the SVG' do
|
||||
expect(page).to have_selector("img.design-img[alt='xss.svg']", count: 1, visible: false)
|
||||
end
|
||||
|
||||
it 'does not execute the JavaScript within the SVG' do
|
||||
# The expectation is that we can call the capybara `page.dismiss_prompt`
|
||||
# method to close a JavaScript alert prompt without a `Capybara::ModalNotFound`
|
||||
# being raised.
|
||||
run_expectation = -> {
|
||||
page.dismiss_prompt(wait: 1)
|
||||
}
|
||||
|
||||
# With the page loaded, there should be no alert modal
|
||||
expect(run_expectation).to raise_error(
|
||||
Capybara::ModalNotFound,
|
||||
'Unable to find modal dialog'
|
||||
)
|
||||
|
||||
# Perform a negative control test of the above expectation.
|
||||
# With an alert modal displaying, the modal should be dismissable.
|
||||
execute_script('alert(true)')
|
||||
|
||||
expect(run_expectation).not_to raise_error
|
||||
end
|
||||
end
|
|
@ -5,9 +5,9 @@ require 'spec_helper'
|
|||
describe AlertManagement::AlertsFinder, '#execute' do
|
||||
let_it_be(:current_user) { create(:user) }
|
||||
let_it_be(:project) { create(:project) }
|
||||
let_it_be(:alert_1) { create(:alert_management_alert, :resolved, project: project, ended_at: 1.year.ago, events: 2, severity: :high) }
|
||||
let_it_be(:alert_2) { create(:alert_management_alert, :ignored, project: project, events: 1, severity: :critical) }
|
||||
let_it_be(:alert_3) { create(:alert_management_alert) }
|
||||
let_it_be(:alert_1) { create(:alert_management_alert, :all_fields, :resolved, project: project, ended_at: 1.year.ago, events: 2, severity: :high) }
|
||||
let_it_be(:alert_2) { create(:alert_management_alert, :all_fields, :ignored, project: project, events: 1, severity: :critical) }
|
||||
let_it_be(:alert_3) { create(:alert_management_alert, :all_fields) }
|
||||
let(:params) { {} }
|
||||
|
||||
subject { described_class.new(current_user, project, params).execute }
|
||||
|
@ -222,5 +222,59 @@ describe AlertManagement::AlertsFinder, '#execute' do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'search query given' do
|
||||
let_it_be(:alert) do
|
||||
create(:alert_management_alert,
|
||||
:with_fingerprint,
|
||||
title: 'Title',
|
||||
description: 'Desc',
|
||||
service: 'Service',
|
||||
monitoring_tool: 'Monitor'
|
||||
)
|
||||
end
|
||||
|
||||
before do
|
||||
alert.project.add_developer(current_user)
|
||||
end
|
||||
|
||||
subject { described_class.new(current_user, alert.project, params).execute }
|
||||
|
||||
context 'searching title' do
|
||||
let(:params) { { search: alert.title } }
|
||||
|
||||
it { is_expected.to match_array([alert]) }
|
||||
end
|
||||
|
||||
context 'searching description' do
|
||||
let(:params) { { search: alert.description } }
|
||||
|
||||
it { is_expected.to match_array([alert]) }
|
||||
end
|
||||
|
||||
context 'searching service' do
|
||||
let(:params) { { search: alert.service } }
|
||||
|
||||
it { is_expected.to match_array([alert]) }
|
||||
end
|
||||
|
||||
context 'searching monitoring tool' do
|
||||
let(:params) { { search: alert.monitoring_tool } }
|
||||
|
||||
it { is_expected.to match_array([alert]) }
|
||||
end
|
||||
|
||||
context 'searching something else' do
|
||||
let(:params) { { search: alert.fingerprint } }
|
||||
|
||||
it { is_expected.to be_empty }
|
||||
end
|
||||
|
||||
context 'empty search' do
|
||||
let(:params) { { search: ' ' } }
|
||||
|
||||
it { is_expected.to match_array([alert]) }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import Vue from 'vue';
|
||||
import createEventHub from '~/helpers/event_hub_factory';
|
||||
import Vuex from 'vuex';
|
||||
|
||||
import { createLocalVue, mount } from '@vue/test-utils';
|
||||
|
@ -132,7 +132,7 @@ describe('DiscussionFilter component', () => {
|
|||
});
|
||||
|
||||
describe('Merge request tabs', () => {
|
||||
eventHub = new Vue();
|
||||
eventHub = createEventHub();
|
||||
|
||||
beforeEach(() => {
|
||||
window.mrTabs = {
|
||||
|
|
|
@ -3,6 +3,7 @@ import { shallowMount, createLocalVue } from '@vue/test-utils';
|
|||
import * as utils from '~/lib/utils/common_utils';
|
||||
import discussionNavigation from '~/notes/mixins/discussion_navigation';
|
||||
import eventHub from '~/notes/event_hub';
|
||||
import createEventHub from '~/helpers/event_hub_factory';
|
||||
import notesModule from '~/notes/stores/modules';
|
||||
import { setHTMLFixture } from 'helpers/fixtures';
|
||||
|
||||
|
@ -67,8 +68,7 @@ describe('Discussion navigation mixin', () => {
|
|||
|
||||
describe('cycle through discussions', () => {
|
||||
beforeEach(() => {
|
||||
// eslint-disable-next-line new-cap
|
||||
window.mrTabs = { eventHub: new localVue(), tabShown: jest.fn() };
|
||||
window.mrTabs = { eventHub: createEventHub(), tabShown: jest.fn() };
|
||||
});
|
||||
|
||||
describe.each`
|
||||
|
|
|
@ -162,7 +162,50 @@ describe AlertManagement::Alert do
|
|||
it { is_expected.to contain_exactly(alert_1) }
|
||||
end
|
||||
|
||||
describe '.details' do
|
||||
describe '.search' do
|
||||
let_it_be(:alert) do
|
||||
create(:alert_management_alert,
|
||||
title: 'Title',
|
||||
description: 'Desc',
|
||||
service: 'Service',
|
||||
monitoring_tool: 'Monitor'
|
||||
)
|
||||
end
|
||||
|
||||
subject { AlertManagement::Alert.search(query) }
|
||||
|
||||
context 'does not contain search string' do
|
||||
let(:query) { 'something else' }
|
||||
|
||||
it { is_expected.to be_empty }
|
||||
end
|
||||
|
||||
context 'title includes query' do
|
||||
let(:query) { alert.title.upcase }
|
||||
|
||||
it { is_expected.to contain_exactly(alert) }
|
||||
end
|
||||
|
||||
context 'description includes query' do
|
||||
let(:query) { alert.description.upcase }
|
||||
|
||||
it { is_expected.to contain_exactly(alert) }
|
||||
end
|
||||
|
||||
context 'service includes query' do
|
||||
let(:query) { alert.service.upcase }
|
||||
|
||||
it { is_expected.to contain_exactly(alert) }
|
||||
end
|
||||
|
||||
context 'monitoring tool includes query' do
|
||||
let(:query) { alert.monitoring_tool.upcase }
|
||||
|
||||
it { is_expected.to contain_exactly(alert) }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#details' do
|
||||
let(:payload) do
|
||||
{
|
||||
'title' => 'Details title',
|
||||
|
|
|
@ -3117,11 +3117,7 @@ describe Ci::Build do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#secret_group_variables' do
|
||||
subject { build.secret_group_variables }
|
||||
|
||||
let!(:variable) { create(:ci_group_variable, protected: true, group: group) }
|
||||
|
||||
shared_examples "secret CI variables" do
|
||||
context 'when ref is branch' do
|
||||
let(:build) { create(:ci_build, ref: 'master', tag: false, project: project) }
|
||||
|
||||
|
@ -3175,62 +3171,28 @@ describe Ci::Build do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#secret_instance_variables' do
|
||||
subject { build.secret_instance_variables }
|
||||
|
||||
let_it_be(:variable) { create(:ci_instance_variable, protected: true) }
|
||||
|
||||
include_examples "secret CI variables"
|
||||
end
|
||||
|
||||
describe '#secret_group_variables' do
|
||||
subject { build.secret_group_variables }
|
||||
|
||||
let_it_be(:variable) { create(:ci_group_variable, protected: true, group: group) }
|
||||
|
||||
include_examples "secret CI variables"
|
||||
end
|
||||
|
||||
describe '#secret_project_variables' do
|
||||
subject { build.secret_project_variables }
|
||||
|
||||
let!(:variable) { create(:ci_variable, protected: true, project: project) }
|
||||
let_it_be(:variable) { create(:ci_variable, protected: true, project: project) }
|
||||
|
||||
context 'when ref is branch' do
|
||||
let(:build) { create(:ci_build, ref: 'master', tag: false, project: project) }
|
||||
|
||||
context 'when ref is protected' do
|
||||
before do
|
||||
create(:protected_branch, :developers_can_merge, name: 'master', project: project)
|
||||
end
|
||||
|
||||
it { is_expected.to include(variable) }
|
||||
end
|
||||
|
||||
context 'when ref is not protected' do
|
||||
it { is_expected.not_to include(variable) }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when ref is tag' do
|
||||
let(:build) { create(:ci_build, ref: 'v1.1.0', tag: true, project: project) }
|
||||
|
||||
context 'when ref is protected' do
|
||||
before do
|
||||
create(:protected_tag, project: project, name: 'v*')
|
||||
end
|
||||
|
||||
it { is_expected.to include(variable) }
|
||||
end
|
||||
|
||||
context 'when ref is not protected' do
|
||||
it { is_expected.not_to include(variable) }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when ref is merge request' do
|
||||
let(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline) }
|
||||
let(:pipeline) { merge_request.pipelines_for_merge_request.first }
|
||||
let(:build) { create(:ci_build, ref: merge_request.source_branch, tag: false, pipeline: pipeline, project: project) }
|
||||
|
||||
context 'when ref is protected' do
|
||||
before do
|
||||
create(:protected_branch, :developers_can_merge, name: merge_request.source_branch, project: project)
|
||||
end
|
||||
|
||||
it 'does not return protected variables as it is not supported for merge request pipelines' do
|
||||
is_expected.not_to include(variable)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when ref is not protected' do
|
||||
it { is_expected.not_to include(variable) }
|
||||
end
|
||||
end
|
||||
include_examples "secret CI variables"
|
||||
end
|
||||
|
||||
describe '#deployment_variables' do
|
||||
|
@ -3283,6 +3245,29 @@ describe Ci::Build do
|
|||
expect(build.scoped_variables_hash).not_to include('MY_VAR': 'myvar')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when overriding CI instance variables' do
|
||||
before do
|
||||
create(:ci_instance_variable, key: 'MY_VAR', value: 'my value 1')
|
||||
group.variables.create!(key: 'MY_VAR', value: 'my value 2')
|
||||
end
|
||||
|
||||
it 'returns a regular hash created using valid ordering' do
|
||||
expect(build.scoped_variables_hash).to include('MY_VAR': 'my value 2')
|
||||
expect(build.scoped_variables_hash).not_to include('MY_VAR': 'my value 1')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when CI instance variables are disabled' do
|
||||
before do
|
||||
create(:ci_instance_variable, key: 'MY_VAR', value: 'my value 1')
|
||||
stub_feature_flags(ci_instance_level_variables: false)
|
||||
end
|
||||
|
||||
it 'does not include instance level variables' do
|
||||
expect(build.scoped_variables_hash).not_to include('MY_VAR': 'my value 1')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#any_unmet_prerequisites?' do
|
||||
|
|
|
@ -31,4 +31,63 @@ describe Ci::InstanceVariable do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.all_cached', :use_clean_rails_memory_store_caching do
|
||||
let_it_be(:unprotected_variable) { create(:ci_instance_variable, protected: false) }
|
||||
let_it_be(:protected_variable) { create(:ci_instance_variable, protected: true) }
|
||||
|
||||
it { expect(described_class.all_cached).to contain_exactly(protected_variable, unprotected_variable) }
|
||||
|
||||
it 'memoizes the result' do
|
||||
expect(described_class).to receive(:store_cache).with(:ci_instance_variable_data).once.and_call_original
|
||||
|
||||
2.times do
|
||||
expect(described_class.all_cached).to contain_exactly(protected_variable, unprotected_variable)
|
||||
end
|
||||
end
|
||||
|
||||
it 'removes scopes' do
|
||||
expect(described_class.unprotected.all_cached).to contain_exactly(protected_variable, unprotected_variable)
|
||||
end
|
||||
|
||||
it 'resets the cache when records are deleted' do
|
||||
expect(described_class.all_cached).to contain_exactly(protected_variable, unprotected_variable)
|
||||
|
||||
protected_variable.destroy
|
||||
|
||||
expect(described_class.all_cached).to contain_exactly(unprotected_variable)
|
||||
end
|
||||
|
||||
it 'resets the cache when records are inserted' do
|
||||
expect(described_class.all_cached).to contain_exactly(protected_variable, unprotected_variable)
|
||||
|
||||
variable = create(:ci_instance_variable, protected: true)
|
||||
|
||||
expect(described_class.all_cached).to contain_exactly(protected_variable, unprotected_variable, variable)
|
||||
end
|
||||
|
||||
it 'resets the cache when the shared key is missing' do
|
||||
expect(Rails.cache).to receive(:read).with(:ci_instance_variable_changed_at).twice.and_return(nil)
|
||||
expect(described_class).to receive(:store_cache).with(:ci_instance_variable_data).thrice.and_call_original
|
||||
|
||||
3.times do
|
||||
expect(described_class.all_cached).to contain_exactly(protected_variable, unprotected_variable)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.unprotected_cached', :use_clean_rails_memory_store_caching do
|
||||
let_it_be(:unprotected_variable) { create(:ci_instance_variable, protected: false) }
|
||||
let_it_be(:protected_variable) { create(:ci_instance_variable, protected: true) }
|
||||
|
||||
it { expect(described_class.unprotected_cached).to contain_exactly(unprotected_variable) }
|
||||
|
||||
it 'memoizes the result' do
|
||||
expect(described_class).to receive(:store_cache).with(:ci_instance_variable_data).once.and_call_original
|
||||
|
||||
2.times do
|
||||
expect(described_class.unprotected_cached).to contain_exactly(unprotected_variable)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,6 +3,12 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Email do
|
||||
describe 'modules' do
|
||||
subject { described_class }
|
||||
|
||||
it { is_expected.to include_module(AsyncDeviseEmail) }
|
||||
end
|
||||
|
||||
describe 'validations' do
|
||||
it_behaves_like 'an object with RFC3696 compliant email-formated attributes', :email do
|
||||
subject { build(:email) }
|
||||
|
@ -45,4 +51,16 @@ describe Email do
|
|||
expect(build(:email, user: user).username).to eq user.username
|
||||
end
|
||||
end
|
||||
|
||||
describe 'Devise emails' do
|
||||
let!(:user) { create(:user) }
|
||||
|
||||
describe 'behaviour' do
|
||||
it 'sends emails asynchronously' do
|
||||
expect do
|
||||
user.emails.create!(email: 'hello@hello.com')
|
||||
end.to have_enqueued_job.on_queue('mailers')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3138,6 +3138,45 @@ describe Project do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#ci_instance_variables_for' do
|
||||
let(:project) { create(:project) }
|
||||
|
||||
let!(:instance_variable) do
|
||||
create(:ci_instance_variable, value: 'secret')
|
||||
end
|
||||
|
||||
let!(:protected_instance_variable) do
|
||||
create(:ci_instance_variable, :protected, value: 'protected')
|
||||
end
|
||||
|
||||
subject { project.ci_instance_variables_for(ref: 'ref') }
|
||||
|
||||
before do
|
||||
stub_application_setting(
|
||||
default_branch_protection: Gitlab::Access::PROTECTION_NONE)
|
||||
end
|
||||
|
||||
context 'when the ref is not protected' do
|
||||
before do
|
||||
allow(project).to receive(:protected_for?).with('ref').and_return(false)
|
||||
end
|
||||
|
||||
it 'contains only the CI variables' do
|
||||
is_expected.to contain_exactly(instance_variable)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the ref is protected' do
|
||||
before do
|
||||
allow(project).to receive(:protected_for?).with('ref').and_return(true)
|
||||
end
|
||||
|
||||
it 'contains all the variables' do
|
||||
is_expected.to contain_exactly(instance_variable, protected_instance_variable)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#any_lfs_file_locks?', :request_store do
|
||||
let_it_be(:project) { create(:project) }
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ describe User do
|
|||
it { is_expected.to include_module(Sortable) }
|
||||
it { is_expected.to include_module(TokenAuthenticatable) }
|
||||
it { is_expected.to include_module(BlocksJsonSerialization) }
|
||||
it { is_expected.to include_module(AsyncDeviseEmail) }
|
||||
end
|
||||
|
||||
describe 'delegations' do
|
||||
|
@ -165,6 +166,18 @@ describe User do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'Devise emails' do
|
||||
let!(:user) { create(:user) }
|
||||
|
||||
describe 'behaviour' do
|
||||
it 'sends emails asynchronously' do
|
||||
expect do
|
||||
user.update!(email: 'hello@hello.com')
|
||||
end.to have_enqueued_job.on_queue('mailers').exactly(:twice)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'validations' do
|
||||
describe 'password' do
|
||||
let!(:user) { create(:user) }
|
||||
|
|
|
@ -10,6 +10,7 @@ describe 'getting Alert Management Alerts' do
|
|||
let_it_be(:alert_1) { create(:alert_management_alert, :all_fields, :resolved, project: project, issue: nil, severity: :low) }
|
||||
let_it_be(:alert_2) { create(:alert_management_alert, :all_fields, project: project, severity: :critical, payload: payload) }
|
||||
let_it_be(:other_project_alert) { create(:alert_management_alert, :all_fields) }
|
||||
let(:params) { {} }
|
||||
|
||||
let(:fields) do
|
||||
<<~QUERY
|
||||
|
@ -23,7 +24,7 @@ describe 'getting Alert Management Alerts' do
|
|||
graphql_query_for(
|
||||
'project',
|
||||
{ 'fullPath' => project.full_path },
|
||||
query_graphql_field('alertManagementAlerts', {}, fields)
|
||||
query_graphql_field('alertManagementAlerts', params, fields)
|
||||
)
|
||||
end
|
||||
|
||||
|
@ -83,13 +84,7 @@ describe 'getting Alert Management Alerts' do
|
|||
end
|
||||
|
||||
context 'with iid given' do
|
||||
let(:query) do
|
||||
graphql_query_for(
|
||||
'project',
|
||||
{ 'fullPath' => project.full_path },
|
||||
query_graphql_field('alertManagementAlerts', { iid: alert_1.iid.to_s }, fields)
|
||||
)
|
||||
end
|
||||
let(:params) { { iid: alert_1.iid.to_s } }
|
||||
|
||||
it_behaves_like 'a working graphql query'
|
||||
|
||||
|
@ -98,14 +93,6 @@ describe 'getting Alert Management Alerts' do
|
|||
end
|
||||
|
||||
context 'sorting data given' do
|
||||
let(:query) do
|
||||
graphql_query_for(
|
||||
'project',
|
||||
{ 'fullPath' => project.full_path },
|
||||
query_graphql_field('alertManagementAlerts', params, fields)
|
||||
)
|
||||
end
|
||||
|
||||
let(:params) { 'sort: SEVERITY_DESC' }
|
||||
let(:iids) { alerts.map { |a| a['iid'] } }
|
||||
|
||||
|
@ -123,6 +110,21 @@ describe 'getting Alert Management Alerts' do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'searching' do
|
||||
let(:params) { { search: alert_1.title } }
|
||||
|
||||
it_behaves_like 'a working graphql query'
|
||||
|
||||
it { expect(alerts.size).to eq(1) }
|
||||
it { expect(first_alert['iid']).to eq(alert_1.iid.to_s) }
|
||||
|
||||
context 'unknown criteria' do
|
||||
let(:params) { { search: 'something random' } }
|
||||
|
||||
it { expect(alerts.size).to eq(0) }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -8,10 +8,10 @@ describe Emails::ConfirmService do
|
|||
subject(:service) { described_class.new(user) }
|
||||
|
||||
describe '#execute' do
|
||||
it 'sends a confirmation email again' do
|
||||
it 'enqueues a background job to send confirmation email again' do
|
||||
email = user.emails.create(email: 'new@email.com')
|
||||
mail = service.execute(email)
|
||||
expect(mail.subject).to eq('Confirmation instructions')
|
||||
|
||||
expect { service.execute(email) }.to have_enqueued_job.on_queue('mailers')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue