flipper-api 0.9.2 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 602b1d466a68b777063b76d351ab845843057406
4
- data.tar.gz: a0a32a90d8accbce74b0dec3b2157cf9bb669dc2
3
+ metadata.gz: fd696bdc261c62486cab5a607d1ef98e21eaf9d1
4
+ data.tar.gz: 3bec1050517cd29ac18233562644ea0d859f00b7
5
5
  SHA512:
6
- metadata.gz: 81401173dd23fe114cde4087e1469ddb5289cf2032f99062225247ad88933c858401c88d60810d16d9ccd51a2ea18bc030b7b3ba42183e3cb1c49c1da33fbce4
7
- data.tar.gz: 8dcdac503eefc843e1c1e61e0e465f70a01d4b4ec556a0212d8c6f373d84663df9b3e0cfc9a10a55d7d2e2cc85c447c87315c1d3c6c319a1d7715a97eebd414a
6
+ metadata.gz: 08e7142b00964657bf6e21afc3fb192c900305b8825e67c500bffc1e51bfe271a80be3f2ac6b0288cf9fb44eb3476bddc0220013e01f6b8339454d0a42159deb
7
+ data.tar.gz: f6c94f090cc3e2745348a06edaf4aed37449df84879888f697e55b14046a7cb1e9ff4f0e9a9b657afd39db3df6a5d743e4640fcd8514999c8c3280a4c94df364
data/lib/flipper/api.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  require 'rack'
2
2
  require 'flipper'
3
3
  require 'flipper/api/middleware'
4
+ require 'flipper/api/actor'
4
5
 
5
6
  module Flipper
6
7
  module Api
@@ -1,5 +1,6 @@
1
1
  require 'forwardable'
2
2
  require 'flipper/api/error'
3
+ require 'flipper/api/error_response'
3
4
  require 'json'
4
5
 
5
6
  module Flipper
@@ -88,6 +89,12 @@ module Flipper
88
89
  throw :halt, response
89
90
  end
90
91
 
92
+ # Public: Call this with a json serializable object (i.e. Hash)
93
+ # to serialize object and respond to request
94
+ #
95
+ # object - json serializable object
96
+ # status - http status code
97
+
91
98
  def json_response(object, status = 200)
92
99
  header 'Content-Type', Api::CONTENT_TYPE
93
100
  status(status)
@@ -95,6 +102,16 @@ module Flipper
95
102
  halt [@code, @headers, [body]]
96
103
  end
97
104
 
105
+ # Public: Call this with an ErrorResponse::ERRORS key to respond
106
+ # with the serialized error object as response body
107
+ #
108
+ # error_key - key to lookup error object
109
+
110
+ def json_error_response(error_key)
111
+ error = ErrorResponse::ERRORS.fetch(error_key.to_sym)
112
+ json_response(error.as_json, error.http_status)
113
+ end
114
+
98
115
  # Public: Set the status code for the response.
99
116
  #
100
117
  # code - The Integer code you would like the response to return.
@@ -0,0 +1,13 @@
1
+ module Flipper
2
+ module Api
3
+ # Internal: Shim for turning a string flipper id into something that responds to
4
+ # flipper_id for Flipper::Types::Actor.
5
+ class Actor
6
+ attr_reader :flipper_id
7
+
8
+ def initialize(flipper_id)
9
+ @flipper_id = flipper_id
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,32 @@
1
+ module Flipper
2
+ module Api
3
+ module ErrorResponse
4
+ class Error
5
+ attr_reader :http_status
6
+
7
+ def initialize(code, message, info, http_status)
8
+ @code = code
9
+ @message = message
10
+ @more_info = info
11
+ @http_status = http_status
12
+ end
13
+
14
+ def as_json
15
+ {
16
+ code: @code,
17
+ message: @message,
18
+ more_info: @more_info,
19
+ }
20
+ end
21
+ end
22
+
23
+ ERRORS = {
24
+ feature_not_found: Error.new(1, "Feature not found.", "", 404),
25
+ group_not_registered: Error.new(2, "Group not registered.", "", 404),
26
+ percentage_invalid: Error.new(3, "Percentage must be a positive number less than or equal to 100.", "", 422),
27
+ flipper_id_invalid: Error.new(4, "Required parameter flipper_id is missing.", "", 422),
28
+ name_invalid: Error.new(5, "Required parameter name is missing.", "", 422),
29
+ }
30
+ end
31
+ end
32
+ end
@@ -35,6 +35,10 @@ module Flipper
35
35
  end
36
36
 
37
37
  @action_collection = ActionCollection.new
38
+ @action_collection.add Api::V1::Actions::PercentageOfTimeGate
39
+ @action_collection.add Api::V1::Actions::PercentageOfActorsGate
40
+ @action_collection.add Api::V1::Actions::ActorsGate
41
+ @action_collection.add Api::V1::Actions::GroupsGate
38
42
  @action_collection.add Api::V1::Actions::BooleanGate
39
43
  @action_collection.add Api::V1::Actions::Feature
40
44
  @action_collection.add Api::V1::Actions::Features
