Commit 48166eb0 authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents 10d28998 b829ab9f
...@@ -32,7 +32,6 @@ Rails/SaveBang: ...@@ -32,7 +32,6 @@ Rails/SaveBang:
- 'ee/spec/models/approval_project_rule_spec.rb' - 'ee/spec/models/approval_project_rule_spec.rb'
- 'ee/spec/models/burndown_spec.rb' - 'ee/spec/models/burndown_spec.rb'
- 'ee/spec/models/elasticsearch_indexed_namespace_spec.rb' - 'ee/spec/models/elasticsearch_indexed_namespace_spec.rb'
- 'ee/spec/models/epic_spec.rb'
- 'ee/spec/models/gitlab_subscription_spec.rb' - 'ee/spec/models/gitlab_subscription_spec.rb'
- 'ee/spec/models/issue_spec.rb' - 'ee/spec/models/issue_spec.rb'
- 'ee/spec/models/label_note_spec.rb' - 'ee/spec/models/label_note_spec.rb'
......
...@@ -105,8 +105,7 @@ class Projects::BranchesController < Projects::ApplicationController ...@@ -105,8 +105,7 @@ class Projects::BranchesController < Projects::ApplicationController
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
def destroy def destroy
@branch_name = Addressable::URI.unescape(params[:id]) result = ::Branches::DeleteService.new(project, current_user).execute(params[:id])
result = ::Branches::DeleteService.new(project, current_user).execute(@branch_name)
respond_to do |format| respond_to do |format|
format.html do format.html do
......
...@@ -10,7 +10,14 @@ module LearnGitlabHelper ...@@ -10,7 +10,14 @@ module LearnGitlabHelper
def onboarding_actions_data(project) def onboarding_actions_data(project)
attributes = onboarding_progress(project).attributes.symbolize_keys attributes = onboarding_progress(project).attributes.symbolize_keys
action_urls.to_h do |action, url| urls_to_use = nil
experiment(:change_continuous_onboarding_link_urls) do |e|
e.use { urls_to_use = action_urls }
e.try { urls_to_use = new_action_urls(project) }
end
urls_to_use.to_h do |action, url|
[ [
action, action,
url: url, url: url,
...@@ -46,6 +53,17 @@ module LearnGitlabHelper ...@@ -46,6 +53,17 @@ module LearnGitlabHelper
.merge(LearnGitlab::Onboarding::ACTION_DOC_URLS) .merge(LearnGitlab::Onboarding::ACTION_DOC_URLS)
end end
def new_action_urls(project)
action_urls.merge(
issue_created: project_issues_path(project),
git_write: project_path(project),
pipeline_created: project_pipelines_path(project),
merge_request_created: project_merge_requests_path(project),
user_added: project_members_url(project),
security_scan_enabled: project_security_configuration_path(project)
)
end
def learn_gitlab_project def learn_gitlab_project
@learn_gitlab_project ||= LearnGitlab::Project.new(current_user).project @learn_gitlab_project ||= LearnGitlab::Project.new(current_user).project
end end
...@@ -54,3 +72,5 @@ module LearnGitlabHelper ...@@ -54,3 +72,5 @@ module LearnGitlabHelper
OnboardingProgress.find_by(namespace: project.namespace) # rubocop: disable CodeReuse/ActiveRecord OnboardingProgress.find_by(namespace: project.namespace) # rubocop: disable CodeReuse/ActiveRecord
end end
end end
LearnGitlabHelper.prepend_mod_with('LearnGitlabHelper')
...@@ -23,6 +23,7 @@ module Ci ...@@ -23,6 +23,7 @@ module Ci
serialize :config_options, Serializers::SymbolizedJson # rubocop:disable Cop/ActiveRecordSerialize serialize :config_options, Serializers::SymbolizedJson # rubocop:disable Cop/ActiveRecordSerialize
serialize :config_variables, Serializers::SymbolizedJson # rubocop:disable Cop/ActiveRecordSerialize serialize :config_variables, Serializers::SymbolizedJson # rubocop:disable Cop/ActiveRecordSerialize
serialize :runtime_runner_features, Serializers::SymbolizedJson # rubocop:disable Cop/ActiveRecordSerialize
chronic_duration_attr_reader :timeout_human_readable, :timeout chronic_duration_attr_reader :timeout_human_readable, :timeout
...@@ -47,6 +48,14 @@ module Ci ...@@ -47,6 +48,14 @@ module Ci
update(timeout: timeout.value, timeout_source: timeout.source) update(timeout: timeout.value, timeout_source: timeout.source)
end end
def set_cancel_gracefully
runtime_runner_features.merge!( { cancel_gracefully: true } )
end
def cancel_gracefully?
runtime_runner_features[:cancel_gracefully] == true
end
private private
def set_build_project def set_build_project
......
...@@ -20,6 +20,8 @@ module Ci ...@@ -20,6 +20,8 @@ module Ci
delegate :interruptible, to: :metadata, prefix: false, allow_nil: true delegate :interruptible, to: :metadata, prefix: false, allow_nil: true
delegate :has_exposed_artifacts?, to: :metadata, prefix: false, allow_nil: true delegate :has_exposed_artifacts?, to: :metadata, prefix: false, allow_nil: true
delegate :environment_auto_stop_in, to: :metadata, prefix: false, allow_nil: true delegate :environment_auto_stop_in, to: :metadata, prefix: false, allow_nil: true
delegate :set_cancel_gracefully, to: :metadata, prefix: false, allow_nil: false
delegate :cancel_gracefully?, to: :metadata, prefix: false, allow_nil: false
before_create :ensure_metadata before_create :ensure_metadata
end end
......
...@@ -15,13 +15,21 @@ module Uploads ...@@ -15,13 +15,21 @@ module Uploads
end end
def delete_keys(keys) def delete_keys(keys)
keys.each do |key| keys.each { |key| delete_object(key) }
connection.delete_object(bucket_name, key)
end
end end
private private
def delete_object(key)
connection.delete_object(bucket_name, key)
# So far, only GoogleCloudStorage raises an exception when the file is not found.
# Other providers support idempotent requests and does not raise an error
# when the file is missing.
rescue ::Google::Apis::ClientError => e
Gitlab::ErrorTracking.log_exception(e)
end
def object_store def object_store
Gitlab.config.uploads.object_store Gitlab.config.uploads.object_store
end end
......
---
name: change_continuous_onboarding_link_urls
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/71408
rollout_issue_url:
milestone: '14.5'
type: experiment
group: group::conversion
default_enabled: false
...@@ -27,6 +27,7 @@ ...@@ -27,6 +27,7 @@
# include_descendant_groups: boolean # include_descendant_groups: boolean
# starts_with_iid: string (containing a number) # starts_with_iid: string (containing a number)
# confidential: boolean # confidential: boolean
# hierarchy_order: :desc or :acs, default :acs when searched by child_id
class EpicsFinder < IssuableFinder class EpicsFinder < IssuableFinder
include TimeFrameFilter include TimeFrameFilter
...@@ -198,8 +199,10 @@ class EpicsFinder < IssuableFinder ...@@ -198,8 +199,10 @@ class EpicsFinder < IssuableFinder
def by_child(items) def by_child(items)
return items unless child_id? return items unless child_id?
ancestor_ids = Epic.find(params[:child_id]).ancestors.reselect(:id) hierarchy_order = params[:hierarchy_order] || :asc
items.where(id: ancestor_ids)
ancestors = Epic.find(params[:child_id]).ancestors(hierarchy_order: hierarchy_order)
ancestors.where(id: items.select(:id))
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
...@@ -272,4 +275,11 @@ class EpicsFinder < IssuableFinder ...@@ -272,4 +275,11 @@ class EpicsFinder < IssuableFinder
def feature_flag_scope def feature_flag_scope
params.group params.group
end end
override :sort
def sort(items)
return items if params[:hierarchy_order]
super
end
end end
...@@ -9,12 +9,18 @@ module Resolvers ...@@ -9,12 +9,18 @@ module Resolvers
description: 'Include epics from ancestor groups.', description: 'Include epics from ancestor groups.',
default_value: true default_value: true
def resolve_with_lookahead(**args)
items = super
offset_pagination(items)
end
private private
def relative_param def relative_param
return {} unless parent return {} unless parent
{ child_id: parent.id } { child_id: parent.id, hierarchy_order: :desc }
end end
end end
end end
# frozen_string_literal: true
module EE
module LearnGitlabHelper
extend ::Gitlab::Utils::Override
GITLAB_COM = 'gitlab.com'
ONBOARDING_START_TRIAL = 'onboarding-start-trial'
ONBOARDING_REQUIRE_MR_APPROVALS = 'onboarding-require-merge-approvals'
ONBOARDING_CODE_OWNERS = 'onboarding-code-owners'
private
override :new_action_urls
def new_action_urls(project)
urls = super(project)
return urls unless ::Gitlab::CurrentSettings.should_check_namespace_plan?
glm_params = { glm_source: GITLAB_COM }
urls.merge(
trial_started: new_trial_path(glm_params.merge(glm_content: ONBOARDING_START_TRIAL)),
required_mr_approvals_enabled: new_trial_path(glm_params.merge(glm_content: ONBOARDING_REQUIRE_MR_APPROVALS)),
code_owners_enabled: new_trial_path(glm_params.merge(glm_content: ONBOARDING_CODE_OWNERS))
)
end
end
end
...@@ -425,10 +425,10 @@ module EE ...@@ -425,10 +425,10 @@ module EE
end end
end end
def ancestors def ancestors(hierarchy_order: :asc)
return self.class.none unless parent_id return self.class.none unless parent_id
hierarchy.ancestors(hierarchy_order: :asc) hierarchy.ancestors(hierarchy_order: hierarchy_order)
end end
def max_hierarchy_depth_achieved? def max_hierarchy_depth_achieved?
......
...@@ -9,8 +9,9 @@ RSpec.describe EpicsFinder do ...@@ -9,8 +9,9 @@ RSpec.describe EpicsFinder do
let_it_be(:another_group) { create(:group) } let_it_be(:another_group) { create(:group) }
let_it_be(:reference_time) { Time.parse('2020-09-15 01:00') } # Arbitrary time used for time/date range filters let_it_be(:reference_time) { Time.parse('2020-09-15 01:00') } # Arbitrary time used for time/date range filters
let_it_be(:epic1) { create(:epic, :opened, group: group, title: 'This is awesome epic', created_at: 1.week.before(reference_time), end_date: 10.days.before(reference_time)) } let_it_be(:epic1) { create(:epic, :opened, group: group, title: 'This is awesome epic', created_at: 1.week.before(reference_time), end_date: 10.days.before(reference_time)) }
let_it_be(:epic2) { create(:epic, :opened, group: group, created_at: 4.days.before(reference_time), author: user, start_date: 2.days.before(reference_time), end_date: 3.days.since(reference_time), parent: epic1) } let_it_be(:epic3, reload: true) { create(:epic, :closed, group: group, description: 'not so awesome', start_date: 5.days.before(reference_time), end_date: 3.days.before(reference_time)) }
let_it_be(:epic3) { create(:epic, :closed, group: group, description: 'not so awesome', start_date: 5.days.before(reference_time), end_date: 3.days.before(reference_time), parent: epic2) } let_it_be(:epic2, reload: true) { create(:epic, :opened, group: group, created_at: 4.days.before(reference_time), author: user, start_date: 2.days.before(reference_time), end_date: 3.days.since(reference_time)) }
let_it_be(:epic5) { create(:epic, group: group, start_date: 6.days.before(reference_time), end_date: 6.days.before(reference_time), parent: epic3) }
let_it_be(:epic4) { create(:epic, :closed, group: another_group) } let_it_be(:epic4) { create(:epic, :closed, group: another_group) }
describe '#execute' do describe '#execute' do
...@@ -55,7 +56,7 @@ RSpec.describe EpicsFinder do ...@@ -55,7 +56,7 @@ RSpec.describe EpicsFinder do
end end
it 'returns all epics that belong to the given group' do it 'returns all epics that belong to the given group' do
expect(epics).to contain_exactly(epic1, epic2, epic3) expect(epics).to contain_exactly(epic1, epic2, epic3, epic5)
end end
it 'does not execute more than 5 SQL queries' do it 'does not execute more than 5 SQL queries' do
...@@ -64,11 +65,11 @@ RSpec.describe EpicsFinder do ...@@ -64,11 +65,11 @@ RSpec.describe EpicsFinder do
context 'sorting' do context 'sorting' do
it 'sorts correctly when supported sorting param provided' do it 'sorts correctly when supported sorting param provided' do
expect(epics(sort: :start_date_asc)).to eq([epic3, epic2, epic1]) expect(epics(sort: :start_date_asc)).to eq([epic5, epic3, epic2, epic1])
end end
it 'sorts by id when not supported sorting param provided' do it 'sorts by id when not supported sorting param provided' do
expect(epics(sort: :not_supported_param)).to eq([epic3, epic2, epic1]) expect(epics(sort: :not_supported_param)).to eq([epic5, epic2, epic3, epic1])
end end
end end
...@@ -78,7 +79,7 @@ RSpec.describe EpicsFinder do ...@@ -78,7 +79,7 @@ RSpec.describe EpicsFinder do
end end
it 'returns all epics created after the given date' do it 'returns all epics created after the given date' do
expect(epics(created_after: 2.days.before(reference_time))).to contain_exactly(epic3) expect(epics(created_after: 2.days.before(reference_time))).to contain_exactly(epic3, epic5)
end end
it 'returns all epics created within the given interval' do it 'returns all epics created within the given interval' do
...@@ -146,7 +147,7 @@ RSpec.describe EpicsFinder do ...@@ -146,7 +147,7 @@ RSpec.describe EpicsFinder do
end end
it 'does not add any filter' do it 'does not add any filter' do
expect(epics(or: { author_username: [epic2.author.username, epic3.author.username] })).to contain_exactly(epic1, epic2, epic3) expect(epics(or: { author_username: [epic2.author.username, epic3.author.username] })).to contain_exactly(epic1, epic2, epic3, epic5)
end end
end end
end end
...@@ -195,7 +196,7 @@ RSpec.describe EpicsFinder do ...@@ -195,7 +196,7 @@ RSpec.describe EpicsFinder do
end end
it 'returns all epics that belong to the given group and its subgroups' do it 'returns all epics that belong to the given group and its subgroups' do
expect(epics).to contain_exactly(epic1, epic2, epic3, subgroup_epic, subgroup2_epic) expect(epics).to contain_exactly(epic1, epic2, epic3, subgroup_epic, subgroup2_epic, epic5)
end end
describe 'hierarchy params' do describe 'hierarchy params' do
...@@ -223,7 +224,7 @@ RSpec.describe EpicsFinder do ...@@ -223,7 +224,7 @@ RSpec.describe EpicsFinder do
subject { finder.execute } subject { finder.execute }
it 'gets only epics from the project ancestor groups' do it 'gets only epics from the project ancestor groups' do
is_expected.to contain_exactly(epic1, epic2, epic3, subgroup3_epic) is_expected.to contain_exactly(epic1, epic2, epic3, subgroup3_epic, epic5)
end end
end end
...@@ -237,7 +238,7 @@ RSpec.describe EpicsFinder do ...@@ -237,7 +238,7 @@ RSpec.describe EpicsFinder do
context 'and include_ancestor_groups is true' do context 'and include_ancestor_groups is true' do
let(:finder_params) { { include_descendant_groups: false, include_ancestor_groups: true } } let(:finder_params) { { include_descendant_groups: false, include_ancestor_groups: true } }
it { is_expected.to contain_exactly(subgroup_epic, epic1, epic2, epic3) } it { is_expected.to contain_exactly(subgroup_epic, epic1, epic2, epic3, epic5) }
context "when user does not have permission to view ancestor groups" do context "when user does not have permission to view ancestor groups" do
let(:finder_params) { { group_id: subgroup.id, include_descendant_groups: false, include_ancestor_groups: true } } let(:finder_params) { { group_id: subgroup.id, include_descendant_groups: false, include_ancestor_groups: true } }
...@@ -259,7 +260,7 @@ RSpec.describe EpicsFinder do ...@@ -259,7 +260,7 @@ RSpec.describe EpicsFinder do
context 'and include_ancestor_groups is true' do context 'and include_ancestor_groups is true' do
let(:finder_params) { { include_ancestor_groups: true } } let(:finder_params) { { include_ancestor_groups: true } }
it { is_expected.to contain_exactly(subgroup_epic, subgroup2_epic, epic1, epic2, epic3) } it { is_expected.to contain_exactly(subgroup_epic, subgroup2_epic, epic1, epic2, epic3, epic5) }
context "when user does not have permission to view ancestor groups" do context "when user does not have permission to view ancestor groups" do
let(:finder_params) { { group_id: subgroup.id, include_ancestor_groups: true } } let(:finder_params) { { group_id: subgroup.id, include_ancestor_groups: true } }
...@@ -344,6 +345,11 @@ RSpec.describe EpicsFinder do ...@@ -344,6 +345,11 @@ RSpec.describe EpicsFinder do
end end
context 'by parent' do context 'by parent' do
before do
epic3.update!(parent_id: epic2.id)
epic2.update!(parent_id: epic1.id)
end
it 'returns direct children of the parent' do it 'returns direct children of the parent' do
params = { parent_id: epic1.id } params = { parent_id: epic1.id }
...@@ -352,10 +358,21 @@ RSpec.describe EpicsFinder do ...@@ -352,10 +358,21 @@ RSpec.describe EpicsFinder do
end end
context 'by child' do context 'by child' do
it 'returns ancestors of the child epic' do before do
params = { child_id: epic3.id } epic3.update!(parent_id: epic2.id)
epic2.update!(parent_id: epic1.id)
end
expect(epics(params)).to contain_exactly(epic1, epic2) it 'returns ancestors of the child epic ordered from the bottom' do
params = { child_id: epic5.id, hierarchy_order: :asc }
expect(epics(params)).to eq([epic3, epic2, epic1])
end
it 'returns ancestors of the child epic ordered from the top if requested' do
params = { child_id: epic5.id, hierarchy_order: :desc }
expect(epics(params)).to eq([epic1, epic2, epic3])
end end
end end
...@@ -741,11 +758,11 @@ RSpec.describe EpicsFinder do ...@@ -741,11 +758,11 @@ RSpec.describe EpicsFinder do
let_it_be(:params) { { not: { label_name: [label.title, label2.title].join(',') } } } let_it_be(:params) { { not: { label_name: [label.title, label2.title].join(',') } } }
it 'returns all epics if no negated labels are present' do it 'returns all epics if no negated labels are present' do
expect(epics).to contain_exactly(negated_epic, negated_epic2, epic1, epic2, epic3) expect(epics).to contain_exactly(negated_epic, negated_epic2, epic1, epic2, epic3, epic5)
end end
it 'returns all epics without negated label' do it 'returns all epics without negated label' do
expect(epics(params)).to contain_exactly(epic1, epic2, epic3) expect(epics(params)).to contain_exactly(epic1, epic2, epic3, epic5)
end end
end end
...@@ -755,11 +772,11 @@ RSpec.describe EpicsFinder do ...@@ -755,11 +772,11 @@ RSpec.describe EpicsFinder do
let_it_be(:params) { { not: { author_id: author.id } } } let_it_be(:params) { { not: { author_id: author.id } } }
it 'returns all epics if no negated author is present' do it 'returns all epics if no negated author is present' do
expect(epics).to contain_exactly(authored_epic, epic1, epic2, epic3) expect(epics).to contain_exactly(authored_epic, epic1, epic2, epic3, epic5)
end end
it 'returns all epics without given author' do it 'returns all epics without given author' do
expect(epics(params)).to contain_exactly(epic1, epic2, epic3) expect(epics(params)).to contain_exactly(epic1, epic2, epic3, epic5)
end end
end end
...@@ -768,7 +785,7 @@ RSpec.describe EpicsFinder do ...@@ -768,7 +785,7 @@ RSpec.describe EpicsFinder do
let_it_be(:params) { { not: { my_reaction_emoji: awarded_emoji.name } } } let_it_be(:params) { { not: { my_reaction_emoji: awarded_emoji.name } } }
it 'returns all epics without given emoji name' do it 'returns all epics without given emoji name' do
expect(epics(params)).to contain_exactly(epic1, epic2) expect(epics(params)).to contain_exactly(epic1, epic2, epic5)
end end
end end
end end
...@@ -820,7 +837,7 @@ RSpec.describe EpicsFinder do ...@@ -820,7 +837,7 @@ RSpec.describe EpicsFinder do
it 'returns correct counts' do it 'returns correct counts' do
results = described_class.new(search_user, group_id: group.id).count_by_state results = described_class.new(search_user, group_id: group.id).count_by_state
expect(results).to eq('opened' => 2, 'closed' => 1, 'all' => 3) expect(results).to eq('opened' => 3, 'closed' => 1, 'all' => 4)
end end
it 'returns -1 if the query times out' do it 'returns -1 if the query times out' do
......
...@@ -54,8 +54,8 @@ RSpec.describe Resolvers::EpicAncestorsResolver do ...@@ -54,8 +54,8 @@ RSpec.describe Resolvers::EpicAncestorsResolver do
sub_group.add_developer(current_user) sub_group.add_developer(current_user)
end end
it 'returns all ancestors' do it 'returns all ancestors in the correct order' do
expect(resolve_ancestors(epic4, args)).to contain_exactly(epic1, epic2, epic3) expect(resolve_ancestors(epic4, args)).to eq([epic1, epic2, epic3])
end end
it 'does not return parent group epics when include_ancestor_groups is false' do it 'does not return parent group epics when include_ancestor_groups is false' do
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe LearnGitlabHelper do
include Devise::Test::ControllerHelpers
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, name: LearnGitlab::Project::PROJECT_NAME, namespace: user.namespace) }
let_it_be(:namespace) { project.namespace }
before do
allow_next_instance_of(LearnGitlab::Project) do |learn_gitlab|
allow(learn_gitlab).to receive(:project).and_return(project)
end
OnboardingProgress.onboard(namespace)
end
describe '#onboarding_actions_data' do
subject(:onboarding_actions_data) { helper.onboarding_actions_data(project) }
context 'when in the new action URLs experiment' do
before do
stub_experiments(change_continuous_onboarding_link_urls: :candidate)
end
context 'for trial- and subscription-related actions' do
context 'when namespace plans are not enabled' do
before do
stub_application_setting(check_namespace_plan: false)
end
it 'provides the default URLs' do
expect(onboarding_actions_data).to include(
trial_started: a_hash_including(
url: a_string_matching(%r{#{namespace.path}/learn_gitlab/-/issues/2})
),
code_owners_enabled: a_hash_including(
url: a_string_matching(%r{#{namespace.path}/learn_gitlab/-/issues/10})
),
required_mr_approvals_enabled: a_hash_including(
url: a_string_matching(%r{#{namespace.path}/learn_gitlab/-/issues/11})
)
)
end
end
context 'when namespace plans are enabled' do
before do
stub_application_setting(check_namespace_plan: true)
end
it 'provides URLs to start a trial for the appropariate actions' do
expect(onboarding_actions_data).to include(
trial_started: a_hash_including(
url: new_trial_path(glm_source: 'gitlab.com', glm_content: 'onboarding-start-trial')
),
code_owners_enabled: a_hash_including(
url: new_trial_path(glm_source: 'gitlab.com', glm_content: 'onboarding-code-owners')
),
required_mr_approvals_enabled: a_hash_including(
url: new_trial_path(glm_source: 'gitlab.com', glm_content: 'onboarding-require-merge-approvals')
)
)
end
end
end
end
end
end
...@@ -361,13 +361,22 @@ RSpec.describe Epic do ...@@ -361,13 +361,22 @@ RSpec.describe Epic do
end end
context 'hierarchy' do context 'hierarchy' do
let(:epic1) { create(:epic, group: group) } let_it_be(:epic2, reload: true) { create(:epic, group: group) }
let(:epic2) { create(:epic, group: group, parent: epic1) } let_it_be(:epic3) { create(:epic, group: group, parent: epic2) }
let(:epic3) { create(:epic, group: group, parent: epic2) } let_it_be(:epic4) { create(:epic, group: group, parent: epic3) }
let_it_be(:epic1) { create(:epic, group: group) }
before do
epic2.update!(parent_id: epic1.id)
end
describe '#ancestors' do describe '#ancestors' do
it 'returns all ancestors for an epic' do it 'returns all ancestors for an epic ordered correctly' do
expect(epic3.ancestors).to eq [epic2, epic1] expect(epic4.ancestors).to eq([epic3, epic2, epic1])
end
it 'returns all ancestors for an epic ordered correctly with the hierarchy_order param' do
expect(epic4.ancestors(hierarchy_order: :desc)).to eq([epic1, epic2, epic3])
end end
it 'returns an empty array if an epic does not have any parent' do it 'returns an empty array if an epic does not have any parent' do
...@@ -377,11 +386,11 @@ RSpec.describe Epic do ...@@ -377,11 +386,11 @@ RSpec.describe Epic do
describe '#descendants' do describe '#descendants' do
it 'returns all descendants for an epic' do it 'returns all descendants for an epic' do
expect(epic1.descendants).to match_array([epic2, epic3]) expect(epic1.descendants).to match_array([epic2, epic3, epic4])
end end
it 'returns an empty array if an epic does not have any descendants' do it 'returns an empty array if an epic does not have any descendants' do
expect(epic3.descendants).to be_empty expect(epic4.descendants).to be_empty
end end
end end
end end
...@@ -705,8 +714,7 @@ RSpec.describe Epic do ...@@ -705,8 +714,7 @@ RSpec.describe Epic do
end end
before do before do
epic.description = ref_text epic.update!(description: ref_text)
epic.save
end end
it 'creates new system notes for cross references' do it 'creates new system notes for cross references' do
......
...@@ -106,6 +106,21 @@ RSpec.describe 'getting epics information' do ...@@ -106,6 +106,21 @@ RSpec.describe 'getting epics information' do
end end
end end
context 'query for epics with ancestors' do
let_it_be(:parent_epic) { create(:epic, group: group) }
let_it_be(:epic) { create(:epic, group: group, parent: parent_epic) }
it 'returns the ancestors' do
query_epic_with_ancestors(epic.iid)
ancestors = graphql_data['group']['epic']['ancestors']['nodes']
expect(ancestors.count).to eq(1)
expect(ancestors.first['id']).to eq(parent_epic.to_global_id.to_s)
expect(graphql_errors).to be_nil
end
end
describe 'N+1 query checks' do describe 'N+1 query checks' do
let_it_be(:epic_a) { create(:epic, group: group) } let_it_be(:epic_a) { create(:epic, group: group) }
let_it_be(:epic_b) { create(:epic, group: group) } let_it_be(:epic_b) { create(:epic, group: group) }
...@@ -157,6 +172,24 @@ RSpec.describe 'getting epics information' do ...@@ -157,6 +172,24 @@ RSpec.describe 'getting epics information' do
) )
end end
def query_epic_with_ancestors(epic_iid)
epics_field = <<~NODE
epic(iid: #{epic_iid}) {
id
ancestors {
nodes {
id
}
}
}
NODE
post_graphql(
graphql_query_for('group', { 'fullPath' => group.full_path }, epics_field),
current_user: user
)
end
def epics_query(group, field, value) def epics_query(group, field, value)
epics_query_by_hash(group, field => value) epics_query_by_hash(group, field => value)
end end
......
...@@ -356,7 +356,7 @@ RSpec.describe Projects::BranchesController do ...@@ -356,7 +356,7 @@ RSpec.describe Projects::BranchesController do
context "valid branch name with encoded slashes" do context "valid branch name with encoded slashes" do
let(:branch) { "improve%2Fawesome" } let(:branch) { "improve%2Fawesome" }
it { expect(response).to have_gitlab_http_status(:ok) } it { expect(response).to have_gitlab_http_status(:not_found) }
it { expect(response.body).to be_blank } it { expect(response.body).to be_blank }
end end
...@@ -396,10 +396,10 @@ RSpec.describe Projects::BranchesController do ...@@ -396,10 +396,10 @@ RSpec.describe Projects::BranchesController do
let(:branch) { 'improve%2Fawesome' } let(:branch) { 'improve%2Fawesome' }
it 'returns JSON response with message' do it 'returns JSON response with message' do
expect(json_response).to eql('message' => 'Branch was deleted') expect(json_response).to eql('message' => 'No such branch')
end end
it { expect(response).to have_gitlab_http_status(:ok) } it { expect(response).to have_gitlab_http_status(:not_found) }
end end
context 'invalid branch name, valid ref' do context 'invalid branch name, valid ref' do
......
...@@ -11,9 +11,6 @@ RSpec.describe LearnGitlabHelper do ...@@ -11,9 +11,6 @@ RSpec.describe LearnGitlabHelper do
let_it_be(:namespace) { project.namespace } let_it_be(:namespace) { project.namespace }
before do before do
project.add_developer(user)
allow(helper).to receive(:user).and_return(user)
allow_next_instance_of(LearnGitlab::Project) do |learn_gitlab| allow_next_instance_of(LearnGitlab::Project) do |learn_gitlab|
allow(learn_gitlab).to receive(:project).and_return(project) allow(learn_gitlab).to receive(:project).and_return(project)
end end
...@@ -22,38 +19,115 @@ RSpec.describe LearnGitlabHelper do ...@@ -22,38 +19,115 @@ RSpec.describe LearnGitlabHelper do
OnboardingProgress.register(namespace, :git_write) OnboardingProgress.register(namespace, :git_write)
end end
describe '.onboarding_actions_data' do describe '#onboarding_actions_data' do
subject(:onboarding_actions_data) { helper.onboarding_actions_data(project) } subject(:onboarding_actions_data) { helper.onboarding_actions_data(project) }
it 'has all actions' do shared_examples 'has all actions' do
expect(onboarding_actions_data.keys).to contain_exactly( it 'has all actions' do
:issue_created, expect(onboarding_actions_data.keys).to contain_exactly(
:git_write, :issue_created,
:pipeline_created, :git_write,
:merge_request_created, :pipeline_created,
:user_added, :merge_request_created,
:trial_started, :user_added,
:required_mr_approvals_enabled, :trial_started,
:code_owners_enabled, :required_mr_approvals_enabled,
:security_scan_enabled :code_owners_enabled,
) :security_scan_enabled
)
end
end end
it 'sets correct path and completion status' do it_behaves_like 'has all actions'
expect(onboarding_actions_data[:git_write]).to eq({
url: project_issue_url(project, LearnGitlab::Onboarding::ACTION_ISSUE_IDS[:git_write]), it 'sets correct paths' do
completed: true, expect(onboarding_actions_data).to match({
svg: helper.image_path("learn_gitlab/git_write.svg") trial_started: a_hash_including(
url: a_string_matching(%r{/learn_gitlab/-/issues/2\z})
),
issue_created: a_hash_including(
url: a_string_matching(%r{/learn_gitlab/-/issues/4\z})
),
git_write: a_hash_including(
url: a_string_matching(%r{/learn_gitlab/-/issues/6\z})
),
pipeline_created: a_hash_including(
url: a_string_matching(%r{/learn_gitlab/-/issues/7\z})
),
user_added: a_hash_including(
url: a_string_matching(%r{/learn_gitlab/-/issues/8\z})
),
merge_request_created: a_hash_including(
url: a_string_matching(%r{/learn_gitlab/-/issues/9\z})
),
code_owners_enabled: a_hash_including(
url: a_string_matching(%r{/learn_gitlab/-/issues/10\z})
),
required_mr_approvals_enabled: a_hash_including(
url: a_string_matching(%r{/learn_gitlab/-/issues/11\z})
),
security_scan_enabled: a_hash_including(
url: a_string_matching(%r{docs\.gitlab\.com/ee/user/application_security/security_dashboard/#gitlab-security-dashboard-security-center-and-vulnerability-reports\z})
)
}) })
expect(onboarding_actions_data[:pipeline_created]).to eq({ end
url: project_issue_url(project, LearnGitlab::Onboarding::ACTION_ISSUE_IDS[:pipeline_created]),
completed: false, it 'sets correct completion statuses' do
svg: helper.image_path("learn_gitlab/pipeline_created.svg") expect(onboarding_actions_data).to match({
issue_created: a_hash_including(completed: false),
git_write: a_hash_including(completed: true),
pipeline_created: a_hash_including(completed: false),
merge_request_created: a_hash_including(completed: false),
user_added: a_hash_including(completed: false),
trial_started: a_hash_including(completed: false),
required_mr_approvals_enabled: a_hash_including(completed: false),
code_owners_enabled: a_hash_including(completed: false),
security_scan_enabled: a_hash_including(completed: false)
}) })
end end
context 'when in the new action URLs experiment' do
before do
stub_experiments(change_continuous_onboarding_link_urls: :candidate)
end
it_behaves_like 'has all actions'
it 'sets mostly new paths' do
expect(onboarding_actions_data).to match({
trial_started: a_hash_including(
url: a_string_matching(%r{/learn_gitlab/-/issues/2\z})
),
issue_created: a_hash_including(
url: a_string_matching(%r{/learn_gitlab/-/issues\z})
),
git_write: a_hash_including(
url: a_string_matching(%r{/learn_gitlab\z})
),
pipeline_created: a_hash_including(
url: a_string_matching(%r{/learn_gitlab/-/pipelines\z})
),
user_added: a_hash_including(
url: a_string_matching(%r{/learn_gitlab/-/project_members\z})
),
merge_request_created: a_hash_including(
url: a_string_matching(%r{/learn_gitlab/-/merge_requests\z})
),
code_owners_enabled: a_hash_including(
url: a_string_matching(%r{/learn_gitlab/-/issues/10\z})
),
required_mr_approvals_enabled: a_hash_including(
url: a_string_matching(%r{/learn_gitlab/-/issues/11\z})
),
security_scan_enabled: a_hash_including(
url: a_string_matching(%r{/learn_gitlab/-/security/configuration\z})
)
})
end
end
end end
describe '.learn_gitlab_enabled?' do describe '#learn_gitlab_enabled?' do
using RSpec::Parameterized::TableSyntax using RSpec::Parameterized::TableSyntax
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
...@@ -89,7 +163,7 @@ RSpec.describe LearnGitlabHelper do ...@@ -89,7 +163,7 @@ RSpec.describe LearnGitlabHelper do
end end
end end
describe '.onboarding_sections_data' do describe '#onboarding_sections_data' do
subject(:sections) { helper.onboarding_sections_data } subject(:sections) { helper.onboarding_sections_data }
it 'has the right keys' do it 'has the right keys' do
......
...@@ -121,4 +121,16 @@ RSpec.describe Ci::BuildMetadata do ...@@ -121,4 +121,16 @@ RSpec.describe Ci::BuildMetadata do
end end
end end
end end
describe 'set_cancel_gracefully' do
it 'sets cancel_gracefully' do
build.set_cancel_gracefully
expect(build.cancel_gracefully?).to be true
end
it 'returns false' do
expect(build.cancel_gracefully?).to be false
end
end
end end
...@@ -35,6 +35,8 @@ RSpec.describe Ci::Build do ...@@ -35,6 +35,8 @@ RSpec.describe Ci::Build do
it { is_expected.to respond_to(:has_trace?) } it { is_expected.to respond_to(:has_trace?) }
it { is_expected.to respond_to(:trace) } it { is_expected.to respond_to(:trace) }
it { is_expected.to respond_to(:set_cancel_gracefully) }
it { is_expected.to respond_to(:cancel_gracefully?) }
it { is_expected.to delegate_method(:merge_request?).to(:pipeline) } it { is_expected.to delegate_method(:merge_request?).to(:pipeline) }
it { is_expected.to delegate_method(:merge_request_ref?).to(:pipeline) } it { is_expected.to delegate_method(:merge_request_ref?).to(:pipeline) }
...@@ -5386,4 +5388,23 @@ RSpec.describe Ci::Build do ...@@ -5386,4 +5388,23 @@ RSpec.describe Ci::Build do
create(:ci_build) create(:ci_build)
end end
end end
describe '#runner_features' do
subject do
build.save!
build.cancel_gracefully?
end
let_it_be(:build) { create(:ci_build, pipeline: pipeline) }
it 'cannot cancel gracefully' do
expect(subject).to be false
end
it 'can cancel gracefully' do
build.set_cancel_gracefully
expect(subject).to be true
end
end
end end
...@@ -40,7 +40,9 @@ RSpec.describe Uploads::Fog do ...@@ -40,7 +40,9 @@ RSpec.describe Uploads::Fog do
end end
describe '#delete_keys' do describe '#delete_keys' do
let(:connection) { ::Fog::Storage.new(FileUploader.object_store_credentials) }
let(:keys) { data_store.keys(relation) } let(:keys) { data_store.keys(relation) }
let(:paths) { relation.pluck(:path) }
let!(:uploads) { create_list(:upload, 2, :with_file, :issuable_upload, model: project) } let!(:uploads) { create_list(:upload, 2, :with_file, :issuable_upload, model: project) }
subject { data_store.delete_keys(keys) } subject { data_store.delete_keys(keys) }
...@@ -50,17 +52,32 @@ RSpec.describe Uploads::Fog do ...@@ -50,17 +52,32 @@ RSpec.describe Uploads::Fog do
end end
it 'deletes multiple data' do it 'deletes multiple data' do
paths = relation.pluck(:path) paths.each do |path|
expect(connection.get_object('uploads', path)[:body]).not_to be_nil
end
subject
paths.each do |path|
expect { connection.get_object('uploads', path)[:body] }.to raise_error(Excon::Error::NotFound)
end
end
::Fog::Storage.new(FileUploader.object_store_credentials).tap do |connection| context 'when one of keys is missing' do
let(:keys) { ['unknown'] + super() }
it 'deletes only existing keys' do
paths.each do |path| paths.each do |path|
expect(connection.get_object('uploads', path)[:body]).not_to be_nil expect(connection.get_object('uploads', path)[:body]).not_to be_nil
end end
end
subject expect_next_instance_of(::Fog::Storage) do |storage|
allow(storage).to receive(:delete_object).and_call_original
expect(storage).to receive(:delete_object).with('uploads', keys.first).and_raise(::Google::Apis::ClientError, 'NotFound')
end
subject
::Fog::Storage.new(FileUploader.object_store_credentials).tap do |connection|
paths.each do |path| paths.each do |path|
expect { connection.get_object('uploads', path)[:body] }.to raise_error(Excon::Error::NotFound) expect { connection.get_object('uploads', path)[:body] }.to raise_error(Excon::Error::NotFound)
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment