flipper-cloud 0.20.0 → 0.21.0.rc1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d17e92045e80beb6c0c83aa2591b75585393d0e6e922b834379fcfa0f5dc9049
4
- data.tar.gz: 6943dde37bab5cadf5e8e8a2db847aa6ddb0e310fd1b7bc9c5638835ae81cfe1
3
+ metadata.gz: 9ba3c2db9aa7175de2301fb4fea82a25164e935b0f0fe5d7ce38911eee319cf9
4
+ data.tar.gz: e97f8ec07dcc20057d6934e1cf933be9517d53549317539900630e43a7ffd23e
5
5
  SHA512:
6
- metadata.gz: cf7073018558fa53b8f7ada5d984b42066d0a5ae5cd81e343cd249cc2d1032ecd4c95a094c2f49027f1bf87de7ddc6c7f1a5804c5658d4700c09f0c0c7d74b8c
7
- data.tar.gz: faa00d96aae37eff0559f88e8a913ff407526825b55eff2404c794b2a6aa7f1f9882d04022746468fc157c52f8cc4eafa2ffc500c11aaa213876b38d963d6cab
6
+ metadata.gz: 62d4f0cc6817cfa654e1980156ca7e4d9ba81d1c0bb761f37651455b20f0b4732ceafe1adbf5a30ca09f8c1f8b80b8609c9d61ef969b82be48eedd183e4a5c15
7
+ data.tar.gz: 4540506c2b1c4eabe0140f3757e0414977ef876454755106bedfffd8159a880ffea37b6bca24bde30e9e6b79259ad86e30afb415219f7a22104c91bcd4edfbe8
Binary file
@@ -1,14 +1,9 @@
1
1
  # Usage (from the repo root):
2
- # env FLIPPER_CLOUD_TOKEN=<token> FLIPPER_CLOUD_SYNC_SECRET=<secret> FLIPPER_CLOUD_SYNC_METHOD=webhook bundle exec rackup examples/ui/basic.ru -p 9999
3
- # env FLIPPER_CLOUD_TOKEN=<token> FLIPPER_CLOUD_SYNC_SECRET=<secret> FLIPPER_CLOUD_SYNC_METHOD=webhook bundle exec shotgun examples/ui/basic.ru -p 9999
2
+ # env FLIPPER_CLOUD_TOKEN=<token> FLIPPER_CLOUD_SYNC_SECRET=<secret> bundle exec rackup examples/cloud/app.ru -p 9999
3
+ # env FLIPPER_CLOUD_TOKEN=<token> FLIPPER_CLOUD_SYNC_SECRET=<secret> bundle exec shotgun examples/cloud/app.ru -p 9999
4
4
  # http://localhost:9999/
5
- # http://localhost:9999/webhooks
6
-
7
- require 'pathname'
8
- root_path = Pathname(__FILE__).dirname.join('..').expand_path
9
- lib_path = root_path.join('lib')
10
- $:.unshift(lib_path)
11
5
 
6
+ require 'bundler/setup'
12
7
  require 'flipper/cloud'
13
8
  Flipper.configure do |config|
14
9
  config.default { Flipper::Cloud.new }
@@ -1,13 +1,8 @@
1
1
  # Usage (from the repo root):
2
- # env TOKEN=<token> bundle exec ruby examples/cloud/basic.rb
3
- require 'pathname'
4
- require 'logger'
5
- root_path = Pathname(__FILE__).dirname.join('..').expand_path
6
- lib_path = root_path.join('lib')
7
- $:.unshift(lib_path)
8
-
2
+ # env FLIPPER_CLOUD_TOKEN=<token> bundle exec ruby examples/cloud/basic.rb
3
+ require 'bundler/setup'
9
4
  require 'flipper/cloud'
10
- flipper = Flipper::Cloud.new(ENV.fetch('TOKEN'))
5
+ flipper = Flipper::Cloud.new
11
6
 
12
7
  flipper[:stats].enable
13
8
 
@@ -1,16 +1,15 @@
1
- require File.expand_path('../../example_setup', __FILE__)
2
-
1
+ # env FLIPPER_CLOUD_TOKEN=<token> bundle exec ruby examples/cloud/cached_in_memory.rb
2
+ require 'bundler/setup'
3
3
  require 'flipper/cloud'
4
4
  require 'flipper/adapters/active_support_cache_store'
5
5
  require 'active_support/cache'
6
6
  require 'active_support/cache/memory_store'
7
7
 
8
- token = ENV.fetch("TOKEN") { abort "TOKEN environment variable not set." }
9
8
  feature_name = ENV.fetch("FEATURE") { "testing" }.to_sym
10
9
 
11
10
  Flipper.configure do |config|
12
11
  config.default do
13
- Flipper::Cloud.new(token) do |cloud|
12
+ Flipper::Cloud.new do |cloud|
14
13
  cloud.debug_output = STDOUT
15
14
  cloud.adapter do |adapter|
16
15
  Flipper::Adapters::ActiveSupportCacheStore.new(adapter,
@@ -1,23 +1,15 @@
1
1
  # Usage (from the repo root):
2
- # env TOKEN=<token> bundle exec ruby examples/cloud/basic.rb
3
- require 'pathname'
4
- require 'logger'
5
- root_path = Pathname(__FILE__).dirname.join('..').expand_path
6
- lib_path = root_path.join('lib')
7
- $:.unshift(lib_path)
8
-
2
+ # env FLIPPER_CLOUD_TOKEN=<token> bundle exec ruby examples/cloud/import.rb
3
+ require 'bundler/setup'
9
4
  require 'flipper'
10
5
  require 'flipper/cloud'
11
6
 
12
- memory_adapter = Flipper::Adapters::Memory.new
13
- memory_flipper = Flipper.new(memory_adapter)
14
-
15
- memory_flipper.enable(:test)
16
- memory_flipper.enable(:search)
17
- memory_flipper.enable_actor(:stats, Flipper::Actor.new("jnunemaker"))
18
- memory_flipper.enable_percentage_of_time(:logging, 5)
7
+ Flipper.enable(:test)
8
+ Flipper.enable(:search)
9
+ Flipper.enable_actor(:stats, Flipper::Actor.new("jnunemaker"))
10
+ Flipper.enable_percentage_of_time(:logging, 5)
19
11
 
20
- flipper = Flipper::Cloud.new(ENV.fetch('TOKEN'))
12
+ cloud = Flipper::Cloud.new
21
13
 
22
- # wipes cloud clean and makes it identical to memory flipper
23
- flipper.import(memory_flipper)
14
+ # makes cloud identical to memory flipper
15
+ cloud.import(Flipper)
@@ -5,13 +5,12 @@
5
5
  # never raise. You could get a slow request every now and then if cloud is
6
6
  # unavailable, but we are hoping to fix that soon by doing the cloud update in a
7
7
  # background thread.
8
- require File.expand_path('../../example_setup', __FILE__)
9
-
8
+ # env FLIPPER_CLOUD_TOKEN=<token> bundle exec ruby examples/cloud/local_adapter.rb
9
+ require 'bundler/setup'
10
10
  require 'logger'
11
11
  require 'flipper/cloud'
12
12
  require 'flipper/adapters/redis'
13
13
 
14
- token = ENV.fetch("TOKEN") { abort "TOKEN environment variable not set." }
15
14
  feature_name = ENV.fetch("FEATURE") { "testing" }.to_sym
16
15
 
17
16
  redis = Redis.new(logger: Logger.new(STDOUT))
@@ -19,7 +18,7 @@ redis.flushdb
19
18
 
20
19
  Flipper.configure do |config|
21
20
  config.default do
22
- Flipper::Cloud.new(token) do |cloud|
21
+ Flipper::Cloud.new do |cloud|
23
22
  cloud.debug_output = STDOUT
24
23
  cloud.local_adapter = Flipper::Adapters::Redis.new(redis)
25
24
  cloud.sync_interval = 10
@@ -1,3 +1,4 @@
1
+ require "socket"
1
2
  require "flipper/adapters/http"
2
3
  require "flipper/adapters/memory"
3
4
  require "flipper/adapters/dual_write"
@@ -12,6 +13,8 @@ module Flipper
12
13
  :webhook,
13
14
  ].freeze
14
15
 
16
+ DEFAULT_URL = "https://www.flippercloud.io/adapter".freeze
17
+
15
18
  # Public: The token corresponding to an environment on flippercloud.io.
16
19
  attr_accessor :token
17
20
 
@@ -56,10 +59,6 @@ module Flipper
56
59
  # the local in sync with cloud (default: 10).
57
60
  attr_accessor :sync_interval
