Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-08-04 21:09:04 +00:00
parent f5a72705e4
commit 155fb78b9a
32 changed files with 304 additions and 267 deletions

View File

@ -25,19 +25,19 @@ export default {
},
stickinessOptions: [
{
value: 'DEFAULT',
value: 'default',
text: __('Available ID'),
},
{
value: 'USERID',
value: 'userId',
text: __('User ID'),
},
{
value: 'SESSIONID',
value: 'sessionId',
text: __('Session ID'),
},
{
value: 'RANDOM',
value: 'random',
text: __('Random'),
},
],

View File

@ -37,7 +37,6 @@ module Namespaces
module Traversal
module Linear
extend ActiveSupport::Concern
include LinearScopes
UnboundedSearch = Class.new(StandardError)
@ -45,6 +44,14 @@ module Namespaces
before_update :lock_both_roots, if: -> { sync_traversal_ids? && parent_id_changed? }
after_create :sync_traversal_ids, if: -> { sync_traversal_ids? }
after_update :sync_traversal_ids, if: -> { sync_traversal_ids? && saved_change_to_parent_id? }
scope :traversal_ids_contains, ->(ids) { where("traversal_ids @> (?)", ids) }
# When filtering namespaces by the traversal_ids column to compile a
# list of namespace IDs, it's much faster to reference the ID in
# traversal_ids than the primary key ID column.
# WARNING This scope must be used behind a linear query feature flag
# such as `use_traversal_ids`.
scope :as_ids, -> { select('traversal_ids[array_length(traversal_ids, 1)] AS id') }
end
def sync_traversal_ids?
@ -157,14 +164,20 @@ module Namespaces
Namespace.lock.select(:id).where(id: roots).order(id: :asc).load
end
# Make sure we drop the STI `type = 'Group'` condition for better performance.
# Logically equivalent so long as hierarchies remain homogeneous.
def without_sti_condition
self.class.unscope(where: :type)
end
# Search this namespace's lineage. Bound inclusively by top node.
def lineage(top: nil, bottom: nil, hierarchy_order: nil)
raise UnboundedSearch, 'Must bound search by either top or bottom' unless top || bottom
skope = self.class.without_sti_condition
skope = without_sti_condition
if top
skope = skope.where("traversal_ids @> ('{?}')", top.id)
skope = skope.traversal_ids_contains("{#{top.id}}")
end
if bottom

View File

@ -1,59 +0,0 @@
# frozen_string_literal: true
module Namespaces
module Traversal
module LinearScopes
extend ActiveSupport::Concern
class_methods do
# When filtering namespaces by the traversal_ids column to compile a
# list of namespace IDs, it can be faster to reference the ID in
# traversal_ids than the primary key ID column.
def as_ids
return super unless use_traversal_ids?
select('namespaces.traversal_ids[array_length(namespaces.traversal_ids, 1)] AS id')
end
def self_and_descendants
return super unless use_traversal_ids?
without_dups = self_and_descendants_with_duplicates
.select('DISTINCT on(namespaces.id) namespaces.*')
# Wrap the `SELECT DISTINCT on(....)` with a normal query so we
# retain expected Rails behavior. Otherwise count and other
# aggregates won't work.
unscoped.without_sti_condition.from(without_dups, :namespaces)
end
def self_and_descendant_ids
return super unless use_traversal_ids?
self_and_descendants_with_duplicates.select('DISTINCT namespaces.id')
end
# Make sure we drop the STI `type = 'Group'` condition for better performance.
# Logically equivalent so long as hierarchies remain homogeneous.
def without_sti_condition
unscope(where: :type)
end
private
def use_traversal_ids?
Feature.enabled?(:use_traversal_ids, default_enabled: :yaml)
end
def self_and_descendants_with_duplicates
base_ids = select(:id)
unscoped
.without_sti_condition
.from("namespaces, (#{base_ids.to_sql}) base")
.where('namespaces.traversal_ids @> ARRAY[base.id]')
end
end
end
end
end

View File

@ -4,7 +4,6 @@ module Namespaces
module Traversal
module Recursive
extend ActiveSupport::Concern
include RecursiveScopes
def root_ancestor
return self if parent.nil?

View File

@ -1,25 +0,0 @@
# frozen_string_literal: true
module Namespaces
module Traversal
module RecursiveScopes
extend ActiveSupport::Concern
class_methods do
def as_ids
select('id')
end
def self_and_descendants
Gitlab::ObjectHierarchy.new(all).base_and_descendants
end
alias_method :recursive_self_and_descendants, :self_and_descendants
def self_and_descendant_ids
self_and_descendants.as_ids
end
alias_method :recursive_self_and_descendant_ids, :self_and_descendant_ids
end
end
end
end

View File

@ -16,7 +16,7 @@ module Operations
STRATEGY_USERWITHID => ['userIds'].freeze
}.freeze
USERID_MAX_LENGTH = 256
STICKINESS_SETTINGS = %w[DEFAULT USERID SESSIONID RANDOM].freeze
STICKINESS_SETTINGS = %w[default userId sessionId random].freeze
self.table_name = 'operations_strategies'

View File

@ -79,7 +79,7 @@ class ProjectFeature < ApplicationRecord
end
end
default_value_for(:container_registry_access_level, allows_nil: false) do |feature|
default_value_for(:container_registry_access_level) do |feature|
if gitlab_config_features.container_registry
ENABLED
else

View File

@ -0,0 +1,29 @@
# frozen_string_literal: true
class OperationsFeatureFlagsCorrectFlexibleRolloutValues < ActiveRecord::Migration[6.1]
STICKINESS = { "USERID" => "userId", "RANDOM" => "random", "SESSIONID" => "sessionId", "DEFAULT" => "default" }.freeze
def up
STICKINESS.each do |before, after|
update_statement = <<-SQL
UPDATE operations_strategies
SET parameters = parameters || jsonb_build_object('stickiness', '#{quote_string(after)}')
WHERE name = 'flexibleRollout' AND parameters->>'stickiness' = '#{quote_string(before)}'
SQL
execute(update_statement)
end
end
def down
STICKINESS.each do |before, after|
update_statement = <<-SQL
UPDATE operations_strategies
SET parameters = parameters || jsonb_build_object('stickiness', '#{quote_string(before)}')
WHERE name = 'flexibleRollout' AND parameters->>'stickiness' = '#{quote_string(after)}'
SQL
execute(update_statement)
end
end
end

View File

@ -0,0 +1 @@
cea8e51f6917be9ad43280fba9f8e7d9b9db1f508e249d9f5df792e43c0b8313

View File

@ -212,7 +212,7 @@
"babel-plugin-istanbul": "^6.0.0",
"chalk": "^2.4.1",
"cheerio": "^1.0.0-rc.9",
"commander": "^2.18.0",
"commander": "^2.20.3",
"custom-jquery-matchers": "^2.1.0",
"docdash": "^1.0.2",
"eslint": "7.31.0",
@ -264,7 +264,7 @@
"webpack-dev-server": "^3.11.2",
"xhr-mock": "^2.5.1",
"yarn-check-webpack-plugin": "^1.2.0",
"yarn-deduplicate": "^1.1.1"
"yarn-deduplicate": "^3.1.0"
},
"blockedDependencies": {
"bootstrap-vue": "https://docs.gitlab.com/ee/development/fe_guide/dependencies.html#bootstrapvue"

View File

@ -84,10 +84,13 @@ module QA
# Get issue comments
#
# @return [Array]
def comments(auto_paginate: false)
def comments(auto_paginate: false, attempts: 0)
return parse_body(api_get_from(api_comments_path)) unless auto_paginate
auto_paginated_response(Runtime::API::Request.new(api_client, api_comments_path, per_page: '100').url)
auto_paginated_response(
Runtime::API::Request.new(api_client, api_comments_path, per_page: '100').url,
attempts: attempts
)
end
end
end

View File

@ -160,10 +160,13 @@ module QA
# Get MR comments
#
# @return [Array]
def comments(auto_paginate: false)
def comments(auto_paginate: false, attempts: 0)
return parse_body(api_get_from(api_comments_path)) unless auto_paginate
auto_paginated_response(Runtime::API::Request.new(api_client, api_comments_path, per_page: '100').url)
auto_paginated_response(
Runtime::API::Request.new(api_client, api_comments_path, per_page: '100').url,
attempts: attempts
)
end
private

View File

@ -264,21 +264,24 @@ module QA
result = parse_body(response)
Runtime::Logger.error("Import failed: #{result[:import_error]}") if result[:import_status] == "failed"
if result[:import_status] == "failed"
Runtime::Logger.error("Import failed: #{result[:import_error]}")
Runtime::Logger.error("Failed relations: #{result[:failed_relations]}")
end
result[:import_status]
end
def commits(auto_paginate: false)
def commits(auto_paginate: false, attempts: 0)
return parse_body(api_get_from(api_commits_path)) unless auto_paginate
auto_paginated_response(request_url(api_commits_path, per_page: '100'))
auto_paginated_response(request_url(api_commits_path, per_page: '100'), attempts: attempts)
end
def merge_requests(auto_paginate: false)
def merge_requests(auto_paginate: false, attempts: 0)
return parse_body(api_get_from(api_merge_requests_path)) unless auto_paginate
auto_paginated_response(request_url(api_merge_requests_path, per_page: '100'))
auto_paginated_response(request_url(api_merge_requests_path, per_page: '100'), attempts: attempts)
end
def merge_request_with_title(title)
@ -302,10 +305,10 @@ module QA
parse_body(response)
end
def repository_branches(auto_paginate: false)
def repository_branches(auto_paginate: false, attempts: 0)
return parse_body(api_get_from(api_repository_branches_path)) unless auto_paginate
auto_paginated_response(request_url(api_repository_branches_path, per_page: '100'))
auto_paginated_response(request_url(api_repository_branches_path, per_page: '100'), attempts: attempts)
end
def repository_tags
@ -328,22 +331,22 @@ module QA
parse_body(response)
end
def issues(auto_paginate: false)
def issues(auto_paginate: false, attempts: 0)
return parse_body(api_get_from(api_issues_path)) unless auto_paginate
auto_paginated_response(request_url(api_issues_path, per_page: '100'))
auto_paginated_response(request_url(api_issues_path, per_page: '100'), attempts: attempts)
end
def labels(auto_paginate: false)
def labels(auto_paginate: false, attempts: 0)
return parse_body(api_get_from(api_labels_path)) unless auto_paginate
auto_paginated_response(request_url(api_labels_path, per_page: '100'))
auto_paginated_response(request_url(api_labels_path, per_page: '100'), attempts: attempts)
end
def milestones(auto_paginate: false)
def milestones(auto_paginate: false, attempts: 0)
return parse_body(api_get_from(api_milestones_path)) unless auto_paginate
auto_paginated_response(request_url(api_milestones_path, per_page: '100'))
auto_paginated_response(request_url(api_milestones_path, per_page: '100'), attempts: attempts)
end
def wikis

View File

@ -6,6 +6,10 @@ module QA
class Request
API_VERSION = 'v4'
def self.masked_url(url)
url.sub(/private_token=.*/, "private_token=[****]")
end
def initialize(api_client, path, **query_string)
query_string[:private_token] ||= api_client.personal_access_token unless query_string[:oauth_access_token]
request_path = request_path(path, **query_string)
@ -13,7 +17,7 @@ module QA
end
def mask_url
@session_address.address.sub(/private_token=.*/, "private_token=[****]")
QA::Runtime::API::Request.masked_url(url)
end
def url

View File

@ -135,10 +135,12 @@ module QA
imported_project # import the project
fetch_github_objects # fetch all objects right after import has started
expect { imported_project.reload!.import_status }.to eventually_eq('finished').within(
duration: 3600,
interval: 30
)
import_status = lambda do
imported_project.reload!.import_status.tap do |status|
raise "Import of '#{imported_project.name}' failed!" if status == 'failed'
end
end
expect(import_status).to eventually_eq('finished').within(duration: 3600, interval: 30)
@import_time = Time.now - start
aggregate_failures do
@ -264,7 +266,7 @@ module QA
def gl_commits
@gl_commits ||= begin
logger.debug("= Fetching commits =")
imported_project.commits(auto_paginate: true).map { |c| c[:id] }
imported_project.commits(auto_paginate: true, attempts: 2).map { |c| c[:id] }
end
end
@ -294,7 +296,7 @@ module QA
def mrs
@mrs ||= begin
logger.debug("= Fetching merge requests =")
imported_mrs = imported_project.merge_requests(auto_paginate: true)
imported_mrs = imported_project.merge_requests(auto_paginate: true, attempts: 2)
logger.debug("= Transforming merge request objects for comparison =")
imported_mrs.each_with_object({}) do |mr, hash|
resource = Resource::MergeRequest.init do |resource|
@ -305,7 +307,7 @@ module QA
hash[mr[:title]] = {
body: mr[:description],
comments: resource.comments(auto_paginate: true)
comments: resource.comments(auto_paginate: true, attempts: 2)
# remove system notes
.reject { |c| c[:system] || c[:body].match?(/^(\*\*Review:\*\*)|(\*Merged by:).*/) }
.map { |c| sanitize(c[:body]) }
@ -320,7 +322,7 @@ module QA
def gl_issues
@gl_issues ||= begin
logger.debug("= Fetching issues =")
imported_issues = imported_project.issues(auto_paginate: true)
imported_issues = imported_project.issues(auto_paginate: true, attempts: 2)
logger.debug("= Transforming issue objects for comparison =")
imported_issues.each_with_object({}) do |issue, hash|
resource = Resource::Issue.init do |issue_resource|
@ -331,7 +333,7 @@ module QA
hash[issue[:title]] = {
body: issue[:description],
comments: resource.comments(auto_paginate: true).map { |c| sanitize(c[:body]) }
comments: resource.comments(auto_paginate: true, attempts: 2).map { |c| sanitize(c[:body]) }
}
end
end

View File

@ -79,16 +79,27 @@ module QA
error.response
end
def auto_paginated_response(url)
def auto_paginated_response(url, attempts: 0)
pages = []
with_paginated_response_body(url) { |response| pages << response }
with_paginated_response_body(url, attempts: attempts) { |response| pages << response }
pages.flatten
end
def with_paginated_response_body(url)
def with_paginated_response_body(url, attempts: 0)
not_ok_error = lambda do |resp|
raise "Failed to GET #{QA::Runtime::API::Request.masked_url(url)} - (#{resp.code}): `#{resp}`."
end
loop do
response = get(url)
response = if attempts > 0
Retrier.retry_on_exception(max_attempts: attempts, log: false) do
get(url).tap { |resp| not_ok_error.call(resp) if resp.code != HTTP_STATUS_OK }
end
else
get(url).tap { |resp| not_ok_error.call(resp) if resp.code != HTTP_STATUS_OK }
end
page, pages = response.headers.values_at(:x_page, :x_total_pages)
api_endpoint = url.match(%r{v4/(\S+)\?})[1]
@ -104,7 +115,10 @@ module QA
end
def pagination_links(response)
response.headers[:link].split(',').map do |link|
link = response.headers[:link]
return unless link
link.split(',').map do |link|
match = link.match(/<(?<url>.*)>; rel="(?<rel>\w+)"/)
break nil unless match

View File

@ -11,7 +11,15 @@ module QA
RetriesExceededError = Class.new(RepeaterConditionExceededError)
WaitExceededError = Class.new(RepeaterConditionExceededError)
def repeat_until(max_attempts: nil, max_duration: nil, reload_page: nil, sleep_interval: 0, raise_on_failure: true, retry_on_exception: false, log: true)
def repeat_until(
max_attempts: nil,
max_duration: nil,
reload_page: nil,
sleep_interval: 0,
raise_on_failure: true,
retry_on_exception: false,
log: true
)
attempts = 0
start = Time.now
@ -29,17 +37,19 @@ module QA
raise unless retry_on_exception
attempts += 1
if remaining_attempts?(attempts, max_attempts) && remaining_time?(start, max_duration)
sleep_and_reload_if_needed(sleep_interval, reload_page)
raise unless remaining_attempts?(attempts, max_attempts) && remaining_time?(start, max_duration)
retry
else
raise
end
sleep_and_reload_if_needed(sleep_interval, reload_page)
retry
end
if raise_on_failure
raise RetriesExceededError, "Retry condition not met after #{max_attempts} #{'attempt'.pluralize(max_attempts)}" unless remaining_attempts?(attempts, max_attempts)
unless remaining_attempts?(attempts, max_attempts)
raise(
RetriesExceededError,
"Retry condition not met after #{max_attempts} #{'attempt'.pluralize(max_attempts)}"
)
end
raise WaitExceededError, "Wait condition not met after #{max_duration} #{'second'.pluralize(max_duration)}"
end

View File

@ -7,21 +7,21 @@ module QA
module_function
def retry_on_exception(max_attempts: 3, reload_page: nil, sleep_interval: 0.5)
QA::Runtime::Logger.debug(
<<~MSG.tr("\n", ' ')
with retry_on_exception: max_attempts: #{max_attempts};
reload_page: #{reload_page};
sleep_interval: #{sleep_interval}
MSG
)
def retry_on_exception(max_attempts: 3, reload_page: nil, sleep_interval: 0.5, log: true)
if log
msg = ["with retry_on_exception: max_attempts: #{max_attempts}"]
msg << "reload_page: #{reload_page}" if reload_page
msg << "sleep_interval: #{sleep_interval}"
QA::Runtime::Logger.debug(msg.join('; '))
end
result = nil
repeat_until(
max_attempts: max_attempts,
reload_page: reload_page,
sleep_interval: sleep_interval,
retry_on_exception: true
retry_on_exception: true,
log: log
) do
result = yield
@ -29,7 +29,7 @@ module QA
# We set it to `true` so that it doesn't repeat if there's no exception
true
end
QA::Runtime::Logger.debug("ended retry_on_exception")
QA::Runtime::Logger.debug("ended retry_on_exception") if log
result
end

View File

@ -70,8 +70,9 @@ RSpec.describe QA::Support::Retrier do
describe '.retry_on_exception' do
context 'when the condition is true' do
it 'logs max_attempts, reload_page, and sleep_interval parameters' do
expect { subject.retry_on_exception(max_attempts: 1, reload_page: nil, sleep_interval: 0) { true } }
.to output(/with retry_on_exception: max_attempts: 1; reload_page: ; sleep_interval: 0/).to_stdout_from_any_process
message = /with retry_on_exception: max_attempts: 1; reload_page: true; sleep_interval: 0/
expect { subject.retry_on_exception(max_attempts: 1, reload_page: true, sleep_interval: 0) { true } }
.to output(message).to_stdout_from_any_process
end
it 'logs the end' do
@ -82,8 +83,9 @@ RSpec.describe QA::Support::Retrier do
context 'when the condition is false' do
it 'logs the start' do
expect { subject.retry_on_exception(max_attempts: 1, reload_page: nil, sleep_interval: 0) { false } }
.to output(/with retry_on_exception: max_attempts: 1; reload_page: ; sleep_interval: 0/).to_stdout_from_any_process
message = /with retry_on_exception: max_attempts: 1; reload_page: true; sleep_interval: 0/
expect { subject.retry_on_exception(max_attempts: 1, reload_page: true, sleep_interval: 0) { false } }
.to output(message).to_stdout_from_any_process
end
it 'logs the end' do

View File

@ -652,7 +652,7 @@ RSpec.describe Projects::FeatureFlagsController do
version: 'new_version_flag',
strategies_attributes: [{
name: 'flexibleRollout',
parameters: { groupId: 'default', rollout: '15', stickiness: 'DEFAULT' },
parameters: { groupId: 'default', rollout: '15', stickiness: 'default' },
scopes_attributes: [{ environment_scope: 'production' }]
}]
}
@ -666,7 +666,7 @@ RSpec.describe Projects::FeatureFlagsController do
strategy_json = json_response['strategies'].first
expect(strategy_json['name']).to eq('flexibleRollout')
expect(strategy_json['parameters']).to eq({ 'groupId' => 'default', 'rollout' => '15', 'stickiness' => 'DEFAULT' })
expect(strategy_json['parameters']).to eq({ 'groupId' => 'default', 'rollout' => '15', 'stickiness' => 'default' })
expect(strategy_json['scopes'].count).to eq(1)
scope_json = strategy_json['scopes'].first
@ -938,7 +938,7 @@ RSpec.describe Projects::FeatureFlagsController do
it 'creates a flexibleRollout strategy' do
put_request(new_version_flag, strategies_attributes: [{
name: 'flexibleRollout',
parameters: { groupId: 'default', rollout: '30', stickiness: 'DEFAULT' }
parameters: { groupId: 'default', rollout: '30', stickiness: 'default' }
}])
expect(response).to have_gitlab_http_status(:ok)
@ -948,7 +948,7 @@ RSpec.describe Projects::FeatureFlagsController do
expect(strategy_json['parameters']).to eq({
'groupId' => 'default',
'rollout' => '30',
'stickiness' => 'DEFAULT'
'stickiness' => 'default'
})
expect(strategy_json['scopes']).to eq([])
end

View File

@ -51,7 +51,7 @@ FactoryBot.define do
ci_job_token_scope_enabled { nil }
end
before(:create) do |project, evaluator|
after(:build) do |project, evaluator|
# Builds and MRs can't have higher visibility level than repository access level.
builds_access_level = [evaluator.builds_access_level, evaluator.repository_access_level].min
merge_requests_access_level = [evaluator.merge_requests_access_level, evaluator.repository_access_level].min
@ -67,21 +67,11 @@ FactoryBot.define do
pages_access_level: evaluator.pages_access_level,
metrics_dashboard_access_level: evaluator.metrics_dashboard_access_level,
operations_access_level: evaluator.operations_access_level,
analytics_access_level: evaluator.analytics_access_level
analytics_access_level: evaluator.analytics_access_level,
container_registry_access_level: evaluator.container_registry_access_level
}
project.build_project_feature(hash)
# This is not included in the `hash` above because the default_value_for in
# the ProjectFeature model overrides the value set by `build_project_feature` when
# evaluator.container_registry_access_level == ProjectFeature::DISABLED.
#
# This is because the default_value_for gem uses the <column>_changed? method
# to determine if the default value should be applied. For new records,
# <column>_changed? returns false if the value of the column is the same as
# the database default.
# See https://github.com/FooBarWidget/default_value_for/blob/release-3.4.0/lib/default_value_for.rb#L158.
project.project_feature.container_registry_access_level = evaluator.container_registry_access_level
end
after(:create) do |project, evaluator|

