flipper 0.28.3 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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