@@ -0,0 +1,54 @@
1
+ require 'flipper/api/action'
2
+ require 'flipper/api/v1/decorators/feature'
3
+
4
+ module Flipper
5
+ module Api
6
+ module V1
7
+ module Actions
8
+ class ActorsGate < Api::Action
9
+ route %r{api/v1/features/[^/]*/actors/?\Z}
10
+
11
+ def post
12
+ ensure_valid_params
13
+ feature = flipper[feature_name]
14
+ actor = Actor.new(flipper_id)
15
+ feature.enable_actor(actor)
16
+ decorated_feature = Decorators::Feature.new(feature)
17
+ json_response(decorated_feature.as_json, 200)
18
+ end
19
+
20
+ def delete
21
+ ensure_valid_params
22
+ feature = flipper[feature_name]
23
+ actor = Actor.new(flipper_id)
24
+ feature.disable_actor(actor)
25
+ decorated_feature = Decorators::Feature.new(feature)
26
+ json_response(decorated_feature.as_json, 200)
27
+ end
28
+
29
+ private
30
+
31
+ def ensure_valid_params
32
+ unless feature_names.include?(feature_name)
33
+ json_error_response(:feature_not_found)
34
+ end
35
+
36
+ json_error_response(:flipper_id_invalid) if flipper_id.nil?
37
+ end
38
+
39
+ def feature_name
40
+ @feature_name ||= Rack::Utils.unescape(path_parts[-2])
41
+ end
42
+
43
+ def flipper_id
44
+ @flipper_id ||= params['flipper_id']
45
+ end
46
+
47
+ def feature_names
48
+ @feature_names ||= flipper.adapter.features
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -1,18 +1,27 @@
1
1
  require 'flipper/api/action'
2
+ require 'flipper/api/v1/decorators/feature'
2
3
 
3
4
  module Flipper
4
5
  module Api
5
6
  module V1
6
7
  module Actions
7
8
  class BooleanGate < Api::Action
8
- route %r{api/v1/features/[^/]*/(enable|disable)/?\Z}
9
-
10
- def put
9
+ route %r{api/v1/features/[^/]*/boolean/?\Z}
10
+
11
+ def post
12
+ feature_name = Rack::Utils.unescape(path_parts[-2])
13
+ feature = flipper[feature_name]
14
+ feature.enable
15
+ decorated_feature = Decorators::Feature.new(feature)
16
+ json_response(decorated_feature.as_json, 200)
17
+ end
18
+
19
+ def delete
11
20
  feature_name = Rack::Utils.unescape(path_parts[-2])
12
21
  feature = flipper[feature_name.to_sym]
13
- action = Rack::Utils.unescape(path_parts.last)
14
- feature.send(action)
15
- json_response({}, 204)
22
+ feature.disable
23
+ decorated_feature = Decorators::Feature.new(feature)
24
+ json_response(decorated_feature.as_json, 200)
16
25
  end
17
26
  end
18
27
  end
@@ -14,17 +14,16 @@ module Flipper
14
14
  feature = Decorators::Feature.new(flipper[feature_name])
15
15
  json_response(feature.as_json)
16
16
  else
17
- json_response({}, 404)
17
+ json_error_response(:feature_not_found)
18
18
  end
19
19
  end
20
20
 
21
21
  def delete
22
22
  if feature_names.include?(feature_name)
23
23
  flipper.remove(feature_name)
24
-
25
24
  json_response({}, 204)
26
25
  else
27
- json_response({}, 404)
26
+ json_error_response(:feature_not_found)
28
27
  end
29
28
  end
30
29
 
@@ -21,16 +21,11 @@ module Flipper
21
21
  end
22
22
 
23
23
  def post
24
- feature_name = params.fetch('name') do
25
- json_response({
26
- errors: [{
27
- message: 'Missing post parameter: name',
28
- }]
29
- }, 422)
30
- end
31
-
32
- flipper.adapter.add(flipper[feature_name])
33
- json_response({}, 200)
24
+ feature_name = params.fetch('name') { json_error_response(:name_invalid) }
25
+ feature = flipper[feature_name]
26
+ flipper.adapter.add(feature)
27
+ decorated_feature = Decorators::Feature.new(feature)
28
+ json_response(decorated_feature.as_json, 200)
34
29
  end
35
30
  end
36
31
  end
