flipper 1.4.0 → 1.4.2
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 +1 -1
- data/.github/workflows/examples.yml +1 -1
- data/.superset/config.json +4 -0
- data/CLAUDE.md +6 -0
- data/lib/flipper/adapters/http.rb +1 -1
- data/lib/flipper/adapters/strict.rb +30 -0
- data/lib/flipper/adapters/sync/synchronizer.rb +4 -1
- data/lib/flipper/cloud/configuration.rb +3 -7
- data/lib/flipper/poller.rb +6 -4
- data/lib/flipper/version.rb +1 -1
- data/spec/flipper/adapters/http_spec.rb +89 -0
- data/spec/flipper/adapters/strict_spec.rb +62 -4
- data/spec/flipper/cloud/configuration_spec.rb +0 -6
- data/spec/flipper/cloud/dsl_spec.rb +2 -10
- data/spec/flipper/cloud/middleware_spec.rb +16 -34
- data/spec/flipper_spec.rb +1 -1
- data/spec/spec_helper.rb +7 -0
- metadata +4 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c886665703939f49361ef42fb82e951383cb53bd99f2b1d902add786f6793da0
|
|
4
|
+
data.tar.gz: b7af2d5972cc409f075390871dc96b225e495133917e0c5024db297760252e73
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 338ccb2e6d9ddeaa6f6a1f20755e7799dcda40341c22831f8eda1f25df5f08bf4c61d67333e9bdebf94172107842b07e7ddb3ec7a54a2d057663f109e78f5a5d
|
|
7
|
+
data.tar.gz: bbfdb72c366e7fa334d8c3903b799366bed22d755cf6771c532d1335cccf7e9480c8a752c700a449f1ae837ae273be5b9c06ef58bc38b56570d96ef417edbded
|
data/.github/workflows/ci.yml
CHANGED
|
@@ -93,7 +93,7 @@ jobs:
|
|
|
93
93
|
- name: Check out repository code
|
|
94
94
|
uses: actions/checkout@v6
|
|
95
95
|
- name: Do some action caching
|
|
96
|
-
uses: actions/cache@
|
|
96
|
+
uses: actions/cache@v5
|
|
97
97
|
with:
|
|
98
98
|
path: vendor/bundle
|
|
99
99
|
key: ${{ runner.os }}-gems-${{ matrix.ruby }}-${{ matrix.rails }}-${{ hashFiles('**/Gemfile.lock') }}
|
|
@@ -73,7 +73,7 @@ jobs:
|
|
|
73
73
|
- name: Check out repository code
|
|
74
74
|
uses: actions/checkout@v6
|
|
75
75
|
- name: Do some action caching
|
|
76
|
-
uses: actions/cache@
|
|
76
|
+
uses: actions/cache@v5
|
|
77
77
|
with:
|
|
78
78
|
path: vendor/bundle
|
|
79
79
|
key: ${{ runner.os }}-gems-${{ matrix.ruby }}-${{ matrix.rails }}-${{ hashFiles('**/Gemfile.lock') }}
|
data/CLAUDE.md
CHANGED
|
@@ -26,6 +26,10 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|
|
26
26
|
- `bundle exec rake build` - Build all gems locally into pkg/ directory
|
|
27
27
|
- `script/release` - Manual fallback for local releases (prompts for OTP)
|
|
28
28
|
|
|
29
|
+
**After releasing**, purge the cached release URLs on flippercloud.io:
|
|
30
|
+
- `/release`
|
|
31
|
+
- `/release.json`
|
|
32
|
+
|
|
29
33
|
## Architecture Overview
|
|
30
34
|
|
|
31
35
|
Flipper is a feature flag library for Ruby with a modular adapter-based architecture:
|
|
@@ -85,3 +89,5 @@ For outbound HTTP requests, use `Flipper::Adapters::Http::Client` instead of raw
|
|
|
85
89
|
### Testing
|
|
86
90
|
|
|
87
91
|
Uses both RSpec (currently preferred for new tests) and Minitest. Shared adapter specs ensure consistency across all storage backends. Extensive testing across multiple Rails versions (5.0-8.0).
|
|
92
|
+
|
|
93
|
+
`Flipper.configuration` is reset to nil before each spec (in `spec/spec_helper.rb`), but `Flipper::UI.configuration` is **not** globally reset. When modifying UI config in tests, set the value in `before` and reset it in `after` to match the existing pattern throughout the spec suite.
|
|
@@ -10,11 +10,40 @@ module Flipper
|
|
|
10
10
|
end
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
+
class << self
|
|
14
|
+
# Returns whether sync mode is enabled for the current thread.
|
|
15
|
+
# When sync mode is enabled, strict checks are not enforced,
|
|
16
|
+
# allowing sync operations to add features and bring local state
|
|
17
|
+
# in line with remote state.
|
|
18
|
+
def sync_mode
|
|
19
|
+
Thread.current[:flipper_strict_sync_mode]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def sync_mode=(value)
|
|
23
|
+
Thread.current[:flipper_strict_sync_mode] = value
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Executes a block with sync mode enabled. Strict checks will
|
|
27
|
+
# not be enforced within the block.
|
|
28
|
+
def with_sync_mode
|
|
29
|
+
old_value = sync_mode
|
|
30
|
+
self.sync_mode = true
|
|
31
|
+
yield
|
|
32
|
+
ensure
|
|
33
|
+
self.sync_mode = old_value
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
13
37
|
def initialize(adapter, handler = nil, &block)
|
|
14
38
|
super(adapter)
|
|
15
39
|
@handler = block || handler
|
|
16
40
|
end
|
|
17
41
|
|
|
42
|
+
def add(feature)
|
|
43
|
+
assert_feature_exists(feature) unless self.class.sync_mode
|
|
44
|
+
super
|
|
45
|
+
end
|
|
46
|
+
|
|
18
47
|
def get(feature)
|
|
19
48
|
assert_feature_exists(feature)
|
|
20
49
|
super
|
|
@@ -28,6 +57,7 @@ module Flipper
|
|
|
28
57
|
private
|
|
29
58
|
|
|
30
59
|
def assert_feature_exists(feature)
|
|
60
|
+
return if self.class.sync_mode
|
|
31
61
|
return if @adapter.features.include?(feature.key)
|
|
32
62
|
|
|
33
63
|
case handler
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
require "flipper/feature"
|
|
2
2
|
require "flipper/gate_values"
|
|
3
3
|
require "flipper/adapters/actor_limit"
|
|
4
|
+
require "flipper/adapters/strict"
|
|
4
5
|
require "flipper/adapters/sync/feature_synchronizer"
|
|
5
6
|
|
|
6
7
|
module Flipper
|
|
@@ -29,7 +30,9 @@ module Flipper
|
|
|
29
30
|
# Public: Forces a sync.
|
|
30
31
|
def call
|
|
31
32
|
@instrumenter.instrument("synchronizer_call.flipper") do
|
|
32
|
-
Flipper::Adapters::
|
|
33
|
+
Flipper::Adapters::Strict.with_sync_mode do
|
|
34
|
+
Flipper::Adapters::ActorLimit.with_sync_mode { sync }
|
|
35
|
+
end
|
|
33
36
|
end
|
|
34
37
|
end
|
|
35
38
|
|
|
@@ -142,7 +142,8 @@ module Flipper
|
|
|
142
142
|
private
|
|
143
143
|
|
|
144
144
|
def app_adapter
|
|
145
|
-
|
|
145
|
+
read_adapter = sync_method == :webhook ? local_adapter : poll_adapter
|
|
146
|
+
Flipper::Adapters::DualWrite.new(read_adapter, http_adapter)
|
|
146
147
|
end
|
|
147
148
|
|
|
148
149
|
def poller
|
|
@@ -201,13 +202,8 @@ module Flipper
|
|
|
201
202
|
end
|
|
202
203
|
|
|
203
204
|
def setup_sync(options)
|
|
205
|
+
set_option :sync_interval, options, default: 10, typecast: :float, minimum: 10
|
|
204
206
|
set_option :sync_secret, options
|
|
205
|
-
|
|
206
|
-
# 1 hour for webhook, 10 seconds for poll. If using webhooks we don't
|
|
207
|
-
# need to sync as often but we should still sync occasionally to avoid
|
|
208
|
-
# any chance of stale data.
|
|
209
|
-
default_interval = sync_method == :webhook ? 3600 : 10
|
|
210
|
-
set_option :sync_interval, options, default: default_interval, typecast: :float, minimum: 10
|
|
211
207
|
end
|
|
212
208
|
|
|
213
209
|
def setup_adapter(options)
|
data/lib/flipper/poller.rb
CHANGED
|
@@ -18,7 +18,10 @@ module Flipper
|
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
def self.reset
|
|
21
|
-
instances.each
|
|
21
|
+
instances.each do |_, instance|
|
|
22
|
+
instance.stop
|
|
23
|
+
instance.thread&.join(1)
|
|
24
|
+
end.clear
|
|
22
25
|
end
|
|
23
26
|
|
|
24
27
|
MINIMUM_POLL_INTERVAL = 10
|
|
@@ -96,9 +99,7 @@ module Flipper
|
|
|
96
99
|
private
|
|
97
100
|
|
|
98
101
|
def jitter
|
|
99
|
-
|
|
100
|
-
max_jitter = [interval * 0.1, 30].min
|
|
101
|
-
rand * max_jitter
|
|
102
|
+
rand
|
|
102
103
|
end
|
|
103
104
|
|
|
104
105
|
def forked?
|
|
@@ -116,6 +117,7 @@ module Flipper
|
|
|
116
117
|
begin
|
|
117
118
|
return if thread_alive?
|
|
118
119
|
@thread = Thread.new { run }
|
|
120
|
+
@thread&.report_on_exception = false
|
|
119
121
|
@instrumenter.instrument("poller.#{InstrumentationNamespace}", {
|
|
120
122
|
operation: :thread_start,
|
|
121
123
|
})
|
data/lib/flipper/version.rb
CHANGED
|
@@ -278,6 +278,95 @@ RSpec.describe Flipper::Adapters::Http do
|
|
|
278
278
|
}.to raise_error(Flipper::Adapters::Http::Error)
|
|
279
279
|
end
|
|
280
280
|
|
|
281
|
+
it "skips If-None-Match header when cache_bust is true" do
|
|
282
|
+
features_response = {
|
|
283
|
+
"features" => [
|
|
284
|
+
{
|
|
285
|
+
"key" => "search",
|
|
286
|
+
"gates" => [
|
|
287
|
+
{"key" => "boolean", "value" => true}
|
|
288
|
+
]
|
|
289
|
+
}
|
|
290
|
+
]
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
# First request - populate the ETag cache
|
|
294
|
+
stub_request(:get, "http://app.com/flipper/features?exclude_gate_names=true")
|
|
295
|
+
.to_return(
|
|
296
|
+
status: 200,
|
|
297
|
+
body: JSON.generate(features_response),
|
|
298
|
+
headers: { 'ETag' => '"abc123"' }
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
adapter = described_class.new(url: 'http://app.com/flipper')
|
|
302
|
+
adapter.get_all
|
|
303
|
+
|
|
304
|
+
# Second request with cache_bust - should NOT send If-None-Match
|
|
305
|
+
cache_bust_stub = stub_request(:get, %r{/flipper/features\?_cb=\d+&exclude_gate_names=true})
|
|
306
|
+
.to_return(
|
|
307
|
+
status: 200,
|
|
308
|
+
body: JSON.generate(features_response),
|
|
309
|
+
headers: { 'ETag' => '"def456"' }
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
adapter.get_all(cache_bust: true)
|
|
313
|
+
|
|
314
|
+
expect(cache_bust_stub).to have_been_requested.once
|
|
315
|
+
expect(
|
|
316
|
+
a_request(:get, %r{/flipper/features\?_cb=\d+&exclude_gate_names=true})
|
|
317
|
+
.with { |req| req.headers['If-None-Match'].nil? }
|
|
318
|
+
).to have_been_made.once
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
it "returns fresh data on cache_bust even when ETag is cached" do
|
|
322
|
+
stale_response = {
|
|
323
|
+
"features" => [
|
|
324
|
+
{
|
|
325
|
+
"key" => "search",
|
|
326
|
+
"gates" => [
|
|
327
|
+
{"key" => "boolean", "value" => nil}
|
|
328
|
+
]
|
|
329
|
+
}
|
|
330
|
+
]
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
fresh_response = {
|
|
334
|
+
"features" => [
|
|
335
|
+
{
|
|
336
|
+
"key" => "search",
|
|
337
|
+
"gates" => [
|
|
338
|
+
{"key" => "boolean", "value" => true}
|
|
339
|
+
]
|
|
340
|
+
}
|
|
341
|
+
]
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
# First request - populate ETag cache with feature disabled
|
|
345
|
+
stub_request(:get, "http://app.com/flipper/features?exclude_gate_names=true")
|
|
346
|
+
.to_return(
|
|
347
|
+
status: 200,
|
|
348
|
+
body: JSON.generate(stale_response),
|
|
349
|
+
headers: { 'ETag' => '"abc123"' }
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
adapter = described_class.new(url: 'http://app.com/flipper')
|
|
353
|
+
stale_result = adapter.get_all
|
|
354
|
+
|
|
355
|
+
expect(stale_result["search"][:boolean]).to be_nil
|
|
356
|
+
|
|
357
|
+
# Cache bust request returns fresh data (feature now enabled)
|
|
358
|
+
stub_request(:get, %r{/flipper/features\?_cb=\d+&exclude_gate_names=true})
|
|
359
|
+
.to_return(
|
|
360
|
+
status: 200,
|
|
361
|
+
body: JSON.generate(fresh_response),
|
|
362
|
+
headers: { 'ETag' => '"def456"' }
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
fresh_result = adapter.get_all(cache_bust: true)
|
|
366
|
+
|
|
367
|
+
expect(fresh_result["search"][:boolean]).to eq("true")
|
|
368
|
+
end
|
|
369
|
+
|
|
281
370
|
it "does not send If-None-Match for other endpoints" do
|
|
282
371
|
stub_request(:get, "http://app.com/flipper/features/search")
|
|
283
372
|
.to_return(status: 404)
|
|
@@ -21,6 +21,12 @@ RSpec.describe Flipper::Adapters::Strict do
|
|
|
21
21
|
expect { subject.get_multi([feature]) }.to raise_error(Flipper::Adapters::Strict::NotFound)
|
|
22
22
|
end
|
|
23
23
|
end
|
|
24
|
+
|
|
25
|
+
context "#add" do
|
|
26
|
+
it "raises an error for unknown feature" do
|
|
27
|
+
expect { subject.add(feature) }.to raise_error(Flipper::Adapters::Strict::NotFound)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
24
30
|
end
|
|
25
31
|
end
|
|
26
32
|
|
|
@@ -28,16 +34,22 @@ RSpec.describe Flipper::Adapters::Strict do
|
|
|
28
34
|
subject { described_class.new(Flipper::Adapters::Memory.new, :warn) }
|
|
29
35
|
|
|
30
36
|
context "#get" do
|
|
31
|
-
it "
|
|
37
|
+
it "warns for unknown feature" do
|
|
32
38
|
expect(capture_output { subject.get(feature) }).to match(/Could not find feature "unknown"/)
|
|
33
39
|
end
|
|
34
40
|
end
|
|
35
41
|
|
|
36
42
|
context "#get_multi" do
|
|
37
|
-
it "
|
|
43
|
+
it "warns for unknown feature" do
|
|
38
44
|
expect(capture_output { subject.get_multi([feature]) }).to match(/Could not find feature "unknown"/)
|
|
39
45
|
end
|
|
40
46
|
end
|
|
47
|
+
|
|
48
|
+
context "#add" do
|
|
49
|
+
it "warns for unknown feature" do
|
|
50
|
+
expect(capture_output { subject.add(feature) }).to match(/Could not find feature "unknown"/)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
41
53
|
end
|
|
42
54
|
|
|
43
55
|
context "handler = Block" do
|
|
@@ -48,17 +60,63 @@ RSpec.describe Flipper::Adapters::Strict do
|
|
|
48
60
|
|
|
49
61
|
|
|
50
62
|
context "#get" do
|
|
51
|
-
it "
|
|
63
|
+
it "calls block for unknown feature" do
|
|
52
64
|
subject.get(feature)
|
|
53
65
|
expect(unknown_features).to eq(["unknown"])
|
|
54
66
|
end
|
|
55
67
|
end
|
|
56
68
|
|
|
57
69
|
context "#get_multi" do
|
|
58
|
-
it "
|
|
70
|
+
it "calls block for unknown feature" do
|
|
59
71
|
subject.get_multi([flipper[:foo], flipper[:bar]])
|
|
60
72
|
expect(unknown_features).to eq(["foo", "bar"])
|
|
61
73
|
end
|
|
62
74
|
end
|
|
75
|
+
|
|
76
|
+
context "#add" do
|
|
77
|
+
it "calls block for unknown feature" do
|
|
78
|
+
subject.add(feature)
|
|
79
|
+
expect(unknown_features).to eq(["unknown"])
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
describe ".with_sync_mode" do
|
|
85
|
+
subject { described_class.new(Flipper::Adapters::Memory.new, :raise) }
|
|
86
|
+
|
|
87
|
+
it "bypasses strict checks for add" do
|
|
88
|
+
described_class.with_sync_mode do
|
|
89
|
+
expect { subject.add(feature) }.not_to raise_error
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
it "bypasses strict checks for get" do
|
|
94
|
+
described_class.with_sync_mode do
|
|
95
|
+
expect { subject.get(feature) }.not_to raise_error
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
it "bypasses strict checks for get_multi" do
|
|
100
|
+
described_class.with_sync_mode do
|
|
101
|
+
expect { subject.get_multi([feature]) }.not_to raise_error
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
it "restores previous sync mode after block" do
|
|
106
|
+
described_class.with_sync_mode do
|
|
107
|
+
# inside sync mode
|
|
108
|
+
end
|
|
109
|
+
expect { subject.add(feature) }.to raise_error(Flipper::Adapters::Strict::NotFound)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
it "restores previous sync mode even on error" do
|
|
113
|
+
begin
|
|
114
|
+
described_class.with_sync_mode do
|
|
115
|
+
raise "boom"
|
|
116
|
+
end
|
|
117
|
+
rescue RuntimeError
|
|
118
|
+
end
|
|
119
|
+
expect { subject.add(feature) }.to raise_error(Flipper::Adapters::Strict::NotFound)
|
|
120
|
+
end
|
|
63
121
|
end
|
|
64
122
|
end
|
|
@@ -135,9 +135,6 @@ RSpec.describe Flipper::Cloud::Configuration do
|
|
|
135
135
|
end
|
|
136
136
|
|
|
137
137
|
it "sets sync_method to :webhook if sync_secret provided" do
|
|
138
|
-
# The initial sync of http to local invokes this web request.
|
|
139
|
-
stub_request(:get, /flippercloud\.io/).to_return(status: 200, body: "{}")
|
|
140
|
-
|
|
141
138
|
instance = described_class.new(required_options.merge({
|
|
142
139
|
sync_secret: "secret",
|
|
143
140
|
}))
|
|
@@ -147,9 +144,6 @@ RSpec.describe Flipper::Cloud::Configuration do
|
|
|
147
144
|
end
|
|
148
145
|
|
|
149
146
|
it "sets sync_method to :webhook if FLIPPER_CLOUD_SYNC_SECRET set" do
|
|
150
|
-
# The initial sync of http to local invokes this web request.
|
|
151
|
-
stub_request(:get, /flippercloud\.io/).to_return(status: 200, body: "{}")
|
|
152
|
-
|
|
153
147
|
ENV["FLIPPER_CLOUD_SYNC_SECRET"] = "abc"
|
|
154
148
|
instance = described_class.new(required_options)
|
|
155
149
|
|
|
@@ -5,9 +5,6 @@ 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
|
-
|
|
11
8
|
cloud_configuration = Flipper::Cloud::Configuration.new({
|
|
12
9
|
token: "asdf",
|
|
13
10
|
sync_secret: "tasty",
|
|
@@ -30,13 +27,10 @@ RSpec.describe Flipper::Cloud::DSL do
|
|
|
30
27
|
})
|
|
31
28
|
dsl = described_class.new(cloud_configuration)
|
|
32
29
|
dsl.sync
|
|
33
|
-
expect(stub).to have_been_requested
|
|
30
|
+
expect(stub).to have_been_requested
|
|
34
31
|
end
|
|
35
32
|
|
|
36
33
|
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
|
-
|
|
40
34
|
cloud_configuration = Flipper::Cloud::Configuration.new({
|
|
41
35
|
token: "asdf",
|
|
42
36
|
sync_secret: "tasty",
|
|
@@ -59,15 +53,13 @@ RSpec.describe Flipper::Cloud::DSL do
|
|
|
59
53
|
end
|
|
60
54
|
|
|
61
55
|
subject do
|
|
62
|
-
# stub the initial sync of http to local
|
|
63
|
-
stub_request(:get, /flippercloud\.io/).to_return(status: 200, body: "{}")
|
|
64
56
|
described_class.new(cloud_configuration)
|
|
65
57
|
end
|
|
66
58
|
|
|
67
59
|
it "sends reads to local adapter" do
|
|
68
60
|
subject.features
|
|
69
61
|
subject.enabled?(:foo)
|
|
70
|
-
expect(local_adapter.count(:features)).to be(
|
|
62
|
+
expect(local_adapter.count(:features)).to be(1)
|
|
71
63
|
expect(local_adapter.count(:get)).to be(1)
|
|
72
64
|
end
|
|
73
65
|
|
|
@@ -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_requested
|
|
66
66
|
end
|
|
67
67
|
end
|
|
68
68
|
|
|
@@ -72,17 +72,15 @@ 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
|
-
|
|
77
|
-
poll_regular_stub = stub_request_for_token('regular', from_webhook: false)
|
|
75
|
+
it 'uses instance to sync' do
|
|
76
|
+
stub = stub_request_for_token('regular')
|
|
78
77
|
env = {
|
|
79
78
|
"HTTP_FLIPPER_CLOUD_SIGNATURE" => signature_header_value,
|
|
80
79
|
}
|
|
81
80
|
post '/', request_body, env
|
|
82
81
|
|
|
83
82
|
expect(last_response.status).to eq(400)
|
|
84
|
-
expect(
|
|
85
|
-
expect(webhook_regular_stub).not_to have_been_requested
|
|
83
|
+
expect(stub).not_to have_been_requested
|
|
86
84
|
end
|
|
87
85
|
end
|
|
88
86
|
|
|
@@ -105,7 +103,7 @@ RSpec.describe Flipper::Cloud::Middleware do
|
|
|
105
103
|
expect(last_response.status).to eq(402)
|
|
106
104
|
expect(last_response.headers["flipper-cloud-response-error-class"]).to eq("Flipper::Adapters::Http::Error")
|
|
107
105
|
expect(last_response.headers["flipper-cloud-response-error-message"]).to include("Failed with status: 402")
|
|
108
|
-
expect(stub).to
|
|
106
|
+
expect(stub).to have_been_requested
|
|
109
107
|
end
|
|
110
108
|
end
|
|
111
109
|
|
|
@@ -128,7 +126,7 @@ RSpec.describe Flipper::Cloud::Middleware do
|
|
|
128
126
|
expect(last_response.status).to eq(500)
|
|
129
127
|
expect(last_response.headers["flipper-cloud-response-error-class"]).to eq("Flipper::Adapters::Http::Error")
|
|
130
128
|
expect(last_response.headers["flipper-cloud-response-error-message"]).to include("Failed with status: 503")
|
|
131
|
-
expect(stub).to
|
|
129
|
+
expect(stub).to have_been_requested
|
|
132
130
|
end
|
|
133
131
|
end
|
|
134
132
|
|
|
@@ -151,7 +149,7 @@ RSpec.describe Flipper::Cloud::Middleware do
|
|
|
151
149
|
expect(last_response.status).to eq(500)
|
|
152
150
|
expect(last_response.headers["flipper-cloud-response-error-class"]).to eq("Net::OpenTimeout")
|
|
153
151
|
expect(last_response.headers["flipper-cloud-response-error-message"]).to eq("execution expired")
|
|
154
|
-
expect(stub).to
|
|
152
|
+
expect(stub).to have_been_requested
|
|
155
153
|
end
|
|
156
154
|
end
|
|
157
155
|
|
|
@@ -162,8 +160,7 @@ RSpec.describe Flipper::Cloud::Middleware do
|
|
|
162
160
|
}
|
|
163
161
|
|
|
164
162
|
it 'uses env instance to sync' do
|
|
165
|
-
|
|
166
|
-
env_stub = stub_request_for_token('env')
|
|
163
|
+
stub = stub_request_for_token('env')
|
|
167
164
|
env = {
|
|
168
165
|
"HTTP_FLIPPER_CLOUD_SIGNATURE" => signature_header_value,
|
|
169
166
|
'flipper' => env_flipper,
|
|
@@ -171,8 +168,7 @@ RSpec.describe Flipper::Cloud::Middleware do
|
|
|
171
168
|
post '/', request_body, env
|
|
172
169
|
|
|
173
170
|
expect(last_response.status).to eq(200)
|
|
174
|
-
expect(
|
|
175
|
-
expect(env_stub).to have_been_made.at_least_once
|
|
171
|
+
expect(stub).to have_been_requested
|
|
176
172
|
end
|
|
177
173
|
end
|
|
178
174
|
|
|
@@ -191,7 +187,7 @@ RSpec.describe Flipper::Cloud::Middleware do
|
|
|
191
187
|
post '/', request_body, env
|
|
192
188
|
|
|
193
189
|
expect(last_response.status).to eq(200)
|
|
194
|
-
expect(stub).to
|
|
190
|
+
expect(stub).to have_been_requested
|
|
195
191
|
end
|
|
196
192
|
end
|
|
197
193
|
|
|
@@ -202,9 +198,7 @@ RSpec.describe Flipper::Cloud::Middleware do
|
|
|
202
198
|
}
|
|
203
199
|
|
|
204
200
|
it 'uses provided env key instead of default' do
|
|
205
|
-
|
|
206
|
-
env_poll_stub = stub_request_for_token('env')
|
|
207
|
-
env_webhook_stub = stub_request_for_token('env', from_webhook: true)
|
|
201
|
+
stub = stub_request_for_token('env')
|
|
208
202
|
env = {
|
|
209
203
|
"HTTP_FLIPPER_CLOUD_SIGNATURE" => signature_header_value,
|
|
210
204
|
'flipper' => flipper,
|
|
@@ -213,9 +207,7 @@ RSpec.describe Flipper::Cloud::Middleware do
|
|
|
213
207
|
post '/', request_body, env
|
|
214
208
|
|
|
215
209
|
expect(last_response.status).to eq(200)
|
|
216
|
-
expect(
|
|
217
|
-
expect(env_poll_stub).to have_been_made.at_least_once
|
|
218
|
-
expect(env_webhook_stub).not_to have_been_requested
|
|
210
|
+
expect(stub).to have_been_requested
|
|
219
211
|
end
|
|
220
212
|
end
|
|
221
213
|
|
|
@@ -230,7 +222,7 @@ RSpec.describe Flipper::Cloud::Middleware do
|
|
|
230
222
|
post '/', request_body, env
|
|
231
223
|
|
|
232
224
|
expect(last_response.status).to eq(200)
|
|
233
|
-
expect(stub).to
|
|
225
|
+
expect(stub).to have_been_requested
|
|
234
226
|
end
|
|
235
227
|
end
|
|
236
228
|
|
|
@@ -260,13 +252,12 @@ RSpec.describe Flipper::Cloud::Middleware do
|
|
|
260
252
|
{"name" => "premium"},
|
|
261
253
|
],
|
|
262
254
|
})
|
|
263
|
-
expect(stub).to
|
|
255
|
+
expect(stub).to have_been_requested
|
|
264
256
|
end
|
|
265
257
|
end
|
|
266
258
|
|
|
267
259
|
describe 'Request method unsupported' do
|
|
268
260
|
it 'skips middleware' do
|
|
269
|
-
stub_request(:get, /flippercloud\.io/).to_return(status: 200, body: "{}")
|
|
270
261
|
get '/'
|
|
271
262
|
expect(last_response.status).to eq(404)
|
|
272
263
|
expect(last_response.content_type).to eq("application/json")
|
|
@@ -276,23 +267,14 @@ RSpec.describe Flipper::Cloud::Middleware do
|
|
|
276
267
|
|
|
277
268
|
describe 'Inspecting the built Rack app' do
|
|
278
269
|
it 'returns a String' do
|
|
279
|
-
stub_request(:get, /flippercloud\.io/).to_return(status: 200, body: "{}")
|
|
280
270
|
expect(Flipper::Cloud.app(flipper).inspect).to eq("Flipper::Cloud")
|
|
281
271
|
end
|
|
282
272
|
end
|
|
283
273
|
|
|
284
274
|
private
|
|
285
275
|
|
|
286
|
-
def stub_request_for_token(token, status: 200
|
|
287
|
-
|
|
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).
|
|
276
|
+
def stub_request_for_token(token, status: 200)
|
|
277
|
+
stub = stub_request(:get, %r{\Ahttps://www\.flippercloud\.io/adapter/features\?(?=.*\bexclude_gate_names=true\b)(?=.*\b_cb=\d+\b)}).
|
|
296
278
|
with({
|
|
297
279
|
headers: {
|
|
298
280
|
'flipper-cloud-token' => token,
|
data/spec/flipper_spec.rb
CHANGED
data/spec/spec_helper.rb
CHANGED
|
@@ -39,6 +39,13 @@ RSpec.configure do |config|
|
|
|
39
39
|
Flipper.configuration = nil
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
+
config.after(:example) do
|
|
43
|
+
# Stop any pollers started during the test BEFORE WebMock clears stubs
|
|
44
|
+
# in its own after hook (RSpec runs after hooks in reverse registration
|
|
45
|
+
# order, so ours runs first since webmock/rspec was required before this).
|
|
46
|
+
Flipper::Poller.reset if defined?(Flipper::Poller)
|
|
47
|
+
end
|
|
48
|
+
|
|
42
49
|
config.disable_monkey_patching!
|
|
43
50
|
|
|
44
51
|
config.filter_run focus: true
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: flipper
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.4.
|
|
4
|
+
version: 1.4.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- John Nunemaker
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-05-11 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: concurrent-ruby
|
|
@@ -38,6 +38,7 @@ files:
|
|
|
38
38
|
- ".github/workflows/examples.yml"
|
|
39
39
|
- ".github/workflows/release.yml"
|
|
40
40
|
- ".rspec"
|
|
41
|
+
- ".superset/config.json"
|
|
41
42
|
- CLAUDE.md
|
|
42
43
|
- CODE_OF_CONDUCT.md
|
|
43
44
|
- Changelog.md
|
|
@@ -322,7 +323,7 @@ metadata:
|
|
|
322
323
|
homepage_uri: https://www.flippercloud.io
|
|
323
324
|
source_code_uri: https://github.com/flippercloud/flipper
|
|
324
325
|
bug_tracker_uri: https://github.com/flippercloud/flipper/issues
|
|
325
|
-
changelog_uri: https://github.com/flippercloud/flipper/releases/tag/v1.4.
|
|
326
|
+
changelog_uri: https://github.com/flippercloud/flipper/releases/tag/v1.4.2
|
|
326
327
|
funding_uri: https://github.com/sponsors/flippercloud
|
|
327
328
|
post_install_message:
|
|
328
329
|
rdoc_options: []
|