flipper-cloud 0.28.2 → 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,176 +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
- poll_adapter = memoized_adapter.adapter
29
- dual_write_adapter = poll_adapter.adapter
30
-
31
- expect(poll_adapter).to be_instance_of(Flipper::Adapters::Poll)
32
- expect(dual_write_adapter).to be_instance_of(Flipper::Adapters::DualWrite)
33
-
34
- http_adapter = dual_write_adapter.remote
35
- client = http_adapter.client
36
- expect(client.uri.scheme).to eq('https')
37
- expect(client.uri.host).to eq('www.flippercloud.io')
38
- expect(client.uri.path).to eq('/adapter')
39
- expect(client.headers['Flipper-Cloud-Token']).to eq(token)
40
- expect(@instance.instrumenter).to be(Flipper::Instrumenters::Noop)
41
- end
42
- end
43
-
44
- context 'initialize with token and options' do
45
- it 'sets correct url' do
46
- @instance = described_class.new(token: 'asdf', url: 'https://www.fakeflipper.com/sadpanda')
47
- uri = @instance.adapter.adapter.adapter.remote.client.uri
48
- expect(uri.scheme).to eq('https')
49
- expect(uri.host).to eq('www.fakeflipper.com')
50
- expect(uri.path).to eq('/sadpanda')
51
- end
52
- end
53
-
54
- it 'can initialize with no token explicitly provided' do
55
- ENV['FLIPPER_CLOUD_TOKEN'] = 'asdf'
56
- expect(described_class.new).to be_instance_of(Flipper::Cloud::DSL)
57
- end
58
-
59
- it 'can set instrumenter' do
60
- instrumenter = Flipper::Instrumenters::Memory.new
61
- instance = described_class.new(token: 'asdf', instrumenter: instrumenter)
62
- expect(instance.instrumenter).to be(instrumenter)
63
- end
64
-
65
- it 'allows wrapping adapter with another adapter like the instrumenter' do
66
- instance = described_class.new(token: 'asdf') do |config|
67
- config.adapter do |adapter|
68
- Flipper::Adapters::Instrumented.new(adapter)
69
- end
70
- end
71
- # instance.adapter is memoizable adapter instance
72
- expect(instance.adapter.adapter).to be_instance_of(Flipper::Adapters::Instrumented)
73
- end
74
-
75
- it 'can set debug_output' do
76
- expect(Flipper::Adapters::Http::Client).to receive(:new)
77
- .with(hash_including(debug_output: STDOUT)).at_least(:once)
78
- described_class.new(token: 'asdf', debug_output: STDOUT)
79
- end
80
-
81
- it 'can set read_timeout' do
82
- expect(Flipper::Adapters::Http::Client).to receive(:new)
83
- .with(hash_including(read_timeout: 1)).at_least(:once)
84
- described_class.new(token: 'asdf', read_timeout: 1)
85
- end
86
-
87
- it 'can set open_timeout' do
88
- expect(Flipper::Adapters::Http::Client).to receive(:new)
89
- .with(hash_including(open_timeout: 1)).at_least(:once)
90
- described_class.new(token: 'asdf', open_timeout: 1)
91
- end
92
-
93
- if RUBY_VERSION >= '2.6.0'
94
- it 'can set write_timeout' do
95
- expect(Flipper::Adapters::Http::Client).to receive(:new)
96
- .with(hash_including(open_timeout: 1)).at_least(:once)
97
- described_class.new(token: 'asdf', open_timeout: 1)
98
- end
99
- end
100
-
101
- it 'can import' do
102
- stub_request(:post, /www\.flippercloud\.io\/adapter\/features.*/).
103
- with(headers: {'Flipper-Cloud-Token'=>'asdf'}).to_return(status: 200, body: "{}", headers: {})
104
-
105
- flipper = Flipper.new(Flipper::Adapters::Memory.new)
106
-
107
- flipper.enable(:test)
108
- flipper.enable(:search)
109
- flipper.enable_actor(:stats, Flipper::Actor.new("jnunemaker"))
110
- flipper.enable_percentage_of_time(:logging, 5)
111
-
112
- cloud_flipper = Flipper::Cloud.new(token: "asdf")
113
-
114
- get_all = {
115
- "logging" => {actors: Set.new, boolean: nil, groups: Set.new, percentage_of_actors: nil, percentage_of_time: "5"},
116
- "search" => {actors: Set.new, boolean: "true", groups: Set.new, percentage_of_actors: nil, percentage_of_time: nil},
117
- "stats" => {actors: Set["jnunemaker"], boolean: nil, groups: Set.new, percentage_of_actors: nil, percentage_of_time: nil},
118
- "test" => {actors: Set.new, boolean: "true", groups: Set.new, percentage_of_actors: nil, percentage_of_time: nil},
119
- }
120
-
121
- expect(flipper.adapter.get_all).to eq(get_all)
122
- cloud_flipper.import(flipper)
123
- expect(flipper.adapter.get_all).to eq(get_all)
124
- expect(cloud_flipper.adapter.get_all).to eq(get_all)
125
- end
126
-
127
- it 'raises error for failure while importing' do
128
- stub_request(:post, /www\.flippercloud\.io\/adapter\/features.*/).
129
- with(headers: {'Flipper-Cloud-Token'=>'asdf'}).to_return(status: 500, body: "{}")
130
-
131
- flipper = Flipper.new(Flipper::Adapters::Memory.new)
132
-
133
- flipper.enable(:test)
134
- flipper.enable(:search)
135
- flipper.enable_actor(:stats, Flipper::Actor.new("jnunemaker"))
136
- flipper.enable_percentage_of_time(:logging, 5)
137
-
138
- cloud_flipper = Flipper::Cloud.new(token: "asdf")
139
-
140
- get_all = {
141
- "logging" => {actors: Set.new, boolean: nil, groups: Set.new, percentage_of_actors: nil, percentage_of_time: "5"},
142
- "search" => {actors: Set.new, boolean: "true", groups: Set.new, percentage_of_actors: nil, percentage_of_time: nil},
143
- "stats" => {actors: Set["jnunemaker"], boolean: nil, groups: Set.new, percentage_of_actors: nil, percentage_of_time: nil},
144
- "test" => {actors: Set.new, boolean: "true", groups: Set.new, percentage_of_actors: nil, percentage_of_time: nil},
145
- }
146
-
147
- expect(flipper.adapter.get_all).to eq(get_all)
148
- expect { cloud_flipper.import(flipper) }.to raise_error(Flipper::Adapters::Http::Error)
149
- expect(flipper.adapter.get_all).to eq(get_all)
150
- end
151
-
152
- it 'raises error for timeout while importing' do
153
- stub_request(:post, /www\.flippercloud\.io\/adapter\/features.*/).
154
- with(headers: {'Flipper-Cloud-Token'=>'asdf'}).to_timeout
155
-
156
- flipper = Flipper.new(Flipper::Adapters::Memory.new)
157
-
158
- flipper.enable(:test)
159
- flipper.enable(:search)
160
- flipper.enable_actor(:stats, Flipper::Actor.new("jnunemaker"))
161
- flipper.enable_percentage_of_time(:logging, 5)
162
-
163
- cloud_flipper = Flipper::Cloud.new(token: "asdf")
164
-
165
- get_all = {
166
- "logging" => {actors: Set.new, boolean: nil, groups: Set.new, percentage_of_actors: nil, percentage_of_time: "5"},
167
- "search" => {actors: Set.new, boolean: "true", groups: Set.new, percentage_of_actors: nil, percentage_of_time: nil},
168
- "stats" => {actors: Set["jnunemaker"], boolean: nil, groups: Set.new, percentage_of_actors: nil, percentage_of_time: nil},
169
- "test" => {actors: Set.new, boolean: "true", groups: Set.new, percentage_of_actors: nil, percentage_of_time: nil},
170
- }
171
-
172
- expect(flipper.adapter.get_all).to eq(get_all)
173
- expect { cloud_flipper.import(flipper) }.to raise_error(Net::OpenTimeout)
174
- expect(flipper.adapter.get_all).to eq(get_all)
175
- end
176
- end