diff --git a/changelogs/unreleased/introduce-feature-flag-api.yml b/changelogs/unreleased/introduce-feature-flag-api.yml
new file mode 100644
index 0000000000000000000000000000000000000000..fa6c3be302fab3f44a2ce0f76431793dfd54b357
--- /dev/null
+++ b/changelogs/unreleased/introduce-feature-flag-api.yml
@@ -0,0 +1,5 @@
+---
+title: Support Create/Read/Destroy operations in Feature Flag API
+merge_request: 18198
+author:
+type: added
diff --git a/ee/app/finders/feature_flags_finder.rb b/ee/app/finders/feature_flags_finder.rb
index 60a2ffac6ded465f8e5fd15bea87908014614af4..5c1945ae6ac58063c2e235ed718f0c49844345d9 100644
--- a/ee/app/finders/feature_flags_finder.rb
+++ b/ee/app/finders/feature_flags_finder.rb
@@ -10,7 +10,7 @@ def initialize(project, current_user, params = {})
@params = params
end
- def execute
+ def execute(preload: true)
unless Ability.allowed?(current_user, :read_feature_flag, project)
return Operations::FeatureFlag.none
end
@@ -19,6 +19,7 @@ def execute
items = by_scope(items)
items = for_list(items)
+ items = items.preload_relations if preload
items.ordered
end
diff --git a/ee/app/serializers/feature_flag_serializer.rb b/ee/app/serializers/feature_flag_serializer.rb
index 3eebafa0bdda10368e7b2f1df1ba39e0fbd5f205..e0ff33cc61af91313bfb3e11c6caad8e49cd8786 100644
--- a/ee/app/serializers/feature_flag_serializer.rb
+++ b/ee/app/serializers/feature_flag_serializer.rb
@@ -5,10 +5,6 @@ class FeatureFlagSerializer < BaseSerializer
entity FeatureFlagEntity
def represent(resource, opts = {})
- if resource.is_a?(ActiveRecord::Relation)
- resource = resource.preload_relations
- end
-
super(resource, opts)
end
end
diff --git a/ee/app/services/feature_flags/create_service.rb b/ee/app/services/feature_flags/create_service.rb
index bcc0c0972768016984cdeb234d2c614f05bcf3ee..8699746478befc262ad08a8ff7907791b905388f 100644
--- a/ee/app/services/feature_flags/create_service.rb
+++ b/ee/app/services/feature_flags/create_service.rb
@@ -3,6 +3,8 @@
module FeatureFlags
class CreateService < FeatureFlags::BaseService
def execute
+ return error('Access Denied', 403) unless can_create?
+
ActiveRecord::Base.transaction do
feature_flag = project.operations_feature_flags.new(params)
@@ -11,7 +13,7 @@ def execute
success(feature_flag: feature_flag)
else
- error(feature_flag.errors.full_messages)
+ error(feature_flag.errors.full_messages, 400)
end
end
end
@@ -28,5 +30,9 @@ def audit_message(feature_flag)
message_parts.join(" ")
end
+
+ def can_create?
+ Ability.allowed?(current_user, :create_feature_flag, project)
+ end
end
end
diff --git a/ee/app/services/feature_flags/destroy_service.rb b/ee/app/services/feature_flags/destroy_service.rb
index c80d869787cdeb25cd0c033b3a5032ee44450755..c77e3e03ec31fa7f0044b3d256bbafe8704c7843 100644
--- a/ee/app/services/feature_flags/destroy_service.rb
+++ b/ee/app/services/feature_flags/destroy_service.rb
@@ -3,6 +3,14 @@
module FeatureFlags
class DestroyService < FeatureFlags::BaseService
def execute(feature_flag)
+ destroy_feature_flag(feature_flag)
+ end
+
+ private
+
+ def destroy_feature_flag(feature_flag)
+ return error('Access Denied', 403) unless can_destroy?(feature_flag)
+
ActiveRecord::Base.transaction do
if feature_flag.destroy
save_audit_event(audit_event(feature_flag))
@@ -14,10 +22,12 @@ def execute(feature_flag)
end
end
- private
-
def audit_message(feature_flag)
"Deleted feature flag #{feature_flag.name}."
end
+
+ def can_destroy?(feature_flag)
+ Ability.allowed?(current_user, :destroy_feature_flag, feature_flag)
+ end
end
end
diff --git a/ee/app/services/feature_flags/update_service.rb b/ee/app/services/feature_flags/update_service.rb
index 250f78a6160a6159d9cb5aba910cc415df4ffe14..5ad0dc004260dea8de21c54fff4318006d7dcbc8 100644
--- a/ee/app/services/feature_flags/update_service.rb
+++ b/ee/app/services/feature_flags/update_service.rb
@@ -9,6 +9,8 @@ class UpdateService < FeatureFlags::BaseService
}.freeze
def execute(feature_flag)
+ return error('Access Denied', 403) unless can_update?(feature_flag)
+
ActiveRecord::Base.transaction do
feature_flag.assign_attributes(params)
@@ -71,5 +73,9 @@ def updated_scope_message(scope)
message + '.'
end
+
+ def can_update?(feature_flag)
+ Ability.allowed?(current_user, :update_feature_flag, feature_flag)
+ end
end
end
diff --git a/ee/lib/api/feature_flags.rb b/ee/lib/api/feature_flags.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f0c577bcb2c3667a6f31e5bf391b3e1305f3cd90
--- /dev/null
+++ b/ee/lib/api/feature_flags.rb
@@ -0,0 +1,121 @@
+# frozen_string_literal: true
+
+module API
+ class FeatureFlags < Grape::API
+ include PaginationParams
+
+ FEATURE_FLAG_ENDPOINT_REQUIREMENTS = API::NAMESPACE_OR_PROJECT_REQUIREMENTS
+ .merge(name: API::NO_SLASH_URL_PART_REGEX)
+
+ before do
+ not_found! unless Feature.enabled?(:feature_flag_api, user_project)
+ authorize_read_feature_flags!
+ end
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource 'projects/:id', requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
+ resource :feature_flags do
+ desc 'Get all feature flags of a project' do
+ success EE::API::Entities::FeatureFlag
+ end
+ params do
+ optional :scope, type: String, desc: 'The scope of feature flags',
+ values: %w[enabled disabled]
+ use :pagination
+ end
+ get do
+ feature_flags = ::FeatureFlagsFinder
+ .new(user_project, current_user, declared_params(include_missing: false))
+ .execute
+
+ present paginate(feature_flags), with: EE::API::Entities::FeatureFlag
+ end
+
+ desc 'Create a new feature flag' do
+ success EE::API::Entities::FeatureFlag
+ end
+ params do
+ requires :name, type: String, desc: 'The name of feature flag'
+ optional :description, type: String, desc: 'The description of the feature flag'
+ optional :scopes, type: Array do
+ requires :environment_scope, type: String, desc: 'The environment scope of the scope'
+ requires :active, type: Boolean, desc: 'Active/inactive of the scope'
+ requires :strategies, type: JSON, desc: 'The strategies of the scope'
+ end
+ end
+ post do
+ authorize_create_feature_flag!
+
+ param = declared_params(include_missing: false)
+ param[:scopes_attributes] = param.delete(:scopes) if param.key?(:scopes)
+
+ result = ::FeatureFlags::CreateService
+ .new(user_project, current_user, param)
+ .execute
+
+ if result[:status] == :success
+ present result[:feature_flag], with: EE::API::Entities::FeatureFlag
+ else
+ render_api_error!(result[:message], result[:http_status])
+ end
+ end
+ end
+
+ params do
+ requires :name, type: String, desc: 'The name of the feature flag'
+ end
+ resource 'feature_flags/:name', requirements: FEATURE_FLAG_ENDPOINT_REQUIREMENTS do
+ desc 'Get a feature flag of a project' do
+ success EE::API::Entities::FeatureFlag
+ end
+ get do
+ authorize_read_feature_flag!
+
+ present feature_flag, with: EE::API::Entities::FeatureFlag
+ end
+
+ desc 'Delete a feature flag' do
+ success EE::API::Entities::FeatureFlag
+ end
+ delete do
+ authorize_destroy_feature_flag!
+
+ result = ::FeatureFlags::DestroyService
+ .new(user_project, current_user, declared_params(include_missing: false))
+ .execute(feature_flag)
+
+ if result[:status] == :success
+ present result[:feature_flag], with: EE::API::Entities::FeatureFlag
+ else
+ render_api_error!(result[:message], result[:http_status])
+ end
+ end
+ end
+ end
+
+ helpers do
+ def authorize_read_feature_flags!
+ authorize! :read_feature_flag, user_project
+ end
+
+ def authorize_read_feature_flag!
+ authorize! :read_feature_flag, feature_flag
+ end
+
+ def authorize_create_feature_flag!
+ authorize! :create_feature_flag, user_project
+ end
+
+ def authorize_destroy_feature_flag!
+ authorize! :destroy_feature_flag, feature_flag
+ end
+
+ def feature_flag
+ @feature_flag ||=
+ user_project.operations_feature_flags.find_by_name!(params[:name])
+ end
+ end
+ end
+end
diff --git a/ee/lib/ee/api/api.rb b/ee/lib/ee/api/api.rb
index 375dd0a67f13bfd56883e7fb8f332327eb3fe363..3adfa1d2250bc4464160aa0eacda260a26dfb6d8 100644
--- a/ee/lib/ee/api/api.rb
+++ b/ee/lib/ee/api/api.rb
@@ -18,6 +18,7 @@ module API
mount ::API::EpicIssues
mount ::API::EpicLinks
mount ::API::Epics
+ mount ::API::FeatureFlags
mount ::API::ContainerRegistryEvent
mount ::API::Geo
mount ::API::GeoNodes
diff --git a/ee/lib/ee/api/entities.rb b/ee/lib/ee/api/entities.rb
index 6a5e8622ef45132d076b05c9eae64bcb2f65b467..b0581731af1110163d7b5da243de351016683bda 100644
--- a/ee/lib/ee/api/entities.rb
+++ b/ee/lib/ee/api/entities.rb
@@ -834,6 +834,23 @@ def can_read_vulnerabilities?(user, project)
Ability.allowed?(user, :read_project_security_dashboard, project)
end
end
+
+ class FeatureFlag < Grape::Entity
+ class Scope < Grape::Entity
+ expose :id
+ expose :active
+ expose :environment_scope
+ expose :strategies
+ expose :created_at
+ expose :updated_at
+ end
+
+ expose :name
+ expose :description
+ expose :created_at
+ expose :updated_at
+ expose :scopes, using: Scope
+ end
end
end
end
diff --git a/ee/spec/finders/feature_flags_finder_spec.rb b/ee/spec/finders/feature_flags_finder_spec.rb
index af3df86a9f20bf78de7dc779f5086d7bebf4d390..90d54c6fa1d82320066af36ec1d0ba45c956bd04 100644
--- a/ee/spec/finders/feature_flags_finder_spec.rb
+++ b/ee/spec/finders/feature_flags_finder_spec.rb
@@ -20,15 +20,22 @@
end
describe '#execute' do
- subject { finder.execute }
+ subject { finder.execute(args) }
let!(:feature_flag_1) { create(:operations_feature_flag, name: 'flag-a', project: project) }
let!(:feature_flag_2) { create(:operations_feature_flag, name: 'flag-b', project: project) }
+ let(:args) { {} }
it 'returns feature flags ordered by name' do
is_expected.to eq([feature_flag_1, feature_flag_2])
end
+ it 'preloads relations by default' do
+ expect(Operations::FeatureFlag).to receive(:preload_relations).and_call_original
+
+ subject
+ end
+
context 'when user is a reporter' do
let(:user) { reporter }
@@ -58,6 +65,16 @@
end
end
+ context 'when preload option is false' do
+ let(:args) { { preload: false } }
+
+ it 'does not preload relations' do
+ expect(Operations::FeatureFlag).not_to receive(:preload_relations)
+
+ subject
+ end
+ end
+
context 'when it is presented for list' do
let!(:feature_flag_1) { create(:operations_feature_flag, project: project, active: false) }
let!(:feature_flag_2) { create(:operations_feature_flag, project: project, active: false) }
diff --git a/ee/spec/fixtures/api/schemas/public_api/v4/feature_flag.json b/ee/spec/fixtures/api/schemas/public_api/v4/feature_flag.json
new file mode 100644
index 0000000000000000000000000000000000000000..43818cdaca85d3eca6beed404e73b628a9068bff
--- /dev/null
+++ b/ee/spec/fixtures/api/schemas/public_api/v4/feature_flag.json
@@ -0,0 +1,12 @@
+{
+ "type": "object",
+ "required": ["name"],
+ "properties": {
+ "name": { "type": "string" },
+ "description": { "type": ["string", "null"] },
+ "created_at": { "type": "date" },
+ "updated_at": { "type": "date" },
+ "scopes": { "type": "array", "items": { "$ref": "feature_flag_scope.json" } }
+ },
+ "additionalProperties": false
+}
diff --git a/ee/spec/fixtures/api/schemas/public_api/v4/feature_flag_scope.json b/ee/spec/fixtures/api/schemas/public_api/v4/feature_flag_scope.json
new file mode 100644
index 0000000000000000000000000000000000000000..18402af482e22ca4958f4b7e6bd7a90fe2d9000a
--- /dev/null
+++ b/ee/spec/fixtures/api/schemas/public_api/v4/feature_flag_scope.json
@@ -0,0 +1,17 @@
+{
+ "type": "object",
+ "required": [
+ "id",
+ "environment_scope",
+ "active"
+ ],
+ "properties": {
+ "id": { "type": "integer" },
+ "environment_scope": { "type": "string" },
+ "active": { "type": "boolean" },
+ "created_at": { "type": "date" },
+ "updated_at": { "type": "date" },
+ "strategies": { "type": "array", "items": { "$ref": "feature_flag_strategy.json" } }
+ },
+ "additionalProperties": false
+}
diff --git a/ee/spec/fixtures/api/schemas/public_api/v4/feature_flag_strategy.json b/ee/spec/fixtures/api/schemas/public_api/v4/feature_flag_strategy.json
new file mode 100644
index 0000000000000000000000000000000000000000..5a2777dc8ea75c72e7019b86a196c85e74cfe08a
--- /dev/null
+++ b/ee/spec/fixtures/api/schemas/public_api/v4/feature_flag_strategy.json
@@ -0,0 +1,13 @@
+{
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": { "type": "string" },
+ "parameters": {
+ "type": "object"
+ }
+ },
+ "additionalProperties": false
+}
diff --git a/ee/spec/fixtures/api/schemas/public_api/v4/feature_flags.json b/ee/spec/fixtures/api/schemas/public_api/v4/feature_flags.json
new file mode 100644
index 0000000000000000000000000000000000000000..c19df0443d983ad72f481ec51d226769958ccbb2
--- /dev/null
+++ b/ee/spec/fixtures/api/schemas/public_api/v4/feature_flags.json
@@ -0,0 +1,9 @@
+{
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "$ref": "./feature_flag.json"
+ }
+ }
+}
diff --git a/ee/spec/requests/api/feature_flags_spec.rb b/ee/spec/requests/api/feature_flags_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..6f1cad6b2443d49d2bc543868f8fd77219b42193
--- /dev/null
+++ b/ee/spec/requests/api/feature_flags_spec.rb
@@ -0,0 +1,204 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe API::FeatureFlags do
+ include FeatureFlagHelpers
+
+ let(:project) { create(:project, :repository) }
+ let(:developer) { create(:user) }
+ let(:reporter) { create(:user) }
+ let(:user) { developer }
+ let(:non_project_member) { create(:user) }
+
+ before do
+ stub_licensed_features(feature_flags: true)
+
+ project.add_developer(developer)
+ project.add_reporter(reporter)
+ end
+
+ shared_examples_for 'check user permission' do
+ context 'when user is reporter' do
+ let(:user) { reporter }
+
+ it 'forbids the request' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+ end
+
+ shared_examples_for 'not found' do
+ it 'returns Not Found' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ describe 'GET /projects/:id/feature_flags' do
+ subject { get api("/projects/#{project.id}/feature_flags", user) }
+
+ context 'when there are two feature flags' do
+ let!(:feature_flag_1) do
+ create(:operations_feature_flag, project: project)
+ end
+
+ let!(:feature_flag_2) do
+ create(:operations_feature_flag, project: project)
+ end
+
+ it 'returns feature flags ordered by name' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('public_api/v4/feature_flags', dir: 'ee')
+ expect(json_response.count).to eq(2)
+ expect(json_response.first['name']).to eq(feature_flag_1.name)
+ expect(json_response.second['name']).to eq(feature_flag_2.name)
+ end
+
+ it 'does not have N+1 problem' do
+ control_count = ActiveRecord::QueryRecorder.new { subject }
+
+ create_list(:operations_feature_flag, 3, project: project)
+
+ expect { get api("/projects/#{project.id}/feature_flags", user) }
+ .not_to exceed_query_limit(control_count)
+ end
+
+ it_behaves_like 'check user permission'
+ end
+ end
+
+ describe 'POST /projects/:id/feature_flags' do
+ subject do
+ post api("/projects/#{project.id}/feature_flags", user), params: params
+ end
+
+ let(:params) do
+ {
+ name: 'awesome-feature',
+ scopes: [default_scope]
+ }
+ end
+
+ it 'creates a new feature flag' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:created)
+ expect(response).to match_response_schema('public_api/v4/feature_flag', dir: 'ee')
+
+ feature_flag = project.operations_feature_flags.last
+ expect(feature_flag.name).to eq(params[:name])
+ expect(feature_flag.description).to eq(params[:description])
+ end
+
+ it_behaves_like 'check user permission'
+
+ 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
+ end
+
+ context 'when there is a feature flag with the same name already' do
+ before do
+ create_flag(project, 'awesome-feature')
+ end
+
+ it 'fails to create a new feature flag' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
+
+ context 'when create a feature flag with two scopes' do
+ let(:params) do
+ {
+ name: 'awesome-feature',
+ description: 'this is awesome',
+ scopes: [
+ default_scope,
+ 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(JSON.parse(params[:scopes][index][:strategies]))
+ end
+ end
+ end
+
+ def default_scope
+ {
+ environment_scope: '*',
+ active: false,
+ strategies: [{ name: 'default', parameters: {} }].to_json
+ }
+ end
+ end
+
+ describe 'GET /projects/:id/feature_flags/:name' do
+ subject { get api("/projects/#{project.id}/feature_flags/#{feature_flag.name}", user) }
+
+ context 'when there is a feature flag' do
+ let!(:feature_flag) { create_flag(project, 'awesome-feature') }
+
+ it 'returns a feature flag entry' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('public_api/v4/feature_flag', dir: 'ee')
+ expect(json_response['name']).to eq(feature_flag.name)
+ expect(json_response['description']).to eq(feature_flag.description)
+ end
+
+ it_behaves_like 'check user permission'
+ end
+ end
+
+ describe 'DELETE /projects/:id/feature_flags/:name' do
+ subject do
+ delete api("/projects/#{project.id}/feature_flags/#{feature_flag.name}", user),
+ params: params
+ end
+
+ let!(:feature_flag) { create(:operations_feature_flag, project: project) }
+ let(:params) { {} }
+
+ it 'destroys the feature flag' do
+ expect { subject }.to change { Operations::FeatureFlag.count }.by(-1)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+end
diff --git a/ee/spec/services/feature_flags/create_service_spec.rb b/ee/spec/services/feature_flags/create_service_spec.rb
index 631a85524ac9ae538f3b7f71c666ec2e24eda7a6..d15138287037e7c7d410d7db04045f9b37c726dd 100644
--- a/ee/spec/services/feature_flags/create_service_spec.rb
+++ b/ee/spec/services/feature_flags/create_service_spec.rb
@@ -4,7 +4,15 @@
describe FeatureFlags::CreateService do
let(:project) { create(:project) }
- let(:user) { create(:user) }
+ let(:developer) { create(:user) }
+ let(:reporter) { create(:user) }
+ let(:user) { developer }
+
+ before do
+ stub_licensed_features(feature_flags: true)
+ project.add_developer(developer)
+ project.add_reporter(reporter)
+ end
describe '#execute' do
subject do
@@ -57,6 +65,15 @@
expect { subject }.to change { AuditEvent.count }.by(1)
expect(AuditEvent.last.present.action).to eq(expected_message)
end
+
+ context 'when user is reporter' do
+ let(:user) { reporter }
+
+ it 'returns error status' do
+ expect(subject[:status]).to eq(:error)
+ expect(subject[:message]).to eq('Access Denied')
+ end
+ end
end
end
end
diff --git a/ee/spec/services/feature_flags/destroy_service_spec.rb b/ee/spec/services/feature_flags/destroy_service_spec.rb
index b7f0d336e15105ee5ddf1e50e7c1fbf74e3fcd86..03dfd73d3adb9284e835d8f3f0dd34abfd90d471 100644
--- a/ee/spec/services/feature_flags/destroy_service_spec.rb
+++ b/ee/spec/services/feature_flags/destroy_service_spec.rb
@@ -3,13 +3,25 @@
require 'spec_helper'
describe FeatureFlags::DestroyService do
+ include FeatureFlagHelpers
+
let(:project) { create(:project) }
- let(:user) { create(:user) }
- let!(:feature_flag) { create(:operations_feature_flag) }
+ let(:developer) { create(:user) }
+ let(:reporter) { create(:user) }
+ let(:user) { developer }
+ let!(:feature_flag) { create(:operations_feature_flag, project: project) }
+
+ before do
+ stub_licensed_features(feature_flags: true)
+ project.add_developer(developer)
+ project.add_reporter(reporter)
+ end
describe '#execute' do
- subject { described_class.new(project, user).execute(feature_flag) }
+ subject { described_class.new(project, user, params).execute(feature_flag) }
+
let(:audit_event_message) { AuditEvent.last.present.action }
+ let(:params) { {} }
it 'returns status success' do
expect(subject[:status]).to eq(:success)
@@ -24,6 +36,15 @@
expect(audit_event_message).to eq("Deleted feature flag #{feature_flag.name}.")
end
+ context 'when user is reporter' do
+ let(:user) { reporter }
+
+ it 'returns error status' do
+ expect(subject[:status]).to eq(:error)
+ expect(subject[:message]).to eq('Access Denied')
+ end
+ end
+
context 'when feature flag can not be destroyed' do
before do
allow(feature_flag).to receive(:destroy).and_return(false)
diff --git a/ee/spec/services/feature_flags/update_service_spec.rb b/ee/spec/services/feature_flags/update_service_spec.rb
index b045d5b10ca2c7fa87faa07717e0b3c79d9e151f..0eb9f462a6fdd8a25fc07cfb4ed2f2589117e523 100644
--- a/ee/spec/services/feature_flags/update_service_spec.rb
+++ b/ee/spec/services/feature_flags/update_service_spec.rb
@@ -4,8 +4,16 @@
describe FeatureFlags::UpdateService do
let(:project) { create(:project) }
- let(:user) { create(:user) }
- let(:feature_flag) { create(:operations_feature_flag) }
+ let(:developer) { create(:user) }
+ let(:reporter) { create(:user) }
+ let(:user) { developer }
+ let(:feature_flag) { create(:operations_feature_flag, project: project) }
+
+ before do
+ stub_licensed_features(feature_flags: true)
+ project.add_developer(developer)
+ project.add_reporter(reporter)
+ end
describe '#execute' do
subject { described_class.new(project, user, params).execute(feature_flag) }
@@ -45,6 +53,15 @@
end
end
+ context 'when user is reporter' do
+ let(:user) { reporter }
+
+ it 'returns error status' do
+ expect(subject[:status]).to eq(:error)
+ expect(subject[:message]).to eq('Access Denied')
+ end
+ end
+
context 'when nothing is changed' do
let(:params) { {} }
diff --git a/ee/spec/support/helpers/feature_flag_helpers.rb b/ee/spec/support/helpers/feature_flag_helpers.rb
index e8b7cd3e58c6c8ebaedd77e90c22e38349fcf3fd..5d5c1e7170cc8ee6e878af3cf84b872110abe961 100644
--- a/ee/spec/support/helpers/feature_flag_helpers.rb
+++ b/ee/spec/support/helpers/feature_flag_helpers.rb
@@ -1,12 +1,12 @@
# frozen_string_literal: true
module FeatureFlagHelpers
- def create_flag(project, name, active, description: nil)
+ def create_flag(project, name, active = true, description: nil)
create(:operations_feature_flag, name: name, active: active,
description: description, project: project)
end
- def create_scope(feature_flag, environment_scope, active, strategies = [{ name: "default", parameters: {} }])
+ def create_scope(feature_flag, environment_scope, active = true, strategies = [{ name: "default", parameters: {} }])
create(:operations_feature_flag_scope,
feature_flag: feature_flag,
environment_scope: environment_scope,