flipper 0.28.3 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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