58
61
 
59
- # Public: The method to be used for synchronizing your local flipper
60
- # adapter with cloud. (default: :poll, can also be :webhook).
61
- attr_reader :sync_method
62
-
63
62
  # Public: The secret used to verify if syncs in the middleware should
64
63
  # occur or not.
65
64
  attr_accessor :sync_secret
@@ -71,6 +70,11 @@ module Flipper
71
70
  raise ArgumentError, "Flipper::Cloud token is missing. Please set FLIPPER_CLOUD_TOKEN or provide the token (e.g. Flipper::Cloud.new('token'))."
72
71
  end
73
72
 
73
+ if ENV["FLIPPER_CLOUD_SYNC_METHOD"]
74
+ warn "FLIPPER_CLOUD_SYNC_METHOD is deprecated and has no effect."
75
+ end
76
+ self.sync_method = options[:sync_method] if options[:sync_method]
77
+
74
78
  @instrumenter = options.fetch(:instrumenter, Instrumenters::Noop)
75
79
  @read_timeout = options.fetch(:read_timeout) { ENV.fetch("FLIPPER_CLOUD_READ_TIMEOUT", 5).to_f }
76
80
  @open_timeout = options.fetch(:open_timeout) { ENV.fetch("FLIPPER_CLOUD_OPEN_TIMEOUT", 5).to_f }
@@ -80,8 +84,7 @@ module Flipper
80
84
  @local_adapter = options.fetch(:local_adapter) { Adapters::Memory.new }
81
85
  @debug_output = options[:debug_output]
82
86
  @adapter_block = ->(adapter) { adapter }
83
- self.sync_method = options.fetch(:sync_method) { ENV.fetch("FLIPPER_CLOUD_SYNC_METHOD", :poll).to_sym }
84
- self.url = options.fetch(:url) { ENV.fetch("FLIPPER_CLOUD_URL", "https://www.flippercloud.io/adapter".freeze) }
87
+ self.url = options.fetch(:url) { ENV.fetch("FLIPPER_CLOUD_URL", DEFAULT_URL) }
85
88
  end
86
89
 
87
90
  # Public: Read or customize the http adapter. Calling without a block will
@@ -113,18 +116,14 @@ module Flipper
113
116
  }).call
114
117
  end
115
118
 
116
- def sync_method=(new_sync_method)
117
- new_sync_method = new_sync_method.to_sym
118
-
119
- unless VALID_SYNC_METHODS.include?(new_sync_method)
120
- raise ArgumentError, "Unsupported sync_method. Valid options are (#{VALID_SYNC_METHODS.to_a.join(', ')})"
121
- end
122
-
123
- if new_sync_method == :webhook && sync_secret.nil?
124
- raise ArgumentError, "Flipper::Cloud sync_secret is missing. Please set FLIPPER_CLOUD_SYNC_SECRET or provide the sync_secret used to validate webhooks."
125
- end
119
+ # Public: The method that will be used to synchronize local adapter with
120
+ # cloud. (default: :poll, will be :webhook if sync_secret is set).
121
+ def sync_method
122
+ sync_secret ? :webhook : :poll
123
+ end
126
124
 
127
- @sync_method = new_sync_method
125
+ def sync_method=(_)
126
+ warn "Flipper::Cloud: sync_method is deprecated and has no effect."
128
127
  end
129
128
 
130
129
  private
@@ -153,6 +152,11 @@ module Flipper
153
152
  headers: {
154
153
  "Flipper-Cloud-Token" => @token,
155
154
  "Feature-Flipper-Token" => @token,
155
+ "Client-Lang" => "ruby",
156
+ "Client-Lang-Version" => "#{RUBY_VERSION} p#{RUBY_PATCHLEVEL} (#{RUBY_RELEASE_DATE})",
157
+ "Client-Platform" => RUBY_PLATFORM,
158
+ "Client-Engine" => defined?(RUBY_ENGINE) ? RUBY_ENGINE : "",
159
+ "Client-Hostname" => Socket.gethostname,
156
160
  },
157
161
  })
158
162
  end
@@ -7,6 +7,8 @@ module Flipper
7
7
  class Middleware
8
8
  # Internal: The path to match for webhook requests.
9
9
  WEBHOOK_PATH = %r{\A/webhooks\/?\Z}
10
+ # Internal: The root path to match for requests.
11
+ ROOT_PATH = %r{\A/\Z}
10
12
 
11
13
  def initialize(app, options = {})
12
14
  @app = app
@@ -19,7 +21,7 @@ module Flipper
19
21
 
20
22
  def call!(env)
21
23
  request = Rack::Request.new(env)
22
- if request.post? && request.path_info.match(WEBHOOK_PATH)
24
+ if request.post? && (request.path_info.match(ROOT_PATH) || request.path_info.match(WEBHOOK_PATH))
23
25
  status = 200
24
26
  headers = {
25
27
  "Content-Type" => "application/json",
@@ -32,10 +34,20 @@ module Flipper
32
34
  begin
33
35
  message_verifier = MessageVerifier.new(secret: flipper.sync_secret)
34
36
  if message_verifier.verify(payload, signature)
35
- flipper.sync
36
- body = JSON.generate({
37
- groups: Flipper.group_names.map { |name| {name: name}}
38
- })
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
39
51
  end
40
52
  rescue MessageVerifier::InvalidSignature
41
53
  status = 400
@@ -1,3 +1,3 @@
1
1
  module Flipper
2
- VERSION = '0.20.0'.freeze
2
+ VERSION = '0.21.0.rc1'.freeze
3
3
  end
@@ -127,53 +127,30 @@ RSpec.describe Flipper::Cloud::Configuration do
127
127
  end
128
128
  end
129
129
 
130
- it "defaults to sync_method to poll" do
131
- memory_adapter = Flipper::Adapters::Memory.new
130
+ it "defaults sync_method to :poll" do
132
131
  instance = described_class.new(required_options)
133
132
 
134
133
  expect(instance.sync_method).to eq(:poll)
135
134
  end
136
135
 
137
- it "can use webhook for sync_method" do
138
- memory_adapter = Flipper::Adapters::Memory.new
136
+ it "sets sync_method to :webhook if sync_secret provided" do
139
137
  instance = described_class.new(required_options.merge({
140
138
  sync_secret: "secret",
141
- sync_method: :webhook,
142
- local_adapter: memory_adapter,
143
139
  }))
144
140
 
145
141
  expect(instance.sync_method).to eq(:webhook)
146
142
  expect(instance.adapter).to be_instance_of(Flipper::Adapters::DualWrite)
147
143
  end
148
144
 
149
- it "raises ArgumentError for invalid sync_method" do
150
- expect {
151
- described_class.new(required_options.merge(sync_method: :foo))
152
- }.to raise_error(ArgumentError, "Unsupported sync_method. Valid options are (poll, webhook)")
153
- end
154
-
155
- it "can use ENV var for sync_method" do
156
- with_modified_env "FLIPPER_CLOUD_SYNC_METHOD" => "webhook" do
157
- instance = described_class.new(required_options.merge({
158
- sync_secret: "secret",
159
- }))
145
+ it "sets sync_method to :webhook if FLIPPER_CLOUD_SYNC_SECRET set" do
146
+ with_modified_env "FLIPPER_CLOUD_SYNC_SECRET" => "abc" do
147
+ instance = described_class.new(required_options)
160
148
 
161
149
  expect(instance.sync_method).to eq(:webhook)
150
+ expect(instance.adapter).to be_instance_of(Flipper::Adapters::DualWrite)
162
151
  end
163
152
  end
164
153
 
165
- it "can use string sync_method instead of symbol" do
166
- memory_adapter = Flipper::Adapters::Memory.new
167
- instance = described_class.new(required_options.merge({
168
- sync_secret: "secret",
169
- sync_method: "webhook",
170
- local_adapter: memory_adapter,
171
- }))
172
-
173
- expect(instance.sync_method).to eq(:webhook)
174
- expect(instance.adapter).to be_instance_of(Flipper::Adapters::DualWrite)
175
- end
176
-
177
154
  it "can set sync_secret" do
178
155
  instance = described_class.new(required_options.merge(sync_secret: "from_config"))
179
156
  expect(instance.sync_secret).to eq("from_config")
@@ -9,7 +9,6 @@ RSpec.describe Flipper::Cloud::DSL do
9
9
  cloud_configuration = Flipper::Cloud::Configuration.new({
10
10
  token: "asdf",
11
11
  sync_secret: "tasty",
12
- sync_method: :webhook,
13
12
  })
14
13
  dsl = described_class.new(cloud_configuration)
15
14
  expect(dsl.features).to eq(Set.new)
@@ -26,7 +25,6 @@ RSpec.describe Flipper::Cloud::DSL do
26
25
  cloud_configuration = Flipper::Cloud::Configuration.new({
27
26
  token: "asdf",
28
27
  sync_secret: "tasty",
29
- sync_method: :webhook,
30
28
  })
31
29
  dsl = described_class.new(cloud_configuration)
32
30
  dsl.sync
@@ -37,7 +35,6 @@ RSpec.describe Flipper::Cloud::DSL do
37
35
  cloud_configuration = Flipper::Cloud::Configuration.new({
38
36
  token: "asdf",
39
37
  sync_secret: "tasty",
40
- sync_method: :webhook,
41
38
  })
42
39
  dsl = described_class.new(cloud_configuration)
43
40
  expect(dsl.sync_secret).to eq("tasty")
@@ -52,7 +49,6 @@ RSpec.describe Flipper::Cloud::DSL do
52
49
  cloud_configuration = Flipper::Cloud::Configuration.new({
53
50
  token: "asdf",
54
51
  sync_secret: "tasty",
55
- sync_method: :webhook,
56
52
  local_adapter: local_adapter
57
53
  })
58
54
  end
@@ -9,7 +9,6 @@ RSpec.describe Flipper::Cloud::Middleware do
9
9
  Flipper::Cloud.new("regular") do |config|
10
10
  config.local_adapter = Flipper::Adapters::OperationLogger.new(Flipper::Adapters::Memory.new)
11
11
  config.sync_secret = "regular_tasty"
12
- config.sync_method = :webhook
13
12
  end
14
13
  }
15
14
 
@@ -17,7 +16,6 @@ RSpec.describe Flipper::Cloud::Middleware do
17
16
  Flipper::Cloud.new("env") do |config|
18
17
  config.local_adapter = Flipper::Adapters::OperationLogger.new(Flipper::Adapters::Memory.new)
19
18
  config.sync_secret = "env_tasty"
20
- config.sync_method = :webhook
21
19
  end
22
20
  }
23
21
 
@@ -53,7 +51,7 @@ RSpec.describe Flipper::Cloud::Middleware do
53
51
  env = {
54
52
  "HTTP_FLIPPER_CLOUD_SIGNATURE" => signature_header_value,
55
53
  }
56
- post '/webhooks', request_body, env
54
+ post '/', request_body, env
57
55
 
58
56
  expect(last_response.status).to eq(200)
59
57
  expect(JSON.parse(last_response.body)).to eq({
@@ -80,13 +78,82 @@ RSpec.describe Flipper::Cloud::Middleware do
80
78
  env = {
81
79
  "HTTP_FLIPPER_CLOUD_SIGNATURE" => signature_header_value,
82
80
  }
83
- post '/webhooks', request_body, env
81
+ post '/', request_body, env
84
82
 
85
83
  expect(last_response.status).to eq(400)
86
84
  expect(stub).not_to have_been_requested
87
85
  end
88
86
  end
89
87
 
88
+ context "when flipper cloud responds with 402" do
89
+ let(:app) { Flipper::Cloud.app(flipper) }
90
+
91
+ it "results in 402" do
92
+ Flipper.register(:admins) { |*args| false }
93
+ Flipper.register(:staff) { |*args| false }
94
+ Flipper.register(:basic) { |*args| false }
95
+ Flipper.register(:plus) { |*args| false }
96
+ Flipper.register(:premium) { |*args| false }
97
+
98
+ stub = stub_request_for_token('regular', status: 402)
99
+ env = {
100
+ "HTTP_FLIPPER_CLOUD_SIGNATURE" => signature_header_value,
101
+ }
102
+ post '/', request_body, env
103
+
104
+ expect(last_response.status).to eq(402)
105
+ expect(last_response.headers["Flipper-Cloud-Response-Error-Class"]).to eq("Flipper::Adapters::Http::Error")
106
+ expect(last_response.headers["Flipper-Cloud-Response-Error-Message"]).to include("Failed with status: 402")
107
+ expect(stub).to have_been_requested
108
+ end
109
+ end
110
+
111
+ context "when flipper cloud responds with non-402 and non-2xx code" do
112
+ let(:app) { Flipper::Cloud.app(flipper) }
113
+
114
+ it "results in 500" do
115
+ Flipper.register(:admins) { |*args| false }
116
+ Flipper.register(:staff) { |*args| false }
117
+ Flipper.register(:basic) { |*args| false }
118
+ Flipper.register(:plus) { |*args| false }
119
+ Flipper.register(:premium) { |*args| false }
120
+
121
+ stub = stub_request_for_token('regular', status: 503)
122
+ env = {
123
+ "HTTP_FLIPPER_CLOUD_SIGNATURE" => signature_header_value,
124
+ }
125
+ post '/', request_body, env
126
+
127
+ expect(last_response.status).to eq(500)
128
+ expect(last_response.headers["Flipper-Cloud-Response-Error-Class"]).to eq("Flipper::Adapters::Http::Error")
129
+ expect(last_response.headers["Flipper-Cloud-Response-Error-Message"]).to include("Failed with status: 503")
130
+ expect(stub).to have_been_requested
131
+ end
132
+ end
133
+
134
+ context "when flipper cloud responds with timeout" do
135
+ let(:app) { Flipper::Cloud.app(flipper) }
136
+
137
+ it "results in 500" do
138
+ Flipper.register(:admins) { |*args| false }
139
+ Flipper.register(:staff) { |*args| false }
140
+ Flipper.register(:basic) { |*args| false }
141
+ Flipper.register(:plus) { |*args| false }
142
+ Flipper.register(:premium) { |*args| false }
143
+
144
+ stub = stub_request_for_token('regular', status: :timeout)
145
+ env = {
146
+ "HTTP_FLIPPER_CLOUD_SIGNATURE" => signature_header_value,
147
+ }
148
+ post '/', request_body, env
149
+
150
+ expect(last_response.status).to eq(500)
151
+ expect(last_response.headers["Flipper-Cloud-Response-Error-Class"]).to eq("Net::OpenTimeout")
152
+ expect(last_response.headers["Flipper-Cloud-Response-Error-Message"]).to eq("execution expired")
153
+ expect(stub).to have_been_requested
154
+ end
155
+ end
156
+
90
157
  context 'when initialized with flipper instance and flipper instance in env' do
91
158
  let(:app) { Flipper::Cloud.app(flipper) }
92
159
  let(:signature) {
@@ -99,7 +166,7 @@ RSpec.describe Flipper::Cloud::Middleware do
99
166
  "HTTP_FLIPPER_CLOUD_SIGNATURE" => signature_header_value,
100
167
  'flipper' => env_flipper,
101
168
  }
102
- post '/webhooks', request_body, env
169
+ post '/', request_body, env
103
170
 
104
171
  expect(last_response.status).to eq(200)
105
172
  expect(stub).to have_been_requested
@@ -118,7 +185,7 @@ RSpec.describe Flipper::Cloud::Middleware do
118
185
  "HTTP_FLIPPER_CLOUD_SIGNATURE" => signature_header_value,
119
186
  'flipper' => env_flipper,
120
187
  }
121
- post '/webhooks', request_body, env
188
+ post '/', request_body, env
122
189
 
123
190
  expect(last_response.status).to eq(200)
124
191
  expect(stub).to have_been_requested
@@ -138,7 +205,7 @@ RSpec.describe Flipper::Cloud::Middleware do
138
205
  'flipper' => flipper,
139
206
  'flipper_cloud' => env_flipper,
140
207
  }
141
- post '/webhooks', request_body, env
208
+ post '/', request_body, env
142
209
 
143
210
  expect(last_response.status).to eq(200)
144
211
  expect(stub).to have_been_requested
@@ -149,6 +216,27 @@ RSpec.describe Flipper::Cloud::Middleware do
149
216
  let(:app) { Flipper::Cloud.app(-> { flipper }) }
150
217
 
151
218
  it 'works' do
219
+ stub = stub_request_for_token('regular')
220
+ env = {
221
+ "HTTP_FLIPPER_CLOUD_SIGNATURE" => signature_header_value,
222
+ }
223
+ post '/', request_body, env
224
+
225
+ expect(last_response.status).to eq(200)
226
+ expect(stub).to have_been_requested
227
+ end
228
+ end
229
+
230
+ context 'when using older /webhooks path' do
231
+ let(:app) { Flipper::Cloud.app(flipper) }
232
+
233
+ it 'uses instance to sync' do
234
+ Flipper.register(:admins) { |*args| false }
235
+ Flipper.register(:staff) { |*args| false }
236
+ Flipper.register(:basic) { |*args| false }
237
+ Flipper.register(:plus) { |*args| false }
238
+ Flipper.register(:premium) { |*args| false }
239
+
152
240
  stub = stub_request_for_token('regular')
153
241
  env = {
154
242
  "HTTP_FLIPPER_CLOUD_SIGNATURE" => signature_header_value,
@@ -156,13 +244,22 @@ RSpec.describe Flipper::Cloud::Middleware do
156
244
  post '/webhooks', request_body, env
157
245
 
158
246
  expect(last_response.status).to eq(200)
247
+ expect(JSON.parse(last_response.body)).to eq({
248
+ "groups" => [
249
+ {"name" => "admins"},
250
+ {"name" => "staff"},
251
+ {"name" => "basic"},
252
+ {"name" => "plus"},
253
+ {"name" => "premium"},
254
+ ],
255
+ })
159
256
  expect(stub).to have_been_requested
160
257
  end
161
258
  end
162
259
 
163
260
  describe 'Request method unsupported' do
164
261
  it 'skips middleware' do
165
- get '/webhooks'
262
+ get '/'
166
263
  expect(last_response.status).to eq(404)
167
264
  expect(last_response.content_type).to eq("application/json")
168
265
  expect(last_response.body).to eq("{}")
@@ -177,12 +274,17 @@ RSpec.describe Flipper::Cloud::Middleware do
177
274
 
178
275
  private
179
276
 
180
- def stub_request_for_token(token)
181
- stub_request(:get, "https://www.flippercloud.io/adapter/features").
277
+ def stub_request_for_token(token, status: 200)
278
+ stub = stub_request(:get, "https://www.flippercloud.io/adapter/features").
182
279
  with({
183
280
  headers: {
184
281
  'Flipper-Cloud-Token' => token,
185
282
  },
186
- }).to_return(status: 200, body: response_body, headers: {})
283
+ })
284
+ if status == :timeout
285
+ stub.to_timeout
286
+ else
287
+ stub.to_return(status: status, body: response_body, headers: {})
288
+ end
187
289
  end
188
290
  end
@@ -114,4 +114,89 @@ RSpec.describe Flipper::Cloud do
114
114
  described_class.new('asdf', open_timeout: 1)
115
115
  end
116
116
  end
