Commit 5a72461a authored by Shinya Maeda's avatar Shinya Maeda

Merge branch 'afontaine/remove-legacy-flag-enum' into 'master'

Remove Legacy Flag from Feature Flag Enum

See merge request gitlab-org/gitlab!68408
parents 76f9e2bd c229d56e
......@@ -10,9 +10,6 @@ class Projects::FeatureFlagsController < Projects::ApplicationController
before_action :feature_flag, only: [:edit, :update, :destroy]
before_action :ensure_flag_writable!, only: [:update]
before_action :exclude_legacy_flags_check, only: [:edit]
feature_category :feature_flags
def index
......@@ -98,18 +95,6 @@ class Projects::FeatureFlagsController < Projects::ApplicationController
@feature_flag ||= @noteable = project.operations_feature_flags.find_by_iid!(params[:iid])
end
def ensure_flag_writable!
if feature_flag.legacy_flag?
render_error_json(['Legacy feature flags are read-only'])
end
end
def exclude_legacy_flags_check
if feature_flag.legacy_flag?
not_found
end
end
def create_params
params.require(:operations_feature_flag)
.permit(:name, :description, :active, :version,
......
......@@ -17,6 +17,7 @@ module Operations
has_internal_id :iid, scope: :project
default_value_for :active, true
default_value_for :version, :new_version_flag
# scopes exists only for the first version
has_many :scopes, class_name: 'Operations::FeatureFlagScope'
......@@ -39,8 +40,6 @@ module Operations
validate :first_default_scope, on: :create, if: :has_scopes?
validate :version_associations
before_create :build_default_scope, if: -> { legacy_flag? && scopes.none? }
accepts_nested_attributes_for :scopes, allow_destroy: true
accepts_nested_attributes_for :strategies, allow_destroy: true
......@@ -52,7 +51,6 @@ module Operations
scope :new_version_only, -> { where(version: :new_version_flag)}
enum version: {
legacy_flag: 1,
new_version_flag: 2
}
......@@ -127,8 +125,6 @@ module Operations
def version_associations
if new_version_flag? && scopes.any?
errors.add(:version_associations, 'version 2 feature flags may not have scopes')
elsif legacy_flag? && strategies.any?
errors.add(:version_associations, 'version 1 feature flags may not have strategies')
end
end
......
# frozen_string_literal: true
# All of the legacy flags have been removed in 14.1, including all of the
# `operations_feature_flag_scopes` rows. Therefore, this model and the database
# table are unused and should be removed.
module Operations
class FeatureFlagScope < ApplicationRecord
prepend HasEnvironmentScope
......
......@@ -48,10 +48,12 @@ module FeatureFlags
end
end
def created_scope_message(scope)
"Created rule #{scope.environment_scope} "\
"and set it as #{scope.active ? "active" : "inactive"} "\
"with strategies #{scope.strategies}."
def created_strategy_message(strategy)
scopes = strategy.scopes
.map { |scope| %Q("#{scope.environment_scope}") }
.join(', ')
%Q(Created strategy \"#{strategy.name}\" )\
"with scopes #{scopes}."
end
def feature_flag_by_name
......
......@@ -24,8 +24,8 @@ module FeatureFlags
def audit_message(feature_flag)
message_parts = ["Created feature flag #{feature_flag.name} with description \"#{feature_flag.description}\"."]
message_parts += feature_flag.scopes.map do |scope|
created_scope_message(scope)
message_parts += feature_flag.strategies.map do |strategy|
created_strategy_message(strategy)
end
message_parts.join(" ")
......
......@@ -2,10 +2,9 @@
module FeatureFlags
class UpdateService < FeatureFlags::BaseService
AUDITABLE_SCOPE_ATTRIBUTES_HUMAN_NAMES = {
'active' => 'active state',
'environment_scope' => 'environment scope',
'strategies' => 'strategies'
AUDITABLE_STRATEGY_ATTRIBUTES_HUMAN_NAMES = {
'scopes' => 'environment scopes',
'parameters' => 'parameters'
}.freeze
def execute(feature_flag)
......@@ -41,7 +40,7 @@ module FeatureFlags
def audit_message(feature_flag)
changes = changed_attributes_messages(feature_flag)
changes += changed_scopes_messages(feature_flag)
changes += changed_strategies_messages(feature_flag)
return if changes.empty?
......@@ -56,29 +55,30 @@ module FeatureFlags
end
end
def changed_scopes_messages(feature_flag)
feature_flag.scopes.map do |scope|
if scope.new_record?
created_scope_message(scope)
elsif scope.marked_for_destruction?
deleted_scope_message(scope)
def changed_strategies_messages(feature_flag)
feature_flag.strategies.map do |strategy|
if strategy.new_record?
created_strategy_message(strategy)
elsif strategy.marked_for_destruction?
deleted_strategy_message(strategy)
else
updated_scope_message(scope)
updated_strategy_message(strategy)
end
end.compact # updated_scope_message can return nil if nothing has been changed
end.compact # updated_strategy_message can return nil if nothing has been changed
end
def deleted_scope_message(scope)
"Deleted rule #{scope.environment_scope}."
def deleted_strategy_message(strategy)
scopes = strategy.scopes.map { |scope| scope.environment_scope }.join(', ')
"Deleted strategy #{strategy.name} with environment scopes #{scopes}."
end
def updated_scope_message(scope)
changes = scope.changes.slice(*AUDITABLE_SCOPE_ATTRIBUTES_HUMAN_NAMES.keys)
def updated_strategy_message(strategy)
changes = strategy.changes.slice(*AUDITABLE_STRATEGY_ATTRIBUTES_HUMAN_NAMES.keys)
return if changes.empty?
message = "Updated rule #{scope.environment_scope} "
message = "Updated strategy #{strategy.name} "
message += changes.map do |attribute_name, change|
name = AUDITABLE_SCOPE_ATTRIBUTES_HUMAN_NAMES[attribute_name]
name = AUDITABLE_STRATEGY_ATTRIBUTES_HUMAN_NAMES[attribute_name]
"#{name} from #{change.first} to #{change.second}"
end.join(' ')
......
......@@ -126,6 +126,7 @@ From there, you can see the following actions:
- User password required for approvals was updated ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/336211) in GitLab 14.2)
- Permission to modify merge requests approval rules in merge requests was updated ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/336211) in GitLab 14.2)
- New approvals requirement when new commits are added to an MR was updated ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/336211) in GitLab 14.2)
- When [strategies for feature flags](../operations/feature_flags.md#feature-flag-strategies) are changed ([introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68408) in GitLab 14.3)
Project events can also be accessed via the [Project Audit Events API](../api/audit_events.md#project-audit-events).
......
......@@ -118,7 +118,7 @@ module API
put do
authorize_update_feature_flag!
exclude_legacy_flags_check!
render_api_error!('PUT operations are not supported for legacy feature flags', :unprocessable_entity) if feature_flag.legacy_flag?
render_api_error!('PUT operations are not supported for legacy feature flags', :unprocessable_entity) unless feature_flag.new_version_flag?
attrs = declared_params(include_missing: false)
......@@ -207,7 +207,7 @@ module API
end
def exclude_legacy_flags_check!
if feature_flag.legacy_flag?
unless feature_flag.new_version_flag?
not_found!
end
end
......
......@@ -94,20 +94,6 @@ RSpec.describe Projects::FeatureFlagsController do
is_expected.to match_response_schema('feature_flags')
end
it 'returns false for active when the feature flag is inactive even if it has an active scope' do
create(:operations_feature_flag_scope,
feature_flag: feature_flag_inactive,
environment_scope: 'production',
active: true)
subject
expect(response).to have_gitlab_http_status(:ok)
feature_flag_json = json_response['feature_flags'].second
expect(feature_flag_json['active']).to eq(false)
end
it 'returns the feature flag iid' do
subject
......@@ -181,7 +167,7 @@ RSpec.describe Projects::FeatureFlagsController do
subject { get(:show, params: params, format: :json) }
let!(:feature_flag) do
create(:operations_feature_flag, :legacy_flag, project: project)
create(:operations_feature_flag, project: project)
end
let(:params) do
......@@ -197,7 +183,7 @@ RSpec.describe Projects::FeatureFlagsController do
expect(json_response['name']).to eq(feature_flag.name)
expect(json_response['active']).to eq(feature_flag.active)
expect(json_response['version']).to eq('legacy_flag')
expect(json_response['version']).to eq('new_version_flag')
end
it 'matches json schema' do
......@@ -245,46 +231,6 @@ RSpec.describe Projects::FeatureFlagsController do
end
end
context 'when feature flags have additional scopes' do
context 'when there is at least one active scope' do
let!(:feature_flag) do
create(:operations_feature_flag, project: project, active: false)
end
let!(:feature_flag_scope_production) do
create(:operations_feature_flag_scope,
feature_flag: feature_flag,
environment_scope: 'review/*',
active: true)
end
it 'returns false for active' do
subject
expect(json_response['active']).to eq(false)
end
end
context 'when all scopes are inactive' do
let!(:feature_flag) do
create(:operations_feature_flag, project: project, active: false)
end
let!(:feature_flag_scope_production) do
create(:operations_feature_flag_scope,
feature_flag: feature_flag,
environment_scope: 'production',
active: false)
end
it 'recognizes the feature flag as inactive' do
subject
expect(json_response['active']).to be_falsy
end
end
end
context 'with a version 2 feature flag' do
let!(:new_version_feature_flag) do
create(:operations_feature_flag, :new_version_flag, project: project)
......@@ -320,22 +266,6 @@ RSpec.describe Projects::FeatureFlagsController do
describe 'GET edit' do
subject { get(:edit, params: params) }
context 'with legacy flags' do
let!(:feature_flag) { create(:operations_feature_flag, :legacy_flag, project: project) }
let(:params) do
{
namespace_id: project.namespace,
project_id: project,
iid: feature_flag.iid
}
end
it 'returns not found' do
is_expected.to have_gitlab_http_status(:not_found)
end
end
context 'with new version flags' do
let!(:feature_flag) { create(:operations_feature_flag, project: project) }
......@@ -378,14 +308,6 @@ RSpec.describe Projects::FeatureFlagsController do
expect(json_response['active']).to be_truthy
end
it 'creates a default scope' do
subject
expect(json_response['scopes'].count).to eq(1)
expect(json_response['scopes'].first['environment_scope']).to eq('*')
expect(json_response['scopes'].first['active']).to be_truthy
end
it 'matches json schema' do
is_expected.to match_response_schema('feature_flag')
end
......@@ -435,119 +357,6 @@ RSpec.describe Projects::FeatureFlagsController do
end
end
context 'when creates additional scope' do
let(:params) do
view_params.merge({
operations_feature_flag: {
name: 'my_feature_flag',
active: true,
scopes_attributes: [{ environment_scope: '*', active: true },
{ environment_scope: 'production', active: false }]
}
})
end
it 'creates feature flag scopes successfully' do
expect { subject }.to change { Operations::FeatureFlagScope.count }.by(2)
expect(response).to have_gitlab_http_status(:ok)
end
it 'creates feature flag scopes in a correct order' do
subject
expect(json_response['scopes'].first['environment_scope']).to eq('*')
expect(json_response['scopes'].second['environment_scope']).to eq('production')
end
context 'when default scope is not placed first' do
let(:params) do
view_params.merge({
operations_feature_flag: {
name: 'my_feature_flag',
active: true,
scopes_attributes: [{ environment_scope: 'production', active: false },
{ environment_scope: '*', active: true }]
}
})
end
it 'returns 400' do
subject
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['message'])
.to include('Default scope has to be the first element')
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 additional scope with a userWithId strategy' 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: 'userWithId',
parameters: { userIds: '123,4,6722' } }] }]
}
})
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' => 'userWithId',
'parameters' => { "userIds" => "123,4,6722" }
}])
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
context 'when creating a version 2 feature flag' do
let(:params) do
{
......@@ -744,7 +553,7 @@ RSpec.describe Projects::FeatureFlagsController do
describe 'DELETE destroy.json' do
subject { delete(:destroy, params: params, format: :json) }
let!(:feature_flag) { create(:operations_feature_flag, :legacy_flag, project: project) }
let!(:feature_flag) { create(:operations_feature_flag, project: project) }
let(:params) do
{
......@@ -762,10 +571,6 @@ RSpec.describe Projects::FeatureFlagsController do
expect { subject }.to change { Operations::FeatureFlag.count }.by(-1)
end
it 'destroys the default scope' do
expect { subject }.to change { Operations::FeatureFlagScope.count }.by(-1)
end
it 'matches json schema' do
is_expected.to match_response_schema('feature_flag')
end
......@@ -792,14 +597,6 @@ RSpec.describe Projects::FeatureFlagsController do
end
end
context 'when there is an additional scope' do
let!(:scope) { create_scope(feature_flag, 'production', false) }
it 'destroys the default scope and production scope' do
expect { subject }.to change { Operations::FeatureFlagScope.count }.by(-2)
end
end
context 'with a version 2 flag' do
let!(:new_version_flag) { create(:operations_feature_flag, :new_version_flag, project: project) }
let(:params) do
......@@ -828,70 +625,9 @@ RSpec.describe Projects::FeatureFlagsController do
put(:update, params: params, format: :json, as: :json)
end
context 'with a legacy feature flag' do
subject { put(:update, params: params, format: :json) }
let!(:feature_flag) do
create(:operations_feature_flag,
:legacy_flag,
name: 'ci_live_trace',
active: true,
project: project)
end
let(:params) do
{
namespace_id: project.namespace,
project_id: project,
iid: feature_flag.iid,
operations_feature_flag: {
name: 'ci_new_live_trace'
}
}
end
context 'when user is reporter' do
let(:user) { reporter }
it 'returns 404' do
is_expected.to have_gitlab_http_status(:not_found)
end
end
context "when changing default scope's spec" do
let(:params) do
{
namespace_id: project.namespace,
project_id: project,
iid: feature_flag.iid,
operations_feature_flag: {
scopes_attributes: [
{
id: feature_flag.default_scope.id,
environment_scope: 'review/*'
}
]
}
}
end
it 'returns 400' do
is_expected.to have_gitlab_http_status(:bad_request)
end
end
it 'does not update a legacy feature flag' do
put_request(feature_flag, name: 'ci_new_live_trace')
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['message']).to eq(["Legacy feature flags are read-only"])
end
end
context 'with a version 2 feature flag' do
let!(:new_version_flag) do
create(:operations_feature_flag,
:new_version_flag,
name: 'new-feature',
active: true,
project: project)
......
# frozen_string_literal: true
FactoryBot.define do
factory :operations_feature_flag_scope, class: 'Operations::FeatureFlagScope' do
association :feature_flag, factory: [:operations_feature_flag, :legacy_flag]
active { true }
strategies { [{ name: "default", parameters: {} }] }
sequence(:environment_scope) { |n| "review/patch-#{n}" }
end
end
......@@ -6,13 +6,5 @@ FactoryBot.define do
project
active { true }
version { :new_version_flag }
trait :legacy_flag do
version { Operations::FeatureFlag.versions['legacy_flag'] }
end
trait :new_version_flag do
version { Operations::FeatureFlag.versions['new_version_flag'] }
end
end
end
......@@ -66,20 +66,4 @@ RSpec.describe 'User updates feature flag', :js do
end
end
end
context 'with a legacy feature flag' do
let!(:feature_flag) do
create_flag(project, 'ci_live_trace', true,
description: 'For live trace feature',
version: :legacy_flag)
end
let!(:scope) { create_scope(feature_flag, 'review/*', true) }
it 'shows not found error' do
visit(edit_project_feature_flag_path(project, feature_flag))
expect(page).to have_text 'Page Not Found'
end
end
end
......@@ -72,13 +72,5 @@ RSpec.describe FeatureFlagsFinder do
subject
end
end
context 'with a legacy flag' do
let!(:feature_flag_3) { create(:operations_feature_flag, :legacy_flag, name: 'flag-c', project: project) }
it 'returns new flags' do
is_expected.to eq([feature_flag_1, feature_flag_2])
end
end
end
end
{
"type": "object",
"required" : [
"id",
"name"
],
"properties" : {
"required": ["id", "name"],
"properties": {
"id": { "type": "integer" },
"iid": { "type": ["integer", "null"] },
"version": { "type": "string" },
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Operations::FeatureFlagScope do
describe 'associations' do
it { is_expected.to belong_to(:feature_flag) }
end
describe 'validations' do
context 'when duplicate environment scope is going to be created' do
let!(:existing_feature_flag_scope) do
create(:operations_feature_flag_scope)
end
let(:new_feature_flag_scope) do
build(:operations_feature_flag_scope,
feature_flag: existing_feature_flag_scope.feature_flag,
environment_scope: existing_feature_flag_scope.environment_scope)
end
it 'validates uniqueness of environment scope' do
new_feature_flag_scope.save
expect(new_feature_flag_scope.errors[:environment_scope])
.to include("(#{existing_feature_flag_scope.environment_scope})" \
" has already been taken")
end
end
context 'when environment scope of a default scope is updated' do
let!(:feature_flag) { create(:operations_feature_flag, :legacy_flag) }
let!(:scope_default) { feature_flag.default_scope }
it 'keeps default scope intact' do
scope_default.update(environment_scope: 'review/*')
expect(scope_default.errors[:environment_scope])
.to include("cannot be changed from default scope")
end
end
context 'when a default scope is destroyed' do
let!(:feature_flag) { create(:operations_feature_flag, :legacy_flag) }
let!(:scope_default) { feature_flag.default_scope }
it 'prevents from destroying the default scope' do
expect { scope_default.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,
environment_scope: 'production', active: true,
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' } | []
'userWithId' | { userIds: 'sam' } | []
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", "gradualRolloutUserId", or "userWithId"' do
feature_flag = create(:operations_feature_flag)
scope = described_class.create(feature_flag: feature_flag,
environment_scope: 'production', active: true,
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
it 'must have parameters' do
feature_flag = create(:operations_feature_flag)
scope = described_class.create(feature_flag: feature_flag,
environment_scope: 'production', active: true,
strategies: [{ name: 'gradualRolloutUserId' }])
expect(scope.errors[:strategies]).to eq(['parameters are invalid'])
end
where(:invalid_parameters) do
[nil, {}, { percentage: '40', groupId: 'mygroup', userIds: '4' }, { percentage: '40' },
{ percentage: '40', groupId: 'mygroup', extra: nil }, { groupId: 'mygroup' }]
end
with_them do
it 'must have valid parameters for the strategy' do
feature_flag = create(:operations_feature_flag)
scope = described_class.create(feature_flag: feature_flag,
environment_scope: 'production', active: true,
strategies: [{ name: 'gradualRolloutUserId',
parameters: invalid_parameters }])
expect(scope.errors[:strategies]).to eq(['parameters are invalid'])
end
end
it 'allows the parameters in any order' do
feature_flag = create(:operations_feature_flag)
scope = described_class.create(feature_flag: feature_flag,
environment_scope: 'production', active: true,
strategies: [{ name: 'gradualRolloutUserId',
parameters: { percentage: '10', groupId: 'mygroup' } }])
expect(scope.errors[:strategies]).to be_empty
end
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,
environment_scope: 'production', active: true,
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,
environment_scope: 'production', active: true,
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,
environment_scope: 'production', active: true,
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,
environment_scope: 'production', active: true,
strategies: [{ name: 'gradualRolloutUserId',
parameters: { groupId: valid_value, percentage: '40' } }])
expect(scope.errors[:strategies]).to eq([])
end
end
end
end
context 'when the strategy name is userWithId' do
it 'must have parameters' do
feature_flag = create(:operations_feature_flag)
scope = described_class.create(feature_flag: feature_flag,
environment_scope: 'production', active: true,
strategies: [{ name: 'userWithId' }])
expect(scope.errors[:strategies]).to eq(['parameters are invalid'])
end
where(:invalid_parameters) do
[nil, { userIds: 'sam', percentage: '40' }, { userIds: 'sam', some: 'param' }, { percentage: '40' }, {}]
end
with_them do
it 'must have valid parameters for the strategy' do
feature_flag = create(:operations_feature_flag)
scope = described_class.create(feature_flag: feature_flag,
environment_scope: 'production', active: true,
strategies: [{ name: 'userWithId', parameters: invalid_parameters }])
expect(scope.errors[:strategies]).to eq(['parameters are invalid'])
end
end
describe 'userIds' do
where(:valid_value) do
["", "sam", "1", "a", "uuid-of-some-kind", "sam,fred,tom,jane,joe,mike",
"gitlab@example.com", "123,4", "UPPER,Case,charActeRS", "0",
"$valid$email#2345#$%..{}+=-)?\\/@example.com", "spaces allowed",
"a" * 256, "a,#{'b' * 256},ccc", "many spaces"]
end
with_them do
it 'is valid with a string of comma separated values' do
feature_flag = create(:operations_feature_flag)
scope = described_class.create(feature_flag: feature_flag,
environment_scope: 'production', active: true,
strategies: [{ name: 'userWithId', parameters: { userIds: valid_value } }])
expect(scope.errors[:strategies]).to be_empty
end
end
where(:invalid_value) do
[1, 2.5, {}, [], nil, "123\n456", "1,2,3,12\t3", "\n", "\n\r",
"joe\r,sam", "1,2,2", "1,,2", "1,2,,,,", "b" * 257, "1, ,2", "tim, ,7", " ",
" ", " ,1", "1, ", " leading,1", "1,trailing ", "1, both ,2"]
end
with_them do
it 'is invalid' do
feature_flag = create(:operations_feature_flag)
scope = described_class.create(feature_flag: feature_flag,
environment_scope: 'production', active: true,
strategies: [{ name: 'userWithId', parameters: { userIds: invalid_value } }])
expect(scope.errors[:strategies]).to include(
'userIds must be a string of unique comma separated values each 256 characters or less'
)
end
end
end
end
context 'when the strategy name is default' do
it 'must have parameters' do
feature_flag = create(:operations_feature_flag)
scope = described_class.create(feature_flag: feature_flag,
environment_scope: 'production', active: true,
strategies: [{ name: 'default' }])
expect(scope.errors[:strategies]).to eq(['parameters are invalid'])
end
where(:invalid_value) do
[{ groupId: "hi", percentage: "7" }, "", "nothing", 7, nil, [], 2.5]
end
with_them do
it 'must be empty' do
feature_flag = create(:operations_feature_flag)
scope = described_class.create(feature_flag: feature_flag,
environment_scope: 'production', active: true,
strategies: [{ name: 'default',
parameters: invalid_value }])
expect(scope.errors[:strategies]).to eq(['parameters are invalid'])
end
end
it 'must be empty' do
feature_flag = create(:operations_feature_flag)
scope = described_class.create(feature_flag: feature_flag,
environment_scope: 'production', active: true,
strategies: [{ name: 'default',
parameters: {} }])
expect(scope.errors[:strategies]).to be_empty
end
end
end
end
end
describe '.enabled' do
subject { described_class.enabled }
let!(:feature_flag_scope) do
create(:operations_feature_flag_scope, active: active)
end
context 'when scope is active' do
let(:active) { true }
it 'returns the scope' do
is_expected.to include(feature_flag_scope)
end
end
context 'when scope is inactive' do
let(:active) { false }
it 'returns an empty array' do
is_expected.not_to include(feature_flag_scope)
end
end
end
describe '.disabled' do
subject { described_class.disabled }
let!(:feature_flag_scope) do
create(:operations_feature_flag_scope, active: active)
end
context 'when scope is active' do
let(:active) { true }
it 'returns an empty array' do
is_expected.not_to include(feature_flag_scope)
end
end
context 'when scope is inactive' do
let(:active) { false }
it 'returns the scope' do
is_expected.to include(feature_flag_scope)
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
......@@ -49,28 +49,7 @@ RSpec.describe Operations::FeatureFlag do
it { is_expected.to validate_presence_of(:project) }
it { is_expected.to validate_presence_of(:name) }
it { is_expected.to validate_uniqueness_of(:name).scoped_to(:project_id) }
it { is_expected.to define_enum_for(:version).with_values(legacy_flag: 1, new_version_flag: 2) }
context 'a version 1 feature flag' do
it 'is valid if associated with Operations::FeatureFlagScope models' do
project = create(:project)
feature_flag = described_class.create!({ name: 'test', project: project, version: 1,
scopes_attributes: [{ environment_scope: '*', active: false }] })
expect(feature_flag).to be_valid
end
it 'is invalid if associated with Operations::FeatureFlags::Strategy models' do
project = create(:project)
feature_flag = described_class.new({ name: 'test', project: project, version: 1,
strategies_attributes: [{ name: 'default', parameters: {} }] })
expect(feature_flag.valid?).to eq(false)
expect(feature_flag.errors.messages).to eq({
version_associations: ["version 1 feature flags may not have strategies"]
})
end
end
it { is_expected.to define_enum_for(:version).with_values(new_version_flag: 2) }
context 'a version 2 feature flag' do
it 'is invalid if associated with Operations::FeatureFlagScope models' do
......@@ -102,64 +81,9 @@ RSpec.describe Operations::FeatureFlag do
end
end
describe 'feature flag version' do
it 'defaults to 1 if unspecified' do
project = create(:project)
feature_flag = described_class.create!(name: 'my_flag', project: project, active: true)
expect(feature_flag).to be_valid
expect(feature_flag.version_before_type_cast).to eq(1)
end
end
describe 'Scope creation' do
subject { described_class.new(**params) }
let(:project) { create(:project) }
let(:params) do
{ name: 'test', project: project, scopes_attributes: scopes_attributes }
end
let(:scopes_attributes) do
[{ environment_scope: '*', active: false },
{ environment_scope: 'review/*', active: true }]
end
it { is_expected.to be_valid }
context 'when the first scope is not wildcard' do
let(:scopes_attributes) do
[{ environment_scope: 'review/*', active: true },
{ environment_scope: '*', active: false }]
end
it { is_expected.not_to be_valid }
end
end
describe 'the default scope' do
let_it_be(:project) { create(:project) }
context 'with a version 1 feature flag' do
it 'creates a default scope' do
feature_flag = described_class.create!({ name: 'test', project: project, scopes_attributes: [], version: 1 })
expect(feature_flag.scopes.count).to eq(1)
expect(feature_flag.scopes.first.environment_scope).to eq('*')
end
it 'allows specifying the default scope in the parameters' do
feature_flag = described_class.create!({ name: 'test', project: project,
scopes_attributes: [{ environment_scope: '*', active: false },
{ environment_scope: 'review/*', active: true }], version: 1 })
expect(feature_flag.scopes.count).to eq(2)
expect(feature_flag.scopes.first.environment_scope).to eq('*')
end
end
context 'with a version 2 feature flag' do
it 'does not create a default scope' do
feature_flag = described_class.create!({ name: 'test', project: project, scopes_attributes: [], version: 2 })
......@@ -180,16 +104,6 @@ RSpec.describe Operations::FeatureFlag do
end
end
context 'when the feature flag is active and all scopes are inactive' do
let!(:feature_flag) { create(:operations_feature_flag, :legacy_flag, active: true) }
it 'returns the flag' do
feature_flag.default_scope.update!(active: false)
is_expected.to eq([feature_flag])
end
end
context 'when the feature flag is inactive' do
let!(:feature_flag) { create(:operations_feature_flag, active: false) }
......@@ -197,16 +111,6 @@ RSpec.describe Operations::FeatureFlag do
is_expected.to be_empty
end
end
context 'when the feature flag is inactive and all scopes are active' do
let!(:feature_flag) { create(:operations_feature_flag, :legacy_flag, active: false) }
it 'does not return the flag' do
feature_flag.default_scope.update!(active: true)
is_expected.to be_empty
end
end
end
describe '.disabled' do
......@@ -220,16 +124,6 @@ RSpec.describe Operations::FeatureFlag do
end
end
context 'when the feature flag is active and all scopes are inactive' do
let!(:feature_flag) { create(:operations_feature_flag, :legacy_flag, active: true) }
it 'does not return the flag' do
feature_flag.default_scope.update!(active: false)
is_expected.to be_empty
end
end
context 'when the feature flag is inactive' do
let!(:feature_flag) { create(:operations_feature_flag, active: false) }
......@@ -237,16 +131,6 @@ RSpec.describe Operations::FeatureFlag do
is_expected.to eq([feature_flag])
end
end
context 'when the feature flag is inactive and all scopes are active' do
let!(:feature_flag) { create(:operations_feature_flag, :legacy_flag, active: false) }
it 'returns the flag' do
feature_flag.default_scope.update!(active: true)
is_expected.to eq([feature_flag])
end
end
end
describe '.for_unleash_client' do
......
......@@ -116,21 +116,6 @@ RSpec.describe API::FeatureFlags do
}])
end
end
context 'with version 1 and 2 feature flags' do
it 'returns both versions of flags ordered by name' do
create(:operations_feature_flag, project: project, name: 'legacy_flag')
feature_flag = create(:operations_feature_flag, :new_version_flag, project: project, name: 'new_version_flag')
strategy = create(:operations_strategy, feature_flag: feature_flag, name: 'default', parameters: {})
create(:operations_scope, strategy: strategy, environment_scope: 'production')
subject
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('public_api/v4/feature_flags')
expect(json_response.map { |f| f['name'] }).to eq(%w[legacy_flag new_version_flag])
end
end
end
describe 'GET /projects/:id/feature_flags/:name' do
......@@ -185,22 +170,13 @@ RSpec.describe API::FeatureFlags do
end
describe 'POST /projects/:id/feature_flags' do
def scope_default
{
environment_scope: '*',
active: false,
strategies: [{ name: 'default', parameters: {} }].to_json
}
end
subject do
post api("/projects/#{project.id}/feature_flags", user), params: params
end
let(:params) do
{
name: 'awesome-feature',
scopes: [scope_default]
name: 'awesome-feature'
}
end
......@@ -215,14 +191,14 @@ RSpec.describe API::FeatureFlags do
expect(feature_flag.description).to eq(params[:description])
end
it 'defaults to a version 1 (legacy) feature flag' do
it 'defaults to a version 2 (new) feature flag' do
subject
expect(response).to have_gitlab_http_status(:created)
expect(response).to match_response_schema('public_api/v4/feature_flag')
feature_flag = project.operations_feature_flags.last
expect(feature_flag.version).to eq('legacy_flag')
expect(feature_flag.version).to eq('new_version_flag')
end
it_behaves_like 'check user permission'
......@@ -232,38 +208,7 @@ RSpec.describe API::FeatureFlags do
expect(response).to have_gitlab_http_status(:created)
expect(response).to match_response_schema('public_api/v4/feature_flag')
expect(json_response['version']).to eq('legacy_flag')
end
context 'with active set to false in the params for a legacy flag' do
let(:params) do
{
name: 'awesome-feature',
version: 'legacy_flag',
active: 'false',
scopes: [scope_default]
}
end
it 'creates an inactive feature flag' do
subject
expect(response).to have_gitlab_http_status(:created)
expect(response).to match_response_schema('public_api/v4/feature_flag')
expect(json_response['active']).to eq(false)
end
end
context 'when no scopes passed in parameters' do
let(:params) { { name: 'awesome-feature' } }
it 'creates a new feature flag with active default scope' do
subject
expect(response).to have_gitlab_http_status(:created)
feature_flag = project.operations_feature_flags.last
expect(feature_flag.default_scope).to be_active
end
expect(json_response['version']).to eq('new_version_flag')
end
context 'when there is a feature flag with the same name already' do
......@@ -278,43 +223,6 @@ RSpec.describe API::FeatureFlags do
end
end
context 'when create a feature flag with two scopes' do
let(:params) do
{
name: 'awesome-feature',
description: 'this is awesome',
scopes: [
scope_default,
scope_with_user_with_id
]
}
end
let(:scope_with_user_with_id) do
{
environment_scope: 'production',
active: true,
strategies: [{
name: 'userWithId',
parameters: { userIds: 'user:1' }
}].to_json
}
end
it 'creates a new feature flag with two scopes' do
subject
expect(response).to have_gitlab_http_status(:created)
feature_flag = project.operations_feature_flags.last
feature_flag.scopes.ordered.each_with_index do |scope, index|
expect(scope.environment_scope).to eq(params[:scopes][index][:environment_scope])
expect(scope.active).to eq(params[:scopes][index][:active])
expect(scope.strategies).to eq(Gitlab::Json.parse(params[:scopes][index][:strategies]))
end
end
end
context 'when creating a version 2 feature flag' do
it 'creates a new feature flag' do
params = {
......@@ -455,23 +363,6 @@ RSpec.describe API::FeatureFlags do
end
describe 'PUT /projects/:id/feature_flags/:name' do
context 'with a legacy feature flag' do
let!(:feature_flag) do
create(:operations_feature_flag, :legacy_flag, project: project,
name: 'feature1', description: 'old description')
end
it 'returns a 404' do
params = { description: 'new description' }
put api("/projects/#{project.id}/feature_flags/feature1", user), params: params
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response).to eq({ 'message' => '404 Not Found' })
expect(feature_flag.reload.description).to eq('old description')
end
end
context 'with a version 2 feature flag' do
let!(:feature_flag) do
create(:operations_feature_flag, :new_version_flag, project: project, active: true,
......@@ -781,7 +672,7 @@ RSpec.describe API::FeatureFlags do
params: params
end
let!(:feature_flag) { create(:operations_feature_flag, :legacy_flag, project: project) }
let!(:feature_flag) { create(:operations_feature_flag, project: project) }
let(:params) { {} }
it 'destroys the feature flag' do
......@@ -794,7 +685,7 @@ RSpec.describe API::FeatureFlags do
subject
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['version']).to eq('legacy_flag')
expect(json_response['version']).to eq('new_version_flag')
end
context 'with a version 2 feature flag' do
......
......@@ -176,25 +176,6 @@ RSpec.describe API::Unleash do
it_behaves_like 'authenticated request'
context 'with version 1 (legacy) feature flags' do
let(:feature_flag) { create(:operations_feature_flag, :legacy_flag, project: project, name: 'feature1', active: true, version: 1) }
it 'does not return a legacy feature flag' do
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)
expect(json_response['features']).to be_empty
end
end
context 'with version 2 feature flags' do
it 'does not return a flag without any strategies' do
create(:operations_feature_flag, project: project,
......
......@@ -48,8 +48,9 @@ RSpec.describe FeatureFlags::CreateService do
{
name: 'feature_flag',
description: 'description',
scopes_attributes: [{ environment_scope: '*', active: true },
{ environment_scope: 'production', active: false }]
version: 'new_version_flag',
strategies_attributes: [{ name: 'default', scopes_attributes: [{ environment_scope: '*' }], parameters: {} },
{ name: 'default', parameters: {}, scopes_attributes: [{ environment_scope: 'production' }] }]
}
end
......@@ -70,10 +71,10 @@ RSpec.describe FeatureFlags::CreateService do
it 'creates audit event' do
expected_message = 'Created feature flag feature_flag '\
'with description "description". '\
'Created rule * and set it as active '\
'with strategies [{"name"=&gt;"default", "parameters"=&gt;{}}]. '\
'Created rule production and set it as inactive '\
'with strategies [{"name"=&gt;"default", "parameters"=&gt;{}}].'
'Created strategy "default" with scopes '\
'"*". '\
'Created strategy "default" with scopes '\
'"production".'
expect { subject }.to change { AuditEvent.count }.by(1)
expect(AuditEvent.last.details[:custom_message]).to eq(expected_message)
......
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