Commit 6c3457d1 authored by Jason Goodman's avatar Jason Goodman Committed by Bob Van Landuyt

Support gradualRolloutUserId strategy for feature flags on the backend

Support saving a strategy of type default or gradualRolloutUserId
Support saving a rollout percentage
Return strategies of each type to Unleash clients
Add audit messages for changing the percentage rollout
parent cc83c155
......@@ -99,14 +99,16 @@ class Projects::FeatureFlagsController < Projects::ApplicationController
def create_params
params.require(:operations_feature_flag)
.permit(:name, :description, :active,
scopes_attributes: [:environment_scope, :active])
.permit(:name, :description, :active,
scopes_attributes: [:environment_scope, :active,
strategies: [:name, parameters: [:groupId, :percentage]]])
end
def update_params
params.require(:operations_feature_flag)
.permit(:name, :description, :active,
scopes_attributes: [:id, :environment_scope, :active, :_destroy])
scopes_attributes: [:id, :environment_scope, :active, :_destroy,
strategies: [:name, parameters: [:groupId, :percentage]]])
end
def feature_flag_json(feature_flag)
......
......@@ -43,26 +43,12 @@ module Operations
where('NOT EXISTS (?)', join_enabled_scopes)
end
scope :for_environment, -> (environment) do
select("operations_feature_flags.*" \
", (#{actual_active_sql(environment)}) AS active")
end
scope :for_list, -> do
select("operations_feature_flags.*" \
", COALESCE((#{join_enabled_scopes.to_sql}), FALSE) AS active")
end
class << self
def actual_active_sql(environment)
Operations::FeatureFlagScope
.where('operations_feature_flag_scopes.feature_flag_id = ' \
'operations_feature_flags.id')
.on_environment(environment, relevant_only: true)
.select('active')
.to_sql
end
def join_enabled_scopes
Operations::FeatureFlagScope
.where('operations_feature_flags.id = feature_flag_id')
......@@ -74,12 +60,6 @@ module Operations
end
end
def strategies
[
{ name: 'default' }
]
end
private
def first_default_scope
......
......@@ -17,12 +17,24 @@ module Operations
if: :default_scope?, on: :update,
inclusion: { in: %w(*), message: 'cannot be changed from default scope' }
validates :strategies, feature_flag_strategies: true
before_destroy :prevent_destroy_default_scope, if: :default_scope?
scope :ordered, -> { order(:id) }
scope :enabled, -> { where(active: true) }
scope :disabled, -> { where(active: false) }
delegate :name, :description, to: :feature_flag
def self.for_unleash_client(project, environment)
select('DISTINCT ON (operations_feature_flag_scopes.feature_flag_id) operations_feature_flag_scopes.*')
.where(feature_flag_id: project.operations_feature_flags.select(:id))
.order(:feature_flag_id)
.on_environment(environment)
.reverse_order
end
private
def default_scope?
......
......@@ -8,4 +8,5 @@ class FeatureFlagScopeEntity < Grape::Entity
expose :environment_scope
expose :created_at
expose :updated_at
expose :strategies
end
# frozen_string_literal: true
class FeatureFlagStrategiesValidator < ActiveModel::EachValidator
STRATEGY_DEFAULT = 'default'.freeze
STRATEGY_GRADUALROLLOUTUSERID = 'gradualRolloutUserId'.freeze
def validate_each(record, attribute, value)
return unless value
if value.is_a?(Array) && value.all? { |s| s.is_a?(Hash) }
value.each do |strategy|
strategy_validations(record, attribute, strategy)
end
else
record.errors.add(attribute, 'must be an array of strategy hashes')
end
end
private
def strategy_validations(record, attribute, strategy)
case strategy['name']
when STRATEGY_DEFAULT
default_parameters_validation(record, attribute, strategy)
when STRATEGY_GRADUALROLLOUTUSERID
gradual_rollout_user_id_parameters_validation(record, attribute, strategy)
else
record.errors.add(attribute, 'strategy name is invalid')
end
end
def gradual_rollout_user_id_parameters_validation(record, attribute, strategy)
percentage = strategy.dig('parameters', 'percentage')
group_id = strategy.dig('parameters', 'groupId')
unless percentage.is_a?(String) && percentage.match(/\A[1-9]?[0-9]\z|\A100\z/)
record.errors.add(attribute, 'percentage must be a string between 0 and 100 inclusive')
end
unless group_id.is_a?(String) && group_id.match(/\A[a-z]{1,32}\z/)
record.errors.add(attribute, 'groupId parameter is invalid')
end
end
def default_parameters_validation(record, attribute, strategy)
unless strategy['parameters'] == {}
record.errors.add(attribute, 'parameters must be empty for default strategy')
end
end
end
---
title: Support feature flag gradualRolloutUserId strategy on backend
merge_request: 14515
author:
type: added
......@@ -72,8 +72,7 @@ module API
def feature_flags
return [] unless unleash_app_name.present?
project.operations_feature_flags.for_environment(unleash_app_name)
.ordered
Operations::FeatureFlagScope.for_unleash_client(project, unleash_app_name)
end
end
end
......
......@@ -415,6 +415,48 @@ describe Projects::FeatureFlagsController do
end
end
end
context 'when creates additional scope with a percentage rollout' do
it 'creates a strategy for the scope' do
params = view_params.merge({
operations_feature_flag: {
name: 'my_feature_flag',
active: true,
scopes_attributes: [{ environment_scope: '*', active: true },
{ environment_scope: 'production', active: false,
strategies: [{ name: 'gradualRolloutUserId',
parameters: { groupId: 'default', percentage: '42' } }] }]
}
})
post(:create, params: params, format: :json)
expect(response).to have_gitlab_http_status(:ok)
production_strategies_json = json_response['scopes'].second['strategies']
expect(production_strategies_json).to eq([{
'name' => 'gradualRolloutUserId',
'parameters' => { "groupId" => "default", "percentage" => "42" }
}])
end
end
context 'when creates an additional scope without a strategy' do
it 'creates a default strategy' do
params = view_params.merge({
operations_feature_flag: {
name: 'my_feature_flag',
active: true,
scopes_attributes: [{ environment_scope: '*', active: true }]
}
})
post(:create, params: params, format: :json)
expect(response).to have_gitlab_http_status(:ok)
default_strategies_json = json_response['scopes'].first['strategies']
expect(default_strategies_json).to eq([{ "name" => "default", "parameters" => {} }])
end
end
end
describe 'DELETE destroy.json' do
......@@ -678,6 +720,140 @@ describe Projects::FeatureFlagsController do
.to be_falsy
end
end
describe "updating the strategy" do
def request_params(scope, strategies)
{
namespace_id: project.namespace,
project_id: project,
id: feature_flag.id,
operations_feature_flag: {
scopes_attributes: [
{
id: scope.id,
strategies: strategies
}
]
}
}
end
it 'creates a default strategy' do
scope = create_scope(feature_flag, 'production', true, [])
params = request_params(scope, [{ name: 'default', parameters: {} }])
put(:update, params: params, format: :json, as: :json)
expect(response).to have_gitlab_http_status(:ok)
scope_json = json_response['scopes'].select do |s|
s['environment_scope'] == 'production'
end.first
expect(scope_json['strategies']).to eq([{
"name" => "default",
"parameters" => {}
}])
end
it 'creates a gradualRolloutUserId strategy' do
scope = create_scope(feature_flag, 'production', true, [])
params = request_params(scope, [{ name: 'gradualRolloutUserId',
parameters: { groupId: 'default', percentage: "70" } }])
put(:update, params: params, format: :json)
expect(response).to have_gitlab_http_status(:ok)
scope_json = json_response['scopes'].select do |s|
s['environment_scope'] == 'production'
end.first
expect(scope_json['strategies']).to eq([{
"name" => "gradualRolloutUserId",
"parameters" => {
"groupId" => "default",
"percentage" => "70"
}
}])
end
it 'updates an existing strategy' do
scope = create_scope(feature_flag, 'production', true, [{ name: 'default', parameters: {} }])
params = request_params(scope, [{ name: 'gradualRolloutUserId',
parameters: { groupId: 'default', percentage: "50" } }])
put(:update, params: params, format: :json)
expect(response).to have_gitlab_http_status(:ok)
scope_json = json_response['scopes'].select do |s|
s['environment_scope'] == 'production'
end.first
expect(scope_json['strategies']).to eq([{
"name" => "gradualRolloutUserId",
"parameters" => {
"groupId" => "default",
"percentage" => "50"
}
}])
end
it 'clears an existing strategy' do
scope = create_scope(feature_flag, 'production', true, [{ name: 'default', parameters: {} }])
params = request_params(scope, [])
put(:update, params: params, format: :json, as: :json)
expect(response).to have_gitlab_http_status(:ok)
scope_json = json_response['scopes'].select do |s|
s['environment_scope'] == 'production'
end.first
expect(scope_json['strategies']).to eq([])
end
it 'does not modify strategies when there is no strategies key in the params' do
scope = create_scope(feature_flag, 'production', true, [{ name: 'default', parameters: {} }])
params = {
namespace_id: project.namespace,
project_id: project,
id: feature_flag.id,
operations_feature_flag: {
scopes_attributes: [{ id: scope.id }]
}
}
put(:update, params: params, format: :json)
expect(response).to have_gitlab_http_status(:ok)
scope_json = json_response['scopes'].select do |s|
s['environment_scope'] == 'production'
end.first
expect(scope_json['strategies']).to eq([{
"name" => "default",
"parameters" => {}
}])
end
it 'leaves an existing strategy when there are no strategies in the params' do
scope = create_scope(feature_flag, 'production', true, [{ name: 'gradualRolloutUserId',
parameters: { groupId: 'default', percentage: '10' } }])
params = {
namespace_id: project.namespace,
project_id: project,
id: feature_flag.id,
operations_feature_flag: {
scopes_attributes: [{ id: scope.id }]
}
}
put(:update, params: params, format: :json, as: :json)
expect(response).to have_gitlab_http_status(:ok)
scope_json = json_response['scopes'].select do |s|
s['environment_scope'] == 'production'
end.first
expect(scope_json['strategies']).to eq([{
"name" => "gradualRolloutUserId",
"parameters" => { "groupId" => "default", "percentage" => "10" }
}])
end
end
end
private
......
......@@ -4,6 +4,7 @@ FactoryBot.define do
factory :operations_feature_flag_scope, class: Operations::FeatureFlagScope do
association :feature_flag, factory: :operations_feature_flag
active true
strategies [{ name: "default", parameters: {} }]
sequence(:environment_scope) { |n| "review/patch-#{n}" }
end
end
......@@ -9,8 +9,10 @@
"id": { "type": "integer" },
"environment_scope": { "type": "string" },
"active": { "type": "boolean" },
"percentage": { "type": ["integer", "null"] },
"created_at": { "type": "date" },
"updated_at": { "type": "date" }
"updated_at": { "type": "date" },
"strategies": { "type": "array", "items": { "$ref": "feature_flag_strategy.json" } }
},
"additionalProperties": false
}
{
"type": "object",
"required": [
"name"
],
"properties": {
"name": { "type": "string" },
"parameters": {
"type": "object"
}
},
"additionalProperties": false
}
......@@ -7,6 +7,18 @@
"properties": {
"name": {
"type": "string"
},
"parameters": {
"type": "object",
"additionalProperties": false,
"properties": {
"groupId": {
"type": "string"
},
"percentage": {
"type": "integer"
}
}
}
}
}
......@@ -16,20 +16,31 @@ describe HasEnvironmentScope do
describe '.on_environment' do
let(:project) { create(:project) }
let!(:cluster1) { create(:cluster, projects: [project], environment_scope: '*') }
let!(:cluster2) { create(:cluster, projects: [project], environment_scope: 'product/*') }
let!(:cluster3) { create(:cluster, projects: [project], environment_scope: 'staging/*') }
let(:environment_name) { 'product/canary-1' }
it 'returns scoped objects' do
expect(project.clusters.on_environment(environment_name)).to eq([cluster1, cluster2])
cluster1 = create(:cluster, projects: [project], environment_scope: '*')
cluster2 = create(:cluster, projects: [project], environment_scope: 'product/*')
create(:cluster, projects: [project], environment_scope: 'staging/*')
expect(project.clusters.on_environment('product/canary-1')).to eq([cluster1, cluster2])
end
context 'when relevant_only option is specified' do
it 'returns only one relevant object' do
expect(project.clusters.on_environment(environment_name, relevant_only: true))
.to eq([cluster2])
end
it 'returns only the most relevant object if relevant_only is true' do
create(:cluster, projects: [project], environment_scope: '*')
cluster2 = create(:cluster, projects: [project], environment_scope: 'product/*')
create(:cluster, projects: [project], environment_scope: 'staging/*')
expect(project.clusters.on_environment('product/canary-1', relevant_only: true)).to eq([cluster2])
end
it 'returns scopes ordered by lowest precedence first' do
create(:cluster, projects: [project], environment_scope: '*')
create(:cluster, projects: [project], environment_scope: 'production*')
create(:cluster, projects: [project], environment_scope: 'production')
result = project.clusters.on_environment('production').map(&:environment_scope)
expect(result).to eq(['*', 'production*', 'production'])
end
end
......
......@@ -48,6 +48,163 @@ describe Operations::FeatureFlagScope do
expect { default_scope.destroy! }.to raise_error(ActiveRecord::ReadOnlyRecord)
end
end
describe 'strategy validations' do
it 'handles null strategies which can occur while adding the column during migration' do
scope = create(:operations_feature_flag_scope, active: true)
allow(scope).to receive(:strategies).and_return(nil)
scope.active = false
scope.save
expect(scope.errors[:strategies]).to be_empty
end
it 'validates multiple strategies' do
feature_flag = create(:operations_feature_flag)
scope = described_class.create(feature_flag: feature_flag,
strategies: [{ name: "default", parameters: {} },
{ name: "invalid", parameters: {} }])
expect(scope.errors[:strategies]).not_to be_empty
end
where(:invalid_value) do
[{}, 600, "bad", [{ name: 'default', parameters: {} }, 300]]
end
with_them do
it 'must be an array of strategy hashes' do
scope = create(:operations_feature_flag_scope)
scope.strategies = invalid_value
scope.save
expect(scope.errors[:strategies]).to eq(['must be an array of strategy hashes'])
end
end
describe 'name' do
using RSpec::Parameterized::TableSyntax
where(:name, :params, :expected) do
'default' | {} | []
'gradualRolloutUserId' | { groupId: 'mygroup', percentage: '50' } | []
5 | nil | ['strategy name is invalid']
nil | nil | ['strategy name is invalid']
"nothing" | nil | ['strategy name is invalid']
"" | nil | ['strategy name is invalid']
40.0 | nil | ['strategy name is invalid']
{} | nil | ['strategy name is invalid']
[] | nil | ['strategy name is invalid']
end
with_them do
it 'must be one of "default" or "gradualRolloutUserId"' do
feature_flag = create(:operations_feature_flag)
scope = described_class.create(feature_flag: feature_flag, strategies: [{ name: name, parameters: params }])
expect(scope.errors[:strategies]).to eq(expected)
end
end
end
describe 'parameters' do
context 'when the strategy name is gradualRolloutUserId' do
describe 'percentage' do
where(:invalid_value) do
[50, 40.0, { key: "value" }, "garbage", "00", "01", "101", "-1", "-10", "0100",
"1000", "10.0", "5%", "25%", "100hi", "e100", "30m", " ", "\r\n", "\n", "\t",
"\n10", "20\n", "\n100", "100\n", "\n ", nil]
end
with_them do
it 'must be a string value between 0 and 100 inclusive and without a percentage sign' do
feature_flag = create(:operations_feature_flag)
scope = described_class.create(feature_flag: feature_flag,
strategies: [{ name: 'gradualRolloutUserId',
parameters: { groupId: 'mygroup', percentage: invalid_value } }])
expect(scope.errors[:strategies]).to eq(['percentage must be a string between 0 and 100 inclusive'])
end
end
where(:valid_value) do
%w[0 1 10 38 100 93]
end
with_them do
it 'must be a string value between 0 and 100 inclusive and without a percentage sign' do
feature_flag = create(:operations_feature_flag)
scope = described_class.create(feature_flag: feature_flag,
strategies: [{ name: 'gradualRolloutUserId',
parameters: { groupId: 'mygroup', percentage: valid_value } }])
expect(scope.errors[:strategies]).to eq([])
end
end
end
describe 'groupId' do
where(:invalid_value) do
[nil, 4, 50.0, {}, 'spaces bad', 'bad$', '%bad', '<bad', 'bad>', '!bad',
'.bad', 'Bad', 'bad1', "", " ", "b" * 33, "ba_d", "ba\nd"]
end
with_them do
it 'must be a string value of up to 32 lowercase characters' do
feature_flag = create(:operations_feature_flag)
scope = described_class.create(feature_flag: feature_flag,
strategies: [{ name: 'gradualRolloutUserId',
parameters: { groupId: invalid_value, percentage: '40' } }])
expect(scope.errors[:strategies]).to eq(['groupId parameter is invalid'])
end
end
where(:valid_value) do
["somegroup", "anothergroup", "okay", "g", "a" * 32]
end
with_them do
it 'must be a string value of up to 32 lowercase characters' do
feature_flag = create(:operations_feature_flag)
scope = described_class.create(feature_flag: feature_flag, strategies: [{ name: 'gradualRolloutUserId',
parameters: { groupId: valid_value, percentage: '40' } }])
expect(scope.errors[:strategies]).to eq([])
end
end
end
it 'must have parameters' do
feature_flag = create(:operations_feature_flag)
scope = described_class.create(feature_flag: feature_flag, strategies: [{ name: 'gradualRolloutUserId' }])
expect(scope.errors[:strategies]).to include('groupId parameter is invalid')
expect(scope.errors[:strategies]).to include('percentage must be a string between 0 and 100 inclusive')
end
end
context 'when the strategy name is default' do
where(:invalid_value) do
[{ groupId: "hi", percentage: "7" }, "", "nothing", 7, nil]
end
with_them do
it 'must be empty' do
feature_flag = create(:operations_feature_flag)
scope = described_class.create(feature_flag: feature_flag,
strategies: [{ name: 'default',
parameters: invalid_value }])
expect(scope.errors[:strategies]).to eq(['parameters must be empty for default strategy'])
end
end
it 'must be empty' do
feature_flag = create(:operations_feature_flag)
scope = described_class.create(feature_flag: feature_flag,
strategies: [{ name: 'default',
parameters: {} }])
expect(scope.errors[:strategies]).to eq([])
end
end
end
end
end
describe '.enabled' do
......@@ -97,4 +254,28 @@ describe Operations::FeatureFlagScope do
end
end
end
describe '.for_unleash_client' do
it 'returns scopes for the specified project' do
project1 = create(:project)
project2 = create(:project)
expected_feature_flag = create(:operations_feature_flag, project: project1)
create(:operations_feature_flag, project: project2)
scopes = described_class.for_unleash_client(project1, 'sandbox').to_a
expect(scopes).to contain_exactly(*expected_feature_flag.scopes)
end
it 'returns a scope that matches exactly over a match with a wild card' do
project = create(:project)
feature_flag = create(:operations_feature_flag, project: project)
create(:operations_feature_flag_scope, feature_flag: feature_flag, environment_scope: 'production*')
expected_scope = create(:operations_feature_flag_scope, feature_flag: feature_flag, environment_scope: 'production')
scopes = described_class.for_unleash_client(project, 'production').to_a
expect(scopes).to contain_exactly(expected_scope)
end
end
end
......@@ -92,73 +92,6 @@ describe Operations::FeatureFlag do
end
end
describe '.for_environment' do
subject { described_class.for_environment(environment_name) }
context 'when feature flag is off on production' do
before do
feature_flag = create(:operations_feature_flag, active: true)
create_scope(feature_flag, 'production', false)
end
context 'when environment is production' do
let(:environment_name) { 'production' }
it 'returns actual active value' do
expect(subject.first.active).to be_falsy
end
end
context 'when environment is staging' do
let(:environment_name) { 'staging' }
it 'returns actual active value' do
expect(subject.first.active).to be_truthy
end
end
end
context 'when feature flag is default disabled but enabled for review apps' do
before do
feature_flag = create(:operations_feature_flag, active: false)
create_scope(feature_flag, 'review/*', true)
end
context 'when environment is review app' do
let(:environment_name) { 'review/patch-1' }
it 'returns actual active value' do
expect(subject.first.active).to be_truthy
end
end
context 'when environment is production' do
let(:environment_name) { 'production' }
it 'returns actual active value' do
expect(subject.first.active).to be_falsy
end
end
end
context 'when there are two flags' do
let!(:feature_flag_1) { create(:operations_feature_flag, active: true) }
let!(:feature_flag_2) { create(:operations_feature_flag, active: true) }
before do
create_scope(feature_flag_1, 'production', false)
end
context 'when environment is production' do
let(:environment_name) { 'production' }
it 'returns multiple actual active values' do
expect(subject.ordered.map(&:active)).to eq([false, true])
end
end
end
end
describe '.for_list' do
subject { described_class.for_list }
......
......@@ -74,11 +74,11 @@ describe API::Unleash do
let(:headers) { base_headers.merge({ "UNLEASH-APPNAME" => "test" }) }
let!(:feature_flag_1) do
create(:operations_feature_flag, project: project, active: true)
create(:operations_feature_flag, name: "feature_flag_1", project: project, active: true)
end
let!(:feature_flag_2) do
create(:operations_feature_flag, project: project, active: false)
create(:operations_feature_flag, name: "feature_flag_2", project: project, active: false)
end
before do
......@@ -92,20 +92,17 @@ describe API::Unleash do
expect(recorded.count).to be_within(8).of(10)
end
it 'calls for_environment method' do
expect(Operations::FeatureFlag).to receive(:for_environment)
subject
end
context 'when app name is staging' do
let(:headers) { base_headers.merge({ "UNLEASH-APPNAME" => "staging" }) }
it 'returns correct active values' do
subject
expect(json_response['features'].first['enabled']).to be_truthy
expect(json_response['features'].second['enabled']).to be_falsy
feature_flag_1 = json_response['features'].select { |f| f['name'] == 'feature_flag_1' }.first
feature_flag_2 = json_response['features'].select { |f| f['name'] == 'feature_flag_2' }.first
expect(feature_flag_1['enabled']).to eq(true)
expect(feature_flag_2['enabled']).to eq(false)
end
end
......@@ -115,8 +112,11 @@ describe API::Unleash do
it 'returns correct active values' do
subject
expect(json_response['features'].first['enabled']).to be_falsy
expect(json_response['features'].second['enabled']).to be_falsy
feature_flag_1 = json_response['features'].select { |f| f['name'] == 'feature_flag_1' }.first
feature_flag_2 = json_response['features'].select { |f| f['name'] == 'feature_flag_2' }.first
expect(feature_flag_1['enabled']).to eq(false)
expect(feature_flag_2['enabled']).to eq(false)
end
end
......@@ -126,8 +126,11 @@ describe API::Unleash do
it 'returns correct active values' do
subject
expect(json_response['features'].first['enabled']).to be_truthy
expect(json_response['features'].second['enabled']).to be_truthy
feature_flag_1 = json_response['features'].select { |f| f['name'] == 'feature_flag_1' }.first
feature_flag_2 = json_response['features'].select { |f| f['name'] == 'feature_flag_2' }.first
expect(feature_flag_1['enabled']).to eq(true)
expect(feature_flag_2['enabled']).to eq(true)
end
end
......@@ -144,9 +147,9 @@ describe API::Unleash do
%w(/feature_flags/unleash/:project_id/features /feature_flags/unleash/:project_id/client/features).each do |features_endpoint|
describe "GET #{features_endpoint}" do
let(:features_url) { features_endpoint.sub(':project_id', project_id) }
let(:features_url) { features_endpoint.sub(':project_id', project_id.to_s) }
subject { get api("/feature_flags/unleash/#{project_id}/features"), params: params, headers: headers }
subject { get api(features_url), params: params, headers: headers }
it_behaves_like 'authenticated request'
it_behaves_like 'support multiple environments'
......@@ -157,13 +160,13 @@ describe API::Unleash do
let!(:enable_feature_flag) { create(:operations_feature_flag, project: project, name: 'feature1', active: true) }
let!(:disabled_feature_flag) { create(:operations_feature_flag, project: project, name: 'feature2', active: false) }
it 'responds with a list' do
it 'responds with a list of features' do
subject
expect(response).to have_gitlab_http_status(200)
expect(json_response['version']).to eq(1)
expect(json_response['features']).not_to be_empty
expect(json_response['features'].first['name']).to eq('feature1')
expect(json_response['features'].map { |f| f['name'] }.sort).to eq(%w[feature1 feature2])
end
it 'matches json schema' do
......@@ -173,6 +176,43 @@ describe API::Unleash do
expect(response).to match_response_schema('unleash/unleash', dir: 'ee')
end
end
it 'returns a feature flag strategy' do
client = create(:operations_feature_flags_client, project: project)
feature_flag = create(:operations_feature_flag, project: project, name: 'feature1', active: true)
create(:operations_feature_flag_scope,
feature_flag: feature_flag,
environment_scope: 'sandbox',
active: true,
strategies: [{ name: "gradualRolloutUserId",
parameters: { groupId: "default", percentage: "50" } }])
headers = { "UNLEASH-INSTANCEID" => client.token, "UNLEASH-APPNAME" => "sandbox" }
get api(features_url), headers: headers
expect(response).to have_gitlab_http_status(:ok)
strategies = json_response['features'].first['strategies']
expect(strategies).to eq([{
"name" => "gradualRolloutUserId",
"parameters" => {
"percentage" => "50",
"groupId" => "default"
}
}])
end
it 'returns a default strategy for a scope' do
client = create(:operations_feature_flags_client, project: project)
feature_flag = create(:operations_feature_flag, project: project, name: 'feature1', active: true)
create(:operations_feature_flag_scope, feature_flag: feature_flag, environment_scope: 'sandbox', active: true)
headers = { "UNLEASH-INSTANCEID" => client.token, "UNLEASH-APPNAME" => "sandbox" }
get api(features_url), headers: headers
expect(response).to have_gitlab_http_status(:ok)
strategies = json_response['features'].first['strategies']
expect(strategies).to eq([{ "name" => "default", "parameters" => {} }])
end
end
end
......
......@@ -6,11 +6,12 @@ module FeatureFlagHelpers
description: description, project: project)
end
def create_scope(feature_flag, environment_scope, active)
def create_scope(feature_flag, environment_scope, active, strategies = [{ name: "default", parameters: {} }])
create(:operations_feature_flag_scope,
feature_flag: feature_flag,
environment_scope: environment_scope,
active: active)
active: active,
strategies: strategies)
end
def within_feature_flag_row(index)
......
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