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.
- checksums.yaml +4 -4
- data/lib/flipper/version.rb +1 -1
- metadata +14 -53
- data/docs/images/flipper_cloud.png +0 -0
- data/examples/cloud/app.ru +0 -12
- data/examples/cloud/basic.rb +0 -22
- data/examples/cloud/cloud_setup.rb +0 -4
- data/examples/cloud/forked.rb +0 -31
- data/examples/cloud/import.rb +0 -17
- data/examples/cloud/threaded.rb +0 -36
- data/flipper-cloud.gemspec +0 -28
- data/lib/flipper/cloud/configuration.rb +0 -189
- data/lib/flipper/cloud/dsl.rb +0 -27
- data/lib/flipper/cloud/engine.rb +0 -29
- data/lib/flipper/cloud/instrumenter.rb +0 -48
- data/lib/flipper/cloud/message_verifier.rb +0 -95
- data/lib/flipper/cloud/middleware.rb +0 -63
- data/lib/flipper/cloud/routes.rb +0 -13
- data/lib/flipper/cloud.rb +0 -57
- data/spec/flipper/cloud/configuration_spec.rb +0 -261
- data/spec/flipper/cloud/dsl_spec.rb +0 -82
- data/spec/flipper/cloud/engine_spec.rb +0 -95
- data/spec/flipper/cloud/message_verifier_spec.rb +0 -104
- data/spec/flipper/cloud/middleware_spec.rb +0 -289
- data/spec/flipper/cloud_spec.rb +0 -179
@@ -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
|
data/lib/flipper/cloud/routes.rb
DELETED
@@ -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
|