@@ -0,0 +1,49 @@
1
+ require 'flipper/api/action'
2
+ require 'flipper/api/v1/decorators/feature'
3
+
4
+ module Flipper
5
+ module Api
6
+ module V1
7
+ module Actions
8
+ class GroupsGate < Api::Action
9
+ route %r{api/v1/features/[^/]*/groups/?\Z}
10
+
11
+ def post
12
+ ensure_valid_params
13
+ feature = flipper[feature_name]
14
+ feature.enable_group(group_name)
15
+ decorated_feature = Decorators::Feature.new(feature)
16
+ json_response(decorated_feature.as_json, 200)
17
+ end
18
+
19
+ def delete
20
+ ensure_valid_params
21
+ feature = flipper[feature_name]
22
+ feature.disable_group(group_name)
23
+ decorated_feature = Decorators::Feature.new(feature)
24
+ json_response(decorated_feature.as_json, 200)
25
+ end
26
+
27
+ private
28
+
29
+ def ensure_valid_params
30
+ json_error_response(:feature_not_found) unless feature_names.include?(feature_name)
31
+ json_error_response(:group_not_registered) unless Flipper.group_exists?(group_name)
32
+ end
33
+
34
+ def feature_name
35
+ @feature_name ||= Rack::Utils.unescape(path_parts[-2])
36
+ end
37
+
38
+ def group_name
39
+ @group_name ||= params['name']
40
+ end
41
+
42
+ def feature_names
43
+ @feature_names ||= flipper.adapter.features
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,64 @@
1
+ require 'flipper/api/action'
2
+ require 'flipper/api/v1/decorators/feature'
3
+
4
+ module Flipper
5
+ module Api
6
+ module V1
7
+ module Actions
8
+ class PercentageOfActorsGate < Api::Action
9
+ route %r{api/v1/features/[^/]*/percentage_of_actors/?\Z}
10
+
11
+ def post
12
+ ensure_valid_enable_params
13
+ feature = flipper[feature_name]
14
+ feature.enable_percentage_of_actors(percentage)
15
+ decorated_feature = Decorators::Feature.new(feature)
16
+ json_response(decorated_feature.as_json, 200)
17
+ end
18
+
19
+ def delete
20
+ ensure_valid_disable_params
21
+ feature = flipper[feature_name]
22
+ feature.disable_percentage_of_actors
23
+ decorated_feature = Decorators::Feature.new(feature)
24
+ json_response(decorated_feature.as_json, 200)
25
+ end
26
+
27
+ private
28
+
29
+ def ensure_valid_enable_params
30
+ unless feature_names.include?(feature_name)
31
+ json_error_response(:feature_not_found)
32
+ end
33
+
34
+ if percentage < 0 || percentage > 100
35
+ json_error_response(:percentage_invalid)
36
+ end
37
+ end
38
+
39
+ def ensure_valid_disable_params
40
+ unless feature_names.include?(feature_name)
41
+ json_error_response(:feature_not_found)
42
+ end
43
+ end
44
+
45
+ def feature_name
46
+ @feature_name ||= Rack::Utils.unescape(path_parts[-2])
47
+ end
48
+
49
+ def percentage
50
+ @percentage ||= begin
51
+ Integer(params['percentage'])
52
+ rescue ArgumentError, TypeError
53
+ -1
54
+ end
55
+ end
56
+
57
+ def feature_names
58
+ @feature_names ||= flipper.adapter.features
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,64 @@
1
+ require 'flipper/api/action'
2
+ require 'flipper/api/v1/decorators/feature'
3
+
4
+ module Flipper
5
+ module Api
6
+ module V1
7
+ module Actions
8
+ class PercentageOfTimeGate < Api::Action
9
+ route %r{api/v1/features/[^/]*/percentage_of_time/?\Z}
10
+
11
+ def post
12
+ ensure_valid_enable_params
13
+ feature = flipper[feature_name]
14
+ feature.enable_percentage_of_time(percentage)
15
+ decorated_feature = Decorators::Feature.new(feature)
16
+ json_response(decorated_feature.as_json, 200)
17
+ end
18
+
19
+ def delete
20
+ ensure_valid_disable_params
21
+ feature = flipper[feature_name]
22
+ feature.disable_percentage_of_time
23
+ decorated_feature = Decorators::Feature.new(feature)
24
+ json_response(decorated_feature.as_json, 200)
25
+ end
26
+
27
+ private
28
+
29
+ def ensure_valid_enable_params
30
+ unless feature_names.include?(feature_name)
31
+ json_error_response(:feature_not_found)
32
+ end
33
+
34
+ if percentage < 0 || percentage > 100
35
+ json_error_response(:percentage_invalid)
36
+ end
37
+ end
38
+
39
+ def ensure_valid_disable_params
40
+ unless feature_names.include?(feature_name)
41
+ json_error_response(:feature_not_found)
42
+ end
43
+ end
44
+
45
+ def feature_name
46
+ @feature_name ||= Rack::Utils.unescape(path_parts[-2])
47
+ end
48
+
49
+ def percentage
50
+ @percentage ||= begin
51
+ Integer(params['percentage'])
52
+ rescue ArgumentError, TypeError
53
+ -1
54
+ end
55
+ end
56
+
57
+ def feature_names
58
+ @feature_names ||= flipper.adapter.features
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -1,3 +1,3 @@
1
1
  module Flipper
2
- VERSION = "0.9.2".freeze
2
+ VERSION = "0.10.0".freeze
3
3
  end
@@ -25,35 +25,84 @@ RSpec.describe Flipper::Api::Action do
25
25
  end
26
26
  }
27
27
 
28
- it "won't run method that isn't whitelisted" do
29
- fake_request = Struct.new(:request_method, :env, :session).new("NOOOOPE", {}, {})
30
- action = action_subclass.new(flipper, fake_request)
31
- expect {
32
- action.run
33
- }.to raise_error(Flipper::Api::RequestMethodNotSupported)
34
- end
28
+ describe 'https verbs' do
35
29
 
36
- it "will run get" do
37
- fake_request = Struct.new(:request_method, :env, :session).new("GET", {}, {})
38
- action = action_subclass.new(flipper, fake_request)
39
- expect(action.run).to eq([200, {}, "get"])
40
- end
30
+ it "won't run method that isn't whitelisted" do
31
+ fake_request = Struct.new(:request_method, :env, :session).new("NOOOOPE", {}, {})
32
+ action = action_subclass.new(flipper, fake_request)
33
+ expect {
34
+ action.run
35
+ }.to raise_error(Flipper::Api::RequestMethodNotSupported)
36
+ end
41
37
 
42
- it "will run post" do
43
- fake_request = Struct.new(:request_method, :env, :session).new("POST", {}, {})
44
- action = action_subclass.new(flipper, fake_request)
45
- expect(action.run).to eq([200, {}, "post"])
46
- end
38
+ it "will run get" do
39
+ fake_request = Struct.new(:request_method, :env, :session).new("GET", {}, {})
40
+ action = action_subclass.new(flipper, fake_request)
41
+ expect(action.run).to eq([200, {}, "get"])
42
+ end
43
+
44
+ it "will run post" do
45
+ fake_request = Struct.new(:request_method, :env, :session).new("POST", {}, {})
46
+ action = action_subclass.new(flipper, fake_request)
47
+ expect(action.run).to eq([200, {}, "post"])
48
+ end
49
+
50
+ it "will run put" do
51
+ fake_request = Struct.new(:request_method, :env, :session).new("PUT", {}, {})
52
+ action = action_subclass.new(flipper, fake_request)
53
+ expect(action.run).to eq([200, {}, "put"])
54
+ end
47
55
 
