flipper 0.28.3 → 1.0.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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/examples.yml +2 -2
  3. data/Changelog.md +190 -165
  4. data/Gemfile +5 -3
  5. data/docs/images/flipper_cloud.png +0 -0
  6. data/examples/cloud/app.ru +12 -0
  7. data/examples/cloud/basic.rb +22 -0
  8. data/examples/cloud/cloud_setup.rb +4 -0
  9. data/examples/cloud/forked.rb +31 -0
  10. data/examples/cloud/import.rb +17 -0
  11. data/examples/cloud/threaded.rb +36 -0
  12. data/examples/dsl.rb +0 -14
  13. data/flipper-cloud.gemspec +19 -0
  14. data/flipper.gemspec +3 -2
  15. data/lib/flipper/cloud/configuration.rb +189 -0
  16. data/lib/flipper/cloud/dsl.rb +27 -0
  17. data/lib/flipper/cloud/instrumenter.rb +48 -0
  18. data/lib/flipper/cloud/message_verifier.rb +95 -0
  19. data/lib/flipper/cloud/middleware.rb +63 -0
  20. data/lib/flipper/cloud/routes.rb +14 -0
  21. data/lib/flipper/cloud.rb +53 -0
  22. data/lib/flipper/dsl.rb +0 -46
  23. data/lib/flipper/{railtie.rb → engine.rb} +19 -3
  24. data/lib/flipper/metadata.rb +5 -1
  25. data/lib/flipper/middleware/memoizer.rb +1 -1
  26. data/lib/flipper/spec/shared_adapter_specs.rb +43 -43
  27. data/lib/flipper/test/shared_adapter_test.rb +43 -43
  28. data/lib/flipper/version.rb +1 -1
  29. data/lib/flipper.rb +3 -5
  30. data/spec/flipper/adapters/dual_write_spec.rb +2 -2
  31. data/spec/flipper/adapters/instrumented_spec.rb +1 -1
  32. data/spec/flipper/adapters/memoizable_spec.rb +6 -6
  33. data/spec/flipper/adapters/operation_logger_spec.rb +2 -2
  34. data/spec/flipper/adapters/read_only_spec.rb +6 -6
  35. data/spec/flipper/cloud/configuration_spec.rb +269 -0
  36. data/spec/flipper/cloud/dsl_spec.rb +82 -0
  37. data/spec/flipper/cloud/message_verifier_spec.rb +104 -0
  38. data/spec/flipper/cloud/middleware_spec.rb +289 -0
  39. data/spec/flipper/cloud_spec.rb +180 -0
  40. data/spec/flipper/dsl_spec.rb +0 -75
  41. data/spec/flipper/engine_spec.rb +190 -0
  42. data/spec/flipper_integration_spec.rb +12 -12
  43. data/spec/flipper_spec.rb +0 -30
  44. data/spec/spec_helper.rb +0 -12
  45. data/spec/support/climate_control.rb +7 -0
  46. metadata +54 -11
  47. data/.tool-versions +0 -1
  48. data/spec/flipper/railtie_spec.rb +0 -109
@@ -0,0 +1,289 @@
1
+ require 'securerandom'
2
+ require 'flipper/cloud'
3
+ require 'flipper/cloud/middleware'
4
+ require 'flipper/adapters/operation_logger'
5
+
6
+ RSpec.describe Flipper::Cloud::Middleware do
7
+ let(:flipper) {
8
+ Flipper::Cloud.new(token: "regular") do |config|
9
+ config.local_adapter = Flipper::Adapters::OperationLogger.new(Flipper::Adapters::Memory.new)
10
+ config.sync_secret = "regular_tasty"
11
+ end
12
+ }
13
+
14
+ let(:env_flipper) {
15
+ Flipper::Cloud.new(token: "env") do |config|
16
+ config.local_adapter = Flipper::Adapters::OperationLogger.new(Flipper::Adapters::Memory.new)
17
+ config.sync_secret = "env_tasty"
18
+ end
19
+ }
20
+
21
+ let(:app) { Flipper::Cloud.app(flipper) }
22
+ let(:response_body) { JSON.generate({features: {}}) }
23
+ let(:request_body) {
24
+ JSON.generate({
25
+ "environment_id" => 1,
26
+ "webhook_id" => 1,
27
+ "delivery_id" => SecureRandom.uuid,
28
+ "action" => "sync",
29
+ })
30
+ }
31
+ let(:timestamp) { Time.now }
32
+ let(:signature) {
33
+ Flipper::Cloud::MessageVerifier.new(secret: flipper.sync_secret).generate(request_body, timestamp)
34
+ }
35
+ let(:signature_header_value) {
36
+ Flipper::Cloud::MessageVerifier.new(secret: "").header(signature, timestamp)
37
+ }
38
+
39
+ context 'when initializing middleware with flipper instance' do
40
+ let(:app) { Flipper::Cloud.app(flipper) }
41
+
42
+ it 'uses instance to sync' do
43
+ Flipper.register(:admins) { |*args| false }
44
+ Flipper.register(:staff) { |*args| false }
45
+ Flipper.register(:basic) { |*args| false }
46
+ Flipper.register(:plus) { |*args| false }
47
+ Flipper.register(:premium) { |*args| false }
48
+
49
+ stub = stub_request_for_token('regular')
50
+ env = {
51
+ "HTTP_FLIPPER_CLOUD_SIGNATURE" => signature_header_value,
52
+ }
53
+ post '/', request_body, env
54
+
55
+ expect(last_response.status).to eq(200)
56
+ expect(JSON.parse(last_response.body)).to eq({
57
+ "groups" => [
58
+ {"name" => "admins"},
59
+ {"name" => "staff"},
60
+ {"name" => "basic"},
61
+ {"name" => "plus"},
62
+ {"name" => "premium"},
63
+ ],
64
+ })
65
+ expect(stub).to have_been_requested
66
+ end
67
+ end
68
+
69
+ context 'when signature is invalid' do
70
+ let(:app) { Flipper::Cloud.app(flipper) }
71
+ let(:signature) {
72
+ Flipper::Cloud::MessageVerifier.new(secret: "nope").generate(request_body, timestamp)
73
+ }
74
+
75
+ it 'uses instance to sync' do
76
+ stub = stub_request_for_token('regular')
77
+ env = {
78
+ "HTTP_FLIPPER_CLOUD_SIGNATURE" => signature_header_value,
79
+ }
80
+ post '/', request_body, env
81
+
82
+ expect(last_response.status).to eq(400)
83
+ expect(stub).not_to have_been_requested
84
+ end
85
+ end
86
+
87
+ context "when flipper cloud responds with 402" do
88
+ let(:app) { Flipper::Cloud.app(flipper) }
89
+
90
+ it "results in 402" do
91
+ Flipper.register(:admins) { |*args| false }
92
+ Flipper.register(:staff) { |*args| false }
93
+ Flipper.register(:basic) { |*args| false }
94
+ Flipper.register(:plus) { |*args| false }
95
+ Flipper.register(:premium) { |*args| false }
96
+
97
+ stub = stub_request_for_token('regular', status: 402)
98
+ env = {
99
+ "HTTP_FLIPPER_CLOUD_SIGNATURE" => signature_header_value,
100
+ }
101
+ post '/', request_body, env
102
+
103
+ expect(last_response.status).to eq(402)
104
+ expect(last_response.headers["Flipper-Cloud-Response-Error-Class"]).to eq("Flipper::Adapters::Http::Error")
105
+ expect(last_response.headers["Flipper-Cloud-Response-Error-Message"]).to include("Failed with status: 402")
106
+ expect(stub).to have_been_requested
107
+ end
108
+ end
109
+
110
+ context "when flipper cloud responds with non-402 and non-2xx code" do
111
+ let(:app) { Flipper::Cloud.app(flipper) }
112
+
113
+ it "results in 500" do
114
+ Flipper.register(:admins) { |*args| false }
115
+ Flipper.register(:staff) { |*args| false }
116
+ Flipper.register(:basic) { |*args| false }
117
+ Flipper.register(:plus) { |*args| false }
118
+ Flipper.register(:premium) { |*args| false }
119
+
120
+ stub = stub_request_for_token('regular', status: 503)
121
+ env = {
122
+ "HTTP_FLIPPER_CLOUD_SIGNATURE" => signature_header_value,
123
+ }
124
+ post '/', request_body, env
125
+
126
+ expect(last_response.status).to eq(500)
127
+ expect(last_response.headers["Flipper-Cloud-Response-Error-Class"]).to eq("Flipper::Adapters::Http::Error")
128
+ expect(last_response.headers["Flipper-Cloud-Response-Error-Message"]).to include("Failed with status: 503")
129
+ expect(stub).to have_been_requested
130
+ end
131
+ end
132
+
133
+ context "when flipper cloud responds with timeout" do
134
+ let(:app) { Flipper::Cloud.app(flipper) }
135
+
136
+ it "results in 500" do
137
+ Flipper.register(:admins) { |*args| false }
138
+ Flipper.register(:staff) { |*args| false }
139
+ Flipper.register(:basic) { |*args| false }
140
+ Flipper.register(:plus) { |*args| false }
141
+ Flipper.register(:premium) { |*args| false }
142
+
143
+ stub = stub_request_for_token('regular', status: :timeout)
144
+ env = {
145
+ "HTTP_FLIPPER_CLOUD_SIGNATURE" => signature_header_value,
146
+ }
147
+ post '/', request_body, env
148
+
149
+ expect(last_response.status).to eq(500)
150
+ expect(last_response.headers["Flipper-Cloud-Response-Error-Class"]).to eq("Net::OpenTimeout")
151
+ expect(last_response.headers["Flipper-Cloud-Response-Error-Message"]).to eq("execution expired")
152
+ expect(stub).to have_been_requested
153
+ end
154
+ end
155
+
156
+ context 'when initialized with flipper instance and flipper instance in env' do
157
+ let(:app) { Flipper::Cloud.app(flipper) }
158
+ let(:signature) {
159
+ Flipper::Cloud::MessageVerifier.new(secret: env_flipper.sync_secret).generate(request_body, timestamp)
160
+ }
161
+
162
+ it 'uses env instance to sync' do
163
+ stub = stub_request_for_token('env')
164
+ env = {
165
+ "HTTP_FLIPPER_CLOUD_SIGNATURE" => signature_header_value,
166
+ 'flipper' => env_flipper,
167
+ }
168
+ post '/', request_body, env
169
+
170
+ expect(last_response.status).to eq(200)
171
+ expect(stub).to have_been_requested
172
+ end
173
+ end
174
+
175
+ context 'when initialized without flipper instance but flipper instance in env' do
176
+ let(:app) { Flipper::Cloud.app }
177
+ let(:signature) {
178
+ Flipper::Cloud::MessageVerifier.new(secret: env_flipper.sync_secret).generate(request_body, timestamp)
179
+ }
180
+
181
+ it 'uses env instance to sync' do
182
+ stub = stub_request_for_token('env')
183
+ env = {
184
+ "HTTP_FLIPPER_CLOUD_SIGNATURE" => signature_header_value,
185
+ 'flipper' => env_flipper,
186
+ }
187
+ post '/', request_body, env
188
+
189
+ expect(last_response.status).to eq(200)
190
+ expect(stub).to have_been_requested
191
+ end
192
+ end
193
+
194
+ context 'when initialized with env_key' do
195
+ let(:app) { Flipper::Cloud.app(flipper, env_key: 'flipper_cloud') }
196
+ let(:signature) {
197
+ Flipper::Cloud::MessageVerifier.new(secret: env_flipper.sync_secret).generate(request_body, timestamp)
198
+ }
199
+
200
+ it 'uses provided env key instead of default' do
201
+ stub = stub_request_for_token('env')
202
+ env = {
203
+ "HTTP_FLIPPER_CLOUD_SIGNATURE" => signature_header_value,
204
+ 'flipper' => flipper,
205
+ 'flipper_cloud' => env_flipper,
206
+ }
207
+ post '/', request_body, env
208
+
209
+ expect(last_response.status).to eq(200)
210
+ expect(stub).to have_been_requested
211
+ end
212
+ end
213
+
214
+ context 'when initializing lazily with a block' do
215
+ let(:app) { Flipper::Cloud.app(-> { flipper }) }
216
+
217
+ it 'works' do
218
+ stub = stub_request_for_token('regular')
219
+ env = {
220
+ "HTTP_FLIPPER_CLOUD_SIGNATURE" => signature_header_value,
221
+ }
222
+ post '/', request_body, env
223
+
224
+ expect(last_response.status).to eq(200)
225
+ expect(stub).to have_been_requested
226
+ end
227
+ end
228
+
229
+ context 'when using older /webhooks path' do
230
+ let(:app) { Flipper::Cloud.app(flipper) }
231
+
232
+ it 'uses instance to sync' do
233
+ Flipper.register(:admins) { |*args| false }
234
+ Flipper.register(:staff) { |*args| false }
235
+ Flipper.register(:basic) { |*args| false }
236
+ Flipper.register(:plus) { |*args| false }
237
+ Flipper.register(:premium) { |*args| false }
238
+
239
+ stub = stub_request_for_token('regular')
240
+ env = {
241
+ "HTTP_FLIPPER_CLOUD_SIGNATURE" => signature_header_value,
242
+ }
243
+ post '/webhooks', request_body, env
244
+
245
+ expect(last_response.status).to eq(200)
246
+ expect(JSON.parse(last_response.body)).to eq({
247
+ "groups" => [
248
+ {"name" => "admins"},
249
+ {"name" => "staff"},
250
+ {"name" => "basic"},
251
+ {"name" => "plus"},
252
+ {"name" => "premium"},
253
+ ],
254
+ })
255
+ expect(stub).to have_been_requested
256
+ end
257
+ end
258
+
259
+ describe 'Request method unsupported' do
260
+ it 'skips middleware' do
261
+ get '/'
262
+ expect(last_response.status).to eq(404)
263
+ expect(last_response.content_type).to eq("application/json")
264
+ expect(last_response.body).to eq("{}")
265
+ end
266
+ end
267
+
268
+ describe 'Inspecting the built Rack app' do
269
+ it 'returns a String' do
270
+ expect(Flipper::Cloud.app(flipper).inspect).to eq("Flipper::Cloud")
271
+ end
272
+ end
273
+
274
+ private
275
+
276
+ def stub_request_for_token(token, status: 200)
277
+ stub = stub_request(:get, "https://www.flippercloud.io/adapter/features?exclude_gate_names=true").
278
+ with({
279
+ headers: {
280
+ 'Flipper-Cloud-Token' => token,
281
+ },
282
+ })
283
+ if status == :timeout
284
+ stub.to_timeout
285
+ else
286
+ stub.to_return(status: status, body: response_body, headers: {})
287
+ end
288
+ end
289
+ end
@@ -0,0 +1,180 @@
1
+ require 'flipper/cloud'
2
+ require 'flipper/adapters/instrumented'
3
+ require 'flipper/instrumenters/memory'
4
+
5
+ RSpec.describe Flipper::Cloud do
6
+ before do
7
+ stub_request(:get, /flippercloud\.io/).to_return(status: 200, body: "{}")
8
+ end
9
+
10
+ context "initialize with token" do
11
+ let(:token) { 'asdf' }
12
+
13
+ before do
14
+ @instance = described_class.new(token: token)
15
+ end
16
+
17
+ it 'returns Flipper::DSL instance' do
18
+ expect(@instance).to be_instance_of(Flipper::Cloud::DSL)
19
+ end
20
+
21
+ it 'can read the cloud configuration' do
22
+ expect(@instance.cloud_configuration).to be_instance_of(Flipper::Cloud::Configuration)
23
+ end
24
+
25
+ it 'configures the correct adapter' do
26
+ # pardon the nesting...
27
+ memoized_adapter = @instance.adapter
28
+ dual_write_adapter = memoized_adapter.adapter
29
+ expect(dual_write_adapter).to be_instance_of(Flipper::Adapters::DualWrite)
30
+ poll_adapter = dual_write_adapter.local
31
+ expect(poll_adapter).to be_instance_of(Flipper::Adapters::Poll)
32
+
33
+ http_adapter = dual_write_adapter.remote
34
+ client = http_adapter.client
35
+ expect(client.uri.scheme).to eq('https')
36
+ expect(client.uri.host).to eq('www.flippercloud.io')
37
+ expect(client.uri.path).to eq('/adapter')
38
+ expect(client.headers['Flipper-Cloud-Token']).to eq(token)
39
+ expect(@instance.instrumenter).to be(Flipper::Instrumenters::Noop)
40
+ end
41
+ end
42
+
43
+ context 'initialize with token and options' do
44
+ it 'sets correct url' do
45
+ instance = described_class.new(token: 'asdf', url: 'https://www.fakeflipper.com/sadpanda')
46
+ # pardon the nesting...
47
+ memoized = instance.adapter
48
+ dual_write = memoized.adapter
49
+ remote = dual_write.remote
50
+ uri = remote.client.uri
51
+ expect(uri.scheme).to eq('https')
52
+ expect(uri.host).to eq('www.fakeflipper.com')
53
+ expect(uri.path).to eq('/sadpanda')
54
+ end
55
+ end
56
+
57
+ it 'can initialize with no token explicitly provided' do
58
+ with_env 'FLIPPER_CLOUD_TOKEN' => 'asdf' do
59
+ expect(described_class.new).to be_instance_of(Flipper::Cloud::DSL)
60
+ end
61
+ end
62
+
63
+ it 'can set instrumenter' do
64
+ instrumenter = Flipper::Instrumenters::Memory.new
65
+ instance = described_class.new(token: 'asdf', instrumenter: instrumenter)
66
+ expect(instance.instrumenter).to be(instrumenter)
67
+ end
68
+
69
+ it 'allows wrapping adapter with another adapter like the instrumenter' do
70
+ instance = described_class.new(token: 'asdf') do |config|
71
+ config.adapter do |adapter|
72
+ Flipper::Adapters::Instrumented.new(adapter)
73
+ end
74
+ end
75
+ # instance.adapter is memoizable adapter instance
76
+ expect(instance.adapter.adapter).to be_instance_of(Flipper::Adapters::Instrumented)
77
+ end
78
+
79
+ it 'can set debug_output' do
80
+ expect(Flipper::Adapters::Http::Client).to receive(:new)
81
+ .with(hash_including(debug_output: STDOUT)).at_least(:once)
82
+ described_class.new(token: 'asdf', debug_output: STDOUT)
83
+ end
84
+
85
+ it 'can set read_timeout' do
86
+ expect(Flipper::Adapters::Http::Client).to receive(:new)
87
+ .with(hash_including(read_timeout: 1)).at_least(:once)
88
+ described_class.new(token: 'asdf', read_timeout: 1)
89
+ end
90
+
91
+ it 'can set open_timeout' do
92
+ expect(Flipper::Adapters::Http::Client).to receive(:new)
93
+ .with(hash_including(open_timeout: 1)).at_least(:once)
94
+ described_class.new(token: 'asdf', open_timeout: 1)
95
+ end
96
+
97
+ if RUBY_VERSION >= '2.6.0'
98
+ it 'can set write_timeout' do
99
+ expect(Flipper::Adapters::Http::Client).to receive(:new)
100
+ .with(hash_including(open_timeout: 1)).at_least(:once)
101
+ described_class.new(token: 'asdf', open_timeout: 1)
102
+ end
103
+ end
104
+
105
+ it 'can import' do
106
+ stub_request(:post, /www\.flippercloud\.io\/adapter\/features.*/).
107
+ with(headers: {'Flipper-Cloud-Token'=>'asdf'}).to_return(status: 200, body: "{}", headers: {})
108
+
109
+ flipper = Flipper.new(Flipper::Adapters::Memory.new)
110
+
111
+ flipper.enable(:test)
112
+ flipper.enable(:search)
113
+ flipper.enable_actor(:stats, Flipper::Actor.new("jnunemaker"))
114
+ flipper.enable_percentage_of_time(:logging, 5)
115
+
116
+ cloud_flipper = Flipper::Cloud.new(token: "asdf")
117
+
118
+ get_all = {
119
+ "logging" => {actors: Set.new, boolean: nil, groups: Set.new, percentage_of_actors: nil, percentage_of_time: "5"},
120
+ "search" => {actors: Set.new, boolean: "true", groups: Set.new, percentage_of_actors: nil, percentage_of_time: nil},
121
+ "stats" => {actors: Set["jnunemaker"], boolean: nil, groups: Set.new, percentage_of_actors: nil, percentage_of_time: nil},
122
+ "test" => {actors: Set.new, boolean: "true", groups: Set.new, percentage_of_actors: nil, percentage_of_time: nil},
123
+ }
124
+
125
+ expect(flipper.adapter.get_all).to eq(get_all)
126
+ cloud_flipper.import(flipper)
127
+ expect(flipper.adapter.get_all).to eq(get_all)
128
+ expect(cloud_flipper.adapter.get_all).to eq(get_all)
129
+ end
130
+
131
+ it 'raises error for failure while importing' do
132
+ stub_request(:post, /www\.flippercloud\.io\/adapter\/features.*/).
133
+ with(headers: {'Flipper-Cloud-Token'=>'asdf'}).to_return(status: 500, body: "{}")
134
+
135
+ flipper = Flipper.new(Flipper::Adapters::Memory.new)
136
+
137
+ flipper.enable(:test)
138
+ flipper.enable(:search)
139
+ flipper.enable_actor(:stats, Flipper::Actor.new("jnunemaker"))
140
+ flipper.enable_percentage_of_time(:logging, 5)
141
+
142
+ cloud_flipper = Flipper::Cloud.new(token: "asdf")
143
+
144
+ get_all = {
145
+ "logging" => {actors: Set.new, boolean: nil, groups: Set.new, percentage_of_actors: nil, percentage_of_time: "5"},
146
+ "search" => {actors: Set.new, boolean: "true", groups: Set.new, percentage_of_actors: nil, percentage_of_time: nil},
147
+ "stats" => {actors: Set["jnunemaker"], boolean: nil, groups: Set.new, percentage_of_actors: nil, percentage_of_time: nil},
148
+ "test" => {actors: Set.new, boolean: "true", groups: Set.new, percentage_of_actors: nil, percentage_of_time: nil},
149
+ }
150
+
151
+ expect(flipper.adapter.get_all).to eq(get_all)
152
+ expect { cloud_flipper.import(flipper) }.to raise_error(Flipper::Adapters::Http::Error)
153
+ expect(flipper.adapter.get_all).to eq(get_all)
154
+ end
155
+
156
+ it 'raises error for timeout while importing' do
157
+ stub_request(:post, /www\.flippercloud\.io\/adapter\/features.*/).
158
+ with(headers: {'Flipper-Cloud-Token'=>'asdf'}).to_timeout
159
+
160
+ flipper = Flipper.new(Flipper::Adapters::Memory.new)
161
+
162
+ flipper.enable(:test)
163
+ flipper.enable(:search)
164
+ flipper.enable_actor(:stats, Flipper::Actor.new("jnunemaker"))
165
+ flipper.enable_percentage_of_time(:logging, 5)
166
+
167
+ cloud_flipper = Flipper::Cloud.new(token: "asdf")
168
+
169
+ get_all = {
170
+ "logging" => {actors: Set.new, boolean: nil, groups: Set.new, percentage_of_actors: nil, percentage_of_time: "5"},
171
+ "search" => {actors: Set.new, boolean: "true", groups: Set.new, percentage_of_actors: nil, percentage_of_time: nil},
172
+ "stats" => {actors: Set["jnunemaker"], boolean: nil, groups: Set.new, percentage_of_actors: nil, percentage_of_time: nil},
173
+ "test" => {actors: Set.new, boolean: "true", groups: Set.new, percentage_of_actors: nil, percentage_of_time: nil},
174
+ }
175
+
176
+ expect(flipper.adapter.get_all).to eq(get_all)
177
+ expect { cloud_flipper.import(flipper) }.to raise_error(Net::OpenTimeout)
178
+ expect(flipper.adapter.get_all).to eq(get_all)
179
+ end
180
+ end
@@ -123,18 +123,6 @@ RSpec.describe Flipper::DSL do
123
123
  end
124
124
  end
125
125
 
126
- describe '#boolean' do
127
- it_should_behave_like 'a DSL boolean method' do
128
- let(:method_name) { :boolean }
129
- end
130
- end
131
-
132
- describe '#bool' do
133
- it_should_behave_like 'a DSL boolean method' do
134
- let(:method_name) { :bool }
135
- end
136
- end
137
-
138
126
  describe '#group' do
139
127
  context 'for registered group' do
140
128
  before do
@@ -148,69 +136,6 @@ RSpec.describe Flipper::DSL do
148
136
  end
149
137
  end
150
138
 
151
- describe '#actor' do
152
- context 'for an actor' do
153
- it 'returns actor instance' do
154
- actor = Flipper::Actor.new(33)
155
- flipper_actor = subject.actor(actor)
156
- expect(flipper_actor).to be_instance_of(Flipper::Types::Actor)
157
- expect(flipper_actor.value).to eq('33')
158
- end
159
- end
160
-
161
- context 'for nil' do
162
- it 'raises argument error' do
163
- expect do
164
- subject.actor(nil)
165
- end.to raise_error(ArgumentError)
166
- end
167
- end
168
-
169
- context 'for something that is not actor wrappable' do
170
- it 'raises argument error' do
171
- expect do
172
- subject.actor(Object.new)
173
- end.to raise_error(ArgumentError)
174
- end
175
- end
176
- end
177
-
178
- describe '#time' do
179
- before do
180
- @result = subject.time(5)
181
- end
182
-
183
- it 'returns percentage of time' do
184
- expect(@result).to be_instance_of(Flipper::Types::PercentageOfTime)
185
- end
186
-
187
- it 'sets value' do
188
- expect(@result.value).to eq(5)
189
- end
190
-
191
- it 'is aliased to percentage_of_time' do
192
- expect(@result).to eq(subject.percentage_of_time(@result.value))
193
- end
194
- end
195
-
196
- describe '#actors' do
197
- before do
198
- @result = subject.actors(17)
199
- end
200
-
201
- it 'returns percentage of actors' do
202
- expect(@result).to be_instance_of(Flipper::Types::PercentageOfActors)
203
- end
204
-
205
- it 'sets value' do
206
- expect(@result.value).to eq(17)
207
- end
208
-
209
- it 'is aliased to percentage_of_actors' do
210
- expect(@result).to eq(subject.percentage_of_actors(@result.value))
211
- end
212
- end
213
-
214
139
  describe '#features' do
215
140
  context 'with no features enabled/disabled' do
216
141
  it 'defaults to empty set' do