diff --git a/app/assets/javascripts/clusters/event_hub.js b/app/assets/javascripts/clusters/event_hub.js
index 0948c2e5352..e31806ad199 100644
--- a/app/assets/javascripts/clusters/event_hub.js
+++ b/app/assets/javascripts/clusters/event_hub.js
@@ -1,3 +1,3 @@
-import Vue from 'vue';
+import createEventHub from '~/helpers/event_hub_factory';
-export default new Vue();
+export default createEventHub();
diff --git a/app/assets/javascripts/deploy_keys/eventhub.js b/app/assets/javascripts/deploy_keys/eventhub.js
index 0948c2e5352..e31806ad199 100644
--- a/app/assets/javascripts/deploy_keys/eventhub.js
+++ b/app/assets/javascripts/deploy_keys/eventhub.js
@@ -1,3 +1,3 @@
-import Vue from 'vue';
+import createEventHub from '~/helpers/event_hub_factory';
-export default new Vue();
+export default createEventHub();
diff --git a/app/assets/javascripts/environments/event_hub.js b/app/assets/javascripts/environments/event_hub.js
index 0948c2e5352..e31806ad199 100644
--- a/app/assets/javascripts/environments/event_hub.js
+++ b/app/assets/javascripts/environments/event_hub.js
@@ -1,3 +1,3 @@
-import Vue from 'vue';
+import createEventHub from '~/helpers/event_hub_factory';
-export default new Vue();
+export default createEventHub();
diff --git a/app/assets/javascripts/filtered_search/event_hub.js b/app/assets/javascripts/filtered_search/event_hub.js
index 0948c2e5352..e31806ad199 100644
--- a/app/assets/javascripts/filtered_search/event_hub.js
+++ b/app/assets/javascripts/filtered_search/event_hub.js
@@ -1,3 +1,3 @@
-import Vue from 'vue';
+import createEventHub from '~/helpers/event_hub_factory';
-export default new Vue();
+export default createEventHub();
diff --git a/app/assets/javascripts/frequent_items/event_hub.js b/app/assets/javascripts/frequent_items/event_hub.js
index 0948c2e5352..e31806ad199 100644
--- a/app/assets/javascripts/frequent_items/event_hub.js
+++ b/app/assets/javascripts/frequent_items/event_hub.js
@@ -1,3 +1,3 @@
-import Vue from 'vue';
+import createEventHub from '~/helpers/event_hub_factory';
-export default new Vue();
+export default createEventHub();
diff --git a/app/assets/javascripts/groups/event_hub.js b/app/assets/javascripts/groups/event_hub.js
index 0948c2e5352..e31806ad199 100644
--- a/app/assets/javascripts/groups/event_hub.js
+++ b/app/assets/javascripts/groups/event_hub.js
@@ -1,3 +1,3 @@
-import Vue from 'vue';
+import createEventHub from '~/helpers/event_hub_factory';
-export default new Vue();
+export default createEventHub();
diff --git a/app/assets/javascripts/import_projects/event_hub.js b/app/assets/javascripts/import_projects/event_hub.js
index 0948c2e5352..e31806ad199 100644
--- a/app/assets/javascripts/import_projects/event_hub.js
+++ b/app/assets/javascripts/import_projects/event_hub.js
@@ -1,3 +1,3 @@
-import Vue from 'vue';
+import createEventHub from '~/helpers/event_hub_factory';
-export default new Vue();
+export default createEventHub();
diff --git a/app/assets/javascripts/integrations/edit/event_hub.js b/app/assets/javascripts/integrations/edit/event_hub.js
index 0948c2e5352..e31806ad199 100644
--- a/app/assets/javascripts/integrations/edit/event_hub.js
+++ b/app/assets/javascripts/integrations/edit/event_hub.js
@@ -1,3 +1,3 @@
-import Vue from 'vue';
+import createEventHub from '~/helpers/event_hub_factory';
-export default new Vue();
+export default createEventHub();
diff --git a/app/assets/javascripts/issue_show/event_hub.js b/app/assets/javascripts/issue_show/event_hub.js
index 0948c2e5352..e31806ad199 100644
--- a/app/assets/javascripts/issue_show/event_hub.js
+++ b/app/assets/javascripts/issue_show/event_hub.js
@@ -1,3 +1,3 @@
-import Vue from 'vue';
+import createEventHub from '~/helpers/event_hub_factory';
-export default new Vue();
+export default createEventHub();
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index 87de58443e0..1795a0dbdf8 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -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);
diff --git a/app/assets/javascripts/pages/milestones/shared/event_hub.js b/app/assets/javascripts/pages/milestones/shared/event_hub.js
index 0948c2e5352..e31806ad199 100644
--- a/app/assets/javascripts/pages/milestones/shared/event_hub.js
+++ b/app/assets/javascripts/pages/milestones/shared/event_hub.js
@@ -1,3 +1,3 @@
-import Vue from 'vue';
+import createEventHub from '~/helpers/event_hub_factory';
-export default new Vue();
+export default createEventHub();
diff --git a/app/assets/javascripts/pages/projects/labels/event_hub.js b/app/assets/javascripts/pages/projects/labels/event_hub.js
index 0948c2e5352..e31806ad199 100644
--- a/app/assets/javascripts/pages/projects/labels/event_hub.js
+++ b/app/assets/javascripts/pages/projects/labels/event_hub.js
@@ -1,3 +1,3 @@
-import Vue from 'vue';
+import createEventHub from '~/helpers/event_hub_factory';
-export default new Vue();
+export default createEventHub();
diff --git a/app/assets/javascripts/serverless/event_hub.js b/app/assets/javascripts/serverless/event_hub.js
index 0948c2e5352..e31806ad199 100644
--- a/app/assets/javascripts/serverless/event_hub.js
+++ b/app/assets/javascripts/serverless/event_hub.js
@@ -1,3 +1,3 @@
-import Vue from 'vue';
+import createEventHub from '~/helpers/event_hub_factory';
-export default new Vue();
+export default createEventHub();
diff --git a/app/assets/javascripts/set_status_modal/event_hub.js b/app/assets/javascripts/set_status_modal/event_hub.js
index 0948c2e5352..e31806ad199 100644
--- a/app/assets/javascripts/set_status_modal/event_hub.js
+++ b/app/assets/javascripts/set_status_modal/event_hub.js
@@ -1,3 +1,3 @@
-import Vue from 'vue';
+import createEventHub from '~/helpers/event_hub_factory';
-export default new Vue();
+export default createEventHub();
diff --git a/app/assets/javascripts/vue_merge_request_widget/event_hub.js b/app/assets/javascripts/vue_merge_request_widget/event_hub.js
index 0948c2e5352..e31806ad199 100644
--- a/app/assets/javascripts/vue_merge_request_widget/event_hub.js
+++ b/app/assets/javascripts/vue_merge_request_widget/event_hub.js
@@ -1,3 +1,3 @@
-import Vue from 'vue';
+import createEventHub from '~/helpers/event_hub_factory';
-export default new Vue();
+export default createEventHub();
diff --git a/app/assets/javascripts/vue_shared/components/recaptcha_eventhub.js b/app/assets/javascripts/vue_shared/components/recaptcha_eventhub.js
index a4e004c3341..e193883b6e9 100644
--- a/app/assets/javascripts/vue_shared/components/recaptcha_eventhub.js
+++ b/app/assets/javascripts/vue_shared/components/recaptcha_eventhub.js
@@ -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!`);
diff --git a/app/finders/alert_management/alerts_finder.rb b/app/finders/alert_management/alerts_finder.rb
index eeb6b5f8491..e5fde50849e 100644
--- a/app/finders/alert_management/alerts_finder.rb
+++ b/app/finders/alert_management/alerts_finder.rb
@@ -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
diff --git a/app/graphql/resolvers/alert_management_alert_resolver.rb b/app/graphql/resolvers/alert_management_alert_resolver.rb
index d2f82ece281..8e7fe566d32 100644
--- a/app/graphql/resolvers/alert_management_alert_resolver.rb
+++ b/app/graphql/resolvers/alert_management_alert_resolver.rb
@@ -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)
diff --git a/app/models/alert_management/alert.rb b/app/models/alert_management/alert.rb
index 8edeb408805..c030987e770 100644
--- a/app/models/alert_management/alert.rb
+++ b/app/models/alert_management/alert.rb
@@ -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) }
diff --git a/app/models/ci/instance_variable.rb b/app/models/ci/instance_variable.rb
index 19511696509..c674f76d229 100644
--- a/app/models/ci/instance_variable.rb
+++ b/app/models/ci/instance_variable.rb
@@ -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
diff --git a/app/models/concerns/async_devise_email.rb b/app/models/concerns/async_devise_email.rb
new file mode 100644
index 00000000000..38c99dc7e71
--- /dev/null
+++ b/app/models/concerns/async_devise_email.rb
@@ -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
diff --git a/app/models/concerns/ci/contextable.rb b/app/models/concerns/ci/contextable.rb
index b19f4518fb2..ccd90ea5900 100644
--- a/app/models/concerns/ci/contextable.rb
+++ b/app/models/concerns/ci/contextable.rb
@@ -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
diff --git a/app/models/email.rb b/app/models/email.rb
index 8d20f2019d1..c5154267ff0 100644
--- a/app/models/email.rb
+++ b/app/models/email.rb
@@ -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)
diff --git a/app/models/project.rb b/app/models/project.rb
index 79cb8b228fe..362a01106c1 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -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)
diff --git a/app/models/user.rb b/app/models/user.rb
index fc4603a6ff8..b2d3978551e 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -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
diff --git a/app/views/shared/_group_tips.html.haml b/app/views/shared/_group_tips.html.haml
index 46e4340511a..2d7f8e36139 100644
--- a/app/views/shared/_group_tips.html.haml
+++ b/app/views/shared/_group_tips.html.haml
@@ -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')
diff --git a/app/views/shared/issuable/_label_dropdown.html.haml b/app/views/shared/issuable/_label_dropdown.html.haml
index bca5db16bd3..535af522c1a 100644
--- a/app/views/shared/issuable/_label_dropdown.html.haml
+++ b/app/views/shared/issuable/_label_dropdown.html.haml
@@ -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
diff --git a/app/views/shared/issuable/_label_page_default.html.haml b/app/views/shared/issuable/_label_page_default.html.haml
index a0fb5229fc3..43e80c9db27 100644
--- a/app/views/shared/issuable/_label_page_default.html.haml
+++ b/app/views/shared/issuable/_label_page_default.html.haml
@@ -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
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index e20573ed3a7..a1c56cdb64f 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -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')
diff --git a/app/views/shared/milestones/_issuable.html.haml b/app/views/shared/milestones/_issuable.html.haml
index 0adfe2f0c04..f8bf3e7ad6a 100644
--- a/app/views/shared/milestones/_issuable.html.haml
+++ b/app/views/shared/milestones/_issuable.html.haml
@@ -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: '')
diff --git a/app/views/shared/milestones/_issues_tab.html.haml b/app/views/shared/milestones/_issues_tab.html.haml
index d7e4f2ed5a0..6684f6d752a 100644
--- a/app/views/shared/milestones/_issues_tab.html.haml
+++ b/app/views/shared/milestones/_issues_tab.html.haml
@@ -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)
diff --git a/changelogs/unreleased/14108-instance-level-ci-variables-logic.yml b/changelogs/unreleased/14108-instance-level-ci-variables-logic.yml
new file mode 100644
index 00000000000..e8efce3ef64
--- /dev/null
+++ b/changelogs/unreleased/14108-instance-level-ci-variables-logic.yml
@@ -0,0 +1,5 @@
+---
+title: Integrate CI instance variables in the build process
+merge_request: 30186
+author:
+type: added
diff --git a/changelogs/unreleased/213884-alert-management-plain-text-search.yml b/changelogs/unreleased/213884-alert-management-plain-text-search.yml
new file mode 100644
index 00000000000..3bef818d616
--- /dev/null
+++ b/changelogs/unreleased/213884-alert-management-plain-text-search.yml
@@ -0,0 +1,5 @@
+---
+title: Add search to Alert Management Alerts GraphQL query
+merge_request: 32047
+author:
+type: added
diff --git a/changelogs/unreleased/22691-externalize-i18n-strings-from---app-views-shared-issuable-_label_.yml b/changelogs/unreleased/22691-externalize-i18n-strings-from---app-views-shared-issuable-_label_.yml
new file mode 100644
index 00000000000..db5e87bc74c
--- /dev/null
+++ b/changelogs/unreleased/22691-externalize-i18n-strings-from---app-views-shared-issuable-_label_.yml
@@ -0,0 +1,5 @@
+---
+title: Externalize i18n strings from ./app/views/shared/issuable/_label_*
+merge_request: 32167
+author: Gilang Gumilar
+type: changed
diff --git a/changelogs/unreleased/22691-externalize-i18n-strings-from---app-views-shared-issuable-_sidebar-.yml b/changelogs/unreleased/22691-externalize-i18n-strings-from---app-views-shared-issuable-_sidebar-.yml
new file mode 100644
index 00000000000..4ce29c54199
--- /dev/null
+++ b/changelogs/unreleased/22691-externalize-i18n-strings-from---app-views-shared-issuable-_sidebar-.yml
@@ -0,0 +1,5 @@
+---
+title: Externalize i18n strings from ./app/views/shared/issuable/_sidebar.html.haml
+merge_request: 32164
+author: Gilang Gumilar
+type: changed
diff --git a/changelogs/unreleased/22691-externalize-i18n-strings-from---app-views-shared-milestones-_issuab.yml b/changelogs/unreleased/22691-externalize-i18n-strings-from---app-views-shared-milestones-_issuab.yml
new file mode 100644
index 00000000000..d4392edbcb5
--- /dev/null
+++ b/changelogs/unreleased/22691-externalize-i18n-strings-from---app-views-shared-milestones-_issuab.yml
@@ -0,0 +1,5 @@
+---
+title: Externalize i18n strings from ./app/views/shared/milestones/_issuable.html.haml
+merge_request: 32161
+author: Gilang Gumilar
+type: changed
diff --git a/changelogs/unreleased/22691-externalize-i18n-strings-from---app-views-shared-milestones-_issues.yml b/changelogs/unreleased/22691-externalize-i18n-strings-from---app-views-shared-milestones-_issues.yml
new file mode 100644
index 00000000000..8c6231007e1
--- /dev/null
+++ b/changelogs/unreleased/22691-externalize-i18n-strings-from---app-views-shared-milestones-_issues.yml
@@ -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
diff --git a/changelogs/unreleased/22691-externelize-i18n-strings-from---app-views-shared-_group_tips-html-h.yml b/changelogs/unreleased/22691-externelize-i18n-strings-from---app-views-shared-_group_tips-html-h.yml
new file mode 100644
index 00000000000..8308e3dcb12
--- /dev/null
+++ b/changelogs/unreleased/22691-externelize-i18n-strings-from---app-views-shared-_group_tips-html-h.yml
@@ -0,0 +1,5 @@
+---
+title: Externalize i18n strings from ./app/views/shared/_group_tips.html.haml
+merge_request: 32127
+author: Gilang Gumilar
+type: changed
diff --git a/changelogs/unreleased/async-secondary-email-devise-mailers.yml b/changelogs/unreleased/async-secondary-email-devise-mailers.yml
new file mode 100644
index 00000000000..facfe0ba4bd
--- /dev/null
+++ b/changelogs/unreleased/async-secondary-email-devise-mailers.yml
@@ -0,0 +1,5 @@
+---
+title: Send Devise emails triggered from the 'Email' model asynchronously
+merge_request: 32286
+author:
+type: fixed
diff --git a/changelogs/unreleased/chore-vue-event-hub-to-mitt-migration.yml b/changelogs/unreleased/chore-vue-event-hub-to-mitt-migration.yml
new file mode 100644
index 00000000000..ea69304982e
--- /dev/null
+++ b/changelogs/unreleased/chore-vue-event-hub-to-mitt-migration.yml
@@ -0,0 +1,5 @@
+---
+title: Migrate from Vue event hub to Mitt
+merge_request: 31666
+author: Arun Kumar Mohan
+type: changed
diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql
index 8bba0061b4e..faa5590152c 100644
--- a/doc/api/graphql/reference/gitlab_schema.graphql
+++ b/doc/api/graphql/reference/gitlab_schema.graphql
@@ -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
"""
diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json
index 88f22f4deca..4e33b30549b 100644
--- a/doc/api/graphql/reference/gitlab_schema.json
+++ b/doc/api/graphql/reference/gitlab_schema.json
@@ -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.",
diff --git a/doc/user/admin_area/merge_requests_approvals.md b/doc/user/admin_area/merge_requests_approvals.md
index 0c7beadad48..d0092b1eaf0 100644
--- a/doc/user/admin_area/merge_requests_approvals.md
+++ b/doc/user/admin_area/merge_requests_approvals.md
@@ -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:
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 037e4e30653..3fc8557d9da 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -874,6 +874,9 @@ msgstr ""
msgid "A fork is a copy of a project.
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 %{project_name}"
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 ""
diff --git a/qa/qa/git/repository.rb b/qa/qa/git/repository.rb
index 771f135a95c..973404ba58e 100644
--- a/qa/qa/git/repository.rb
+++ b/qa/qa/git/repository.rb
@@ -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
diff --git a/qa/spec/git/repository_spec.rb b/qa/spec/git/repository_spec.rb
index 0559fabbfd5..8355c77f493 100644
--- a/qa/spec/git/repository_spec.rb
+++ b/qa/spec/git/repository_spec.rb
@@ -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
diff --git a/spec/controllers/profiles/emails_controller_spec.rb b/spec/controllers/profiles/emails_controller_spec.rb
index 91850e429a5..ffec43fea2c 100644
--- a/spec/controllers/profiles/emails_controller_spec.rb
+++ b/spec/controllers/profiles/emails_controller_spec.rb
@@ -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" } }
diff --git a/spec/factories/alert_management/alerts.rb b/spec/factories/alert_management/alerts.rb
index 0e1c0433905..01f40a7a465 100644
--- a/spec/factories/alert_management/alerts.rb
+++ b/spec/factories/alert_management/alerts.rb
@@ -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
diff --git a/spec/features/dashboard/todos/todos_spec.rb b/spec/features/dashboard/todos/todos_spec.rb
index 867281da1e6..63867d5796a 100644
--- a/spec/features/dashboard/todos/todos_spec.rb
+++ b/spec/features/dashboard/todos/todos_spec.rb
@@ -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
diff --git a/spec/features/profiles/emails_spec.rb b/spec/features/profiles/emails_spec.rb
index 5dfc03d711a..a41ef9e86ae 100644
--- a/spec/features/profiles/emails_spec.rb
+++ b/spec/features/profiles/emails_spec.rb
@@ -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
diff --git a/spec/features/projects/activity/user_sees_design_comment_spec.rb b/spec/features/projects/activity/user_sees_design_comment_spec.rb
new file mode 100644
index 00000000000..9864e9ce29f
--- /dev/null
+++ b/spec/features/projects/activity/user_sees_design_comment_spec.rb
@@ -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
diff --git a/spec/features/projects/issues/design_management/user_paginates_designs_spec.rb b/spec/features/projects/issues/design_management/user_paginates_designs_spec.rb
new file mode 100644
index 00000000000..d9a72f2d5c5
--- /dev/null
+++ b/spec/features/projects/issues/design_management/user_paginates_designs_spec.rb
@@ -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
diff --git a/spec/features/projects/issues/design_management/user_permissions_upload_spec.rb b/spec/features/projects/issues/design_management/user_permissions_upload_spec.rb
new file mode 100644
index 00000000000..2238e86a47f
--- /dev/null
+++ b/spec/features/projects/issues/design_management/user_permissions_upload_spec.rb
@@ -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
diff --git a/spec/features/projects/issues/design_management/user_uploads_designs_spec.rb b/spec/features/projects/issues/design_management/user_uploads_designs_spec.rb
new file mode 100644
index 00000000000..d160ab95a65
--- /dev/null
+++ b/spec/features/projects/issues/design_management/user_uploads_designs_spec.rb
@@ -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
diff --git a/spec/features/projects/issues/design_management/user_views_design_images_spec.rb b/spec/features/projects/issues/design_management/user_views_design_images_spec.rb
new file mode 100644
index 00000000000..3d0f4df55c4
--- /dev/null
+++ b/spec/features/projects/issues/design_management/user_views_design_images_spec.rb
@@ -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
diff --git a/spec/features/projects/issues/design_management/user_views_design_spec.rb b/spec/features/projects/issues/design_management/user_views_design_spec.rb
new file mode 100644
index 00000000000..707049b0068
--- /dev/null
+++ b/spec/features/projects/issues/design_management/user_views_design_spec.rb
@@ -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
diff --git a/spec/features/projects/issues/design_management/user_views_designs_spec.rb b/spec/features/projects/issues/design_management/user_views_designs_spec.rb
new file mode 100644
index 00000000000..a4fb7456922
--- /dev/null
+++ b/spec/features/projects/issues/design_management/user_views_designs_spec.rb
@@ -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
diff --git a/spec/features/projects/issues/design_management/user_views_designs_with_svg_xss_spec.rb b/spec/features/projects/issues/design_management/user_views_designs_with_svg_xss_spec.rb
new file mode 100644
index 00000000000..a9e4aa899a7
--- /dev/null
+++ b/spec/features/projects/issues/design_management/user_views_designs_with_svg_xss_spec.rb
@@ -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("")
+ 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
diff --git a/spec/finders/alert_management/alerts_finder_spec.rb b/spec/finders/alert_management/alerts_finder_spec.rb
index 70130fbd392..99db4ecbf03 100644
--- a/spec/finders/alert_management/alerts_finder_spec.rb
+++ b/spec/finders/alert_management/alerts_finder_spec.rb
@@ -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
diff --git a/spec/frontend/notes/components/discussion_filter_spec.js b/spec/frontend/notes/components/discussion_filter_spec.js
index b8d2d721443..7f042c0e9de 100644
--- a/spec/frontend/notes/components/discussion_filter_spec.js
+++ b/spec/frontend/notes/components/discussion_filter_spec.js
@@ -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 = {
diff --git a/spec/frontend/notes/mixins/discussion_navigation_spec.js b/spec/frontend/notes/mixins/discussion_navigation_spec.js
index 4e5325b8bc3..120de023099 100644
--- a/spec/frontend/notes/mixins/discussion_navigation_spec.js
+++ b/spec/frontend/notes/mixins/discussion_navigation_spec.js
@@ -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`
diff --git a/spec/models/alert_management/alert_spec.rb b/spec/models/alert_management/alert_spec.rb
index c82e1617f7d..97bffa23b31 100644
--- a/spec/models/alert_management/alert_spec.rb
+++ b/spec/models/alert_management/alert_spec.rb
@@ -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',
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index ea7ce8cadc4..6605866d9c0 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -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
diff --git a/spec/models/ci/instance_variable_spec.rb b/spec/models/ci/instance_variable_spec.rb
index b879965a261..ff8676e1424 100644
--- a/spec/models/ci/instance_variable_spec.rb
+++ b/spec/models/ci/instance_variable_spec.rb
@@ -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
diff --git a/spec/models/email_spec.rb b/spec/models/email_spec.rb
index dabf2bb80b5..f7b194abcee 100644
--- a/spec/models/email_spec.rb
+++ b/spec/models/email_spec.rb
@@ -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
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 40536f96ff4..1c7e47f8114 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -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) }
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index c1e49d4b0a7..94a3f6bafea 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -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) }
diff --git a/spec/requests/api/graphql/project/alert_management/alerts_spec.rb b/spec/requests/api/graphql/project/alert_management/alerts_spec.rb
index cafa7366411..a751d8ce63a 100644
--- a/spec/requests/api/graphql/project/alert_management/alerts_spec.rb
+++ b/spec/requests/api/graphql/project/alert_management/alerts_spec.rb
@@ -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
diff --git a/spec/services/emails/confirm_service_spec.rb b/spec/services/emails/confirm_service_spec.rb
index 6a274ca9dfe..973d2731b2f 100644
--- a/spec/services/emails/confirm_service_spec.rb
+++ b/spec/services/emails/confirm_service_spec.rb
@@ -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