View File

@ -66,15 +66,14 @@ describe('feature_flags/components/strategies/flexible_rollout.vue', () => {
});
it('emits a change when the stickiness value changes', async () => {
stickinessSelect.setValue('USERID');
await wrapper.vm.$nextTick();
await stickinessSelect.setValue('userId');
expect(wrapper.emitted('change')).toEqual([
[
{
parameters: {
rollout: flexibleRolloutStrategy.parameters.rollout,
groupId: PERCENT_ROLLOUT_GROUP_ID,
stickiness: 'USERID',
stickiness: 'userId',
},
},
],

View File

@ -76,7 +76,7 @@ export const percentRolloutStrategy = {
export const flexibleRolloutStrategy = {
name: ROLLOUT_STRATEGY_FLEXIBLE_ROLLOUT,
parameters: { rollout: '50', groupId: 'default', stickiness: 'DEFAULT' },
parameters: { rollout: '50', groupId: 'default', stickiness: 'default' },
scopes: [],
};

View File

@ -0,0 +1,66 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!('operations_feature_flags_correct_flexible_rollout_values')
RSpec.describe OperationsFeatureFlagsCorrectFlexibleRolloutValues, :migration do
let_it_be(:strategies) { table(:operations_strategies) }
let(:namespace) { table(:namespaces).create!(name: 'feature_flag', path: 'feature_flag') }
let(:project) { table(:projects).create!(namespace_id: namespace.id) }
let(:feature_flag) { table(:operations_feature_flags).create!(project_id: project.id, active: true, name: 'foo', iid: 1) }
describe "#up" do
described_class::STICKINESS.each do |old, new|
it "corrects parameters for flexible rollout stickiness #{old}" do
reversible_migration do |migration|
parameters = { groupId: "default", rollout: "100", stickiness: old }
strategy = create_strategy(parameters)
migration.before -> {
expect(strategy.reload.parameters).to eq({ "groupId" => "default", "rollout" => "100", "stickiness" => old })
}
migration.after -> {
expect(strategy.reload.parameters).to eq({ "groupId" => "default", "rollout" => "100", "stickiness" => new })
}
end
end
end
it 'ignores other strategies' do
reversible_migration do |migration|
parameters = { "groupId" => "default", "rollout" => "100", "stickiness" => "USERID" }
strategy = create_strategy(parameters, name: 'default')
migration.before -> {
expect(strategy.reload.parameters).to eq(parameters)
}
migration.after -> {
expect(strategy.reload.parameters).to eq(parameters)
}
end
end
it 'ignores other stickiness' do
reversible_migration do |migration|
parameters = { "groupId" => "default", "rollout" => "100", "stickiness" => "FOO" }
strategy = create_strategy(parameters)
migration.before -> {
expect(strategy.reload.parameters).to eq(parameters)
}
migration.after -> {
expect(strategy.reload.parameters).to eq(parameters)
}
end
end
end
def create_strategy(params, name: 'flexibleRollout')
strategies.create!(name: name, parameters: params, feature_flag_id: feature_flag.id)
end
end

View File

@ -208,23 +208,9 @@ RSpec.describe Namespace do
it { is_expected.to include_module(Gitlab::VisibilityLevel) }
it { is_expected.to include_module(Namespaces::Traversal::Recursive) }
it { is_expected.to include_module(Namespaces::Traversal::Linear) }
it { is_expected.to include_module(Namespaces::Traversal::RecursiveScopes) }
it { is_expected.to include_module(Namespaces::Traversal::LinearScopes) }
end
context 'traversal scopes' do
context 'recursive' do
before do
stub_feature_flags(use_traversal_ids: false)
end
it_behaves_like 'namespace traversal scopes'
end
context 'linear' do
it_behaves_like 'namespace traversal scopes'
end
end
it_behaves_like 'linear namespace traversal'
context 'traversal_ids on create' do
context 'default traversal_ids' do

View File

@ -112,7 +112,7 @@ RSpec.describe Operations::FeatureFlags::Strategy do
end
context 'when the strategy name is flexibleRollout' do
valid_parameters = { rollout: '40', groupId: 'mygroup', stickiness: 'DEFAULT' }
valid_parameters = { rollout: '40', groupId: 'mygroup', stickiness: 'default' }
where(invalid_parameters: [
nil,
{},
@ -133,7 +133,7 @@ RSpec.describe Operations::FeatureFlags::Strategy do
[
[:rollout, '10'],
[:stickiness, 'DEFAULT'],
[:stickiness, 'default'],
[:groupId, 'mygroup']
].permutation(3).each do |parameters|
it "allows the parameters in the order #{parameters.map { |p| p.first }.join(', ')}" do
@ -151,7 +151,7 @@ RSpec.describe Operations::FeatureFlags::Strategy do
"\n", "\t", "\n10", "20\n", "\n100", "100\n", "\n ", nil])
with_them do
it 'must be a string value between 0 and 100 inclusive and without a percentage sign' do
parameters = { stickiness: 'DEFAULT', groupId: 'mygroup', rollout: invalid_value }
parameters = { stickiness: 'default', groupId: 'mygroup', rollout: invalid_value }
strategy = described_class.create(feature_flag: feature_flag,
name: 'flexibleRollout',
parameters: parameters)
@ -165,7 +165,7 @@ RSpec.describe Operations::FeatureFlags::Strategy do
where(valid_value: %w[0 1 10 38 100 93])
with_them do
it 'must be a string value between 0 and 100 inclusive and without a percentage sign' do
parameters = { stickiness: 'DEFAULT', groupId: 'mygroup', rollout: valid_value }
parameters = { stickiness: 'default', groupId: 'mygroup', rollout: valid_value }
strategy = described_class.create(feature_flag: feature_flag,
name: 'flexibleRollout',
parameters: parameters)
@ -180,7 +180,7 @@ RSpec.describe Operations::FeatureFlags::Strategy do
'!bad', '.bad', 'Bad', 'bad1', "", " ", "b" * 33, "ba_d", "ba\nd"])
with_them do
it 'must be a string value of up to 32 lowercase characters' do
parameters = { stickiness: 'DEFAULT', groupId: invalid_value, rollout: '40' }
parameters = { stickiness: 'default', groupId: invalid_value, rollout: '40' }
strategy = described_class.create(feature_flag: feature_flag,
name: 'flexibleRollout',
parameters: parameters)
@ -192,7 +192,7 @@ RSpec.describe Operations::FeatureFlags::Strategy do
where(valid_value: ["somegroup", "anothergroup", "okay", "g", "a" * 32])
with_them do
it 'must be a string value of up to 32 lowercase characters' do
parameters = { stickiness: 'DEFAULT', groupId: valid_value, rollout: '40' }
parameters = { stickiness: 'default', groupId: valid_value, rollout: '40' }
strategy = described_class.create(feature_flag: feature_flag,
name: 'flexibleRollout',
parameters: parameters)
@ -203,7 +203,7 @@ RSpec.describe Operations::FeatureFlags::Strategy do
end
describe 'stickiness' do
where(invalid_value: [nil, " ", "default", "DEFAULT\n", "UserId", "USER", "USERID "])
where(invalid_value: [nil, " ", "DEFAULT", "DEFAULT\n", "UserId", "USER", "USERID "])
with_them do
it 'must be a string representing a supported stickiness setting' do
parameters = { stickiness: invalid_value, groupId: 'mygroup', rollout: '40' }
@ -212,12 +212,12 @@ RSpec.describe Operations::FeatureFlags::Strategy do
parameters: parameters)
expect(strategy.errors[:parameters]).to eq([
'stickiness parameter must be DEFAULT, USERID, SESSIONID, or RANDOM'
'stickiness parameter must be default, userId, sessionId, or random'
])
end
end
where(valid_value: %w[DEFAULT USERID SESSIONID RANDOM])
where(valid_value: %w[default userId sessionId random])
with_them do
it 'must be a string representing a supported stickiness setting' do
parameters = { stickiness: valid_value, groupId: 'mygroup', rollout: '40' }
@ -425,7 +425,7 @@ RSpec.describe Operations::FeatureFlags::Strategy do
user_list: user_list,
parameters: { groupId: 'default',
rollout: '10',
stickiness: 'DEFAULT' })
stickiness: 'default' })
expect(strategy.errors[:user_list]).to eq(['must be blank'])
end
@ -435,7 +435,7 @@ RSpec.describe Operations::FeatureFlags::Strategy do
name: 'flexibleRollout',
parameters: { groupId: 'default',
rollout: '10',
stickiness: 'DEFAULT' })
stickiness: 'default' })
expect(strategy.errors[:user_list]).to be_empty
end

