Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-05-18 09:08:12 +00:00
parent 7e5f8d0881
commit 1b9a2ce278
69 changed files with 1103 additions and 188 deletions

View File

@ -1,3 +1,3 @@
import Vue from 'vue';
import createEventHub from '~/helpers/event_hub_factory';
export default new Vue();
export default createEventHub();

View File

@ -1,3 +1,3 @@
import Vue from 'vue';
import createEventHub from '~/helpers/event_hub_factory';
export default new Vue();
export default createEventHub();

View File

@ -1,3 +1,3 @@
import Vue from 'vue';
import createEventHub from '~/helpers/event_hub_factory';
export default new Vue();
export default createEventHub();

View File

@ -1,3 +1,3 @@
import Vue from 'vue';
import createEventHub from '~/helpers/event_hub_factory';
export default new Vue();
export default createEventHub();

View File

@ -1,3 +1,3 @@
import Vue from 'vue';
import createEventHub from '~/helpers/event_hub_factory';
export default new Vue();
export default createEventHub();

View File

@ -1,3 +1,3 @@
import Vue from 'vue';
import createEventHub from '~/helpers/event_hub_factory';
export default new Vue();
export default createEventHub();

View File

@ -1,3 +1,3 @@
import Vue from 'vue';
import createEventHub from '~/helpers/event_hub_factory';
export default new Vue();
export default createEventHub();

View File

@ -1,3 +1,3 @@
import Vue from 'vue';
import createEventHub from '~/helpers/event_hub_factory';
export default new Vue();
export default createEventHub();

View File

@ -1,3 +1,3 @@
import Vue from 'vue';
import createEventHub from '~/helpers/event_hub_factory';
export default new Vue();
export default createEventHub();

View File

@ -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);

View File

@ -1,3 +1,3 @@
import Vue from 'vue';
import createEventHub from '~/helpers/event_hub_factory';
export default new Vue();
export default createEventHub();

View File

@ -1,3 +1,3 @@
import Vue from 'vue';
import createEventHub from '~/helpers/event_hub_factory';
export default new Vue();
export default createEventHub();

View File

@ -1,3 +1,3 @@
import Vue from 'vue';
import createEventHub from '~/helpers/event_hub_factory';
export default new Vue();
export default createEventHub();

View File

@ -1,3 +1,3 @@
import Vue from 'vue';
import createEventHub from '~/helpers/event_hub_factory';
export default new Vue();
export default createEventHub();

View File

@ -1,3 +1,3 @@
import Vue from 'vue';
import createEventHub from '~/helpers/event_hub_factory';
export default new Vue();
export default createEventHub();

View File

@ -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!`);

View File

@ -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

View File

@ -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)

View File

@ -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) }

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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')

View File

@ -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

View File

@ -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

View File

@ -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')

View File

@ -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: '')

View File

@ -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)

View File

@ -0,0 +1,5 @@
---
title: Integrate CI instance variables in the build process
merge_request: 30186
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Add search to Alert Management Alerts GraphQL query
merge_request: 32047
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Externalize i18n strings from ./app/views/shared/issuable/_label_*
merge_request: 32167
author: Gilang Gumilar
type: changed

View File

@ -0,0 +1,5 @@
---
title: Externalize i18n strings from ./app/views/shared/issuable/_sidebar.html.haml
merge_request: 32164
author: Gilang Gumilar
type: changed

View File

@ -0,0 +1,5 @@
---
title: Externalize i18n strings from ./app/views/shared/milestones/_issuable.html.haml
merge_request: 32161
author: Gilang Gumilar
type: changed

View File

@ -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

View File

@ -0,0 +1,5 @@
---
title: Externalize i18n strings from ./app/views/shared/_group_tips.html.haml
merge_request: 32127
author: Gilang Gumilar
type: changed

View File

@ -0,0 +1,5 @@
---
title: Send Devise emails triggered from the 'Email' model asynchronously
merge_request: 32286
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Migrate from Vue event hub to Mitt
merge_request: 31666
author: Arun Kumar Mohan
type: changed

View File

@ -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
"""

View File

@ -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.",

View File

@ -19,6 +19,22 @@ To enable merge request approval rules for an instance:
GitLab administrators can later override these settings in a projects 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:

View File

@ -874,6 +874,9 @@ msgstr ""
msgid "A fork is a copy of a project.<br />Forking a repository allows you to make changes without affecting the original project."
msgstr ""
msgid "A group is a collection of several projects"
msgstr ""
msgid "A group represents your organization in GitLab."
msgstr ""
@ -2779,6 +2782,9 @@ msgstr ""
msgid "Assigned Merge Requests"
msgstr ""
msgid "Assigned to %{assignee_name}"
msgstr ""
msgid "Assigned to me"
msgstr ""
@ -8828,6 +8834,9 @@ msgstr ""
msgid "Existing members and groups"
msgstr ""
msgid "Existing projects may be moved into a group"
msgstr ""
msgid "Existing projects will be able to use expiration policies. Avoid enabling this if an external Container Registry is being used, as there is a performance risk if many images exist on one project."
msgstr ""
@ -9482,6 +9491,9 @@ msgstr ""
msgid "Filter by issues that are currently closed."
msgstr ""
msgid "Filter by label"
msgstr ""
msgid "Filter by merge requests that are currently closed and unmerged."
msgstr ""
@ -10673,6 +10685,9 @@ msgstr ""
msgid "Group pipeline minutes were successfully reset."
msgstr ""
msgid "Group project URLs are prefixed with the group namespace"
msgstr ""
msgid "Group requires separate account"
msgstr ""
@ -13214,6 +13229,9 @@ msgstr ""
msgid "Members of <strong>%{project_name}</strong>"
msgstr ""
msgid "Members of a group may only view projects they have permission to access"
msgstr ""
msgid "Members with access to %{strong_start}%{group_name}%{strong_end}"
msgstr ""
@ -13677,6 +13695,9 @@ msgstr ""
msgid "Milestones| Youre 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 ""

View File

@ -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

View File

@ -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

View File

@ -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" } }

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,55 @@
# frozen_string_literal: true
require 'spec_helper'
describe 'User views an SVG design that contains XSS', :js do
include DesignManagementTestHelpers
let(:project) { create(:project_empty_repo, :public) }
let(:issue) { create(:issue, project: project) }
let(:file) { Rails.root.join('spec', 'fixtures', 'logo_sample.svg') }
let(:design) { create(:design, :with_file, filename: 'xss.svg', file: file, issue: issue) }
before do
enable_design_management
visit designs_project_issue_path(
project,
issue,
{ vueroute: design.filename }
)
wait_for_requests
end
it 'has XSS within the SVG file' do
file_content = File.read(file)
expect(file_content).to include("<script>alert('FAIL')</script>")
end
it 'displays the SVG' do
expect(page).to have_selector("img.design-img[alt='xss.svg']", count: 1, visible: false)
end
it 'does not execute the JavaScript within the SVG' do
# The expectation is that we can call the capybara `page.dismiss_prompt`
# method to close a JavaScript alert prompt without a `Capybara::ModalNotFound`
# being raised.
run_expectation = -> {
page.dismiss_prompt(wait: 1)
}
# With the page loaded, there should be no alert modal
expect(run_expectation).to raise_error(
Capybara::ModalNotFound,
'Unable to find modal dialog'
)
# Perform a negative control test of the above expectation.
# With an alert modal displaying, the modal should be dismissable.
execute_script('alert(true)')
expect(run_expectation).not_to raise_error
end
end

View File

@ -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

View File

@ -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 = {

View File

@ -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`

View File

@ -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',

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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) }

View File

@ -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) }

View File

@ -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

View File

@ -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