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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/examples.yml +2 -2
  3. data/Changelog.md +190 -165
  4. data/Gemfile +5 -3
  5. data/docs/images/flipper_cloud.png +0 -0
  6. data/examples/cloud/app.ru +12 -0
  7. data/examples/cloud/basic.rb +22 -0
  8. data/examples/cloud/cloud_setup.rb +4 -0
  9. data/examples/cloud/forked.rb +31 -0
  10. data/examples/cloud/import.rb +17 -0
  11. data/examples/cloud/threaded.rb +36 -0
  12. data/examples/dsl.rb +0 -14
  13. data/flipper-cloud.gemspec +19 -0
  14. data/flipper.gemspec +3 -2
  15. data/lib/flipper/cloud/configuration.rb +189 -0
  16. data/lib/flipper/cloud/dsl.rb +27 -0
  17. data/lib/flipper/cloud/instrumenter.rb +48 -0
  18. data/lib/flipper/cloud/message_verifier.rb +95 -0
  19. data/lib/flipper/cloud/middleware.rb +63 -0
  20. data/lib/flipper/cloud/routes.rb +14 -0
  21. data/lib/flipper/cloud.rb +53 -0
  22. data/lib/flipper/dsl.rb +0 -46
  23. data/lib/flipper/{railtie.rb → engine.rb} +19 -3
  24. data/lib/flipper/metadata.rb +5 -1
  25. data/lib/flipper/middleware/memoizer.rb +1 -1
  26. data/lib/flipper/spec/shared_adapter_specs.rb +43 -43
  27. data/lib/flipper/test/shared_adapter_test.rb +43 -43
  28. data/lib/flipper/version.rb +1 -1
  29. data/lib/flipper.rb +3 -5
  30. data/spec/flipper/adapters/dual_write_spec.rb +2 -2
  31. data/spec/flipper/adapters/instrumented_spec.rb +1 -1
  32. data/spec/flipper/adapters/memoizable_spec.rb +6 -6
  33. data/spec/flipper/adapters/operation_logger_spec.rb +2 -2
  34. data/spec/flipper/adapters/read_only_spec.rb +6 -6
  35. data/spec/flipper/cloud/configuration_spec.rb +269 -0
  36. data/spec/flipper/cloud/dsl_spec.rb +82 -0
  37. data/spec/flipper/cloud/message_verifier_spec.rb +104 -0
  38. data/spec/flipper/cloud/middleware_spec.rb +289 -0
  39. data/spec/flipper/cloud_spec.rb +180 -0
  40. data/spec/flipper/dsl_spec.rb +0 -75
  41. data/spec/flipper/engine_spec.rb +190 -0
  42. data/spec/flipper_integration_spec.rb +12 -12
  43. data/spec/flipper_spec.rb +0 -30
  44. data/spec/spec_helper.rb +0 -12
  45. data/spec/support/climate_control.rb +7 -0
  46. metadata +54 -11
  47. data/.tool-versions +0 -1
  48. data/spec/flipper/railtie_spec.rb +0 -109
@@ -1,3 +1,3 @@
1
1
  module Flipper
2
- VERSION = '0.28.3'.freeze
2
+ VERSION = '1.0.0'.freeze
3
3
  end
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, :bool, :boolean,
60
- :enable_actor, :disable_actor, :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/railtie" if defined?(Rails::Railtie)
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), local.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), local.boolean(false)
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) { flipper.actors(22) }
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, flipper.bool)
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, flipper.bool)
206
- adapter_result = adapter.enable(feature, gate, flipper.bool)
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, flipper.bool)
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, flipper.bool)
236
- adapter_result = adapter.disable(feature, gate, flipper.bool)
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 = flipper.bool
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 = flipper.bool
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, flipper.boolean)
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, flipper.actor(actor22))
48
- adapter.enable(feature, actors_gate, flipper.actors(25))
49
- adapter.enable(feature, time_gate, flipper.time(45))
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, flipper.boolean) }
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, flipper.boolean) }
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