flipper 1.3.2 → 1.4.1
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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +9 -6
- data/.github/workflows/examples.yml +5 -4
- data/.github/workflows/release.yml +54 -0
- data/.superset/config.json +4 -0
- data/CLAUDE.md +93 -0
- data/Gemfile +6 -2
- data/README.md +4 -3
- data/examples/cloud/backoff_policy.rb +1 -1
- data/examples/cloud/poll_interval/README.md +111 -0
- data/examples/cloud/poll_interval/client.rb +108 -0
- data/examples/cloud/poll_interval/server.rb +98 -0
- data/examples/expressions.rb +35 -11
- data/lib/flipper/adapter.rb +17 -1
- data/lib/flipper/adapters/actor_limit.rb +27 -1
- data/lib/flipper/adapters/cache_base.rb +21 -3
- data/lib/flipper/adapters/dual_write.rb +6 -2
- data/lib/flipper/adapters/failover.rb +9 -3
- data/lib/flipper/adapters/failsafe.rb +2 -2
- data/lib/flipper/adapters/http/client.rb +15 -4
- data/lib/flipper/adapters/http/error.rb +1 -1
- data/lib/flipper/adapters/http.rb +39 -4
- data/lib/flipper/adapters/instrumented.rb +2 -2
- data/lib/flipper/adapters/memoizable.rb +3 -3
- data/lib/flipper/adapters/memory.rb +1 -1
- data/lib/flipper/adapters/poll.rb +15 -0
- data/lib/flipper/adapters/pstore.rb +1 -1
- data/lib/flipper/adapters/strict.rb +30 -0
- data/lib/flipper/adapters/sync/feature_synchronizer.rb +5 -1
- data/lib/flipper/adapters/sync/synchronizer.rb +13 -5
- data/lib/flipper/adapters/sync.rb +7 -3
- data/lib/flipper/cli.rb +51 -0
- data/lib/flipper/cloud/configuration.rb +14 -6
- data/lib/flipper/cloud/dsl.rb +2 -2
- data/lib/flipper/cloud/middleware.rb +1 -1
- data/lib/flipper/cloud/migrate.rb +71 -0
- data/lib/flipper/cloud/telemetry/backoff_policy.rb +6 -3
- data/lib/flipper/cloud/telemetry/submitter.rb +3 -1
- data/lib/flipper/cloud/telemetry.rb +3 -3
- data/lib/flipper/cloud.rb +1 -0
- data/lib/flipper/dsl.rb +1 -1
- data/lib/flipper/export.rb +0 -2
- data/lib/flipper/expressions/all.rb +0 -2
- data/lib/flipper/expressions/feature_enabled.rb +34 -0
- data/lib/flipper/expressions/time.rb +8 -1
- data/lib/flipper/feature.rb +8 -1
- data/lib/flipper/gate.rb +1 -1
- data/lib/flipper/gates/expression.rb +2 -2
- data/lib/flipper/instrumentation/log_subscriber.rb +1 -2
- data/lib/flipper/instrumentation/statsd.rb +4 -2
- data/lib/flipper/instrumentation/subscriber.rb +0 -4
- data/lib/flipper/metadata.rb +1 -0
- data/lib/flipper/poller.rb +54 -11
- data/lib/flipper/version.rb +1 -1
- data/lib/flipper.rb +17 -1
- data/lib/generators/flipper/setup_generator.rb +5 -0
- data/lib/generators/flipper/templates/initializer.rb +45 -0
- data/spec/flipper/adapter_spec.rb +20 -0
- data/spec/flipper/adapters/actor_limit_spec.rb +55 -0
- data/spec/flipper/adapters/dual_write_spec.rb +13 -0
- data/spec/flipper/adapters/failover_spec.rb +12 -0
- data/spec/flipper/adapters/http_spec.rb +241 -0
- data/spec/flipper/adapters/poll_spec.rb +41 -0
- data/spec/flipper/adapters/strict_spec.rb +62 -4
- data/spec/flipper/adapters/sync/feature_synchronizer_spec.rb +12 -0
- data/spec/flipper/adapters/sync/synchronizer_spec.rb +87 -0
- data/spec/flipper/adapters/sync_spec.rb +13 -0
- data/spec/flipper/cli_spec.rb +51 -0
- data/spec/flipper/cloud/configuration_spec.rb +6 -0
- data/spec/flipper/cloud/dsl_spec.rb +11 -3
- data/spec/flipper/cloud/middleware_spec.rb +34 -16
- data/spec/flipper/cloud/migrate_spec.rb +160 -0
- data/spec/flipper/cloud/telemetry/backoff_policy_spec.rb +3 -3
- data/spec/flipper/cloud/telemetry/submitter_spec.rb +4 -4
- data/spec/flipper/cloud/telemetry_spec.rb +6 -6
- data/spec/flipper/cloud_spec.rb +9 -4
- data/spec/flipper/dsl_spec.rb +0 -3
- data/spec/flipper/engine_spec.rb +3 -2
- data/spec/flipper/expressions/time_spec.rb +16 -0
- data/spec/flipper/feature_spec.rb +22 -11
- data/spec/flipper/gates/expression_spec.rb +82 -0
- data/spec/flipper/instrumentation/log_subscriber_spec.rb +1 -0
- data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +1 -1
- data/spec/flipper/middleware/memoizer_spec.rb +41 -11
- data/spec/flipper/model/active_record_spec.rb +11 -0
- data/spec/flipper/poller_spec.rb +347 -4
- data/spec/flipper_integration_spec.rb +133 -0
- data/spec/flipper_spec.rb +7 -2
- data/spec/spec_helper.rb +15 -5
- data/test_rails/generators/flipper/setup_generator_test.rb +5 -0
- data/test_rails/generators/flipper/update_generator_test.rb +1 -1
- data/test_rails/helper.rb +3 -0
- metadata +17 -111
- data/lib/flipper/expressions/duration.rb +0 -28
- data/spec/flipper/expressions/duration_spec.rb +0 -43
|
@@ -5,6 +5,9 @@ require 'flipper/adapters/instrumented'
|
|
|
5
5
|
|
|
6
6
|
RSpec.describe Flipper::Cloud::DSL do
|
|
7
7
|
it 'delegates everything to flipper instance' do
|
|
8
|
+
# stub the initial sync of http to local
|
|
9
|
+
stub_request(:get, /flippercloud\.io/).to_return(status: 200, body: "{}")
|
|
10
|
+
|
|
8
11
|
cloud_configuration = Flipper::Cloud::Configuration.new({
|
|
9
12
|
token: "asdf",
|
|
10
13
|
sync_secret: "tasty",
|
|
@@ -27,10 +30,13 @@ RSpec.describe Flipper::Cloud::DSL do
|
|
|
27
30
|
})
|
|
28
31
|
dsl = described_class.new(cloud_configuration)
|
|
29
32
|
dsl.sync
|
|
30
|
-
expect(stub).to have_been_requested
|
|
33
|
+
expect(stub).to have_been_requested.at_least_once
|
|
31
34
|
end
|
|
32
35
|
|
|
33
36
|
it 'delegates sync_secret to cloud configuration' do
|
|
37
|
+
# stub the initial sync of http to local
|
|
38
|
+
stub_request(:get, /flippercloud\.io/).to_return(status: 200, body: "{}")
|
|
39
|
+
|
|
34
40
|
cloud_configuration = Flipper::Cloud::Configuration.new({
|
|
35
41
|
token: "asdf",
|
|
36
42
|
sync_secret: "tasty",
|
|
@@ -45,7 +51,7 @@ RSpec.describe Flipper::Cloud::DSL do
|
|
|
45
51
|
end
|
|
46
52
|
|
|
47
53
|
let(:cloud_configuration) do
|
|
48
|
-
|
|
54
|
+
Flipper::Cloud::Configuration.new({
|
|
49
55
|
token: "asdf",
|
|
50
56
|
sync_secret: "tasty",
|
|
51
57
|
local_adapter: local_adapter
|
|
@@ -53,13 +59,15 @@ RSpec.describe Flipper::Cloud::DSL do
|
|
|
53
59
|
end
|
|
54
60
|
|
|
55
61
|
subject do
|
|
62
|
+
# stub the initial sync of http to local
|
|
63
|
+
stub_request(:get, /flippercloud\.io/).to_return(status: 200, body: "{}")
|
|
56
64
|
described_class.new(cloud_configuration)
|
|
57
65
|
end
|
|
58
66
|
|
|
59
67
|
it "sends reads to local adapter" do
|
|
60
68
|
subject.features
|
|
61
69
|
subject.enabled?(:foo)
|
|
62
|
-
expect(local_adapter.count(:features)).to be(
|
|
70
|
+
expect(local_adapter.count(:features)).to be(2)
|
|
63
71
|
expect(local_adapter.count(:get)).to be(1)
|
|
64
72
|
end
|
|
65
73
|
|
|
@@ -62,7 +62,7 @@ RSpec.describe Flipper::Cloud::Middleware do
|
|
|
62
62
|
{"name" => "premium"},
|
|
63
63
|
],
|
|
64
64
|
})
|
|
65
|
-
expect(stub).to
|
|
65
|
+
expect(stub).to have_been_made.at_least_once
|
|
66
66
|
end
|
|
67
67
|
end
|
|
68
68
|
|
|
@@ -72,15 +72,17 @@ RSpec.describe Flipper::Cloud::Middleware do
|
|
|
72
72
|
Flipper::Cloud::MessageVerifier.new(secret: "nope").generate(request_body, timestamp)
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
it '
|
|
76
|
-
|
|
75
|
+
it 'does not perform webhook sync' do
|
|
76
|
+
webhook_regular_stub = stub_request_for_token('regular', from_webhook: true)
|
|
77
|
+
poll_regular_stub = stub_request_for_token('regular', from_webhook: false)
|
|
77
78
|
env = {
|
|
78
79
|
"HTTP_FLIPPER_CLOUD_SIGNATURE" => signature_header_value,
|
|
79
80
|
}
|
|
80
81
|
post '/', request_body, env
|
|
81
82
|
|
|
82
83
|
expect(last_response.status).to eq(400)
|
|
83
|
-
expect(
|
|
84
|
+
expect(poll_regular_stub).to have_been_requested.at_least_once
|
|
85
|
+
expect(webhook_regular_stub).not_to have_been_requested
|
|
84
86
|
end
|
|
85
87
|
end
|
|
86
88
|
|
|
@@ -103,7 +105,7 @@ RSpec.describe Flipper::Cloud::Middleware do
|
|
|
103
105
|
expect(last_response.status).to eq(402)
|
|
104
106
|
expect(last_response.headers["flipper-cloud-response-error-class"]).to eq("Flipper::Adapters::Http::Error")
|
|
105
107
|
expect(last_response.headers["flipper-cloud-response-error-message"]).to include("Failed with status: 402")
|
|
106
|
-
expect(stub).to
|
|
108
|
+
expect(stub).to have_been_made.at_least_once
|
|
107
109
|
end
|
|
108
110
|
end
|
|
109
111
|
|
|
@@ -126,7 +128,7 @@ RSpec.describe Flipper::Cloud::Middleware do
|
|
|
126
128
|
expect(last_response.status).to eq(500)
|
|
127
129
|
expect(last_response.headers["flipper-cloud-response-error-class"]).to eq("Flipper::Adapters::Http::Error")
|
|
128
130
|
expect(last_response.headers["flipper-cloud-response-error-message"]).to include("Failed with status: 503")
|
|
129
|
-
expect(stub).to
|
|
131
|
+
expect(stub).to have_been_made.at_least_once
|
|
130
132
|
end
|
|
131
133
|
end
|
|
132
134
|
|
|
@@ -149,7 +151,7 @@ RSpec.describe Flipper::Cloud::Middleware do
|
|
|
149
151
|
expect(last_response.status).to eq(500)
|
|
150
152
|
expect(last_response.headers["flipper-cloud-response-error-class"]).to eq("Net::OpenTimeout")
|
|
151
153
|
expect(last_response.headers["flipper-cloud-response-error-message"]).to eq("execution expired")
|
|
152
|
-
expect(stub).to
|
|
154
|
+
expect(stub).to have_been_made.at_least_once
|
|
153
155
|
end
|
|
154
156
|
end
|
|
155
157
|
|
|
@@ -160,7 +162,8 @@ RSpec.describe Flipper::Cloud::Middleware do
|
|
|
160
162
|
}
|
|
161
163
|
|
|
162
164
|
it 'uses env instance to sync' do
|
|
163
|
-
|
|
165
|
+
regular_stub = stub_request_for_token('regular')
|
|
166
|
+
env_stub = stub_request_for_token('env')
|
|
164
167
|
env = {
|
|
165
168
|
"HTTP_FLIPPER_CLOUD_SIGNATURE" => signature_header_value,
|
|
166
169
|
'flipper' => env_flipper,
|
|
@@ -168,7 +171,8 @@ RSpec.describe Flipper::Cloud::Middleware do
|
|
|
168
171
|
post '/', request_body, env
|
|
169
172
|
|
|
170
173
|
expect(last_response.status).to eq(200)
|
|
171
|
-
expect(
|
|
174
|
+
expect(regular_stub).to have_been_made.at_least_once
|
|
175
|
+
expect(env_stub).to have_been_made.at_least_once
|
|
172
176
|
end
|
|
173
177
|
end
|
|
174
178
|
|
|
@@ -187,7 +191,7 @@ RSpec.describe Flipper::Cloud::Middleware do
|
|
|
187
191
|
post '/', request_body, env
|
|
188
192
|
|
|
189
193
|
expect(last_response.status).to eq(200)
|
|
190
|
-
expect(stub).to
|
|
194
|
+
expect(stub).to have_been_made.at_least_once
|
|
191
195
|
end
|
|
192
196
|
end
|
|
193
197
|
|
|
@@ -198,7 +202,9 @@ RSpec.describe Flipper::Cloud::Middleware do
|
|
|
198
202
|
}
|
|
199
203
|
|
|
200
204
|
it 'uses provided env key instead of default' do
|
|
201
|
-
|
|
205
|
+
regular_poll_stub = stub_request_for_token('regular')
|
|
206
|
+
env_poll_stub = stub_request_for_token('env')
|
|
207
|
+
env_webhook_stub = stub_request_for_token('env', from_webhook: true)
|
|
202
208
|
env = {
|
|
203
209
|
"HTTP_FLIPPER_CLOUD_SIGNATURE" => signature_header_value,
|
|
204
210
|
'flipper' => flipper,
|
|
@@ -207,7 +213,9 @@ RSpec.describe Flipper::Cloud::Middleware do
|
|
|
207
213
|
post '/', request_body, env
|
|
208
214
|
|
|
209
215
|
expect(last_response.status).to eq(200)
|
|
210
|
-
expect(
|
|
216
|
+
expect(regular_poll_stub).to have_been_made.at_least_once
|
|
217
|
+
expect(env_poll_stub).to have_been_made.at_least_once
|
|
218
|
+
expect(env_webhook_stub).not_to have_been_requested
|
|
211
219
|
end
|
|
212
220
|
end
|
|
213
221
|
|
|
@@ -222,7 +230,7 @@ RSpec.describe Flipper::Cloud::Middleware do
|
|
|
222
230
|
post '/', request_body, env
|
|
223
231
|
|
|
224
232
|
expect(last_response.status).to eq(200)
|
|
225
|
-
expect(stub).to
|
|
233
|
+
expect(stub).to have_been_made.at_least_once
|
|
226
234
|
end
|
|
227
235
|
end
|
|
228
236
|
|
|
@@ -252,12 +260,13 @@ RSpec.describe Flipper::Cloud::Middleware do
|
|
|
252
260
|
{"name" => "premium"},
|
|
253
261
|
],
|
|
254
262
|
})
|
|
255
|
-
expect(stub).to
|
|
263
|
+
expect(stub).to have_been_made.at_least_once
|
|
256
264
|
end
|
|
257
265
|
end
|
|
258
266
|
|
|
259
267
|
describe 'Request method unsupported' do
|
|
260
268
|
it 'skips middleware' do
|
|
269
|
+
stub_request(:get, /flippercloud\.io/).to_return(status: 200, body: "{}")
|
|
261
270
|
get '/'
|
|
262
271
|
expect(last_response.status).to eq(404)
|
|
263
272
|
expect(last_response.content_type).to eq("application/json")
|
|
@@ -267,14 +276,23 @@ RSpec.describe Flipper::Cloud::Middleware do
|
|
|
267
276
|
|
|
268
277
|
describe 'Inspecting the built Rack app' do
|
|
269
278
|
it 'returns a String' do
|
|
279
|
+
stub_request(:get, /flippercloud\.io/).to_return(status: 200, body: "{}")
|
|
270
280
|
expect(Flipper::Cloud.app(flipper).inspect).to eq("Flipper::Cloud")
|
|
271
281
|
end
|
|
272
282
|
end
|
|
273
283
|
|
|
274
284
|
private
|
|
275
285
|
|
|
276
|
-
def stub_request_for_token(token, status: 200)
|
|
277
|
-
|
|
286
|
+
def stub_request_for_token(token, status: 200, from_webhook: false)
|
|
287
|
+
if from_webhook
|
|
288
|
+
# Match URL with both exclude_gate_names=true and _cb=integer
|
|
289
|
+
url_pattern = %r{https://www\.flippercloud\.io/adapter/features\?.*exclude_gate_names=true.*&_cb=\d+}
|
|
290
|
+
else
|
|
291
|
+
# Match URL with just exclude_gate_names=true
|
|
292
|
+
url_pattern = %r{https://www\.flippercloud\.io/adapter/features\?.*exclude_gate_names=true}
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
stub = stub_request(:get, url_pattern).
|
|
278
296
|
with({
|
|
279
297
|
headers: {
|
|
280
298
|
'flipper-cloud-token' => token,
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
require "flipper/cloud/migrate"
|
|
2
|
+
require "flipper/typecast"
|
|
3
|
+
require "webmock/rspec"
|
|
4
|
+
|
|
5
|
+
RSpec.describe Flipper::Cloud, ".migrate" do
|
|
6
|
+
let(:flipper) { Flipper.new(Flipper::Adapters::Memory.new) }
|
|
7
|
+
|
|
8
|
+
before do
|
|
9
|
+
flipper.enable :search
|
|
10
|
+
flipper.disable :analytics
|
|
11
|
+
flipper.enable_percentage_of_actors :checkout, 50
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
around do |example|
|
|
15
|
+
original = ENV["FLIPPER_CLOUD_URL"]
|
|
16
|
+
ENV["FLIPPER_CLOUD_URL"] = "http://localhost:5555"
|
|
17
|
+
example.run
|
|
18
|
+
ensure
|
|
19
|
+
ENV["FLIPPER_CLOUD_URL"] = original
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def decompress_request_body
|
|
23
|
+
raw = WebMock::RequestRegistry.instance.requested_signatures.hash.keys.last.body
|
|
24
|
+
JSON.parse(Flipper::Typecast.from_gzip(raw))
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
describe ".migrate" do
|
|
28
|
+
it "returns a MigrateResult with code and url on success" do
|
|
29
|
+
stub_request(:post, "http://localhost:5555/api/migrate")
|
|
30
|
+
.to_return(status: 200, body: '{"url":"http://localhost:5555/cloud/setup/abc123"}', headers: {"Content-Type" => "application/json"})
|
|
31
|
+
|
|
32
|
+
result = Flipper::Cloud.migrate(flipper)
|
|
33
|
+
|
|
34
|
+
expect(result).to be_a(Flipper::Cloud::MigrateResult)
|
|
35
|
+
expect(result.code).to eq(200)
|
|
36
|
+
expect(result.url).to eq("http://localhost:5555/cloud/setup/abc123")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it "sends export data and metadata in the request body" do
|
|
40
|
+
stub = stub_request(:post, "http://localhost:5555/api/migrate")
|
|
41
|
+
.to_return(status: 200, body: '{"url":"http://localhost:5555/cloud/setup/abc123"}')
|
|
42
|
+
|
|
43
|
+
Flipper::Cloud.migrate(flipper, app_name: "MyApp")
|
|
44
|
+
|
|
45
|
+
expect(stub).to have_been_requested
|
|
46
|
+
body = decompress_request_body
|
|
47
|
+
expect(body["metadata"]["app_name"]).to eq("MyApp")
|
|
48
|
+
expect(body["export"]["version"]).to eq(1)
|
|
49
|
+
expect(body["export"]["features"]).to have_key("search")
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it "sends gzip-compressed request body" do
|
|
53
|
+
stub = stub_request(:post, "http://localhost:5555/api/migrate")
|
|
54
|
+
.with(headers: {"content-encoding" => "gzip"})
|
|
55
|
+
.to_return(status: 200, body: '{"url":"http://localhost:5555/cloud/setup/abc"}')
|
|
56
|
+
|
|
57
|
+
Flipper::Cloud.migrate(flipper)
|
|
58
|
+
|
|
59
|
+
expect(stub).to have_been_requested
|
|
60
|
+
body = decompress_request_body
|
|
61
|
+
expect(body["export"]["features"]).to have_key("search")
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
it "handles error responses" do
|
|
65
|
+
stub_request(:post, "http://localhost:5555/api/migrate")
|
|
66
|
+
.to_return(status: 500, body: '{"error":"Internal Server Error"}')
|
|
67
|
+
|
|
68
|
+
result = Flipper::Cloud.migrate(flipper)
|
|
69
|
+
|
|
70
|
+
expect(result.code).to eq(500)
|
|
71
|
+
expect(result.url).to be_nil
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
it "includes error message from response body" do
|
|
75
|
+
stub_request(:post, "http://localhost:5555/api/migrate")
|
|
76
|
+
.to_return(status: 422, body: '{"error":"Invalid export format"}')
|
|
77
|
+
|
|
78
|
+
result = Flipper::Cloud.migrate(flipper)
|
|
79
|
+
|
|
80
|
+
expect(result.code).to eq(422)
|
|
81
|
+
expect(result.message).to eq("Invalid export format")
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
it "uses FLIPPER_CLOUD_URL environment variable" do
|
|
85
|
+
stub = stub_request(:post, "http://localhost:5555/api/migrate")
|
|
86
|
+
.to_return(status: 200, body: '{"url":"http://localhost:5555/cloud/setup/abc"}')
|
|
87
|
+
|
|
88
|
+
Flipper::Cloud.migrate(flipper)
|
|
89
|
+
|
|
90
|
+
expect(stub).to have_been_requested
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
it "sends content-type and accept headers" do
|
|
94
|
+
stub = stub_request(:post, "http://localhost:5555/api/migrate")
|
|
95
|
+
.with(headers: {
|
|
96
|
+
"content-type" => "application/json",
|
|
97
|
+
"accept" => "application/json",
|
|
98
|
+
})
|
|
99
|
+
.to_return(status: 200, body: '{"url":"http://localhost:5555/cloud/setup/abc"}')
|
|
100
|
+
|
|
101
|
+
Flipper::Cloud.migrate(flipper)
|
|
102
|
+
|
|
103
|
+
expect(stub).to have_been_requested
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
describe ".push" do
|
|
108
|
+
it "returns a MigrateResult with code on success" do
|
|
109
|
+
stub_request(:post, "http://localhost:5555/adapter/import")
|
|
110
|
+
.to_return(status: 204, body: "")
|
|
111
|
+
|
|
112
|
+
result = Flipper::Cloud.push("test-token", flipper)
|
|
113
|
+
|
|
114
|
+
expect(result).to be_a(Flipper::Cloud::MigrateResult)
|
|
115
|
+
expect(result.code).to eq(204)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
it "sends the token as a header" do
|
|
119
|
+
stub = stub_request(:post, "http://localhost:5555/adapter/import")
|
|
120
|
+
.with(headers: {"flipper-cloud-token" => "test-token"})
|
|
121
|
+
.to_return(status: 204, body: "")
|
|
122
|
+
|
|
123
|
+
Flipper::Cloud.push("test-token", flipper)
|
|
124
|
+
|
|
125
|
+
expect(stub).to have_been_requested
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
it "sends gzip-compressed export contents as the body" do
|
|
129
|
+
stub = stub_request(:post, "http://localhost:5555/adapter/import")
|
|
130
|
+
.with(headers: {"content-encoding" => "gzip"})
|
|
131
|
+
.to_return(status: 204, body: "")
|
|
132
|
+
|
|
133
|
+
Flipper::Cloud.push("test-token", flipper)
|
|
134
|
+
|
|
135
|
+
expect(stub).to have_been_requested
|
|
136
|
+
body = decompress_request_body
|
|
137
|
+
expect(body["version"]).to eq(1)
|
|
138
|
+
expect(body["features"]).to have_key("search")
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
it "handles error responses" do
|
|
142
|
+
stub_request(:post, "http://localhost:5555/adapter/import")
|
|
143
|
+
.to_return(status: 401, body: '{"error":"Unauthorized"}')
|
|
144
|
+
|
|
145
|
+
result = Flipper::Cloud.push("bad-token", flipper)
|
|
146
|
+
|
|
147
|
+
expect(result.code).to eq(401)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
it "includes error message from response body" do
|
|
151
|
+
stub_request(:post, "http://localhost:5555/adapter/import")
|
|
152
|
+
.to_return(status: 401, body: '{"error":"Invalid token"}')
|
|
153
|
+
|
|
154
|
+
result = Flipper::Cloud.push("bad-token", flipper)
|
|
155
|
+
|
|
156
|
+
expect(result.code).to eq(401)
|
|
157
|
+
expect(result.message).to eq("Invalid token")
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
@@ -4,8 +4,8 @@ RSpec.describe Flipper::Cloud::Telemetry::BackoffPolicy do
|
|
|
4
4
|
context "#initialize" do
|
|
5
5
|
it "with no options" do
|
|
6
6
|
policy = described_class.new
|
|
7
|
-
expect(policy.min_timeout_ms).to eq(
|
|
8
|
-
expect(policy.max_timeout_ms).to eq(
|
|
7
|
+
expect(policy.min_timeout_ms).to eq(30_000)
|
|
8
|
+
expect(policy.max_timeout_ms).to eq(120_000)
|
|
9
9
|
expect(policy.multiplier).to eq(1.5)
|
|
10
10
|
expect(policy.randomization_factor).to eq(0.5)
|
|
11
11
|
end
|
|
@@ -87,7 +87,7 @@ RSpec.describe Flipper::Cloud::Telemetry::BackoffPolicy do
|
|
|
87
87
|
randomization_factor: 0.5,
|
|
88
88
|
})
|
|
89
89
|
10.times { policy.next_interval }
|
|
90
|
-
expect(policy.next_interval).to
|
|
90
|
+
expect(policy.next_interval).to be_within(10_000*0.1).of(10_000)
|
|
91
91
|
end
|
|
92
92
|
end
|
|
93
93
|
|
|
@@ -71,15 +71,15 @@ RSpec.describe Flipper::Cloud::Telemetry::Submitter do
|
|
|
71
71
|
to_return(status: 429, body: "{}").
|
|
72
72
|
to_return(status: 200, body: "{}")
|
|
73
73
|
instance = described_class.new(cloud_configuration)
|
|
74
|
-
expect(instance.backoff_policy.min_timeout_ms).to eq(
|
|
75
|
-
expect(instance.backoff_policy.max_timeout_ms).to eq(
|
|
74
|
+
expect(instance.backoff_policy.min_timeout_ms).to eq(30_000)
|
|
75
|
+
expect(instance.backoff_policy.max_timeout_ms).to eq(120_000)
|
|
76
76
|
end
|
|
77
77
|
|
|
78
78
|
it "tries 10 times by default" do
|
|
79
79
|
stub_request(:post, "https://www.flippercloud.io/adapter/telemetry").
|
|
80
80
|
to_return(status: 500, body: "{}")
|
|
81
81
|
subject.call(enabled_metrics)
|
|
82
|
-
expect(subject.backoff_policy.retries).to eq(
|
|
82
|
+
expect(subject.backoff_policy.retries).to eq(4) # 4 retries + 1 initial attempt
|
|
83
83
|
end
|
|
84
84
|
|
|
85
85
|
[
|
|
@@ -105,7 +105,7 @@ RSpec.describe Flipper::Cloud::Telemetry::Submitter do
|
|
|
105
105
|
stub_request(:post, "https://www.flippercloud.io/adapter/telemetry").
|
|
106
106
|
to_raise(error_class)
|
|
107
107
|
subject.call(enabled_metrics)
|
|
108
|
-
expect(subject.backoff_policy.retries).to eq(
|
|
108
|
+
expect(subject.backoff_policy.retries).to eq(4)
|
|
109
109
|
end
|
|
110
110
|
end
|
|
111
111
|
|
|
@@ -25,7 +25,7 @@ RSpec.describe Flipper::Cloud::Telemetry do
|
|
|
25
25
|
|
|
26
26
|
expect(telemetry.interval).to eq(60)
|
|
27
27
|
expect(telemetry.timer.execution_interval).to eq(60)
|
|
28
|
-
expect(stub).to have_been_requested
|
|
28
|
+
expect(stub).to have_been_requested.at_least_once
|
|
29
29
|
end
|
|
30
30
|
|
|
31
31
|
it "phones home and updates telemetry interval if present" do
|
|
@@ -45,7 +45,7 @@ RSpec.describe Flipper::Cloud::Telemetry do
|
|
|
45
45
|
|
|
46
46
|
expect(telemetry.interval).to eq(120)
|
|
47
47
|
expect(telemetry.timer.execution_interval).to eq(120)
|
|
48
|
-
expect(stub).to have_been_requested
|
|
48
|
+
expect(stub).to have_been_requested.at_least_once
|
|
49
49
|
end
|
|
50
50
|
|
|
51
51
|
it "phones home and requests shutdown if telemetry-shutdown header is true" do
|
|
@@ -67,7 +67,7 @@ RSpec.describe Flipper::Cloud::Telemetry do
|
|
|
67
67
|
result: true,
|
|
68
68
|
})
|
|
69
69
|
telemetry.stop
|
|
70
|
-
expect(stub).to have_been_requested
|
|
70
|
+
expect(stub).to have_been_requested.at_least_once
|
|
71
71
|
expect(output.string).to match(/action=telemetry_shutdown message=The server has requested that telemetry be shut down./)
|
|
72
72
|
end
|
|
73
73
|
|
|
@@ -90,7 +90,7 @@ RSpec.describe Flipper::Cloud::Telemetry do
|
|
|
90
90
|
result: true,
|
|
91
91
|
})
|
|
92
92
|
telemetry.stop
|
|
93
|
-
expect(stub).to have_been_requested
|
|
93
|
+
expect(stub).to have_been_requested.at_least_once
|
|
94
94
|
expect(output.string).not_to match(/action=telemetry_shutdown message=The server has requested that telemetry be shut down./)
|
|
95
95
|
end
|
|
96
96
|
|
|
@@ -122,7 +122,7 @@ RSpec.describe Flipper::Cloud::Telemetry do
|
|
|
122
122
|
# Check the conig interval and the timer interval.
|
|
123
123
|
expect(telemetry.interval).to eq(120)
|
|
124
124
|
expect(telemetry.timer.execution_interval).to eq(120)
|
|
125
|
-
expect(stub).to have_been_requested.
|
|
125
|
+
expect(stub).to have_been_requested.at_least_times(5)
|
|
126
126
|
end
|
|
127
127
|
|
|
128
128
|
it "doesn't try to update telemetry interval from error if not response error" do
|
|
@@ -152,7 +152,7 @@ RSpec.describe Flipper::Cloud::Telemetry do
|
|
|
152
152
|
|
|
153
153
|
expect(telemetry.interval).to eq(60)
|
|
154
154
|
expect(telemetry.timer.execution_interval).to eq(60)
|
|
155
|
-
expect(stub).to have_been_requested.times(
|
|
155
|
+
expect(stub).to have_been_requested.times(5)
|
|
156
156
|
end
|
|
157
157
|
|
|
158
158
|
describe '#record' do
|
data/spec/flipper/cloud_spec.rb
CHANGED
|
@@ -43,6 +43,7 @@ RSpec.describe Flipper::Cloud do
|
|
|
43
43
|
|
|
44
44
|
context 'initialize with token and options' do
|
|
45
45
|
it 'sets correct url' do
|
|
46
|
+
stub_request(:any, %r{fakeflipper.com}).to_return(status: 200)
|
|
46
47
|
instance = described_class.new(token: 'asdf', url: 'https://www.fakeflipper.com/sadpanda')
|
|
47
48
|
# pardon the nesting...
|
|
48
49
|
memoized = instance.adapter
|
|
@@ -78,27 +79,31 @@ RSpec.describe Flipper::Cloud do
|
|
|
78
79
|
end
|
|
79
80
|
|
|
80
81
|
it 'can set debug_output' do
|
|
82
|
+
instance = Flipper::Adapters::Http::Client.new(token: 'asdf', url: 'https://www.flippercloud.io/adapter')
|
|
81
83
|
expect(Flipper::Adapters::Http::Client).to receive(:new)
|
|
82
|
-
.with(hash_including(debug_output: STDOUT)).at_least(:once)
|
|
84
|
+
.with(hash_including(debug_output: STDOUT)).at_least(:once).and_return(instance)
|
|
83
85
|
described_class.new(token: 'asdf', debug_output: STDOUT)
|
|
84
86
|
end
|
|
85
87
|
|
|
86
88
|
it 'can set read_timeout' do
|
|
89
|
+
instance = Flipper::Adapters::Http::Client.new(token: 'asdf', url: 'https://www.flippercloud.io/adapter')
|
|
87
90
|
expect(Flipper::Adapters::Http::Client).to receive(:new)
|
|
88
|
-
.with(hash_including(read_timeout: 1)).at_least(:once)
|
|
91
|
+
.with(hash_including(read_timeout: 1)).at_least(:once).and_return(instance)
|
|
89
92
|
described_class.new(token: 'asdf', read_timeout: 1)
|
|
90
93
|
end
|
|
91
94
|
|
|
92
95
|
it 'can set open_timeout' do
|
|
96
|
+
instance = Flipper::Adapters::Http::Client.new(token: 'asdf', url: 'https://www.flippercloud.io/adapter')
|
|
93
97
|
expect(Flipper::Adapters::Http::Client).to receive(:new)
|
|
94
|
-
.with(hash_including(open_timeout: 1)).at_least(:once)
|
|
98
|
+
.with(hash_including(open_timeout: 1)).at_least(:once).and_return(instance)
|
|
95
99
|
described_class.new(token: 'asdf', open_timeout: 1)
|
|
96
100
|
end
|
|
97
101
|
|
|
98
102
|
if RUBY_VERSION >= '2.6.0'
|
|
99
103
|
it 'can set write_timeout' do
|
|
104
|
+
instance = Flipper::Adapters::Http::Client.new(token: 'asdf', url: 'https://www.flippercloud.io/adapter')
|
|
100
105
|
expect(Flipper::Adapters::Http::Client).to receive(:new)
|
|
101
|
-
.with(hash_including(open_timeout: 1)).at_least(:once)
|
|
106
|
+
.with(hash_including(open_timeout: 1)).at_least(:once).and_return(instance)
|
|
102
107
|
described_class.new(token: 'asdf', open_timeout: 1)
|
|
103
108
|
end
|
|
104
109
|
end
|
data/spec/flipper/dsl_spec.rb
CHANGED
|
@@ -225,9 +225,6 @@ RSpec.describe Flipper::DSL do
|
|
|
225
225
|
|
|
226
226
|
describe '#enable_group/disable_group' do
|
|
227
227
|
it 'enables and disables the feature for group' do
|
|
228
|
-
actor = Flipper::Actor.new(5)
|
|
229
|
-
group = Flipper.register(:fives) { |actor| actor.flipper_id == 5 }
|
|
230
|
-
|
|
231
228
|
expect(subject[:stats].groups_value).to be_empty
|
|
232
229
|
subject.enable_group(:stats, :fives)
|
|
233
230
|
expect(subject[:stats].groups_value).to eq(Set['fives'])
|
data/spec/flipper/engine_spec.rb
CHANGED
|
@@ -12,6 +12,7 @@ RSpec.describe Flipper::Engine do
|
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
before do
|
|
15
|
+
stub_request(:get, /flippercloud\.io/).to_return(status: 200, body: "{}")
|
|
15
16
|
Rails.application = nil
|
|
16
17
|
ActiveSupport::Dependencies.autoload_paths = ActiveSupport::Dependencies.autoload_paths.dup
|
|
17
18
|
ActiveSupport::Dependencies.autoload_once_paths = ActiveSupport::Dependencies.autoload_once_paths.dup
|
|
@@ -262,10 +263,10 @@ RSpec.describe Flipper::Engine do
|
|
|
262
263
|
Flipper::Cloud::MessageVerifier.new(secret: "").header(signature, timestamp)
|
|
263
264
|
}
|
|
264
265
|
|
|
265
|
-
it "configures webhook app" do
|
|
266
|
+
it "configures webhook app and uses cache busting" do
|
|
266
267
|
silence { application.initialize! }
|
|
267
268
|
|
|
268
|
-
stub = stub_request(:get,
|
|
269
|
+
stub = stub_request(:get, /https:\/\/www\.flippercloud\.io\/adapter\/features\?_cb=\d+&exclude_gate_names=true/).with({
|
|
269
270
|
headers: { "flipper-cloud-token" => ENV["FLIPPER_CLOUD_TOKEN"] },
|
|
270
271
|
}).to_return(status: 200, body: JSON.generate({ features: {} }), headers: {})
|
|
271
272
|
|
|
@@ -9,5 +9,21 @@ RSpec.describe Flipper::Expressions::Time do
|
|
|
9
9
|
it "returns time for #iso8601 format" do
|
|
10
10
|
expect(described_class.call(time.iso8601)).to eq(time)
|
|
11
11
|
end
|
|
12
|
+
|
|
13
|
+
it "returns time for epoch integer" do
|
|
14
|
+
expect(described_class.call(time.to_i)).to eq(Time.at(time.to_i).utc)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it "returns time for epoch float" do
|
|
18
|
+
expect(described_class.call(time.to_f)).to be_within(0.001).of(time)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it "returns utc for string input" do
|
|
22
|
+
expect(described_class.call(time.to_s).utc?).to be(true)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it "returns utc for numeric input" do
|
|
26
|
+
expect(described_class.call(time.to_i).utc?).to be(true)
|
|
27
|
+
end
|
|
12
28
|
end
|
|
13
29
|
end
|
|
@@ -76,6 +76,26 @@ RSpec.describe Flipper::Feature do
|
|
|
76
76
|
expect(subject.enabled?(actors)).to be(false)
|
|
77
77
|
end
|
|
78
78
|
end
|
|
79
|
+
|
|
80
|
+
context "for an object that implements .nil? == true" do
|
|
81
|
+
let(:actor) { Flipper::Actor.new("User;1") }
|
|
82
|
+
|
|
83
|
+
before do
|
|
84
|
+
def actor.nil?
|
|
85
|
+
true
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
it 'returns true if feature is enabled' do
|
|
90
|
+
subject.enable
|
|
91
|
+
expect(subject.enabled?(actor)).to be(true)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
it 'returns false if feature is disabled' do
|
|
95
|
+
subject.disable
|
|
96
|
+
expect(subject.enabled?(actor)).to be(false)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
79
99
|
end
|
|
80
100
|
|
|
81
101
|
describe '#to_s' do
|
|
@@ -195,7 +215,7 @@ RSpec.describe Flipper::Feature do
|
|
|
195
215
|
|
|
196
216
|
it 'is recorded for enable' do
|
|
197
217
|
actor = Flipper::Types::Actor.new(Flipper::Actor.new('1'))
|
|
198
|
-
|
|
218
|
+
subject.gate_for(actor)
|
|
199
219
|
|
|
200
220
|
subject.enable(actor)
|
|
201
221
|
|
|
@@ -210,7 +230,7 @@ RSpec.describe Flipper::Feature do
|
|
|
210
230
|
|
|
211
231
|
it 'always instruments flipper type instance for enable' do
|
|
212
232
|
actor = Flipper::Actor.new('1')
|
|
213
|
-
|
|
233
|
+
subject.gate_for(actor)
|
|
214
234
|
|
|
215
235
|
subject.enable(actor)
|
|
216
236
|
|
|
@@ -221,7 +241,6 @@ RSpec.describe Flipper::Feature do
|
|
|
221
241
|
|
|
222
242
|
it 'is recorded for disable' do
|
|
223
243
|
thing = Flipper::Types::Boolean.new
|
|
224
|
-
gate = subject.gate_for(thing)
|
|
225
244
|
|
|
226
245
|
subject.disable(thing)
|
|
227
246
|
|
|
@@ -266,7 +285,6 @@ RSpec.describe Flipper::Feature do
|
|
|
266
285
|
|
|
267
286
|
it 'always instruments flipper type instance for disable' do
|
|
268
287
|
actor = Flipper::Actor.new('1')
|
|
269
|
-
gate = subject.gate_for(actor)
|
|
270
288
|
|
|
271
289
|
subject.disable(actor)
|
|
272
290
|
|
|
@@ -709,7 +727,6 @@ RSpec.describe Flipper::Feature do
|
|
|
709
727
|
context "with expression instance" do
|
|
710
728
|
it "updates gate values to equal expression or clears expression" do
|
|
711
729
|
expression = Flipper.property(:plan).eq("basic")
|
|
712
|
-
other_expression = Flipper.property(:age).gte(21)
|
|
713
730
|
expect(subject.gate_values.expression).to be(nil)
|
|
714
731
|
subject.enable_expression(expression)
|
|
715
732
|
expect(subject.gate_values.expression).to eq(expression.value)
|
|
@@ -721,7 +738,6 @@ RSpec.describe Flipper::Feature do
|
|
|
721
738
|
context "with Hash" do
|
|
722
739
|
it "updates gate values to equal expression or clears expression" do
|
|
723
740
|
expression = Flipper.property(:plan).eq("basic")
|
|
724
|
-
other_expression = Flipper.property(:age).gte(21)
|
|
725
741
|
expect(subject.gate_values.expression).to be(nil)
|
|
726
742
|
subject.enable_expression(expression.value)
|
|
727
743
|
expect(subject.gate_values.expression).to eq(expression.value)
|
|
@@ -1078,8 +1094,6 @@ RSpec.describe Flipper::Feature do
|
|
|
1078
1094
|
describe '#enable_group/disable_group' do
|
|
1079
1095
|
context 'with symbol group name' do
|
|
1080
1096
|
it 'updates the gate values to include the group' do
|
|
1081
|
-
actor = Flipper::Actor.new(5)
|
|
1082
|
-
group = Flipper.register(:five_only) { |actor| actor.flipper_id == 5 }
|
|
1083
1097
|
expect(subject.gate_values.groups).to be_empty
|
|
1084
1098
|
subject.enable_group(:five_only)
|
|
1085
1099
|
expect(subject.gate_values.groups).to eq(Set['five_only'])
|
|
@@ -1090,8 +1104,6 @@ RSpec.describe Flipper::Feature do
|
|
|
1090
1104
|
|
|
1091
1105
|
context 'with string group name' do
|
|
1092
1106
|
it 'updates the gate values to include the group' do
|
|
1093
|
-
actor = Flipper::Actor.new(5)
|
|
1094
|
-
group = Flipper.register(:five_only) { |actor| actor.flipper_id == 5 }
|
|
1095
1107
|
expect(subject.gate_values.groups).to be_empty
|
|
1096
1108
|
subject.enable_group('five_only')
|
|
1097
1109
|
expect(subject.gate_values.groups).to eq(Set['five_only'])
|
|
@@ -1102,7 +1114,6 @@ RSpec.describe Flipper::Feature do
|
|
|
1102
1114
|
|
|
1103
1115
|
context 'with group instance' do
|
|
1104
1116
|
it 'updates the gate values for the group' do
|
|
1105
|
-
actor = Flipper::Actor.new(5)
|
|
1106
1117
|
group = Flipper.register(:five_only) { |actor| actor.flipper_id == 5 }
|
|
1107
1118
|
expect(subject.gate_values.groups).to be_empty
|
|
1108
1119
|
subject.enable_group(group)
|