flipper-api 0.9.2 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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