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