flipper-cloud 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.
@@ -1,63 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "flipper/cloud/message_verifier"
4
-
5
- module Flipper
6
- module Cloud
7
- class Middleware
8
- # Internal: The path to match for webhook requests.
9
- WEBHOOK_PATH = %r{\A/webhooks\/?\Z}
10
- # Internal: The root path to match for requests.
11
- ROOT_PATH = %r{\A/\Z}
12
-
13
- def initialize(app, options = {})
14
- @app = app
15
- @env_key = options.fetch(:env_key, 'flipper')
16
- end
17
-
18
- def call(env)
19
- dup.call!(env)
20
- end
21
-
22
- def call!(env)
23
- request = Rack::Request.new(env)
24
- if request.post? && (request.path_info.match(ROOT_PATH) || request.path_info.match(WEBHOOK_PATH))
25
- status = 200
26
- headers = {
27
- "Content-Type" => "application/json",
28
- }
29
- body = "{}"
30
- payload = request.body.read
31
- signature = request.env["HTTP_FLIPPER_CLOUD_SIGNATURE"]
32
- flipper = env.fetch(@env_key)
33
-
34
- begin
35
- message_verifier = MessageVerifier.new(secret: flipper.sync_secret)
36
- if message_verifier.verify(payload, signature)
37
- begin
38
- flipper.sync
39
- body = JSON.generate({
40
- groups: Flipper.group_names.map { |name| {name: name}}
41
- })
42
- rescue Flipper::Adapters::Http::Error => error
43
- status = error.response.code.to_i == 402 ? 402 : 500
44
- headers["Flipper-Cloud-Response-Error-Class"] = error.class.name
45
- headers["Flipper-Cloud-Response-Error-Message"] = error.message
46
- rescue => error
47
- status = 500
48
- headers["Flipper-Cloud-Response-Error-Class"] = error.class.name
49
- headers["Flipper-Cloud-Response-Error-Message"] = error.message
50
- end
51
- end
52
- rescue MessageVerifier::InvalidSignature
53
- status = 400
54
- end
55
-
56
- [status, headers, [body]]
57
- else
58
- @app.call(env)
59
- end
60
- end
61
- end
62
- end
63
- end
@@ -1,13 +0,0 @@
1
- # Default routes loaded by Flipper::Cloud::Engine
2
- Rails.application.routes.draw do
3
- if ENV["FLIPPER_CLOUD_TOKEN"] && ENV["FLIPPER_CLOUD_SYNC_SECRET"]
4
- config = Rails.application.config.flipper
5
-
6
- cloud_app = Flipper::Cloud.app(nil,
7
- env_key: config.env_key,
8
- memoizer_options: { preload: config.preload }
9
- )
10
-
11
- mount cloud_app, at: config.cloud_path
12
- end
13
- end
data/lib/flipper/cloud.rb DELETED
@@ -1,57 +0,0 @@
1
- require "flipper"
2
- require "flipper/middleware/setup_env"
3
- require "flipper/middleware/memoizer"
4
- require "flipper/cloud/configuration"
5
- require "flipper/cloud/dsl"
6
- require "flipper/cloud/middleware"
7
- require "flipper/cloud/engine" if defined?(Rails::Engine)
8
-
9
- module Flipper
10
- module Cloud
11
- # Public: Returns a new Flipper instance with an http adapter correctly
12
- # configured for flipper cloud.
13
- #
14
- # token - The String token for the environment from the website.
15
- # options - The Hash of options. See Flipper::Cloud::Configuration.
16
- # block - The block that configuration will be yielded to allowing you to
17
- # customize this cloud instance and its adapter.
18
- def self.new(options = {})
19
- configuration = Configuration.new(options)
20
- yield configuration if block_given?
21
- DSL.new(configuration)
22
- end
23
-
24
- def self.app(flipper = nil, options = {})
25
- env_key = options.fetch(:env_key, 'flipper')
26
- memoizer_options = options.fetch(:memoizer_options, {})
27
-
28
- app = ->(_) { [404, { 'Content-Type'.freeze => 'application/json'.freeze }, ['{}'.freeze]] }
29
- builder = Rack::Builder.new
30
- yield builder if block_given?
31
- builder.use Flipper::Middleware::SetupEnv, flipper, env_key: env_key
32
- builder.use Flipper::Middleware::Memoizer, memoizer_options.merge(env_key: env_key)
33
- builder.use Flipper::Cloud::Middleware, env_key: env_key
34
- builder.run app
35
- klass = self
36
- app = builder.to_app
37
- app.define_singleton_method(:inspect) { klass.inspect } # pretty rake routes output
38
- app
39
- end
40
-
41
- # Private: Configure Flipper to use Cloud by default
42
- def self.set_default
43
- Flipper.configure do |config|
44
- config.default do
45
- if ENV["FLIPPER_CLOUD_TOKEN"]
46
- self.new(local_adapter: config.adapter)
47
- else
48
- warn "Missing FLIPPER_CLOUD_TOKEN environment variable. Disabling Flipper::Cloud."
49
- Flipper.new(config.adapter)
50
- end
51
- end
52
- end
53
- end
54
- end
55
- end
56
-
57
- Flipper::Cloud.set_default
@@ -1,261 +0,0 @@
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
- ENV["FLIPPER_CLOUD_TOKEN"] = "from_env"
16
- instance = described_class.new(required_options.reject { |k, v| k == :token })
17
- expect(instance.token).to eq("from_env")
18
- end
19
-
20
- it "can set instrumenter" do
21
- instrumenter = Object.new
22
- instance = described_class.new(required_options.merge(instrumenter: instrumenter))
23
- expect(instance.instrumenter).to be(instrumenter)
24
- end
25
-
26
- it "can set read_timeout" do
27
- instance = described_class.new(required_options.merge(read_timeout: 5))
28
- expect(instance.read_timeout).to eq(5)
29
- end
30
-
31
- it "can set read_timeout from ENV var" do
32
- ENV["FLIPPER_CLOUD_READ_TIMEOUT"] = "9"
33
- instance = described_class.new(required_options.reject { |k, v| k == :read_timeout })
34
- expect(instance.read_timeout).to eq(9)
35
- end
36
-
37
- it "can set open_timeout" do
38
- instance = described_class.new(required_options.merge(open_timeout: 5))
39
- expect(instance.open_timeout).to eq(5)
40
- end
41
-
42
- it "can set open_timeout from ENV var" do
43
- ENV["FLIPPER_CLOUD_OPEN_TIMEOUT"] = "9"
44
- instance = described_class.new(required_options.reject { |k, v| k == :open_timeout })
45
- expect(instance.open_timeout).to eq(9)
46
- end
47
-
48
- it "can set write_timeout" do
49
- instance = described_class.new(required_options.merge(write_timeout: 5))
50
- expect(instance.write_timeout).to eq(5)
51
- end
52
-
53
- it "can set write_timeout from ENV var" do
54
- ENV["FLIPPER_CLOUD_WRITE_TIMEOUT"] = "9"
55
- instance = described_class.new(required_options.reject { |k, v| k == :write_timeout })
56
- expect(instance.write_timeout).to eq(9)
57
- end
58
-
59
- it "can set sync_interval" do
60
- instance = described_class.new(required_options.merge(sync_interval: 1))
61
- expect(instance.sync_interval).to eq(1)
62
- end
63
-
64
- it "can set sync_interval from ENV var" do
65
- ENV["FLIPPER_CLOUD_SYNC_INTERVAL"] = "5"
66
- instance = described_class.new(required_options.reject { |k, v| k == :sync_interval })
67
- expect(instance.sync_interval).to eq(5)
68
- end
69
-
70
- it "passes sync_interval into sync adapter" do
71
- # The initial sync of http to local invokes this web request.
72
- stub_request(:get, /flippercloud\.io/).to_return(status: 200, body: "{}")
73
-
74
- instance = described_class.new(required_options.merge(sync_interval: 1))
75
- poller = instance.send(:poller)
76
- expect(poller.interval).to eq(1)
77
- end
78
-
79
- it "can set debug_output" do
80
- instance = described_class.new(required_options.merge(debug_output: STDOUT))
81
- expect(instance.debug_output).to eq(STDOUT)
82
- end
83
-
84
- it "defaults adapter block" do
85
- # The initial sync of http to local invokes this web request.
86
- stub_request(:get, /flippercloud\.io/).to_return(status: 200, body: "{}")
87
-
88
- instance = described_class.new(required_options)
89
- expect(instance.adapter).to be_instance_of(Flipper::Adapters::DualWrite)
90
- end
91
-
92
- it "can override adapter block" do
93
- # The initial sync of http to local invokes this web request.
94
- stub_request(:get, /flippercloud\.io/).to_return(status: 200, body: "{}")
95
-
96
- instance = described_class.new(required_options)
97
- instance.adapter do |adapter|
98
- Flipper::Adapters::Instrumented.new(adapter)
99
- end
100
- expect(instance.adapter).to be_instance_of(Flipper::Adapters::Instrumented)
101
- end
102
-
103
- it "defaults url" do
104
- instance = described_class.new(required_options.reject { |k, v| k == :url })
105
- expect(instance.url).to eq("https://www.flippercloud.io/adapter")
106
- end
107
-
108
- it "can override url using options" do
109
- options = required_options.merge(url: "http://localhost:5000/adapter")
110
- instance = described_class.new(options)
111
- expect(instance.url).to eq("http://localhost:5000/adapter")
112
-
113
- instance = described_class.new(required_options)
114
- instance.url = "http://localhost:5000/adapter"
115
- expect(instance.url).to eq("http://localhost:5000/adapter")
116
- end
117
-
118
- it "can override URL using ENV var" do
119
- ENV["FLIPPER_CLOUD_URL"] = "https://example.com"
120
- instance = described_class.new(required_options.reject { |k, v| k == :url })
121
- expect(instance.url).to eq("https://example.com")
122
- end
123
-
124
- it "defaults sync_method to :poll" do
125
- instance = described_class.new(required_options)
126
-
127
- expect(instance.sync_method).to eq(:poll)
128
- end
129
-
130
- it "sets sync_method to :webhook if sync_secret provided" do
131
- instance = described_class.new(required_options.merge({
132
- sync_secret: "secret",
133
- }))
134
-
135
- expect(instance.sync_method).to eq(:webhook)
136
- expect(instance.adapter).to be_instance_of(Flipper::Adapters::DualWrite)
137
- end
138
-
139
- it "sets sync_method to :webhook if FLIPPER_CLOUD_SYNC_SECRET set" do
140
- ENV["FLIPPER_CLOUD_SYNC_SECRET"] = "abc"
141
- instance = described_class.new(required_options)
142
-
143
- expect(instance.sync_method).to eq(:webhook)
144
- expect(instance.adapter).to be_instance_of(Flipper::Adapters::DualWrite)
145
- end
146
-
147
- it "can set sync_secret" do
148
- instance = described_class.new(required_options.merge(sync_secret: "from_config"))
149
- expect(instance.sync_secret).to eq("from_config")
150
- end
151
-
152
- it "can override sync_secret using ENV var" do
153
- ENV["FLIPPER_CLOUD_SYNC_SECRET"] = "from_env"
154
- instance = described_class.new(required_options.reject { |k, v| k == :sync_secret })
155
- expect(instance.sync_secret).to eq("from_env")
156
- end
157
-
158
- it "can sync with cloud" do
159
- body = JSON.generate({
160
- "features": [
161
- {
162
- "key": "search",
163
- "state": "on",
164
- "gates": [
165
- {
166
- "key": "boolean",
167
- "name": "boolean",
168
- "value": true
169
- },
170
- {
171
- "key": "groups",
172
- "name": "group",
173
- "value": []
174
- },
175
- {
176
- "key": "actors",
177
- "name": "actor",
178
- "value": []
179
- },
180
- {
181
- "key": "percentage_of_actors",
182
- "name": "percentage_of_actors",
183
- "value": 0
184
- },
185
- {
186
- "key": "percentage_of_time",
187
- "name": "percentage_of_time",
188
- "value": 0
189
- }
190
- ]
191
- },
192
- {
193
- "key": "history",
194
- "state": "off",
195
- "gates": [
196
- {
197
- "key": "boolean",
198
- "name": "boolean",
199
- "value": false
200
- },
201
- {
202
- "key": "groups",
203
- "name": "group",
204
- "value": []
205
- },
206
- {
207
- "key": "actors",
208
- "name": "actor",
209
- "value": []
210
- },
211
- {
212
- "key": "percentage_of_actors",
213
- "name": "percentage_of_actors",
214
- "value": 0
215
- },
216
- {
217
- "key": "percentage_of_time",
218
- "name": "percentage_of_time",
219
- "value": 0
220
- }
221
- ]
222
- }
223
- ]
224
- })
225
- stub = stub_request(:get, "https://www.flippercloud.io/adapter/features?exclude_gate_names=true").
226
- with({
227
- headers: {
228
- 'Flipper-Cloud-Token'=>'asdf',
229
- },
230
- }).to_return(status: 200, body: body, headers: {})
231
- instance = described_class.new(required_options)
232
- instance.sync
233
-
234
- # Check that remote was fetched.
235
- expect(stub).to have_been_requested
236
-
237
- # Check that local adapter really did sync.
238
- local_adapter = instance.local_adapter
239
- all = local_adapter.get_all
240
- expect(all.keys).to eq(["search", "history"])
241
- expect(all["search"][:boolean]).to eq("true")
242
- expect(all["history"][:boolean]).to eq(nil)
243
- end
244
-
245
- it "can setup brow to report events to cloud" do
246
- # skip logging brow
247
- Brow.logger = Logger.new(File::NULL)
248
- brow = described_class.new(required_options).brow
249
-
250
- stub = stub_request(:post, "https://www.flippercloud.io/adapter/events")
251
- .with { |request|
252
- data = JSON.parse(request.body)
253
- data.keys == ["uuid", "messages"] && data["messages"] == [{"n" => 1}]
254
- }
255
- .to_return(status: 201, body: "{}", headers: {})
256
-
257
- brow.push({"n" => 1})
258
- brow.worker.stop
259
- expect(stub).to have_been_requested.times(1)
260
- end
261
- end
@@ -1,82 +0,0 @@
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
@@ -1,95 +0,0 @@
1
- require 'rails'
2
- require 'flipper/cloud'
3
-
4
- RSpec.describe Flipper::Cloud::Engine do
5
- let(:env) do
6
- { "FLIPPER_CLOUD_TOKEN" => "test-token" }
7
- end
8
-
9
- let(:application) do
10
- Class.new(Rails::Application) do
11
- config.eager_load = false
12
- config.logger = ActiveSupport::Logger.new($stdout)
13
- end
14
- end
15
-
16
- # App for Rack::Test
17
- let(:app) { application.routes }
18
-
19
- before do
20
- Rails.application = nil
21
- ActiveSupport::Dependencies.autoload_paths = ActiveSupport::Dependencies.autoload_paths.dup
22
- ActiveSupport::Dependencies.autoload_once_paths = ActiveSupport::Dependencies.autoload_once_paths.dup
23
-
24
- # Force loading of flipper to configure itself
25
- load 'flipper/cloud.rb'
26
- end
27
-
28
- it "initializes cloud configuration" do
29
- stub_request(:get, /flippercloud\.io/).to_return(status: 200, body: "{}")
30
-
31
- ENV.update(env)
32
- application.initialize!
33
-
34
- expect(Flipper.instance).to be_a(Flipper::Cloud::DSL)
35
- expect(Flipper.instance.instrumenter).to be(ActiveSupport::Notifications)
36
- end
37
-
38
- context "with CLOUD_SYNC_SECRET" do
39
- before do
40
- env.update "FLIPPER_CLOUD_SYNC_SECRET" => "test-secret"
41
- end
42
-
43
- let(:request_body) do
44
- JSON.generate({
45
- "environment_id" => 1,
46
- "webhook_id" => 1,
47
- "delivery_id" => SecureRandom.uuid,
48
- "action" => "sync",
49
- })
50
- end
51
- let(:timestamp) { Time.now }
52
- let(:signature) {
53
- Flipper::Cloud::MessageVerifier.new(secret: env["FLIPPER_CLOUD_SYNC_SECRET"]).generate(request_body, timestamp)
54
- }
55
- let(:signature_header_value) {
56
- Flipper::Cloud::MessageVerifier.new(secret: "").header(signature, timestamp)
57
- }
58
-
59
- it "configures webhook app" do
60
- ENV.update(env)
61
- application.initialize!
62
-
63
- stub = stub_request(:get, "https://www.flippercloud.io/adapter/features?exclude_gate_names=true").with({
64
- headers: { "Flipper-Cloud-Token" => ENV["FLIPPER_CLOUD_TOKEN"] },
65
- }).to_return(status: 200, body: JSON.generate({ features: {} }), headers: {})
66
-
67
- post "/_flipper", request_body, { "HTTP_FLIPPER_CLOUD_SIGNATURE" => signature_header_value }
68
-
69
- expect(last_response.status).to eq(200)
70
- expect(stub).to have_been_requested
71
- end
72
- end
73
-
74
- context "without CLOUD_SYNC_SECRET" do
75
- it "does not configure webhook app" do
76
- ENV.update(env)
77
- application.initialize!
78
-
79
- post "/_flipper"
80
- expect(last_response.status).to eq(404)
81
- end
82
- end
83
-
84
- context "without FLIPPER_CLOUD_TOKEN" do
85
- it "gracefully skips configuring webhook app" do
86
- ENV["FLIPPER_CLOUD_TOKEN"] = nil
87
- application.initialize!
88
- expect(silence { Flipper.instance }).to match(/Missing FLIPPER_CLOUD_TOKEN/)
89
- expect(Flipper.instance).to be_a(Flipper::DSL)
90
-
91
- post "/_flipper"
92
- expect(last_response.status).to eq(404)
93
- end
94
- end
95
- end
@@ -1,104 +0,0 @@
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