Commit c229d56e authored by Andrew Fontaine's avatar Andrew Fontaine

Remove Legacy Flag from Feature Flag Enum

The beginning of deleting all the legacy feature flags code involves
removing the enum value for legacy flags. Some changes need to be made
to keep the checks (basically checking that everything is always a
new_version_flag).
parent cc12468f
......@@ -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
......
......@@ -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,
......
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