flipper-cloud 0.20.1 → 0.21.0.rc2

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: 35dc166068996854a78fc08810a942999539735d61f8f0215d6c10101a57d23d
4
- data.tar.gz: bdccf48ff9d058fc20ded13e1c64d7f64964c2daad12fe0ccc11b8aafcd2d0ab
3
+ metadata.gz: 6c1f88aaef562296d69c7d601cde538b3494e5356664123e302d96cd951d1cab
4
+ data.tar.gz: 6be448c6a0c689aa38f831baf8f48cfcf1381c72f6a24541bb196da80ec7f962
5
5
  SHA512:
6
- metadata.gz: a2f6190b1ff9a30ce518a0044323c55a9997db8cf3ec383c4d425ce5eb02121e99a355ca370928df1b9a23ea5add521133fb30a051b56a93b8ce088707d1f2a3
7
- data.tar.gz: 132a9a42b22803fa362b85acfd1c741519ce7eff91d5443bf47b8d0d869cd29d6a161ded0e15ef80fa7a642d46b948a454a0c633a72ff5cbbc22949dd2459622
6
+ metadata.gz: f1c48a398d01d7b3efcb50329778520b6a241f06e9aec3ac6bd478148aafaad7a6a6808bef14d9094b1170992372aa4f3c641a1acc5dc37d1f6fd0f6af701b9b
7
+ data.tar.gz: 7c4b07f27b3daa727ddafe1bc2fa51581fd15fda3f677f1e322ddfe42ad0eed19101e40436906de49d684e2ec238b88d02daa1cbdf508c245813ba93126af788
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,25 +1,19 @@
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'))
11
5
 
12
- flipper[:stats].enable
6
+ Flipper[:stats].enable
13
7
 
14
- if flipper[:stats].enabled?
8
+ if Flipper[:stats].enabled?
15
9
  puts 'Enabled!'
16
10
  else
17
11
  puts 'Disabled!'
18
12
  end
19
13
 
20
- flipper[:stats].disable
14
+ Flipper[:stats].disable
21
15
 
22
- if flipper[:stats].enabled?
16
+ if Flipper[:stats].enabled?
23
17
  puts 'Enabled!'
24
18
  else
25
19
  puts 'Disabled!'
@@ -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)
data/lib/flipper/cloud.rb CHANGED
@@ -4,6 +4,7 @@ require "flipper/middleware/memoizer"
4
4
  require "flipper/cloud/configuration"
5
5
  require "flipper/cloud/dsl"
6
6
  require "flipper/cloud/middleware"
7
+ require "flipper/cloud/engine" if defined?(Rails::Engine)
7
8
 
8
9
  module Flipper
9
10
  module Cloud
@@ -14,8 +15,14 @@ module Flipper
14
15
  # options - The Hash of options. See Flipper::Cloud::Configuration.
15
16
  # block - The block that configuration will be yielded to allowing you to
16
17
  # customize this cloud instance and its adapter.
17
- def self.new(token = nil, options = {})
18
- options = options.merge(token: token) if token
18
+ def self.new(options = {}, deprecated_options = {})
19
+ if options.is_a?(String)
20
+ warn "`Flipper::Cloud.new(token)` is deprecated. Use `Flipper::Cloud.new(token: token)` " +
21
+ "or set the `FLIPPER_CLOUD_TOKEN` environment variable.\n" +
22
+ caller[0]
23
+ options = deprecated_options.merge(token: options)
24
+ end
25
+
19
26
  configuration = Configuration.new(options)
20
27
  yield configuration if block_given?
21
28
  DSL.new(configuration)
@@ -36,5 +43,21 @@ module Flipper
36
43
  builder.define_singleton_method(:inspect) { klass.inspect } # pretty rake routes output
37
44
  builder
38
45
  end
46
+
47
+ # Private: Configure Flipper to use Cloud by default
48
+ def self.set_default
49
+ Flipper.configure do |config|
50
+ config.default do
51
+ if ENV["FLIPPER_CLOUD_TOKEN"]
52
+ self.new(local_adapter: config.adapter)
53
+ else
54
+ warn "Missing FLIPPER_CLOUD_TOKEN environment variable. Disabling Flipper::Cloud."
55
+ Flipper.new(config.adapter)
56
+ end
57
+ end
58
+ end
59
+ end
39
60
  end
40
61
  end
62
+
63
+ Flipper::Cloud.set_default
@@ -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
@@ -68,9 +67,14 @@ module Flipper
68
67
  @token = options.fetch(:token) { ENV["FLIPPER_CLOUD_TOKEN"] }
69
68
 
70
69
  if @token.nil?
71
- raise ArgumentError, "Flipper::Cloud token is missing. Please set FLIPPER_CLOUD_TOKEN or provide the token (e.g. Flipper::Cloud.new('token'))."
70
+ raise ArgumentError, "Flipper::Cloud token is missing. Please set FLIPPER_CLOUD_TOKEN or provide the token (e.g. Flipper::Cloud.new(token: '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
@@ -0,0 +1,29 @@
1
+ require "flipper/railtie"
2
+
3
+ module Flipper
4
+ module Cloud
5
+ class Engine < Rails::Engine
6
+ paths["config/routes.rb"] = ["lib/flipper/cloud/routes.rb"]
7
+
8
+ config.before_configuration do
9
+ config.flipper.cloud_path = "_flipper"
10
+ end
11
+
12
+ initializer "flipper.cloud.default", before: :load_config_initializers do |app|
13
+ Flipper.configure do |config|
14
+ config.default do
15
+ if ENV["FLIPPER_CLOUD_TOKEN"]
16
+ Flipper::Cloud.new(
17
+ local_adapter: config.adapter,
18
+ instrumenter: app.config.flipper.instrumenter
19
+ )
20
+ else
21
+ warn "Missing FLIPPER_CLOUD_TOKEN environment variable. Disabling Flipper::Cloud."
22
+ Flipper.new(config.adapter)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ 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",
@@ -0,0 +1,13 @@
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
@@ -1,3 +1,3 @@
1
1
  module Flipper
2
- VERSION = '0.20.1'.freeze
2
+ VERSION = '0.21.0.rc2'.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
@@ -0,0 +1,98 @@
1
+ require 'helper'
2
+ require 'rails'
3
+ require 'flipper/cloud'
4
+
5
+ RSpec.describe Flipper::Cloud::Engine do
6
+ let(:env) do
7
+ { "FLIPPER_CLOUD_TOKEN" => "test-token" }
8
+ end
9
+
10
+ let(:application) do
11
+ Class.new(Rails::Application) do
12
+ config.eager_load = false
13
+ config.logger = ActiveSupport::Logger.new($stdout)
14
+ end
15
+ end
16
+
17
+ # App for Rack::Test
18
+ let(:app) { application.routes }
19
+
20
+ before do
21
+ Rails.application = nil
22
+
23
+ # Force loading of flipper to configure itself
24
+ load 'flipper/cloud.rb'
25
+ end
26
+
27
+ it "initializes cloud configuration" do
28
+ stub_request(:get, /flippercloud\.io/).to_return(status: 200, body: "{}")
29
+
30
+ with_modified_env env do
31
+ application.initialize!
32
+
33
+ expect(Flipper.instance).to be_a(Flipper::Cloud::DSL)
34
+ expect(Flipper.instance.instrumenter).to be(ActiveSupport::Notifications)
35
+ end
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
+ with_modified_env env do
61
+ application.initialize!
62
+
63
+ stub = stub_request(:get, "https://www.flippercloud.io/adapter/features").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
+ end
74
+
75
+ context "without CLOUD_SYNC_SECRET" do
76
+ it "does not configure webhook app" do
77
+ with_modified_env env do
78
+ application.initialize!
79
+
80
+ post "/_flipper"
81
+ expect(last_response.status).to eq(404)
82
+ end
83
+ end
84
+ end
85
+
86
+ context "without FLIPPER_CLOUD_TOKEN" do
87
+ it "gracefully skips configuring webhook app" do
88
+ with_modified_env "FLIPPER_CLOUD_TOKEN" => nil do
89
+ application.initialize!
90
+ expect(silence { Flipper.instance }).to match(/Missing FLIPPER_CLOUD_TOKEN/)
91
+ expect(Flipper.instance).to be_a(Flipper::DSL)
92
+
93
+ post "/_flipper"
94
+ expect(last_response.status).to eq(404)
95
+ end
96
+ end
97
+ end
98
+ end
@@ -6,18 +6,16 @@ require 'flipper/adapters/operation_logger'
6
6
 
7
7
  RSpec.describe Flipper::Cloud::Middleware do
8
8
  let(:flipper) {
9
- Flipper::Cloud.new("regular") do |config|
9
+ Flipper::Cloud.new(token: "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
 
16
15
  let(:env_flipper) {
17
- Flipper::Cloud.new("env") do |config|
16
+ Flipper::Cloud.new(token: "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,7 +78,7 @@ 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
@@ -101,11 +99,11 @@ RSpec.describe Flipper::Cloud::Middleware do
101
99
  env = {
102
100
  "HTTP_FLIPPER_CLOUD_SIGNATURE" => signature_header_value,
103
101
  }
104
- post '/webhooks', request_body, env
102
+ post '/', request_body, env
105
103
 
106
104
  expect(last_response.status).to eq(402)
107
105
  expect(last_response.headers["Flipper-Cloud-Response-Error-Class"]).to eq("Flipper::Adapters::Http::Error")
108
- expect(last_response.headers["Flipper-Cloud-Response-Error-Message"]).to eq("Failed with status: 402")
106
+ expect(last_response.headers["Flipper-Cloud-Response-Error-Message"]).to include("Failed with status: 402")
109
107
  expect(stub).to have_been_requested
110
108
  end
111
109
  end
@@ -124,11 +122,11 @@ RSpec.describe Flipper::Cloud::Middleware do
124
122
  env = {
125
123
  "HTTP_FLIPPER_CLOUD_SIGNATURE" => signature_header_value,
126
124
  }
127
- post '/webhooks', request_body, env
125
+ post '/', request_body, env
128
126
 
129
127
  expect(last_response.status).to eq(500)
130
128
  expect(last_response.headers["Flipper-Cloud-Response-Error-Class"]).to eq("Flipper::Adapters::Http::Error")
131
- expect(last_response.headers["Flipper-Cloud-Response-Error-Message"]).to eq("Failed with status: 503")
129
+ expect(last_response.headers["Flipper-Cloud-Response-Error-Message"]).to include("Failed with status: 503")
132
130
  expect(stub).to have_been_requested
133
131
  end
134
132
  end
@@ -147,7 +145,7 @@ RSpec.describe Flipper::Cloud::Middleware do
147
145
  env = {
148
146
  "HTTP_FLIPPER_CLOUD_SIGNATURE" => signature_header_value,
149
147
  }
150
- post '/webhooks', request_body, env
148
+ post '/', request_body, env
151
149
 
152
150
  expect(last_response.status).to eq(500)
153
151
  expect(last_response.headers["Flipper-Cloud-Response-Error-Class"]).to eq("Net::OpenTimeout")
@@ -168,7 +166,7 @@ RSpec.describe Flipper::Cloud::Middleware do
168
166
  "HTTP_FLIPPER_CLOUD_SIGNATURE" => signature_header_value,
169
167
  'flipper' => env_flipper,
170
168
  }
171
- post '/webhooks', request_body, env
169
+ post '/', request_body, env
172
170
 
173
171
  expect(last_response.status).to eq(200)
174
172
  expect(stub).to have_been_requested
@@ -187,7 +185,7 @@ RSpec.describe Flipper::Cloud::Middleware do
187
185
  "HTTP_FLIPPER_CLOUD_SIGNATURE" => signature_header_value,
188
186
  'flipper' => env_flipper,
189
187
  }
190
- post '/webhooks', request_body, env
188
+ post '/', request_body, env
191
189
 
192
190
  expect(last_response.status).to eq(200)
193
191
  expect(stub).to have_been_requested
@@ -207,7 +205,7 @@ RSpec.describe Flipper::Cloud::Middleware do
207
205
  'flipper' => flipper,
208
206
  'flipper_cloud' => env_flipper,
209
207
  }
210
- post '/webhooks', request_body, env
208
+ post '/', request_body, env
211
209
 
212
210
  expect(last_response.status).to eq(200)
213
211
  expect(stub).to have_been_requested
@@ -218,6 +216,27 @@ RSpec.describe Flipper::Cloud::Middleware do
218
216
  let(:app) { Flipper::Cloud.app(-> { flipper }) }
219
217
 
220
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
+
221
240
  stub = stub_request_for_token('regular')
222
241
  env = {
223
242
  "HTTP_FLIPPER_CLOUD_SIGNATURE" => signature_header_value,
@@ -225,13 +244,22 @@ RSpec.describe Flipper::Cloud::Middleware do
225
244
  post '/webhooks', request_body, env
226
245
 
227
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
+ })
228
256
  expect(stub).to have_been_requested
229
257
  end
230
258
  end
231
259
 
232
260
  describe 'Request method unsupported' do
233
261
  it 'skips middleware' do
234
- get '/webhooks'
262
+ get '/'
235
263
  expect(last_response.status).to eq(404)
236
264
  expect(last_response.content_type).to eq("application/json")
237
265
  expect(last_response.body).to eq("{}")
@@ -12,7 +12,7 @@ RSpec.describe Flipper::Cloud do
12
12
  let(:token) { 'asdf' }
13
13
 
14
14
  before do
15
- @instance = described_class.new(token)
15
+ @instance = described_class.new(token: token)
16
16
  memoized_adapter = @instance.adapter
17
17
  sync_adapter = memoized_adapter.adapter
18
18
  @http_adapter = sync_adapter.instance_variable_get('@remote')
@@ -52,7 +52,7 @@ RSpec.describe Flipper::Cloud do
52
52
  before do
53
53
  stub_request(:get, /fakeflipper\.com/).to_return(status: 200, body: "{}")
54
54
 
55
- @instance = described_class.new('asdf', url: 'https://www.fakeflipper.com/sadpanda')
55
+ @instance = described_class.new(token: 'asdf', url: 'https://www.fakeflipper.com/sadpanda')
56
56
  memoized_adapter = @instance.adapter
57
57
  sync_adapter = memoized_adapter.adapter
58
58
  @http_adapter = sync_adapter.instance_variable_get('@remote')
@@ -75,12 +75,12 @@ RSpec.describe Flipper::Cloud do
75
75
 
76
76
  it 'can set instrumenter' do
77
77
  instrumenter = Flipper::Instrumenters::Memory.new
78
- instance = described_class.new('asdf', instrumenter: instrumenter)
78
+ instance = described_class.new(token: 'asdf', instrumenter: instrumenter)
79
79
  expect(instance.instrumenter).to be(instrumenter)
80
80
  end
81
81
 
82
82
  it 'allows wrapping adapter with another adapter like the instrumenter' do
83
- instance = described_class.new('asdf') do |config|
83
+ instance = described_class.new(token: 'asdf') do |config|
84
84
  config.adapter do |adapter|
85
85
  Flipper::Adapters::Instrumented.new(adapter)
86
86
  end
@@ -92,26 +92,111 @@ RSpec.describe Flipper::Cloud do
92
92
  it 'can set debug_output' do
93
93
  expect(Flipper::Adapters::Http::Client).to receive(:new)
94
94
  .with(hash_including(debug_output: STDOUT))
95
- described_class.new('asdf', debug_output: STDOUT)
95
+ described_class.new(token: 'asdf', debug_output: STDOUT)
96
96
  end
97
97
 
98
98
  it 'can set read_timeout' do
99
99
  expect(Flipper::Adapters::Http::Client).to receive(:new)
100
100
  .with(hash_including(read_timeout: 1))
101
- described_class.new('asdf', read_timeout: 1)
101
+ described_class.new(token: 'asdf', read_timeout: 1)
102
102
  end
103
103
 
104
104
  it 'can set open_timeout' do
105
105
  expect(Flipper::Adapters::Http::Client).to receive(:new)
106
106
  .with(hash_including(open_timeout: 1))
107
- described_class.new('asdf', open_timeout: 1)
107
+ described_class.new(token: 'asdf', open_timeout: 1)
108
108
  end
109
109
 
110
110
  if RUBY_VERSION >= '2.6.0'
111
111
  it 'can set write_timeout' do
112
112
  expect(Flipper::Adapters::Http::Client).to receive(:new)
113
113
  .with(hash_including(open_timeout: 1))
114
- described_class.new('asdf', open_timeout: 1)
114
+ described_class.new(token: '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(token: "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(token: "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(token: "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.1
4
+ version: 0.21.0.rc2
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-31 00:00:00.000000000 Z
11
+ date: 2021-05-08 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.1
19
+ version: 0.21.0.rc2
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.1
26
+ version: 0.21.0.rc2
27
27
  description:
28
28
  email:
29
29
  - nunemaker@gmail.com
@@ -31,21 +31,23 @@ 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
- - examples/cloud/cached_in_memory.rb
37
37
  - examples/cloud/import.rb
38
- - examples/cloud/local_adapter.rb
39
38
  - flipper-cloud.gemspec
40
39
  - lib/flipper-cloud.rb
41
40
  - lib/flipper/cloud.rb
42
41
  - lib/flipper/cloud/configuration.rb
43
42
  - lib/flipper/cloud/dsl.rb
43
+ - lib/flipper/cloud/engine.rb
44
44
  - lib/flipper/cloud/message_verifier.rb
45
45
  - lib/flipper/cloud/middleware.rb
46
+ - lib/flipper/cloud/routes.rb
46
47
  - lib/flipper/version.rb
47
48
  - spec/flipper/cloud/configuration_spec.rb
48
49
  - spec/flipper/cloud/dsl_spec.rb
50
+ - spec/flipper/cloud/engine_spec.rb
49
51
  - spec/flipper/cloud/message_verifier_spec.rb
50
52
  - spec/flipper/cloud/middleware_spec.rb
51
53
  - spec/flipper/cloud_spec.rb
@@ -65,9 +67,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
65
67
  version: '0'
66
68
  required_rubygems_version: !ruby/object:Gem::Requirement
67
69
  requirements:
68
- - - ">="
70
+ - - ">"
69
71
  - !ruby/object:Gem::Version
70
- version: '0'
72
+ version: 1.3.1
71
73
  requirements: []
72
74
  rubygems_version: 3.0.3
73
75
  signing_key:
@@ -76,6 +78,7 @@ summary: FeatureFlipper.com adapter for Flipper
76
78
  test_files:
77
79
  - spec/flipper/cloud/configuration_spec.rb
78
80
  - spec/flipper/cloud/dsl_spec.rb
81
+ - spec/flipper/cloud/engine_spec.rb
79
82
  - spec/flipper/cloud/message_verifier_spec.rb
80
83
  - spec/flipper/cloud/middleware_spec.rb
81
84
  - spec/flipper/cloud_spec.rb
@@ -1,29 +0,0 @@
1
- require File.expand_path('../../example_setup', __FILE__)
2
-
3
- require 'flipper/cloud'
4
- require 'flipper/adapters/active_support_cache_store'
5
- require 'active_support/cache'
6
- require 'active_support/cache/memory_store'
7
-
8
- token = ENV.fetch("TOKEN") { abort "TOKEN environment variable not set." }
9
- feature_name = ENV.fetch("FEATURE") { "testing" }.to_sym
10
-
11
- Flipper.configure do |config|
12
- config.default do
13
- Flipper::Cloud.new(token) do |cloud|
14
- cloud.debug_output = STDOUT
15
- cloud.adapter do |adapter|
16
- Flipper::Adapters::ActiveSupportCacheStore.new(adapter,
17
- ActiveSupport::Cache::MemoryStore.new, {expires_in: 5.seconds})
18
- end
19
- end
20
- end
21
- end
22
-
23
- loop do
24
- # Should only print out http call every 5 seconds
25
- p Flipper.enabled?(feature_name)
26
- puts "\n\n"
27
-
28
- sleep 1
29
- end
@@ -1,36 +0,0 @@
1
- # This is an example of using cloud with a local adapter. All cloud feature
2
- # changes are synced to the local adapter on an interval. All feature reads are
3
- # directed to the local adapter, which means reads are fast and not dependent on
4
- # cloud being available. You can turn internet on/off and more and this should
5
- # never raise. You could get a slow request every now and then if cloud is
6
- # unavailable, but we are hoping to fix that soon by doing the cloud update in a
7
- # background thread.
8
- require File.expand_path('../../example_setup', __FILE__)
9
-
10
- require 'logger'
11
- require 'flipper/cloud'
12
- require 'flipper/adapters/redis'
13
-
14
- token = ENV.fetch("TOKEN") { abort "TOKEN environment variable not set." }
15
- feature_name = ENV.fetch("FEATURE") { "testing" }.to_sym
16
-
17
- redis = Redis.new(logger: Logger.new(STDOUT))
18
- redis.flushdb
19
-
20
- Flipper.configure do |config|
21
- config.default do
22
- Flipper::Cloud.new(token) do |cloud|
23
- cloud.debug_output = STDOUT
24
- cloud.local_adapter = Flipper::Adapters::Redis.new(redis)
25
- cloud.sync_interval = 10
26
- end
27
- end
28
- end
29
-
30
- loop do
31
- # Should only print out http call every 10 seconds
32
- p Flipper.enabled?(feature_name)
33
- puts "\n\n"
34
-
35
- sleep 1
36
- end