48
- it "will run put" do
49
- fake_request = Struct.new(:request_method, :env, :session).new("PUT", {}, {})
50
- action = action_subclass.new(flipper, fake_request)
51
- expect(action.run).to eq([200, {}, "put"])
56
+ it "will run delete" do
57
+ fake_request = Struct.new(:request_method, :env, :session).new("DELETE", {}, {})
58
+ action = action_subclass.new(flipper, fake_request)
59
+ expect(action.run).to eq([200, {}, "delete"])
60
+ end
52
61
  end
53
62
 
54
- it "will run delete" do
55
- fake_request = Struct.new(:request_method, :env, :session).new("DELETE", {}, {})
56
- action = action_subclass.new(flipper, fake_request)
57
- expect(action.run).to eq([200, {}, "delete"])
63
+ describe '#json_error_response' do
64
+ describe ":feature_not_found" do
65
+
66
+ it 'locates and serializes error correctly' do
67
+ action = action_subclass.new({}, {})
68
+ response = catch(:halt) do
69
+ action.json_error_response(:feature_not_found)
70
+ end
71
+ status, headers, body = response
72
+ parsed_body = JSON.parse(body[0])
73
+
74
+ expect(headers["Content-Type"]).to eq("application/json")
75
+ expect(parsed_body["code"]).to eq(1)
76
+ expect(parsed_body["message"]).to eq("Feature not found.")
77
+ expect(parsed_body["more_info"]).to eq("")
78
+ end
79
+ end
80
+
81
+ describe ':group_not_registered' do
82
+
83
+ it 'locates and serializes error correctly' do
84
+ action = action_subclass.new({}, {})
85
+ response = catch(:halt) do
86
+ action.json_error_response(:group_not_registered)
87
+ end
88
+ status, headers, body = response
89
+ parsed_body = JSON.parse(body[0])
90
+
91
+ expect(headers["Content-Type"]).to eq("application/json")
92
+ expect(parsed_body["code"]).to eq(2)
93
+ expect(parsed_body["message"]).to eq("Group not registered.")
94
+ expect(parsed_body["more_info"]).to eq("")
95
+ end
96
+ end
97
+
98
+ describe 'invalid error key' do
99
+
100
+ it 'raises descriptive error' do
101
+ action = action_subclass.new({}, {})
102
+ catch(:halt) do
103
+ expect{ action.json_error_response(:invalid_error_key) }.to raise_error(KeyError)
104
+ end
105
+ end
106
+ end
58
107
  end
59
108
  end
@@ -0,0 +1,115 @@
1
+ require 'helper'
2
+
3
+ RSpec.describe Flipper::Api::V1::Actions::ActorsGate do
4
+ let(:app) { build_api(flipper) }
5
+
6
+ describe 'enable' do
7
+ let(:actor) { Flipper::Api::Actor.new("1") }
8
+
9
+ before do
10
+ flipper[:my_feature].disable_actor(actor)
11
+ post '/api/v1/features/my_feature/actors', { flipper_id: actor.flipper_id }
12
+ end
13
+
14
+ it 'enables feature for actor' do
15
+ expect(last_response.status).to eq(200)
16
+ expect(flipper[:my_feature].enabled?(actor)).to be_truthy
17
+ expect(flipper[:my_feature].enabled_gate_names).to eq([:actor])
18
+ end
19
+
20
+ it 'returns decorated feature with actor enabled' do
21
+ gate = json_response['gates'].find { |gate| gate['key'] == 'actors' }
22
+ expect(gate['value']).to eq(["1"])
23
+ end
24
+ end
25
+
26
+ describe 'disable' do
27
+ let(:actor) { Flipper::Api::Actor.new("1") }
28
+
29
+ before do
30
+ flipper[:my_feature].enable_actor(actor)
31
+ delete '/api/v1/features/my_feature/actors', { flipper_id: actor.flipper_id }
32
+ end
33
+
34
+ it 'disables feature' do
35
+ expect(last_response.status).to eq(200)
36
+ expect(flipper[:my_feature].enabled?(actor)).to be_falsy
37
+ expect(flipper[:my_feature].enabled_gate_names).to be_empty
38
+ end
39
+
40
+ it 'returns decorated feature with boolean gate disabled' do
41
+ gate = json_response['gates'].find { |gate| gate['key'] == 'actors' }
42
+ expect(gate['value']).to be_empty
43
+ end
44
+ end
45
+
46
+ describe 'enable missing flipper_id parameter' do
47
+ before do
48
+ flipper[:my_feature].enable
49
+ post '/api/v1/features/my_feature/actors'
50
+ end
51
+
52
+ it 'returns correct error response' do
53
+ expect(last_response.status).to eq(422)
54
+ expect(json_response).to eq({ 'code' => 4, 'message' => 'Required parameter flipper_id is missing.', 'more_info' => '' })
55
+ end
56
+ end
57
+
58
+ describe 'disable missing flipper_id parameter' do
59
+ before do
60
+ flipper[:my_feature].enable
61
+ delete '/api/v1/features/my_feature/actors'
62
+ end
63
+
64
+ it 'returns correct error response' do
65
+ expect(last_response.status).to eq(422)
66
+ expect(json_response).to eq({ 'code' => 4, 'message' => 'Required parameter flipper_id is missing.', 'more_info' => '' })
67
+ end
68
+ end
69
+
70
+ describe 'enable nil flipper_id parameter' do
71
+ before do
72
+ flipper[:my_feature].enable
73
+ post '/api/v1/features/my_feature/actors', { flipper_id: nil }
74
+ end
75
+
76
+ it 'returns correct error response' do
77
+ expect(last_response.status).to eq(422)
78
+ expect(json_response).to eq({ 'code' => 4, 'message' => 'Required parameter flipper_id is missing.', 'more_info' => '' })
79
+ end
80
+ end
81
+
82
+ describe 'disable nil flipper_id parameter' do
83
+ before do
84
+ flipper[:my_feature].enable
85
+ delete '/api/v1/features/my_feature/actors', { flipper_id: nil }
86
+ end
87
+
88
+ it 'returns correct error response' do
89
+ expect(last_response.status).to eq(422)
90
+ expect(json_response).to eq({ 'code' => 4, 'message' => 'Required parameter flipper_id is missing.', 'more_info' => '' })
91
+ end
92
+ end
93
+
94
+ describe 'enable missing feature' do
95
+ before do
96
+ post '/api/v1/features/my_feature/actors'
97
+ end
98
+
99
+ it 'returns correct error response' do
100
+ expect(last_response.status).to eq(404)
101
+ expect(json_response).to eq({ 'code' => 1, 'message' => 'Feature not found.', 'more_info' => '' })
102
+ end
103
+ end
104
+
105
+ describe 'disable missing feature' do
106
+ before do
107
+ delete '/api/v1/features/my_feature/actors'
108
+ end
109
+
110
+ it 'returns correct error response' do
111
+ expect(last_response.status).to eq(404)
112
+ expect(json_response).to eq({ 'code' => 1, 'message' => 'Feature not found.', 'more_info' => '' })
113
+ end
114
+ end
115
+ end
@@ -6,34 +6,34 @@ RSpec.describe Flipper::Api::V1::Actions::BooleanGate do
6
6
  describe 'enable' do
7
7
  before do
8
8
  flipper[:my_feature].disable
9
- put '/api/v1/features/my_feature/enable'
9
+ post '/api/v1/features/my_feature/boolean'
10
10
  end
11
11
 
12
12
  it 'enables feature' do
13
- expect(last_response.status).to eq(204)
13
+ expect(last_response.status).to eq(200)
14
14
  expect(flipper[:my_feature].on?).to be_truthy
15
15
  end
16
+
17
+ it 'returns decorated feature with boolean gate enabled' do
18
+ boolean_gate = json_response['gates'].find { |gate| gate['key'] == 'boolean' }
19
+ expect(boolean_gate['value']).to be_truthy
20
+ end
16
21
  end
17
22
 
18
23
  describe 'disable' do
19
24
  before do
20
25
  flipper[:my_feature].enable
21
- put '/api/v1/features/my_feature/disable'
26
+ delete '/api/v1/features/my_feature/boolean'
22
27
  end
23
28
 
24
29
  it 'disables feature' do
25
- expect(last_response.status).to eq(204)
30
+ expect(last_response.status).to eq(200)
26
31
  expect(flipper[:my_feature].off?).to be_truthy
27
32
  end
28
- end
29
-
30
- describe 'invalid paremeter' do
31
- before do
32
- put '/api/v1/features/my_feature/invalid_param'
33
- end
34
33
 
35
- it 'responds with 404 when not sent enable or disable parameter' do
36
- expect(last_response.status).to eq(404)
34
+ it 'returns decorated feature with boolean gate disabled' do
35
+ boolean_gate = json_response['gates'].find { |gate| gate['key'] == 'boolean' }
36
+ expect(boolean_gate['value']).to be_falsy
37
37
  end
38
38
  end
39
39
  end
@@ -103,16 +103,36 @@ RSpec.describe Flipper::Api::V1::Actions::Feature do
103
103
  it 'returns 404' do
104
104
  expect(last_response.status).to eq(404)
105
105
  end
106
+
107
+ it 'returns formatted error response body' do
108
+ expect(json_response).to eq({ "code" => 1, "message" => "Feature not found.", "more_info" => "" })
109
+ end
106
110
  end
107
111
  end
108
112
 
109
113
  describe 'delete' do
110
- it 'deletes feature' do
111
- flipper[:my_feature].enable
112
- expect(flipper.features.map(&:key)).to include('my_feature')
113
- delete 'api/v1/features/my_feature'
114
- expect(last_response.status).to eq(204)
115
- expect(flipper.features.map(&:key)).not_to include('my_feature')
114
+ context 'succesful request' do
115
+ it 'deletes feature' do
116
+ flipper[:my_feature].enable
117
+ expect(flipper.features.map(&:key)).to include('my_feature')
118
+ delete 'api/v1/features/my_feature'
119
+ expect(last_response.status).to eq(204)
120
+ expect(flipper.features.map(&:key)).not_to include('my_feature')
121
+ end
122
+ end
123
+
124
+ context 'feature not found' do
125
+ before do
126
+ delete 'api/v1/features/my_feature'
127
+ end
128
+
129
+ it 'returns 404' do
130
+ expect(last_response.status).to eq(404)
131
+ end
132
+
133
+ it 'returns formatted error response body' do
134
+ expect(json_response).to eq({ "code" => 1, "message" => "Feature not found.", "more_info" => "" })
135
+ end
116
136
  end