View File

@ -218,5 +218,29 @@ RSpec.describe ProjectFeature do
end
end
end
context 'test build factory' do
let(:project) { build(:project, container_registry_access_level: level) }
subject { project.container_registry_access_level }
context 'private' do
let(:level) { ProjectFeature::PRIVATE }
it { is_expected.to eq(level) }
end
context 'enabled' do
let(:level) { ProjectFeature::ENABLED }
it { is_expected.to eq(level) }
end
context 'disabled' do
let(:level) { ProjectFeature::DISABLED }
it { is_expected.to eq(level) }
end
end
end
end

View File

@ -318,7 +318,8 @@ RSpec.describe Project, factory_default: :keep do
end
it 'validates presence of project_feature' do
project = build(:project, project_feature: nil)
project = build(:project)
project.project_feature = nil
expect(project).not_to be_valid
end

View File

@ -417,7 +417,7 @@ RSpec.describe API::FeatureFlags do
version: 'new_version_flag',
strategies: [{
name: 'flexibleRollout',
parameters: { groupId: 'default', rollout: '50', stickiness: 'DEFAULT' },
parameters: { groupId: 'default', rollout: '50', stickiness: 'default' },
scopes: [{
environment_scope: 'staging'
}]
@ -434,7 +434,7 @@ RSpec.describe API::FeatureFlags do
expect(feature_flag.version).to eq('new_version_flag')
expect(feature_flag.strategies.map { |s| s.slice(:name, :parameters).deep_symbolize_keys }).to eq([{
name: 'flexibleRollout',
parameters: { groupId: 'default', rollout: '50', stickiness: 'DEFAULT' }
parameters: { groupId: 'default', rollout: '50', stickiness: 'default' }
}])
expect(feature_flag.strategies.first.scopes.map { |s| s.slice(:environment_scope).deep_symbolize_keys }).to eq([{
environment_scope: 'staging'
@ -630,7 +630,7 @@ RSpec.describe API::FeatureFlags do
strategies: [{
id: strategy.id,
name: 'flexibleRollout',
parameters: { groupId: 'default', rollout: '10', stickiness: 'DEFAULT' }
parameters: { groupId: 'default', rollout: '10', stickiness: 'default' }
}]
}
@ -642,7 +642,7 @@ RSpec.describe API::FeatureFlags do
expect(result).to eq([{
id: strategy.id,
name: 'flexibleRollout',
parameters: { groupId: 'default', rollout: '10', stickiness: 'DEFAULT' }
parameters: { groupId: 'default', rollout: '10', stickiness: 'default' }
}])
end
@ -677,7 +677,7 @@ RSpec.describe API::FeatureFlags do
params = {
strategies: [{
name: 'flexibleRollout',
parameters: { groupId: 'default', rollout: '10', stickiness: 'DEFAULT' }
parameters: { groupId: 'default', rollout: '10', stickiness: 'default' }
}]
}
@ -694,7 +694,7 @@ RSpec.describe API::FeatureFlags do
parameters: {}
}, {
name: 'flexibleRollout',
parameters: { groupId: 'default', rollout: '10', stickiness: 'DEFAULT' }
parameters: { groupId: 'default', rollout: '10', stickiness: 'default' }
}])
end

View File

@ -0,0 +1,23 @@
# frozen_string_literal: true
# Traversal examples common to linear and recursive methods are in
# spec/support/shared_examples/namespaces/traversal_examples.rb
RSpec.shared_examples 'linear namespace traversal' do
context 'when use_traversal_ids feature flag is enabled' do
before do
stub_feature_flags(use_traversal_ids: true)
end
context 'scopes' do
describe '.as_ids' do
let_it_be(:namespace1) { create(:group) }
let_it_be(:namespace2) { create(:group) }
subject { Namespace.where(id: [namespace1, namespace2]).as_ids.pluck(:id) }
it { is_expected.to contain_exactly(namespace1.id, namespace2.id) }
end
end
end
end

View File

@ -1,51 +0,0 @@
# frozen_string_literal: true
RSpec.shared_examples 'namespace traversal scopes' do
# Hierarchy 1
let_it_be(:group_1) { create(:group) }
let_it_be(:nested_group_1) { create(:group, parent: group_1) }
let_it_be(:deep_nested_group_1) { create(:group, parent: nested_group_1) }
# Hierarchy 2
let_it_be(:group_2) { create(:group) }
let_it_be(:nested_group_2) { create(:group, parent: group_2) }
let_it_be(:deep_nested_group_2) { create(:group, parent: nested_group_2) }
# All groups
let_it_be(:groups) do
[
group_1, nested_group_1, deep_nested_group_1,
group_2, nested_group_2, deep_nested_group_2
]
end
describe '.as_ids' do
subject { described_class.where(id: [group_1, group_2]).as_ids.pluck(:id) }
it { is_expected.to contain_exactly(group_1.id, group_2.id) }
end
describe '.without_sti_condition' do
subject { described_class.without_sti_condition }
it { expect(subject.where_values_hash).not_to have_key(:type) }
end
describe '.self_and_descendants' do
subject { described_class.where(id: [nested_group_1, nested_group_2]).self_and_descendants }
it { is_expected.to contain_exactly(nested_group_1, deep_nested_group_1, nested_group_2, deep_nested_group_2) }
context 'with duplicate descendants' do
subject { described_class.where(id: [group_1, group_2, nested_group_1]).self_and_descendants }
it { is_expected.to match_array(groups) }
end
end
describe '.self_and_descendant_ids' do
subject { described_class.where(id: [nested_group_1, nested_group_2]).self_and_descendant_ids.pluck(:id) }
it { is_expected.to contain_exactly(nested_group_1.id, deep_nested_group_1.id, nested_group_2.id, deep_nested_group_2.id) }
end
end

View File

@ -3418,12 +3418,12 @@ combined-stream@^1.0.6, combined-stream@~1.0.6:
dependencies:
delayed-stream "~1.0.0"
commander@2, commander@^2.10.0, commander@^2.18.0, commander@^2.19.0, commander@^2.20.0, commander@~2.20.0:
version "2.20.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422"
integrity sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==
commander@2, commander@^2.19.0, commander@^2.20.0, commander@^2.20.3, commander@~2.20.0:
version "2.20.3"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
commander@^6.0.0, commander@^6.2.0, commander@~6.2.1:
commander@^6.0.0, commander@^6.1.0, commander@^6.2.0, commander@~6.2.1:
version "6.2.1"
resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c"
integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==
@ -10560,7 +10560,7 @@ semver-diff@^3.1.1:
dependencies:
semver "^6.3.0"
"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.6.0, semver@^5.7.1:
"semver@2 || 3 || 4 || 5", semver@^5.4.1, semver@^5.5.0, semver@^5.6.0, semver@^5.7.1:
version "5.7.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
@ -12882,14 +12882,14 @@ yarn-check-webpack-plugin@^1.2.0:
dependencies:
chalk "^2.4.2"
yarn-deduplicate@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/yarn-deduplicate/-/yarn-deduplicate-1.1.1.tgz#19b4a87654b66f55bf3a4bd6b153b4e4ab1b6e6d"
integrity sha512-2FDJ1dFmtvqhRmfja89ohYzpaheCYg7BFBSyaUq+kxK0y61C9oHv1XaQovCWGJtP2WU8PksQOgzMVV7oQOobzw==
yarn-deduplicate@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/yarn-deduplicate/-/yarn-deduplicate-3.1.0.tgz#3018d93e95f855f236a215b591fe8bc4bcabba3e"
integrity sha512-q2VZ6ThNzQpGfNpkPrkmV7x5HT9MOhCUsTxVTzyyZB0eSXz1NTodHn+r29DlLb+peKk8iXxzdUVhQG9pI7moFw==
dependencies:
"@yarnpkg/lockfile" "^1.1.0"
commander "^2.10.0"
semver "^5.3.0"
commander "^6.1.0"
semver "^7.3.2"
yeast@0.1.2:
version "0.1.2"