flipper-cloud 0.28.3 → 1.0.0.pre

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,289 +0,0 @@
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
@@ -1,179 +0,0 @@
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
- ENV['FLIPPER_CLOUD_TOKEN'] = 'asdf'
59
- expect(described_class.new).to be_instance_of(Flipper::Cloud::DSL)
60
- end
61
-
62
- it 'can set instrumenter' do
63
- instrumenter = Flipper::Instrumenters::Memory.new
64
- instance = described_class.new(token: 'asdf', instrumenter: instrumenter)
65
- expect(instance.instrumenter).to be(instrumenter)
66
- end
67
-
68
- it 'allows wrapping adapter with another adapter like the instrumenter' do
69
- instance = described_class.new(token: 'asdf') do |config|
70
- config.adapter do |adapter|
71
- Flipper::Adapters::Instrumented.new(adapter)
72
- end
73
- end
74
- # instance.adapter is memoizable adapter instance
75
- expect(instance.adapter.adapter).to be_instance_of(Flipper::Adapters::Instrumented)
76
- end
77
-
78
- it 'can set debug_output' do
79
- expect(Flipper::Adapters::Http::Client).to receive(:new)
80
- .with(hash_including(debug_output: STDOUT)).at_least(:once)
81
- described_class.new(token: 'asdf', debug_output: STDOUT)
82
- end
83
-
84
- it 'can set read_timeout' do
85
- expect(Flipper::Adapters::Http::Client).to receive(:new)
86
- .with(hash_including(read_timeout: 1)).at_least(:once)
87
- described_class.new(token: 'asdf', read_timeout: 1)
88
- end
89
-
90
- it 'can set open_timeout' do
91
- expect(Flipper::Adapters::Http::Client).to receive(:new)
92
- .with(hash_including(open_timeout: 1)).at_least(:once)
93
- described_class.new(token: 'asdf', open_timeout: 1)
94
- end
95
-
96
- if RUBY_VERSION >= '2.6.0'
97
- it 'can set write_timeout' do
98
- expect(Flipper::Adapters::Http::Client).to receive(:new)
99
- .with(hash_including(open_timeout: 1)).at_least(:once)
100
- described_class.new(token: 'asdf', open_timeout: 1)
101
- end
102
- end
103
-
104
- it 'can import' do
105
- stub_request(:post, /www\.flippercloud\.io\/adapter\/features.*/).
106
- with(headers: {'Flipper-Cloud-Token'=>'asdf'}).to_return(status: 200, body: "{}", headers: {})
107
-
108
- flipper = Flipper.new(Flipper::Adapters::Memory.new)
109
-
110
- flipper.enable(:test)
111
- flipper.enable(:search)
112
- flipper.enable_actor(:stats, Flipper::Actor.new("jnunemaker"))
113
- flipper.enable_percentage_of_time(:logging, 5)
114
-
115
- cloud_flipper = Flipper::Cloud.new(token: "asdf")
116
-
117
- get_all = {
118
- "logging" => {actors: Set.new, boolean: nil, groups: Set.new, percentage_of_actors: nil, percentage_of_time: "5"},
119
- "search" => {actors: Set.new, boolean: "true", groups: Set.new, percentage_of_actors: nil, percentage_of_time: nil},
120
- "stats" => {actors: Set["jnunemaker"], boolean: nil, groups: Set.new, percentage_of_actors: nil, percentage_of_time: nil},
121
- "test" => {actors: Set.new, boolean: "true", groups: Set.new, percentage_of_actors: nil, percentage_of_time: nil},
122
- }
123
-
124
- expect(flipper.adapter.get_all).to eq(get_all)
125
- cloud_flipper.import(flipper)
126
- expect(flipper.adapter.get_all).to eq(get_all)
127
- expect(cloud_flipper.adapter.get_all).to eq(get_all)
128
- end
129
-
130
- it 'raises error for failure while importing' do
131
- stub_request(:post, /www\.flippercloud\.io\/adapter\/features.*/).
132
- with(headers: {'Flipper-Cloud-Token'=>'asdf'}).to_return(status: 500, body: "{}")
133
-
134
- flipper = Flipper.new(Flipper::Adapters::Memory.new)
135
-
136
- flipper.enable(:test)
137
- flipper.enable(:search)
138
- flipper.enable_actor(:stats, Flipper::Actor.new("jnunemaker"))
139
- flipper.enable_percentage_of_time(:logging, 5)
140
-
141
- cloud_flipper = Flipper::Cloud.new(token: "asdf")
142
-
143
- get_all = {
144
- "logging" => {actors: Set.new, boolean: nil, groups: Set.new, percentage_of_actors: nil, percentage_of_time: "5"},
145
- "search" => {actors: Set.new, boolean: "true", groups: Set.new, percentage_of_actors: nil, percentage_of_time: nil},
146
- "stats" => {actors: Set["jnunemaker"], boolean: nil, groups: Set.new, percentage_of_actors: nil, percentage_of_time: nil},
147
- "test" => {actors: Set.new, boolean: "true", groups: Set.new, percentage_of_actors: nil, percentage_of_time: nil},
148
- }
149
-
150
- expect(flipper.adapter.get_all).to eq(get_all)
151
- expect { cloud_flipper.import(flipper) }.to raise_error(Flipper::Adapters::Http::Error)
152
- expect(flipper.adapter.get_all).to eq(get_all)
153
- end
154
-
155
- it 'raises error for timeout while importing' do
156
- stub_request(:post, /www\.flippercloud\.io\/adapter\/features.*/).
157
- with(headers: {'Flipper-Cloud-Token'=>'asdf'}).to_timeout
158
-
159
- flipper = Flipper.new(Flipper::Adapters::Memory.new)
160
-
161
- flipper.enable(:test)
162
- flipper.enable(:search)
163
- flipper.enable_actor(:stats, Flipper::Actor.new("jnunemaker"))
164
- flipper.enable_percentage_of_time(:logging, 5)
165
-
166
- cloud_flipper = Flipper::Cloud.new(token: "asdf")
167
-
168
- get_all = {
169
- "logging" => {actors: Set.new, boolean: nil, groups: Set.new, percentage_of_actors: nil, percentage_of_time: "5"},
170
- "search" => {actors: Set.new, boolean: "true", groups: Set.new, percentage_of_actors: nil, percentage_of_time: nil},
171
- "stats" => {actors: Set["jnunemaker"], boolean: nil, groups: Set.new, percentage_of_actors: nil, percentage_of_time: nil},
172
- "test" => {actors: Set.new, boolean: "true", groups: Set.new, percentage_of_actors: nil, percentage_of_time: nil},
173
- }
174
-
175
- expect(flipper.adapter.get_all).to eq(get_all)
176
- expect { cloud_flipper.import(flipper) }.to raise_error(Net::OpenTimeout)
177
- expect(flipper.adapter.get_all).to eq(get_all)
178
- end
179
- end