117
137
  end
118
138
  end
@@ -17,22 +17,23 @@ RSpec.describe Flipper::Api::V1::Actions::Features do
17
17
  expected_response = {
18
18
  "features" => [
19
19
  {
20
- "key" =>"my_feature",
20
+ "key" => "my_feature",
21
21
  "state" => "on",
22
22
  "gates" => [
23
23
  {
24
24
  "key"=> "boolean",
25
25
  "name"=> "boolean",
26
- "value" => true},
27
- {
28
- "key" =>"groups",
26
+ "value" => true
27
+ },
28
+ {
29
+ "key" => "groups",
29
30
  "name" => "group",
30
- "value" =>[],
31
+ "value" => [],
31
32
  },
32
33
  {
33
34
  "key" => "actors",
34
- "name"=>"actor",
35
- "value"=>["10"],
35
+ "name" => "actor",
36
+ "value" => ["10"],
36
37
  },
37
38
  {
38
39
  "key" => "percentage_of_actors",
@@ -44,7 +45,7 @@ RSpec.describe Flipper::Api::V1::Actions::Features do
44
45
  "name"=> "percentage_of_time",
45
46
  "value"=> 0,
46
47
  },
47
- ],
48
+ ],
48
49
  },
49
50
  ]
50
51
  }
@@ -74,9 +75,44 @@ RSpec.describe Flipper::Api::V1::Actions::Features do
74
75
  post 'api/v1/features', { name: 'my_feature' }
75
76
  end
76
77
 
77
- it 'responds 200 on success' do
78
+ it 'responds 200 ' do
78
79
  expect(last_response.status).to eq(200)
79
- expect(json_response).to eq({})
80
+ end
81
+
82
+ it 'returns decorated feature' do
83
+ expected_response = {
84
+
85
+ "key" => "my_feature",
86
+ "state" => "off",
87
+ "gates" => [
88
+ {
89
+ "key"=> "boolean",
90
+ "name"=> "boolean",
91
+ "value" => false,
92
+ },
93
+ {
94
+ "key" => "groups",
95
+ "name" => "group",
96
+ "value" => [],
97
+ },
98
+ {
99
+ "key" => "actors",
100
+ "name" => "actor",
101
+ "value" => [],
102
+ },
103
+ {
104
+ "key" => "percentage_of_actors",
105
+ "name" => "percentage_of_actors",
106
+ "value" => 0,
107
+ },
108
+ {
109
+ "key"=> "percentage_of_time",
110
+ "name"=> "percentage_of_time",
111
+ "value"=> 0,
112
+ },
113
+ ],
114
+ }
115
+ expect(json_response).to eq(expected_response)
80
116
  end
81
117
 
82
118
  it 'adds feature' do
@@ -98,8 +134,7 @@ RSpec.describe Flipper::Api::V1::Actions::Features do
98
134
  end
99
135
 
100
136
  it 'returns formatted error' do
101
- errors = json_response['errors']
102
- expect(errors.first['message']).to eq('Missing post parameter: name')
137
+ expect(json_response).to eq({ 'code' => 5, 'message' => 'Required parameter name is missing.', 'more_info' => '' })
103
138
  end
104
139
  end
105
140
  end
@@ -0,0 +1,75 @@
1
+ require 'helper'
2
+
3
+ RSpec.describe Flipper::Api::V1::Actions::GroupsGate do
4
+ let(:app) { build_api(flipper) }
5
+
6
+ describe 'enable' do
7
+ before do
8
+ flipper[:my_feature].disable
9
+ Flipper.register(:admins) do |actor|
10
+ actor.respond_to?(:admin?) && actor.admin?
11
+ end
12
+ post '/api/v1/features/my_feature/groups', { name: 'admins' }
13
+ end
14
+
15
+ it 'enables feature for group' do
16
+ person = double
17
+ allow(person).to receive(:flipper_id).and_return(1)
18
+ allow(person).to receive(:admin?).and_return(true)
19
+ expect(last_response.status).to eq(200)
20
+ expect(flipper[:my_feature].enabled?(person)).to be_truthy
21
+ end
22
+
23
+ it 'returns decorated feature with group enabled' do
24
+ group_gate = json_response['gates'].find { |m| m['name'] == 'group' }
25
+ expect(group_gate['value']).to eq(['admins'])
26
+ end
27
+ end
28
+
29
+ describe 'disable' do
30
+ before do
31
+ flipper[:my_feature].disable
32
+ Flipper.register(:admins) do |actor|
33
+ actor.respond_to?(:admin?) && actor.admin?
34
+ end
35
+ flipper[:my_feature].enable_group(:admins)
36
+ delete '/api/v1/features/my_feature/groups', { name: 'admins' }
37
+ end
38
+
39
+ it 'disables feature for group' do
40
+ person = double
41
+ allow(person).to receive(:flipper_id).and_return(1)
42
+ allow(person).to receive(:admin?).and_return(true)
43
+ expect(last_response.status).to eq(200)
44
+ expect(flipper[:my_feature].enabled?(person)).to be_falsey
45
+ end
46
+
47
+ it 'returns decorated feature with group disabled' do
48
+ group_gate = json_response['gates'].find { |m| m['name'] == 'group' }
49
+ expect(group_gate['value']).to eq([])
50
+ end
51
+ end
52
+
53
+ describe 'non-existent feature' do
54
+ before do
55
+ delete '/api/v1/features/my_feature/groups', { name: 'admins' }
56
+ end
57
+
58
+ it '404s with correct error response when feature does not exist' do
59
+ expect(last_response.status).to eq(404)
60
+ expect(json_response).to eq({ 'code' => 1, 'message' => 'Feature not found.', 'more_info' => '' })
61
+ end
62
+ end
63
+
64
+ describe 'group not registered' do
65
+ before do
66
+ flipper[:my_feature].disable
67
+ delete '/api/v1/features/my_feature/groups', { name: 'admins' }
68
+ end
69
+
70
+ it '404s with correct error response when group not registered' do
71
+ expect(last_response.status).to eq(404)
72
+ expect(json_response).to eq({ 'code' => 2, 'message' => 'Group not registered.', 'more_info' => '' })
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,85 @@
1
+ require 'helper'
2
+
3
+ RSpec.describe Flipper::Api::V1::Actions::PercentageOfActorsGate do
4
+ let(:app) { build_api(flipper) }
5
+
6
+ describe 'enable' do
7
+ before do
8
+ flipper[:my_feature].disable
9
+ post '/api/v1/features/my_feature/percentage_of_actors', { percentage: '10' }
10
+ end
11
+
12
+ it 'enables gate for feature' do
13
+ expect(flipper[:my_feature].enabled_gate_names).to include(:percentage_of_actors)
14
+ end
15
+
16
+ it 'returns decorated feature with gate enabled for 10 percent of actors' do
17
+ gate = json_response['gates'].find { |gate| gate['name'] == 'percentage_of_actors' }
18
+ expect(gate['value']).to eq(10)
19
+ end
20
+
21
+ end
22
+
23
+ describe 'disable' do
24
+ before do
25
+ flipper[:my_feature].enable_percentage_of_actors(10)
26
+ delete '/api/v1/features/my_feature/percentage_of_actors'
27
+ end
28
+
29
+ it 'disables gate for feature' do
30
+ expect(flipper[:my_feature].enabled_gates).to be_empty
31
+ end
32
+
33
+ it 'returns decorated feature with gate disabled' do
34
+ gate = json_response['gates'].find { |gate| gate['name'] == 'percentage_of_actors' }
35
+ expect(gate['value']).to eq(0)
36
+ end
37
+ end
38
+
39
+ describe 'non-existent feature' do
40
+ before do
41
+ delete '/api/v1/features/my_feature/percentage_of_actors'
42
+ end
43
+
44
+ it '404s with correct error response when feature does not exist' do
45
+ expect(last_response.status).to eq(404)
46
+ expect(json_response).to eq({ 'code' => 1, 'message' => 'Feature not found.', 'more_info' => '' })
47
+ end
48
+ end
49
+
50
+ describe 'out of range parameter percentage parameter' do
51
+ before do
52
+ flipper[:my_feature].disable
53
+ post '/api/v1/features/my_feature/percentage_of_actors', { percentage: '300' }
54
+ end
55
+
56
+ it '422s with correct error response when percentage parameter is invalid' do
57
+ expect(last_response.status).to eq(422)
58
+ expect(json_response).to eq({ 'code' => 3, 'message' => 'Percentage must be a positive number less than or equal to 100.', 'more_info' => '' })
59
+ end
60
+ end
61
+
62
+ describe 'percentage parameter not an integer' do
63
+ before do
64
+ flipper[:my_feature].disable
65
+ post '/api/v1/features/my_feature/percentage_of_actors', { percentage: 'foo' }
66
+ end
67
+
68
+ it '422s with correct error response when percentage parameter is invalid' do
69
+ expect(last_response.status).to eq(422)
70
+ expect(json_response).to eq({ 'code' => 3, 'message' => 'Percentage must be a positive number less than or equal to 100.', 'more_info' => '' })
71
+ end
72
+ end
73
+
74
+ describe 'missing percentage parameter' do
75
+ before do
76
+ flipper[:my_feature].disable
77
+ post '/api/v1/features/my_feature/percentage_of_actors'
78
+ end
79
+
80
+ it '422s with correct error response when percentage parameter is missing' do
81
+ expect(last_response.status).to eq(422)
82
+ expect(json_response).to eq({ 'code' => 3, 'message' => 'Percentage must be a positive number less than or equal to 100.', 'more_info' => '' })
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,84 @@
1
+ require 'helper'
2
+
3
+ RSpec.describe Flipper::Api::V1::Actions::PercentageOfTimeGate do
4
+ let(:app) { build_api(flipper) }
5
+
6
+ describe 'enable' do
7
+ before do
8
+ flipper[:my_feature].disable
9
+ post '/api/v1/features/my_feature/percentage_of_time', { percentage: '10' }
10
+ end
11
+
12
+ it 'enables gate for feature' do
13
+ expect(flipper[:my_feature].enabled_gate_names).to include(:percentage_of_time)
14
+ end
15
+
16
+ it 'returns decorated feature with gate enabled for 5% of time' do
17
+ gate = json_response['gates'].find { |gate| gate['name'] == 'percentage_of_time' }
18
+ expect(gate['value']).to eq(10)
19
+ end
20
+ end
21
+
22
+ describe 'disable' do
23
+ before do
24
+ flipper[:my_feature].enable_percentage_of_time(10)
25
+ delete '/api/v1/features/my_feature/percentage_of_time'
26
+ end
27
+
28
+ it 'disables gate for feature' do
29
+ expect(flipper[:my_feature].enabled_gates).to be_empty
30
+ end
31
+
32
+ it 'returns decorated feature with gate disabled' do
33
+ gate = json_response['gates'].find { |gate| gate['name'] == 'percentage_of_time' }
34
+ expect(gate['value']).to eq(0)
35
+ end
36
+ end
37
+
38
+ describe 'non-existent feature' do
39
+ before do
40
+ delete '/api/v1/features/my_feature/percentage_of_time'
41
+ end
42
+
43
+ it '404s with correct error response when feature does not exist' do
44
+ expect(last_response.status).to eq(404)
45
+ expect(json_response).to eq({ 'code' => 1, 'message' => 'Feature not found.', 'more_info' => '' })
46
+ end
47
+ end
48
+
49
+ describe 'out of range parameter percentage parameter' do
50
+ before do
51
+ flipper[:my_feature].disable
52
+ post '/api/v1/features/my_feature/percentage_of_time', { percentage: '300' }
53
+ end
54
+
55
+ it '422s with correct error response when percentage parameter is invalid' do
56
+ expect(last_response.status).to eq(422)
57
+ expect(json_response).to eq({ 'code' => 3, 'message' => 'Percentage must be a positive number less than or equal to 100.', 'more_info' => '' })
58
+ end
59
+ end
60
+
61
+ describe 'percentage parameter not an integer' do
62
+ before do
63
+ flipper[:my_feature].disable
64
+ post '/api/v1/features/my_feature/percentage_of_time', { percentage: 'foo' }
65
+ end
66
+
67
+ it '422s with correct error response when percentage parameter is invalid' do
68
+ expect(last_response.status).to eq(422)
69
+ expect(json_response).to eq({ 'code' => 3, 'message' => 'Percentage must be a positive number less than or equal to 100.', 'more_info' => '' })
70
+ end
71
+ end
72
+
73
+ describe 'missing percentage parameter' do
74
+ before do
75
+ flipper[:my_feature].disable
76
+ post '/api/v1/features/my_feature/percentage_of_time'
77
+ end
78
+
79
+ it '422s with correct error response when percentage parameter is missing' do
80
+ expect(last_response.status).to eq(422)
81
+ expect(json_response).to eq({ 'code' => 3, 'message' => 'Percentage must be a positive number less than or equal to 100.', 'more_info' => '' })
82
+ end
83
+ end
84
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: flipper-api
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.2
4
+ version: 0.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - John Nunemaker
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-08-22 00:00:00.000000000 Z
11
+ date: 2016-11-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack
@@ -36,14 +36,14 @@ dependencies:
36
36
  requirements:
37
37
  - - "~>"
38
38
  - !ruby/object:Gem::Version
39
- version: 0.9.2
39
+ version: 0.10.0
40
40
  type: :runtime
41
41
  prerelease: false
42
42
  version_requirements: !ruby/object:Gem::Requirement
43
43
  requirements:
44
44
  - - "~>"
45
45
  - !ruby/object:Gem::Version
46
- version: 0.9.2
46
+ version: 0.10.0
47
47
  description: Rack middleware that provides an API for the flipper gem.
48
48
  email:
49
49
  - nunemaker@gmail.com
@@ -56,18 +56,28 @@ files:
56
56
  - lib/flipper/api.rb
57
57
  - lib/flipper/api/action.rb
58
58
  - lib/flipper/api/action_collection.rb
59
+ - lib/flipper/api/actor.rb
59
60
  - lib/flipper/api/error.rb
61
+ - lib/flipper/api/error_response.rb
60
62
  - lib/flipper/api/middleware.rb
63
+ - lib/flipper/api/v1/actions/actors_gate.rb
61
64
  - lib/flipper/api/v1/actions/boolean_gate.rb
62
65
  - lib/flipper/api/v1/actions/feature.rb
63
66
  - lib/flipper/api/v1/actions/features.rb
67
+ - lib/flipper/api/v1/actions/groups_gate.rb
68
+ - lib/flipper/api/v1/actions/percentage_of_actors_gate.rb
69
+ - lib/flipper/api/v1/actions/percentage_of_time_gate.rb
64
70
  - lib/flipper/api/v1/decorators/feature.rb
65
71
  - lib/flipper/api/v1/decorators/gate.rb
66
72
  - lib/flipper/version.rb
67
73
  - spec/flipper/api/action_spec.rb
74
+ - spec/flipper/api/v1/actions/actors_gate_spec.rb
68
75
  - spec/flipper/api/v1/actions/boolean_gate_spec.rb
69
76
  - spec/flipper/api/v1/actions/feature_spec.rb
70
77
  - spec/flipper/api/v1/actions/features_spec.rb
78
+ - spec/flipper/api/v1/actions/groups_gate_spec.rb
79
+ - spec/flipper/api/v1/actions/percentage_of_actors_gate_spec.rb
80
+ - spec/flipper/api/v1/actions/percentage_of_time_gate_spec.rb
71
81
  homepage: https://github.com/jnunemaker/flipper
72
82
  licenses:
73
83
  - MIT
@@ -94,6 +104,10 @@ specification_version: 4
94
104
  summary: API for the Flipper gem
95
105
  test_files:
96
106
  - spec/flipper/api/action_spec.rb
107
+ - spec/flipper/api/v1/actions/actors_gate_spec.rb
97
108
  - spec/flipper/api/v1/actions/boolean_gate_spec.rb
98
109
  - spec/flipper/api/v1/actions/feature_spec.rb
99
110
  - spec/flipper/api/v1/actions/features_spec.rb
111
+ - spec/flipper/api/v1/actions/groups_gate_spec.rb
112
+ - spec/flipper/api/v1/actions/percentage_of_actors_gate_spec.rb
113
+ - spec/flipper/api/v1/actions/percentage_of_time_gate_spec.rb