flipper 0.28.3 → 1.0.0
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/examples.yml +2 -2
- data/Changelog.md +190 -165
- data/Gemfile +5 -3
- data/docs/images/flipper_cloud.png +0 -0
- data/examples/cloud/app.ru +12 -0
- data/examples/cloud/basic.rb +22 -0
- data/examples/cloud/cloud_setup.rb +4 -0
- data/examples/cloud/forked.rb +31 -0
- data/examples/cloud/import.rb +17 -0
- data/examples/cloud/threaded.rb +36 -0
- data/examples/dsl.rb +0 -14
- data/flipper-cloud.gemspec +19 -0
- data/flipper.gemspec +3 -2
- data/lib/flipper/cloud/configuration.rb +189 -0
- data/lib/flipper/cloud/dsl.rb +27 -0
- data/lib/flipper/cloud/instrumenter.rb +48 -0
- data/lib/flipper/cloud/message_verifier.rb +95 -0
- data/lib/flipper/cloud/middleware.rb +63 -0
- data/lib/flipper/cloud/routes.rb +14 -0
- data/lib/flipper/cloud.rb +53 -0
- data/lib/flipper/dsl.rb +0 -46
- data/lib/flipper/{railtie.rb → engine.rb} +19 -3
- data/lib/flipper/metadata.rb +5 -1
- data/lib/flipper/middleware/memoizer.rb +1 -1
- data/lib/flipper/spec/shared_adapter_specs.rb +43 -43
- data/lib/flipper/test/shared_adapter_test.rb +43 -43
- data/lib/flipper/version.rb +1 -1
- data/lib/flipper.rb +3 -5
- data/spec/flipper/adapters/dual_write_spec.rb +2 -2
- data/spec/flipper/adapters/instrumented_spec.rb +1 -1
- data/spec/flipper/adapters/memoizable_spec.rb +6 -6
- data/spec/flipper/adapters/operation_logger_spec.rb +2 -2
- data/spec/flipper/adapters/read_only_spec.rb +6 -6
- data/spec/flipper/cloud/configuration_spec.rb +269 -0
- data/spec/flipper/cloud/dsl_spec.rb +82 -0
- data/spec/flipper/cloud/message_verifier_spec.rb +104 -0
- data/spec/flipper/cloud/middleware_spec.rb +289 -0
- data/spec/flipper/cloud_spec.rb +180 -0
- data/spec/flipper/dsl_spec.rb +0 -75
- data/spec/flipper/engine_spec.rb +190 -0
- data/spec/flipper_integration_spec.rb +12 -12
- data/spec/flipper_spec.rb +0 -30
- data/spec/spec_helper.rb +0 -12
- data/spec/support/climate_control.rb +7 -0
- metadata +54 -11
- data/.tool-versions +0 -1
- data/spec/flipper/railtie_spec.rb +0 -109
data/lib/flipper/version.rb
CHANGED
data/lib/flipper.rb
CHANGED
@@ -56,13 +56,11 @@ module Flipper
|
|
56
56
|
# Public: All the methods delegated to instance. These should match the
|
57
57
|
# interface of Flipper::DSL.
|
58
58
|
def_delegators :instance,
|
59
|
-
:enabled?, :enable, :disable,
|
60
|
-
:enable_actor, :disable_actor,
|
59
|
+
:enabled?, :enable, :disable,
|
60
|
+
:enable_actor, :disable_actor,
|
61
61
|
:enable_group, :disable_group,
|
62
62
|
:enable_percentage_of_actors, :disable_percentage_of_actors,
|
63
|
-
:actors, :percentage_of_actors,
|
64
63
|
:enable_percentage_of_time, :disable_percentage_of_time,
|
65
|
-
:time, :percentage_of_time,
|
66
64
|
:features, :feature, :[], :preload, :preload_all,
|
67
65
|
:adapter, :add, :exist?, :remove, :import, :export,
|
68
66
|
:memoize=, :memoizing?,
|
@@ -167,4 +165,4 @@ require 'flipper/types/percentage_of_time'
|
|
167
165
|
require 'flipper/typecast'
|
168
166
|
require 'flipper/version'
|
169
167
|
|
170
|
-
require "flipper/
|
168
|
+
require "flipper/engine" if defined?(Rails)
|
@@ -55,14 +55,14 @@ RSpec.describe Flipper::Adapters::DualWrite do
|
|
55
55
|
|
56
56
|
it 'updates remote and local for #enable' do
|
57
57
|
feature = sync[:search]
|
58
|
-
subject.enable feature, feature.gate(:boolean),
|
58
|
+
subject.enable feature, feature.gate(:boolean), Flipper::Types::Boolean.new(true)
|
59
59
|
expect(remote_adapter.count(:enable)).to be(1)
|
60
60
|
expect(local_adapter.count(:enable)).to be(1)
|
61
61
|
end
|
62
62
|
|
63
63
|
it 'updates remote and local for #disable' do
|
64
64
|
feature = sync[:search]
|
65
|
-
subject.disable feature, feature.gate(:boolean),
|
65
|
+
subject.disable feature, feature.gate(:boolean), Flipper::Types::Boolean.new(false)
|
66
66
|
expect(remote_adapter.count(:disable)).to be(1)
|
67
67
|
expect(local_adapter.count(:disable)).to be(1)
|
68
68
|
end
|
@@ -8,7 +8,7 @@ RSpec.describe Flipper::Adapters::Instrumented do
|
|
8
8
|
|
9
9
|
let(:feature) { flipper[:stats] }
|
10
10
|
let(:gate) { feature.gate(:percentage_of_actors) }
|
11
|
-
let(:thing) {
|
11
|
+
let(:thing) { Flipper::Types::PercentageOfActors.new(22) }
|
12
12
|
|
13
13
|
subject do
|
14
14
|
described_class.new(adapter, instrumenter: instrumenter)
|
@@ -189,7 +189,7 @@ RSpec.describe Flipper::Adapters::Memoizable do
|
|
189
189
|
feature = flipper[:stats]
|
190
190
|
gate = feature.gate(:boolean)
|
191
191
|
cache[described_class.key_for(feature.key)] = { some: 'thing' }
|
192
|
-
subject.enable(feature, gate,
|
192
|
+
subject.enable(feature, gate, Flipper::Types::Boolean.new)
|
193
193
|
expect(cache[described_class.key_for(feature.key)]).to be_nil
|
194
194
|
end
|
195
195
|
end
|
@@ -202,8 +202,8 @@ RSpec.describe Flipper::Adapters::Memoizable do
|
|
202
202
|
it 'returns result' do
|
203
203
|
feature = flipper[:stats]
|
204
204
|
gate = feature.gate(:boolean)
|
205
|
-
result = subject.enable(feature, gate,
|
206
|
-
adapter_result = adapter.enable(feature, gate,
|
205
|
+
result = subject.enable(feature, gate, Flipper::Types::Boolean.new)
|
206
|
+
adapter_result = adapter.enable(feature, gate, Flipper::Types::Boolean.new)
|
207
207
|
expect(result).to eq(adapter_result)
|
208
208
|
end
|
209
209
|
end
|
@@ -219,7 +219,7 @@ RSpec.describe Flipper::Adapters::Memoizable do
|
|
219
219
|
feature = flipper[:stats]
|
220
220
|
gate = feature.gate(:boolean)
|
221
221
|
cache[described_class.key_for(feature.key)] = { some: 'thing' }
|
222
|
-
subject.disable(feature, gate,
|
222
|
+
subject.disable(feature, gate, Flipper::Types::Boolean.new)
|
223
223
|
expect(cache[described_class.key_for(feature.key)]).to be_nil
|
224
224
|
end
|
225
225
|
end
|
@@ -232,8 +232,8 @@ RSpec.describe Flipper::Adapters::Memoizable do
|
|
232
232
|
it 'returns result' do
|
233
233
|
feature = flipper[:stats]
|
234
234
|
gate = feature.gate(:boolean)
|
235
|
-
result = subject.disable(feature, gate,
|
236
|
-
adapter_result = adapter.disable(feature, gate,
|
235
|
+
result = subject.disable(feature, gate, Flipper::Types::Boolean.new)
|
236
|
+
adapter_result = adapter.disable(feature, gate, Flipper::Types::Boolean.new)
|
237
237
|
expect(result).to eq(adapter_result)
|
238
238
|
end
|
239
239
|
end
|
@@ -37,7 +37,7 @@ RSpec.describe Flipper::Adapters::OperationLogger do
|
|
37
37
|
before do
|
38
38
|
@feature = flipper[:stats]
|
39
39
|
@gate = @feature.gate(:boolean)
|
40
|
-
@thing =
|
40
|
+
@thing = Flipper::Types::Boolean.new
|
41
41
|
@result = subject.enable(@feature, @gate, @thing)
|
42
42
|
end
|
43
43
|
|
@@ -54,7 +54,7 @@ RSpec.describe Flipper::Adapters::OperationLogger do
|
|
54
54
|
before do
|
55
55
|
@feature = flipper[:stats]
|
56
56
|
@gate = @feature.gate(:boolean)
|
57
|
-
@thing =
|
57
|
+
@thing = Flipper::Types::Boolean.new
|
58
58
|
@result = subject.disable(@feature, @gate, @thing)
|
59
59
|
end
|
60
60
|
|
@@ -42,11 +42,11 @@ RSpec.describe Flipper::Adapters::ReadOnly do
|
|
42
42
|
|
43
43
|
it 'can get feature' do
|
44
44
|
actor22 = Flipper::Actor.new('22')
|
45
|
-
adapter.enable(feature, boolean_gate,
|
45
|
+
adapter.enable(feature, boolean_gate, Flipper::Types::Boolean.new)
|
46
46
|
adapter.enable(feature, group_gate, flipper.group(:admins))
|
47
|
-
adapter.enable(feature, actor_gate,
|
48
|
-
adapter.enable(feature, actors_gate,
|
49
|
-
adapter.enable(feature, time_gate,
|
47
|
+
adapter.enable(feature, actor_gate, Flipper::Types::Actor.new(actor22))
|
48
|
+
adapter.enable(feature, actors_gate, Flipper::Types::PercentageOfActors.new(25))
|
49
|
+
adapter.enable(feature, time_gate, Flipper::Types::PercentageOfTime.new(45))
|
50
50
|
|
51
51
|
expect(subject.get(feature)).to eq(boolean: 'true',
|
52
52
|
groups: Set['admins'],
|
@@ -74,12 +74,12 @@ RSpec.describe Flipper::Adapters::ReadOnly do
|
|
74
74
|
end
|
75
75
|
|
76
76
|
it 'raises error on enable' do
|
77
|
-
expect { subject.enable(feature, boolean_gate,
|
77
|
+
expect { subject.enable(feature, boolean_gate, Flipper::Types::Boolean.new) }
|
78
78
|
.to raise_error(Flipper::Adapters::ReadOnly::WriteAttempted)
|
79
79
|
end
|
80
80
|
|
81
81
|
it 'raises error on disable' do
|
82
|
-
expect { subject.disable(feature, boolean_gate,
|
82
|
+
expect { subject.disable(feature, boolean_gate, Flipper::Types::Boolean.new) }
|
83
83
|
.to raise_error(Flipper::Adapters::ReadOnly::WriteAttempted)
|
84
84
|
end
|
85
85
|
end
|
@@ -0,0 +1,269 @@
|
|
1
|
+
require 'flipper/cloud/configuration'
|
2
|
+
require 'flipper/adapters/instrumented'
|
3
|
+
|
4
|
+
RSpec.describe Flipper::Cloud::Configuration do
|
5
|
+
let(:required_options) do
|
6
|
+
{ token: "asdf" }
|
7
|
+
end
|
8
|
+
|
9
|
+
it "can set token" do
|
10
|
+
instance = described_class.new(required_options)
|
11
|
+
expect(instance.token).to eq(required_options[:token])
|
12
|
+
end
|
13
|
+
|
14
|
+
it "can set token from ENV var" do
|
15
|
+
with_env "FLIPPER_CLOUD_TOKEN" => "from_env" do
|
16
|
+
instance = described_class.new(required_options.reject { |k, v| k == :token })
|
17
|
+
expect(instance.token).to eq("from_env")
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
it "can set instrumenter" do
|
22
|
+
instrumenter = Object.new
|
23
|
+
instance = described_class.new(required_options.merge(instrumenter: instrumenter))
|
24
|
+
expect(instance.instrumenter).to be(instrumenter)
|
25
|
+
end
|
26
|
+
|
27
|
+
it "can set read_timeout" do
|
28
|
+
instance = described_class.new(required_options.merge(read_timeout: 5))
|
29
|
+
expect(instance.read_timeout).to eq(5)
|
30
|
+
end
|
31
|
+
|
32
|
+
it "can set read_timeout from ENV var" do
|
33
|
+
with_env "FLIPPER_CLOUD_READ_TIMEOUT" => "9" do
|
34
|
+
instance = described_class.new(required_options.reject { |k, v| k == :read_timeout })
|
35
|
+
expect(instance.read_timeout).to eq(9)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
it "can set open_timeout" do
|
40
|
+
instance = described_class.new(required_options.merge(open_timeout: 5))
|
41
|
+
expect(instance.open_timeout).to eq(5)
|
42
|
+
end
|
43
|
+
|
44
|
+
it "can set open_timeout from ENV var" do
|
45
|
+
with_env "FLIPPER_CLOUD_OPEN_TIMEOUT" => "9" do
|
46
|
+
instance = described_class.new(required_options.reject { |k, v| k == :open_timeout })
|
47
|
+
expect(instance.open_timeout).to eq(9)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
it "can set write_timeout" do
|
52
|
+
instance = described_class.new(required_options.merge(write_timeout: 5))
|
53
|
+
expect(instance.write_timeout).to eq(5)
|
54
|
+
end
|
55
|
+
|
56
|
+
it "can set write_timeout from ENV var" do
|
57
|
+
with_env "FLIPPER_CLOUD_WRITE_TIMEOUT" => "9" do
|
58
|
+
instance = described_class.new(required_options.reject { |k, v| k == :write_timeout })
|
59
|
+
expect(instance.write_timeout).to eq(9)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
it "can set sync_interval" do
|
64
|
+
instance = described_class.new(required_options.merge(sync_interval: 1))
|
65
|
+
expect(instance.sync_interval).to eq(1)
|
66
|
+
end
|
67
|
+
|
68
|
+
it "can set sync_interval from ENV var" do
|
69
|
+
with_env "FLIPPER_CLOUD_SYNC_INTERVAL" => "5" do
|
70
|
+
instance = described_class.new(required_options.reject { |k, v| k == :sync_interval })
|
71
|
+
expect(instance.sync_interval).to eq(5)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
it "passes sync_interval into sync adapter" do
|
76
|
+
# The initial sync of http to local invokes this web request.
|
77
|
+
stub_request(:get, /flippercloud\.io/).to_return(status: 200, body: "{}")
|
78
|
+
|
79
|
+
instance = described_class.new(required_options.merge(sync_interval: 1))
|
80
|
+
poller = instance.send(:poller)
|
81
|
+
expect(poller.interval).to eq(1)
|
82
|
+
end
|
83
|
+
|
84
|
+
it "can set debug_output" do
|
85
|
+
instance = described_class.new(required_options.merge(debug_output: STDOUT))
|
86
|
+
expect(instance.debug_output).to eq(STDOUT)
|
87
|
+
end
|
88
|
+
|
89
|
+
it "defaults adapter block" do
|
90
|
+
# The initial sync of http to local invokes this web request.
|
91
|
+
stub_request(:get, /flippercloud\.io/).to_return(status: 200, body: "{}")
|
92
|
+
|
93
|
+
instance = described_class.new(required_options)
|
94
|
+
expect(instance.adapter).to be_instance_of(Flipper::Adapters::DualWrite)
|
95
|
+
end
|
96
|
+
|
97
|
+
it "can override adapter block" do
|
98
|
+
# The initial sync of http to local invokes this web request.
|
99
|
+
stub_request(:get, /flippercloud\.io/).to_return(status: 200, body: "{}")
|
100
|
+
|
101
|
+
instance = described_class.new(required_options)
|
102
|
+
instance.adapter do |adapter|
|
103
|
+
Flipper::Adapters::Instrumented.new(adapter)
|
104
|
+
end
|
105
|
+
expect(instance.adapter).to be_instance_of(Flipper::Adapters::Instrumented)
|
106
|
+
end
|
107
|
+
|
108
|
+
it "defaults url" do
|
109
|
+
instance = described_class.new(required_options.reject { |k, v| k == :url })
|
110
|
+
expect(instance.url).to eq("https://www.flippercloud.io/adapter")
|
111
|
+
end
|
112
|
+
|
113
|
+
it "can override url using options" do
|
114
|
+
options = required_options.merge(url: "http://localhost:5000/adapter")
|
115
|
+
instance = described_class.new(options)
|
116
|
+
expect(instance.url).to eq("http://localhost:5000/adapter")
|
117
|
+
|
118
|
+
instance = described_class.new(required_options)
|
119
|
+
instance.url = "http://localhost:5000/adapter"
|
120
|
+
expect(instance.url).to eq("http://localhost:5000/adapter")
|
121
|
+
end
|
122
|
+
|
123
|
+
it "can override URL using ENV var" do
|
124
|
+
with_env "FLIPPER_CLOUD_URL" => "https://example.com" do
|
125
|
+
instance = described_class.new(required_options.reject { |k, v| k == :url })
|
126
|
+
expect(instance.url).to eq("https://example.com")
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
it "defaults sync_method to :poll" do
|
131
|
+
instance = described_class.new(required_options)
|
132
|
+
|
133
|
+
expect(instance.sync_method).to eq(:poll)
|
134
|
+
end
|
135
|
+
|
136
|
+
it "sets sync_method to :webhook if sync_secret provided" do
|
137
|
+
instance = described_class.new(required_options.merge({
|
138
|
+
sync_secret: "secret",
|
139
|
+
}))
|
140
|
+
|
141
|
+
expect(instance.sync_method).to eq(:webhook)
|
142
|
+
expect(instance.adapter).to be_instance_of(Flipper::Adapters::DualWrite)
|
143
|
+
end
|
144
|
+
|
145
|
+
it "sets sync_method to :webhook if FLIPPER_CLOUD_SYNC_SECRET set" do
|
146
|
+
with_env "FLIPPER_CLOUD_SYNC_SECRET" => "abc" do
|
147
|
+
instance = described_class.new(required_options)
|
148
|
+
|
149
|
+
expect(instance.sync_method).to eq(:webhook)
|
150
|
+
expect(instance.adapter).to be_instance_of(Flipper::Adapters::DualWrite)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
it "can set sync_secret" do
|
155
|
+
instance = described_class.new(required_options.merge(sync_secret: "from_config"))
|
156
|
+
expect(instance.sync_secret).to eq("from_config")
|
157
|
+
end
|
158
|
+
|
159
|
+
it "can override sync_secret using ENV var" do
|
160
|
+
with_env "FLIPPER_CLOUD_SYNC_SECRET" => "from_env" do
|
161
|
+
instance = described_class.new(required_options.reject { |k, v| k == :sync_secret })
|
162
|
+
expect(instance.sync_secret).to eq("from_env")
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
it "can sync with cloud" do
|
167
|
+
body = JSON.generate({
|
168
|
+
"features": [
|
169
|
+
{
|
170
|
+
"key": "search",
|
171
|
+
"state": "on",
|
172
|
+
"gates": [
|
173
|
+
{
|
174
|
+
"key": "boolean",
|
175
|
+
"name": "boolean",
|
176
|
+
"value": true
|
177
|
+
},
|
178
|
+
{
|
179
|
+
"key": "groups",
|
180
|
+
"name": "group",
|
181
|
+
"value": []
|
182
|
+
},
|
183
|
+
{
|
184
|
+
"key": "actors",
|
185
|
+
"name": "actor",
|
186
|
+
"value": []
|
187
|
+
},
|
188
|
+
{
|
189
|
+
"key": "percentage_of_actors",
|
190
|
+
"name": "percentage_of_actors",
|
191
|
+
"value": 0
|
192
|
+
},
|
193
|
+
{
|
194
|
+
"key": "percentage_of_time",
|
195
|
+
"name": "percentage_of_time",
|
196
|
+
"value": 0
|
197
|
+
}
|
198
|
+
]
|
199
|
+
},
|
200
|
+
{
|
201
|
+
"key": "history",
|
202
|
+
"state": "off",
|
203
|
+
"gates": [
|
204
|
+
{
|
205
|
+
"key": "boolean",
|
206
|
+
"name": "boolean",
|
207
|
+
"value": false
|
208
|
+
},
|
209
|
+
{
|
210
|
+
"key": "groups",
|
211
|
+
"name": "group",
|
212
|
+
"value": []
|
213
|
+
},
|
214
|
+
{
|
215
|
+
"key": "actors",
|
216
|
+
"name": "actor",
|
217
|
+
"value": []
|
218
|
+
},
|
219
|
+
{
|
220
|
+
"key": "percentage_of_actors",
|
221
|
+
"name": "percentage_of_actors",
|
222
|
+
"value": 0
|
223
|
+
},
|
224
|
+
{
|
225
|
+
"key": "percentage_of_time",
|
226
|
+
"name": "percentage_of_time",
|
227
|
+
"value": 0
|
228
|
+
}
|
229
|
+
]
|
230
|
+
}
|
231
|
+
]
|
232
|
+
})
|
233
|
+
stub = stub_request(:get, "https://www.flippercloud.io/adapter/features?exclude_gate_names=true").
|
234
|
+
with({
|
235
|
+
headers: {
|
236
|
+
'Flipper-Cloud-Token'=>'asdf',
|
237
|
+
},
|
238
|
+
}).to_return(status: 200, body: body, headers: {})
|
239
|
+
instance = described_class.new(required_options)
|
240
|
+
instance.sync
|
241
|
+
|
242
|
+
# Check that remote was fetched.
|
243
|
+
expect(stub).to have_been_requested
|
244
|
+
|
245
|
+
# Check that local adapter really did sync.
|
246
|
+
local_adapter = instance.local_adapter
|
247
|
+
all = local_adapter.get_all
|
248
|
+
expect(all.keys).to eq(["search", "history"])
|
249
|
+
expect(all["search"][:boolean]).to eq("true")
|
250
|
+
expect(all["history"][:boolean]).to eq(nil)
|
251
|
+
end
|
252
|
+
|
253
|
+
it "can setup brow to report events to cloud" do
|
254
|
+
# skip logging brow
|
255
|
+
Brow.logger = Logger.new(File::NULL)
|
256
|
+
brow = described_class.new(required_options).brow
|
257
|
+
|
258
|
+
stub = stub_request(:post, "https://www.flippercloud.io/adapter/events")
|
259
|
+
.with { |request|
|
260
|
+
data = JSON.parse(request.body)
|
261
|
+
data.keys == ["uuid", "messages"] && data["messages"] == [{"n" => 1}]
|
262
|
+
}
|
263
|
+
.to_return(status: 201, body: "{}", headers: {})
|
264
|
+
|
265
|
+
brow.push({"n" => 1})
|
266
|
+
brow.worker.stop
|
267
|
+
expect(stub).to have_been_requested.times(1)
|
268
|
+
end
|
269
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'flipper/cloud/configuration'
|
2
|
+
require 'flipper/cloud/dsl'
|
3
|
+
require 'flipper/adapters/operation_logger'
|
4
|
+
require 'flipper/adapters/instrumented'
|
5
|
+
|
6
|
+
RSpec.describe Flipper::Cloud::DSL do
|
7
|
+
it 'delegates everything to flipper instance' do
|
8
|
+
cloud_configuration = Flipper::Cloud::Configuration.new({
|
9
|
+
token: "asdf",
|
10
|
+
sync_secret: "tasty",
|
11
|
+
})
|
12
|
+
dsl = described_class.new(cloud_configuration)
|
13
|
+
expect(dsl.features).to eq(Set.new)
|
14
|
+
expect(dsl.enabled?(:foo)).to be(false)
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'delegates sync to cloud configuration' do
|
18
|
+
stub = stub_request(:get, "https://www.flippercloud.io/adapter/features?exclude_gate_names=true").
|
19
|
+
with({
|
20
|
+
headers: {
|
21
|
+
'Flipper-Cloud-Token'=>'asdf',
|
22
|
+
},
|
23
|
+
}).to_return(status: 200, body: '{"features": {}}', headers: {})
|
24
|
+
cloud_configuration = Flipper::Cloud::Configuration.new({
|
25
|
+
token: "asdf",
|
26
|
+
sync_secret: "tasty",
|
27
|
+
})
|
28
|
+
dsl = described_class.new(cloud_configuration)
|
29
|
+
dsl.sync
|
30
|
+
expect(stub).to have_been_requested
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'delegates sync_secret to cloud configuration' do
|
34
|
+
cloud_configuration = Flipper::Cloud::Configuration.new({
|
35
|
+
token: "asdf",
|
36
|
+
sync_secret: "tasty",
|
37
|
+
})
|
38
|
+
dsl = described_class.new(cloud_configuration)
|
39
|
+
expect(dsl.sync_secret).to eq("tasty")
|
40
|
+
end
|
41
|
+
|
42
|
+
context "when sync_method is webhook" do
|
43
|
+
let(:local_adapter) do
|
44
|
+
Flipper::Adapters::OperationLogger.new Flipper::Adapters::Memory.new
|
45
|
+
end
|
46
|
+
|
47
|
+
let(:cloud_configuration) do
|
48
|
+
cloud_configuration = Flipper::Cloud::Configuration.new({
|
49
|
+
token: "asdf",
|
50
|
+
sync_secret: "tasty",
|
51
|
+
local_adapter: local_adapter
|
52
|
+
})
|
53
|
+
end
|
54
|
+
|
55
|
+
subject do
|
56
|
+
described_class.new(cloud_configuration)
|
57
|
+
end
|
58
|
+
|
59
|
+
it "sends reads to local adapter" do
|
60
|
+
subject.features
|
61
|
+
subject.enabled?(:foo)
|
62
|
+
expect(local_adapter.count(:features)).to be(1)
|
63
|
+
expect(local_adapter.count(:get)).to be(1)
|
64
|
+
end
|
65
|
+
|
66
|
+
it "sends writes to cloud and local" do
|
67
|
+
add_stub = stub_request(:post, "https://www.flippercloud.io/adapter/features").
|
68
|
+
with({headers: {'Flipper-Cloud-Token'=>'asdf'}}).
|
69
|
+
to_return(status: 200, body: '{}', headers: {})
|
70
|
+
enable_stub = stub_request(:post, "https://www.flippercloud.io/adapter/features/foo/boolean").
|
71
|
+
with(headers: {'Flipper-Cloud-Token'=>'asdf'}).
|
72
|
+
to_return(status: 200, body: '{}', headers: {})
|
73
|
+
|
74
|
+
subject.enable(:foo)
|
75
|
+
|
76
|
+
expect(local_adapter.count(:add)).to be(1)
|
77
|
+
expect(local_adapter.count(:enable)).to be(1)
|
78
|
+
expect(add_stub).to have_been_requested
|
79
|
+
expect(enable_stub).to have_been_requested
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
require 'flipper/cloud/message_verifier'
|
2
|
+
|
3
|
+
RSpec.describe Flipper::Cloud::MessageVerifier do
|
4
|
+
let(:payload) { "some payload" }
|
5
|
+
let(:secret) { "secret" }
|
6
|
+
let(:timestamp) { Time.now }
|
7
|
+
|
8
|
+
describe "#generate" do
|
9
|
+
it "generates signature that can be verified" do
|
10
|
+
message_verifier = Flipper::Cloud::MessageVerifier.new(secret: secret)
|
11
|
+
signature = message_verifier.generate(payload, timestamp)
|
12
|
+
header = generate_header(timestamp: timestamp, signature: signature)
|
13
|
+
expect(message_verifier.verify(payload, header)).to be(true)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
describe "#header" do
|
18
|
+
it "generates a header in valid format" do
|
19
|
+
version = "v1"
|
20
|
+
message_verifier = Flipper::Cloud::MessageVerifier.new(secret: secret, version: version)
|
21
|
+
signature = message_verifier.generate(payload, timestamp)
|
22
|
+
header = message_verifier.header(signature, timestamp)
|
23
|
+
expect(header).to eq("t=#{timestamp.to_i},#{version}=#{signature}")
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
describe ".header" do
|
28
|
+
it "generates a header in valid format" do
|
29
|
+
version = "v1"
|
30
|
+
message_verifier = Flipper::Cloud::MessageVerifier.new(secret: secret, version: version)
|
31
|
+
signature = message_verifier.generate(payload, timestamp)
|
32
|
+
|
33
|
+
header = Flipper::Cloud::MessageVerifier.header(signature, timestamp, version)
|
34
|
+
expect(header).to eq("t=#{timestamp.to_i},#{version}=#{signature}")
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
describe "#verify" do
|
39
|
+
it "raises a InvalidSignature when the header does not have the expected format" do
|
40
|
+
header = "i'm not even a real signature header"
|
41
|
+
expect {
|
42
|
+
message_verifier = Flipper::Cloud::MessageVerifier.new(secret: "secret")
|
43
|
+
message_verifier.verify(payload, header)
|
44
|
+
}.to raise_error(Flipper::Cloud::MessageVerifier::InvalidSignature, "Unable to extract timestamp and signatures from header")
|
45
|
+
end
|
46
|
+
|
47
|
+
it "raises a InvalidSignature when there are no signatures with the expected version" do
|
48
|
+
header = generate_header(version: "v0")
|
49
|
+
expect {
|
50
|
+
message_verifier = Flipper::Cloud::MessageVerifier.new(secret: "secret")
|
51
|
+
message_verifier.verify(payload, header)
|
52
|
+
}.to raise_error(Flipper::Cloud::MessageVerifier::InvalidSignature, /No signatures found with expected version/)
|
53
|
+
end
|
54
|
+
|
55
|
+
it "raises a InvalidSignature when there are no valid signatures for the payload" do
|
56
|
+
header = generate_header(signature: "bad_signature")
|
57
|
+
expect {
|
58
|
+
message_verifier = Flipper::Cloud::MessageVerifier.new(secret: "secret")
|
59
|
+
message_verifier.verify(payload, header)
|
60
|
+
}.to raise_error(Flipper::Cloud::MessageVerifier::InvalidSignature, "No signatures found matching the expected signature for payload")
|
61
|
+
end
|
62
|
+
|
63
|
+
it "raises a InvalidSignature when the timestamp is not within the tolerance" do
|
64
|
+
header = generate_header(timestamp: Time.now - 15)
|
65
|
+
expect {
|
66
|
+
message_verifier = Flipper::Cloud::MessageVerifier.new(secret: secret)
|
67
|
+
message_verifier.verify(payload, header, tolerance: 10)
|
68
|
+
}.to raise_error(Flipper::Cloud::MessageVerifier::InvalidSignature, /Timestamp outside the tolerance zone/)
|
69
|
+
end
|
70
|
+
|
71
|
+
it "returns true when the header contains a valid signature and the timestamp is within the tolerance" do
|
72
|
+
header = generate_header
|
73
|
+
message_verifier = Flipper::Cloud::MessageVerifier.new(secret: "secret")
|
74
|
+
expect(message_verifier.verify(payload, header, tolerance: 10)).to be(true)
|
75
|
+
end
|
76
|
+
|
77
|
+
it "returns true when the header contains at least one valid signature" do
|
78
|
+
header = generate_header + ",v1=bad_signature"
|
79
|
+
message_verifier = Flipper::Cloud::MessageVerifier.new(secret: secret)
|
80
|
+
expect(message_verifier.verify(payload, header, tolerance: 10)).to be(true)
|
81
|
+
end
|
82
|
+
|
83
|
+
it "returns true when the header contains a valid signature and the timestamp is off but no tolerance is provided" do
|
84
|
+
header = generate_header(timestamp: Time.at(12_345))
|
85
|
+
message_verifier = Flipper::Cloud::MessageVerifier.new(secret: secret)
|
86
|
+
expect(message_verifier.verify(payload, header)).to be(true)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
def generate_header(options = {})
|
93
|
+
options[:secret] ||= secret
|
94
|
+
options[:version] ||= "v1"
|
95
|
+
|
96
|
+
message_verifier = Flipper::Cloud::MessageVerifier.new(secret: options[:secret], version: options[:version])
|
97
|
+
|
98
|
+
options[:timestamp] ||= timestamp
|
99
|
+
options[:payload] ||= payload
|
100
|
+
options[:signature] ||= message_verifier.generate(options[:payload], options[:timestamp])
|
101
|
+
|
102
|
+
Flipper::Cloud::MessageVerifier.header(options[:signature], options[:timestamp], options[:version])
|
103
|
+
end
|
104
|
+
end
|