flipper 0.28.3 → 1.0.0.pre

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/Changelog.md +23 -0
  3. data/Gemfile +5 -3
  4. data/docs/images/flipper_cloud.png +0 -0
  5. data/examples/cloud/app.ru +12 -0
  6. data/examples/cloud/basic.rb +22 -0
  7. data/examples/cloud/cloud_setup.rb +4 -0
  8. data/examples/cloud/forked.rb +31 -0
  9. data/examples/cloud/import.rb +17 -0
  10. data/examples/cloud/threaded.rb +36 -0
  11. data/examples/dsl.rb +0 -14
  12. data/flipper-cloud.gemspec +19 -0
  13. data/flipper.gemspec +3 -2
  14. data/lib/flipper/cloud/configuration.rb +189 -0
  15. data/lib/flipper/cloud/dsl.rb +27 -0
  16. data/lib/flipper/cloud/instrumenter.rb +48 -0
  17. data/lib/flipper/cloud/message_verifier.rb +95 -0
  18. data/lib/flipper/cloud/middleware.rb +63 -0
  19. data/lib/flipper/cloud/routes.rb +14 -0
  20. data/lib/flipper/cloud.rb +53 -0
  21. data/lib/flipper/dsl.rb +0 -46
  22. data/lib/flipper/{railtie.rb → engine.rb} +19 -3
  23. data/lib/flipper/metadata.rb +5 -1
  24. data/lib/flipper/spec/shared_adapter_specs.rb +43 -43
  25. data/lib/flipper/test/shared_adapter_test.rb +43 -43
  26. data/lib/flipper/version.rb +1 -1
  27. data/lib/flipper.rb +3 -5
  28. data/spec/flipper/adapters/dual_write_spec.rb +2 -2
  29. data/spec/flipper/adapters/instrumented_spec.rb +1 -1
  30. data/spec/flipper/adapters/memoizable_spec.rb +6 -6
  31. data/spec/flipper/adapters/operation_logger_spec.rb +2 -2
  32. data/spec/flipper/adapters/read_only_spec.rb +6 -6
  33. data/spec/flipper/cloud/configuration_spec.rb +269 -0
  34. data/spec/flipper/cloud/dsl_spec.rb +82 -0
  35. data/spec/flipper/cloud/message_verifier_spec.rb +104 -0
  36. data/spec/flipper/cloud/middleware_spec.rb +289 -0
  37. data/spec/flipper/cloud_spec.rb +180 -0
  38. data/spec/flipper/dsl_spec.rb +0 -75
  39. data/spec/flipper/engine_spec.rb +190 -0
  40. data/spec/flipper_integration_spec.rb +12 -12
  41. data/spec/flipper_spec.rb +0 -30
  42. data/spec/spec_helper.rb +0 -12
  43. data/spec/support/climate_control.rb +7 -0
  44. metadata +54 -10
  45. data/spec/flipper/railtie_spec.rb +0 -109
@@ -0,0 +1,82 @@
1
+ require 'flipper/cloud/configuration'
2
+ require 'flipper/cloud/dsl'
3
+ require 'flipper/adapters/operation_logger'
4
+ require 'flipper/adapters/instrumented'
5
+
6
+ RSpec.describe Flipper::Cloud::DSL do
7
+ it 'delegates everything to flipper instance' do
8
+ cloud_configuration = Flipper::Cloud::Configuration.new({
9
+ token: "asdf",
10
+ sync_secret: "tasty",
11
+ })
12
+ dsl = described_class.new(cloud_configuration)
13
+ expect(dsl.features).to eq(Set.new)
14
+ expect(dsl.enabled?(:foo)).to be(false)
15
+ end
16
+
17
+ it 'delegates sync to cloud configuration' do
18
+ stub = stub_request(:get, "https://www.flippercloud.io/adapter/features?exclude_gate_names=true").
19
+ with({
20
+ headers: {
21
+ 'Flipper-Cloud-Token'=>'asdf',
22
+ },
23
+ }).to_return(status: 200, body: '{"features": {}}', headers: {})
24
+ cloud_configuration = Flipper::Cloud::Configuration.new({
25
+ token: "asdf",
26
+ sync_secret: "tasty",
27
+ })
28
+ dsl = described_class.new(cloud_configuration)
29
+ dsl.sync
30
+ expect(stub).to have_been_requested
31
+ end
32
+
33
+ it 'delegates sync_secret to cloud configuration' do
34
+ cloud_configuration = Flipper::Cloud::Configuration.new({
35
+ token: "asdf",
36
+ sync_secret: "tasty",
37
+ })
38
+ dsl = described_class.new(cloud_configuration)
39
+ expect(dsl.sync_secret).to eq("tasty")
40
+ end
41
+
42
+ context "when sync_method is webhook" do
43
+ let(:local_adapter) do
44
+ Flipper::Adapters::OperationLogger.new Flipper::Adapters::Memory.new
45
+ end
46
+
47
+ let(:cloud_configuration) do
48
+ cloud_configuration = Flipper::Cloud::Configuration.new({
49
+ token: "asdf",
50
+ sync_secret: "tasty",
51
+ local_adapter: local_adapter
52
+ })
53
+ end
54
+
55
+ subject do
56
+ described_class.new(cloud_configuration)
57
+ end
58
+
59
+ it "sends reads to local adapter" do
60
+ subject.features
61
+ subject.enabled?(:foo)
62
+ expect(local_adapter.count(:features)).to be(1)
63
+ expect(local_adapter.count(:get)).to be(1)
64
+ end
65
+
66
+ it "sends writes to cloud and local" do
67
+ add_stub = stub_request(:post, "https://www.flippercloud.io/adapter/features").
68
+ with({headers: {'Flipper-Cloud-Token'=>'asdf'}}).
69
+ to_return(status: 200, body: '{}', headers: {})
70
+ enable_stub = stub_request(:post, "https://www.flippercloud.io/adapter/features/foo/boolean").
71
+ with(headers: {'Flipper-Cloud-Token'=>'asdf'}).
72
+ to_return(status: 200, body: '{}', headers: {})
73
+
74
+ subject.enable(:foo)
75
+
76
+ expect(local_adapter.count(:add)).to be(1)
77
+ expect(local_adapter.count(:enable)).to be(1)
78
+ expect(add_stub).to have_been_requested
79
+ expect(enable_stub).to have_been_requested
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,104 @@
1
+ require 'flipper/cloud/message_verifier'
2
+
3
+ RSpec.describe Flipper::Cloud::MessageVerifier do
4
+ let(:payload) { "some payload" }
5
+ let(:secret) { "secret" }
6
+ let(:timestamp) { Time.now }
7
+
8
+ describe "#generate" do
9
+ it "generates signature that can be verified" do
10
+ message_verifier = Flipper::Cloud::MessageVerifier.new(secret: secret)
11
+ signature = message_verifier.generate(payload, timestamp)
12
+ header = generate_header(timestamp: timestamp, signature: signature)
13
+ expect(message_verifier.verify(payload, header)).to be(true)
14
+ end
15
+ end
16
+
17
+ describe "#header" do
18
+ it "generates a header in valid format" do
19
+ version = "v1"
20
+ message_verifier = Flipper::Cloud::MessageVerifier.new(secret: secret, version: version)
21
+ signature = message_verifier.generate(payload, timestamp)
22
+ header = message_verifier.header(signature, timestamp)
23
+ expect(header).to eq("t=#{timestamp.to_i},#{version}=#{signature}")
24
+ end
25
+ end
26
+
27
+ describe ".header" do
28
+ it "generates a header in valid format" do
29
+ version = "v1"
30
+ message_verifier = Flipper::Cloud::MessageVerifier.new(secret: secret, version: version)
31
+ signature = message_verifier.generate(payload, timestamp)
32
+
33
+ header = Flipper::Cloud::MessageVerifier.header(signature, timestamp, version)
34
+ expect(header).to eq("t=#{timestamp.to_i},#{version}=#{signature}")
35
+ end
36
+ end
37
+
38
+ describe "#verify" do
39
+ it "raises a InvalidSignature when the header does not have the expected format" do
40
+ header = "i'm not even a real signature header"
41
+ expect {
42
+ message_verifier = Flipper::Cloud::MessageVerifier.new(secret: "secret")
43
+ message_verifier.verify(payload, header)
44
+ }.to raise_error(Flipper::Cloud::MessageVerifier::InvalidSignature, "Unable to extract timestamp and signatures from header")
45
+ end
46
+
47
+ it "raises a InvalidSignature when there are no signatures with the expected version" do
48
+ header = generate_header(version: "v0")
49
+ expect {
50
+ message_verifier = Flipper::Cloud::MessageVerifier.new(secret: "secret")
51
+ message_verifier.verify(payload, header)
52
+ }.to raise_error(Flipper::Cloud::MessageVerifier::InvalidSignature, /No signatures found with expected version/)
53
+ end
54
+
55
+ it "raises a InvalidSignature when there are no valid signatures for the payload" do
56
+ header = generate_header(signature: "bad_signature")
57
+ expect {
58
+ message_verifier = Flipper::Cloud::MessageVerifier.new(secret: "secret")
59
+ message_verifier.verify(payload, header)
60
+ }.to raise_error(Flipper::Cloud::MessageVerifier::InvalidSignature, "No signatures found matching the expected signature for payload")
61
+ end
62
+
63
+ it "raises a InvalidSignature when the timestamp is not within the tolerance" do
64
+ header = generate_header(timestamp: Time.now - 15)
65
+ expect {
66
+ message_verifier = Flipper::Cloud::MessageVerifier.new(secret: secret)
67
+ message_verifier.verify(payload, header, tolerance: 10)
68
+ }.to raise_error(Flipper::Cloud::MessageVerifier::InvalidSignature, /Timestamp outside the tolerance zone/)
69
+ end
70
+
71
+ it "returns true when the header contains a valid signature and the timestamp is within the tolerance" do
72
+ header = generate_header
73
+ message_verifier = Flipper::Cloud::MessageVerifier.new(secret: "secret")
74
+ expect(message_verifier.verify(payload, header, tolerance: 10)).to be(true)
75
+ end
76
+
77
+ it "returns true when the header contains at least one valid signature" do
78
+ header = generate_header + ",v1=bad_signature"
79
+ message_verifier = Flipper::Cloud::MessageVerifier.new(secret: secret)
80
+ expect(message_verifier.verify(payload, header, tolerance: 10)).to be(true)
81
+ end
82
+
83
+ it "returns true when the header contains a valid signature and the timestamp is off but no tolerance is provided" do
84
+ header = generate_header(timestamp: Time.at(12_345))
85
+ message_verifier = Flipper::Cloud::MessageVerifier.new(secret: secret)
86
+ expect(message_verifier.verify(payload, header)).to be(true)
87
+ end
88
+ end
89
+
90
+ private
91
+
92
+ def generate_header(options = {})
93
+ options[:secret] ||= secret
94
+ options[:version] ||= "v1"
95
+
96
+ message_verifier = Flipper::Cloud::MessageVerifier.new(secret: options[:secret], version: options[:version])
97
+
98
+ options[:timestamp] ||= timestamp
99
+ options[:payload] ||= payload
100
+ options[:signature] ||= message_verifier.generate(options[:payload], options[:timestamp])
101
+
102
+ Flipper::Cloud::MessageVerifier.header(options[:signature], options[:timestamp], options[:version])
103
+ end
104
+ end
@@ -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