117
+
118
+ it 'can import' do
119
+ stub_request(:post, /www\.flippercloud\.io\/adapter\/features.*/).
120
+ with(headers: {
121
+ 'Feature-Flipper-Token'=>'asdf',
122
+ 'Flipper-Cloud-Token'=>'asdf',
123
+ }).to_return(status: 200, body: "{}", headers: {})
124
+
125
+ flipper = Flipper.new(Flipper::Adapters::Memory.new)
126
+
127
+ flipper.enable(:test)
128
+ flipper.enable(:search)
129
+ flipper.enable_actor(:stats, Flipper::Actor.new("jnunemaker"))
130
+ flipper.enable_percentage_of_time(:logging, 5)
131
+
132
+ cloud_flipper = Flipper::Cloud.new("asdf")
133
+
134
+ get_all = {
135
+ "logging" => {actors: Set.new, boolean: nil, groups: Set.new, percentage_of_actors: nil, percentage_of_time: "5"},
136
+ "search" => {actors: Set.new, boolean: "true", groups: Set.new, percentage_of_actors: nil, percentage_of_time: nil},
137
+ "stats" => {actors: Set["jnunemaker"], boolean: nil, groups: Set.new, percentage_of_actors: nil, percentage_of_time: nil},
138
+ "test" => {actors: Set.new, boolean: "true", groups: Set.new, percentage_of_actors: nil, percentage_of_time: nil},
139
+ }
140
+
141
+ expect(flipper.adapter.get_all).to eq(get_all)
142
+ cloud_flipper.import(flipper)
143
+ expect(flipper.adapter.get_all).to eq(get_all)
144
+ expect(cloud_flipper.adapter.get_all).to eq(get_all)
145
+ end
146
+
147
+ it 'raises error for failure while importing' do
148
+ stub_request(:post, /www\.flippercloud\.io\/adapter\/features.*/).
149
+ with(headers: {
150
+ 'Feature-Flipper-Token'=>'asdf',
151
+ 'Flipper-Cloud-Token'=>'asdf',
152
+ }).to_return(status: 500, body: "{}")
153
+
154
+ flipper = Flipper.new(Flipper::Adapters::Memory.new)
155
+
156
+ flipper.enable(:test)
157
+ flipper.enable(:search)
158
+ flipper.enable_actor(:stats, Flipper::Actor.new("jnunemaker"))
159
+ flipper.enable_percentage_of_time(:logging, 5)
160
+
161
+ cloud_flipper = Flipper::Cloud.new("asdf")
162
+
163
+ get_all = {
164
+ "logging" => {actors: Set.new, boolean: nil, groups: Set.new, percentage_of_actors: nil, percentage_of_time: "5"},
165
+ "search" => {actors: Set.new, boolean: "true", groups: Set.new, percentage_of_actors: nil, percentage_of_time: nil},
166
+ "stats" => {actors: Set["jnunemaker"], boolean: nil, groups: Set.new, percentage_of_actors: nil, percentage_of_time: nil},
167
+ "test" => {actors: Set.new, boolean: "true", groups: Set.new, percentage_of_actors: nil, percentage_of_time: nil},
168
+ }
169
+
170
+ expect(flipper.adapter.get_all).to eq(get_all)
171
+ expect { cloud_flipper.import(flipper) }.to raise_error(Flipper::Adapters::Http::Error)
172
+ expect(flipper.adapter.get_all).to eq(get_all)
173
+ end
174
+
175
+ it 'raises error for timeout while importing' do
176
+ stub_request(:post, /www\.flippercloud\.io\/adapter\/features.*/).
177
+ with(headers: {
178
+ 'Feature-Flipper-Token'=>'asdf',
179
+ 'Flipper-Cloud-Token'=>'asdf',
180
+ }).to_timeout
181
+
182
+ flipper = Flipper.new(Flipper::Adapters::Memory.new)
183
+
184
+ flipper.enable(:test)
185
+ flipper.enable(:search)
186
+ flipper.enable_actor(:stats, Flipper::Actor.new("jnunemaker"))
187
+ flipper.enable_percentage_of_time(:logging, 5)
188
+
189
+ cloud_flipper = Flipper::Cloud.new("asdf")
190
+
191
+ get_all = {
192
+ "logging" => {actors: Set.new, boolean: nil, groups: Set.new, percentage_of_actors: nil, percentage_of_time: "5"},
193
+ "search" => {actors: Set.new, boolean: "true", groups: Set.new, percentage_of_actors: nil, percentage_of_time: nil},
194
+ "stats" => {actors: Set["jnunemaker"], boolean: nil, groups: Set.new, percentage_of_actors: nil, percentage_of_time: nil},
195
+ "test" => {actors: Set.new, boolean: "true", groups: Set.new, percentage_of_actors: nil, percentage_of_time: nil},
196
+ }
197
+
198
+ expect(flipper.adapter.get_all).to eq(get_all)
199
+ expect { cloud_flipper.import(flipper) }.to raise_error(Net::OpenTimeout)
200
+ expect(flipper.adapter.get_all).to eq(get_all)
201
+ end
117
202
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: flipper-cloud
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.20.0
4
+ version: 0.21.0.rc1
5
5
  platform: ruby
6
6
  authors:
7
7
  - John Nunemaker
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-12-20 00:00:00.000000000 Z
11
+ date: 2021-05-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: flipper
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: 0.20.0
19
+ version: 0.21.0.rc1
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: 0.20.0
26
+ version: 0.21.0.rc1
27
27
  description:
28
28
  email:
29
29
  - nunemaker@gmail.com
@@ -31,6 +31,7 @@ executables: []
31
31
  extensions: []
32
32
  extra_rdoc_files: []
33
33
  files:
34
+ - docs/images/flipper_cloud.png
34
35
  - examples/cloud/app.ru
35
36
  - examples/cloud/basic.rb
36
37
  - examples/cloud/cached_in_memory.rb
@@ -65,9 +66,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
65
66
  version: '0'
66
67
  required_rubygems_version: !ruby/object:Gem::Requirement
67
68
  requirements:
68
- - - ">="
69
+ - - ">"
69
70
  - !ruby/object:Gem::Version
70
- version: '0'
71
+ version: 1.3.1
71
72
  requirements: []
72
73
  rubygems_version: 3.0.3
73
74
  signing_key: