flipper 1.1.2 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +9 -2
  3. data/.github/workflows/examples.yml +8 -2
  4. data/Changelog.md +1 -647
  5. data/Gemfile +3 -2
  6. data/README.md +3 -1
  7. data/Rakefile +2 -2
  8. data/docs/images/banner.jpg +0 -0
  9. data/exe/flipper +5 -0
  10. data/flipper.gemspec +5 -1
  11. data/lib/flipper/adapters/actor_limit.rb +28 -0
  12. data/lib/flipper/adapters/cache_base.rb +143 -0
  13. data/lib/flipper/adapters/http/client.rb +25 -16
  14. data/lib/flipper/adapters/operation_logger.rb +18 -88
  15. data/lib/flipper/adapters/read_only.rb +6 -39
  16. data/lib/flipper/adapters/strict.rb +16 -18
  17. data/lib/flipper/adapters/wrapper.rb +54 -0
  18. data/lib/flipper/cli.rb +263 -0
  19. data/lib/flipper/cloud/configuration.rb +9 -4
  20. data/lib/flipper/cloud/middleware.rb +5 -5
  21. data/lib/flipper/cloud/telemetry/instrumenter.rb +4 -8
  22. data/lib/flipper/cloud/telemetry/submitter.rb +2 -2
  23. data/lib/flipper/cloud/telemetry.rb +10 -2
  24. data/lib/flipper/cloud.rb +1 -1
  25. data/lib/flipper/engine.rb +32 -17
  26. data/lib/flipper/instrumentation/log_subscriber.rb +12 -3
  27. data/lib/flipper/metadata.rb +3 -1
  28. data/lib/flipper/poller.rb +6 -5
  29. data/lib/flipper/serializers/gzip.rb +3 -5
  30. data/lib/flipper/serializers/json.rb +3 -5
  31. data/lib/flipper/spec/shared_adapter_specs.rb +17 -16
  32. data/lib/flipper/test/shared_adapter_test.rb +17 -17
  33. data/lib/flipper/test_help.rb +43 -0
  34. data/lib/flipper/typecast.rb +3 -3
  35. data/lib/flipper/version.rb +11 -1
  36. data/lib/flipper.rb +3 -1
  37. data/lib/generators/flipper/setup_generator.rb +63 -0
  38. data/package-lock.json +41 -0
  39. data/package.json +10 -0
  40. data/spec/fixtures/environment.rb +1 -0
  41. data/spec/flipper/adapter_builder_spec.rb +1 -2
  42. data/spec/flipper/adapters/actor_limit_spec.rb +20 -0
  43. data/spec/flipper/adapters/http/client_spec.rb +61 -0
  44. data/spec/flipper/adapters/http_spec.rb +102 -76
  45. data/spec/flipper/adapters/strict_spec.rb +11 -9
  46. data/spec/flipper/cli_spec.rb +164 -0
  47. data/spec/flipper/cloud/configuration_spec.rb +35 -36
  48. data/spec/flipper/cloud/dsl_spec.rb +5 -5
  49. data/spec/flipper/cloud/middleware_spec.rb +8 -8
  50. data/spec/flipper/cloud/telemetry/backoff_policy_spec.rb +8 -9
  51. data/spec/flipper/cloud/telemetry/submitter_spec.rb +24 -24
  52. data/spec/flipper/cloud/telemetry_spec.rb +53 -1
  53. data/spec/flipper/cloud_spec.rb +10 -9
  54. data/spec/flipper/engine_spec.rb +140 -58
  55. data/spec/flipper/instrumentation/log_subscriber_spec.rb +9 -2
  56. data/spec/flipper/middleware/memoizer_spec.rb +7 -4
  57. data/spec/flipper_spec.rb +1 -1
  58. data/spec/spec_helper.rb +1 -0
  59. data/spec/support/fail_on_output.rb +8 -0
  60. data/spec/support/spec_helpers.rb +12 -5
  61. data/test/adapters/actor_limit_test.rb +20 -0
  62. data/test_rails/generators/flipper/setup_generator_test.rb +64 -0
  63. data/test_rails/system/test_help_test.rb +51 -0
  64. metadata +31 -9
  65. data/spec/support/climate_control.rb +0 -7
@@ -104,19 +104,19 @@ module Flipper
104
104
  actor22 = Flipper::Actor.new('22')
105
105
  actor_asdf = Flipper::Actor.new('asdf')
106
106
 
107
- assert_equal true, @adapter.enable(@feature, @actor_gate, Flipper::Types::Actor.new(actor22))
108
- assert_equal true, @adapter.enable(@feature, @actor_gate, Flipper::Types::Actor.new(actor_asdf))
107
+ assert_equal true, @feature.enable(actor22)
108
+ assert_equal true, @feature.enable(actor_asdf)
109
109
 
110
- result = @adapter.get(@feature)
111
- assert_equal Set['22', 'asdf'], result[:actors]
110
+ assert @feature.enabled?(actor22)
111
+ assert @feature.enabled?(actor_asdf)
112
112
 
113
- assert true, @adapter.disable(@feature, @actor_gate, Flipper::Types::Actor.new(actor22))
114
- result = @adapter.get(@feature)
115
- assert_equal Set['asdf'], result[:actors]
113
+ assert_equal true, @feature.disable(actor22)
114
+ refute @feature.enabled?(actor22)
115
+ assert @feature.enabled?(actor_asdf)
116
116
 
117
- assert_equal true, @adapter.disable(@feature, @actor_gate, Flipper::Types::Actor.new(actor_asdf))
118
- result = @adapter.get(@feature)
119
- assert_equal Set.new, result[:actors]
117
+ assert_equal true, @feature.disable(actor_asdf)
118
+ refute @feature.enabled?(actor22)
119
+ refute @feature.enabled?(actor_asdf)
120
120
  end
121
121
 
122
122
  def test_can_enable_disable_get_value_for_percentage_of_actors_gate
@@ -178,10 +178,10 @@ module Flipper
178
178
  end
179
179
 
180
180
  def test_converts_the_actor_value_to_a_string
181
- assert_equal true,
182
- @adapter.enable(@feature, @actor_gate, Flipper::Types::Actor.new(Flipper::Actor.new(22)))
183
- result = @adapter.get(@feature)
184
- assert_equal Set['22'], result[:actors]
181
+ actor = Flipper::Actor.new(22)
182
+ refute @feature.enabled?(actor)
183
+ @feature.enable_actor actor
184
+ assert @feature.enabled?(actor)
185
185
  end
186
186
 
187
187
  def test_converts_group_value_to_a_string
@@ -292,9 +292,9 @@ module Flipper
292
292
 
293
293
  def test_can_double_enable_an_actor_without_error
294
294
  actor = Flipper::Actor.new('Flipper::Actor;22')
295
- assert_equal true, @adapter.enable(@feature, @actor_gate, Flipper::Types::Actor.new(actor))
296
- assert_equal true, @adapter.enable(@feature, @actor_gate, Flipper::Types::Actor.new(actor))
297
- assert_equal Set['Flipper::Actor;22'], @adapter.get(@feature).fetch(:actors)
295
+ assert_equal true, @feature.enable(actor)
296
+ assert_equal true, @feature.enable(actor)
297
+ assert @feature.enabled?(actor)
298
298
  end
299
299
 
300
300
  def test_can_double_enable_a_group_without_error
@@ -0,0 +1,43 @@
1
+ module Flipper
2
+ module TestHelp
3
+ extend self
4
+
5
+ def flipper_configure
6
+ # Use a shared Memory adapter for all tests. This is instantiated outside of the
7
+ # `configure` block so the same instance is returned in new threads.
8
+ adapter = Flipper::Adapters::Memory.new
9
+
10
+ Flipper.configure do |config|
11
+ config.adapter { adapter }
12
+ config.default { Flipper.new(config.adapter) }
13
+ end
14
+ end
15
+
16
+ def flipper_reset
17
+ # Remove all features
18
+ Flipper.features.each(&:remove) rescue nil
19
+
20
+ # Reset previous DSL instance
21
+ Flipper.instance = nil
22
+ end
23
+ end
24
+ end
25
+
26
+ if defined?(RSpec) && RSpec.respond_to?(:configure)
27
+ RSpec.configure do |config|
28
+ config.include Flipper::TestHelp
29
+ config.before(:suite) { Flipper::TestHelp.flipper_configure }
30
+ config.before(:each) { flipper_reset }
31
+ end
32
+ end
33
+ if defined?(ActiveSupport)
34
+ ActiveSupport.on_load(:active_support_test_case) do
35
+ Flipper::TestHelp.flipper_configure
36
+
37
+ ActiveSupport::TestCase.class_eval do
38
+ include Flipper::TestHelp
39
+
40
+ setup :flipper_reset
41
+ end
42
+ end
43
+ end
@@ -3,8 +3,8 @@ require "flipper/serializers/json"
3
3
  require "flipper/serializers/gzip"
4
4
 
5
5
  module Flipper
6
- module Typecast
7
- TruthMap = {
6
+ class Typecast
7
+ TRUTH_MAP = {
8
8
  true => true,
9
9
  1 => true,
10
10
  'true' => true,
@@ -15,7 +15,7 @@ module Flipper
15
15
  #
16
16
  # Returns true or false.
17
17
  def self.to_boolean(value)
18
- !!TruthMap[value]
18
+ !!TRUTH_MAP[value]
19
19
  end
20
20
 
21
21
  # Internal: Convert value to an integer.
@@ -1,3 +1,13 @@
1
1
  module Flipper
2
- VERSION = '1.1.2'.freeze
2
+ VERSION = '1.3.0'.freeze
3
+
4
+ REQUIRED_RUBY_VERSION = '2.6'.freeze
5
+ NEXT_REQUIRED_RUBY_VERSION = '3.0'.freeze
6
+
7
+ REQUIRED_RAILS_VERSION = '5.2'.freeze
8
+ NEXT_REQUIRED_RAILS_VERSION = '6.1.0'.freeze
9
+
10
+ def self.deprecated_ruby_version?
11
+ Gem::Version.new(RUBY_VERSION) < Gem::Version.new(NEXT_REQUIRED_RUBY_VERSION)
12
+ end
3
13
  end
data/lib/flipper.rb CHANGED
@@ -174,10 +174,12 @@ end
174
174
 
175
175
  require 'flipper/actor'
176
176
  require 'flipper/adapter'
177
+ require 'flipper/adapters/wrapper'
178
+ require 'flipper/adapters/actor_limit'
179
+ require 'flipper/adapters/instrumented'
177
180
  require 'flipper/adapters/memoizable'
178
181
  require 'flipper/adapters/memory'
179
182
  require 'flipper/adapters/strict'
180
- require 'flipper/adapters/instrumented'
181
183
  require 'flipper/adapter_builder'
182
184
  require 'flipper/configuration'
183
185
  require 'flipper/dsl'
@@ -0,0 +1,63 @@
1
+ require 'rails/generators/active_record'
2
+
3
+ module Flipper
4
+ module Generators
5
+ class SetupGenerator < ::Rails::Generators::Base
6
+ desc 'Peform any necessary steps to install Flipper'
7
+
8
+ class_option :token, type: :string, default: nil, aliases: '-t',
9
+ desc: "Your personal environment token for Flipper Cloud"
10
+
11
+ def generate_active_record
12
+ invoke 'flipper:active_record' if defined?(Flipper::Adapters::ActiveRecord)
13
+ end
14
+
15
+ def configure_cloud_token
16
+ return unless options[:token]
17
+
18
+ configure_with_dotenv || configure_with_credentials
19
+ end
20
+
21
+ private
22
+
23
+ def configure_with_dotenv
24
+ ['.env.development', '.env.local', '.env'].detect do |file|
25
+ next unless exists?(file)
26
+ append_to_file file, "\nFLIPPER_CLOUD_TOKEN=#{options[:token]}\n"
27
+ end
28
+ end
29
+
30
+ def configure_with_credentials
31
+ return unless exists?("config/credentials.yml.enc") && (ENV["RAILS_MASTER_KEY"] || exists?("config/master.key"))
32
+
33
+ content = "flipper:\n cloud_token: #{options[:token]}\n"
34
+ action InjectIntoEncryptedFile.new(self, Rails.application.credentials, content, after: /\z/)
35
+ end
36
+
37
+ # Check if a file exists in the destination root
38
+ def exists?(path)
39
+ File.exist?(File.expand_path(path, destination_root))
40
+ end
41
+
42
+ # Action to inject content into ActiveSupport::EncryptedFile
43
+ class InjectIntoEncryptedFile < Thor::Actions::InjectIntoFile
44
+ def initialize(base, encrypted_file, data, config)
45
+ @encrypted_file = encrypted_file
46
+ super(base, encrypted_file.content_path, data, config)
47
+ end
48
+
49
+ def content
50
+ @content ||= @encrypted_file.read
51
+ end
52
+
53
+ def replace!(regexp, string, force)
54
+ if force || !replacement_present?
55
+ success = content.gsub!(regexp, string)
56
+ @encrypted_file.write content unless pretend?
57
+ success
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
data/package-lock.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "flipper",
3
+ "lockfileVersion": 3,
4
+ "requires": true,
5
+ "packages": {
6
+ "": {
7
+ "hasInstallScript": true,
8
+ "dependencies": {
9
+ "@popperjs/core": "^2.11.8",
10
+ "bootstrap": "^5.3.3"
11
+ }
12
+ },
13
+ "node_modules/@popperjs/core": {
14
+ "version": "2.11.8",
15
+ "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
16
+ "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
17
+ "funding": {
18
+ "type": "opencollective",
19
+ "url": "https://opencollective.com/popperjs"
20
+ }
21
+ },
22
+ "node_modules/bootstrap": {
23
+ "version": "5.3.3",
24
+ "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.3.tgz",
25
+ "integrity": "sha512-8HLCdWgyoMguSO9o+aH+iuZ+aht+mzW0u3HIMzVu7Srrpv7EBBxTnrFlSCskwdY1+EOFQSm7uMJhNQHkdPcmjg==",
26
+ "funding": [
27
+ {
28
+ "type": "github",
29
+ "url": "https://github.com/sponsors/twbs"
30
+ },
31
+ {
32
+ "type": "opencollective",
33
+ "url": "https://opencollective.com/bootstrap"
34
+ }
35
+ ],
36
+ "peerDependencies": {
37
+ "@popperjs/core": "^2.11.8"
38
+ }
39
+ }
40
+ }
41
+ }
data/package.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "private": true,
3
+ "dependencies": {
4
+ "@popperjs/core": "^2.11.8",
5
+ "bootstrap": "^5.3.3"
6
+ },
7
+ "scripts": {
8
+ "postinstall": "script/vendor-assets"
9
+ }
10
+ }
@@ -0,0 +1 @@
1
+ # Placeholder for config/environment.rb
@@ -38,10 +38,9 @@ RSpec.describe Flipper::AdapterBuilder do
38
38
  strict_adapter = memoizable_adapter.adapter
39
39
  memory_adapter = strict_adapter.adapter
40
40
 
41
-
42
41
  expect(memoizable_adapter).to be_instance_of(Flipper::Adapters::Memoizable)
43
42
  expect(strict_adapter).to be_instance_of(Flipper::Adapters::Strict)
44
- expect(strict_adapter.handler).to be(Flipper::Adapters::Strict::HANDLERS.fetch(:warn))
43
+ expect(strict_adapter.handler).to be(:warn)
45
44
  expect(memory_adapter).to be_instance_of(Flipper::Adapters::Memory)
46
45
  end
47
46
 
@@ -0,0 +1,20 @@
1
+ require "flipper/adapters/actor_limit"
2
+
3
+ RSpec.describe Flipper::Adapters::ActorLimit do
4
+ it_should_behave_like 'a flipper adapter' do
5
+ let(:limit) { 5 }
6
+ let(:adapter) { Flipper::Adapters::ActorLimit.new(Flipper::Adapters::Memory.new, limit) }
7
+
8
+ subject { adapter }
9
+
10
+ describe '#enable' do
11
+ it "fails when limit exceeded" do
12
+ 5.times { |i| feature.enable Flipper::Actor.new("User;#{i}") }
13
+
14
+ expect {
15
+ feature.enable Flipper::Actor.new("User;6")
16
+ }.to raise_error(Flipper::Adapters::ActorLimit::LimitExceeded)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,61 @@
1
+ require "flipper/adapters/http/client"
2
+
3
+ RSpec.describe Flipper::Adapters::Http::Client do
4
+ describe "#initialize" do
5
+ it "requires url" do
6
+ expect { described_class.new }.to raise_error(KeyError, "key not found: :url")
7
+ end
8
+
9
+ it "sets default headers" do
10
+ client = described_class.new(url: "http://example.com")
11
+ expect(client.headers).to eq({
12
+ 'content-type' => 'application/json',
13
+ 'accept' => 'application/json',
14
+ 'user-agent' => "Flipper HTTP Adapter v#{Flipper::VERSION}",
15
+ })
16
+ end
17
+
18
+ it "adds custom headers" do
19
+ client = described_class.new(url: "http://example.com", headers: {'custom-header' => 'value'})
20
+ expect(client.headers).to include('custom-header' => 'value')
21
+ end
22
+
23
+ it "overrides default headers with custom headers" do
24
+ client = described_class.new(url: "http://example.com", headers: {'content-type' => 'text/plain'})
25
+ expect(client.headers['content-type']).to eq('text/plain')
26
+ end
27
+ end
28
+
29
+ describe "#add_header" do
30
+ it "can add string header" do
31
+ client = described_class.new(url: "http://example.com")
32
+ client.add_header("key", "value")
33
+ expect(client.headers.fetch("key")).to eq("value")
34
+ end
35
+
36
+ it "standardizes key to lowercase" do
37
+ client = described_class.new(url: "http://example.com")
38
+ client.add_header("Content-Type", "value")
39
+ expect(client.headers.fetch("content-type")).to eq("value")
40
+ end
41
+
42
+ it "standardizes key to dashes" do
43
+ client = described_class.new(url: "http://example.com")
44
+ client.add_header(:content_type, "value")
45
+ expect(client.headers.fetch("content-type")).to eq("value")
46
+ end
47
+
48
+ it "can add symbol header" do
49
+ client = described_class.new(url: "http://example.com")
50
+ client.add_header(:key, "value")
51
+ expect(client.headers.fetch("key")).to eq("value")
52
+ end
53
+
54
+ it "overrides existing header" do
55
+ client = described_class.new(url: "http://example.com")
56
+ client.add_header("key", "value 1")
57
+ client.add_header("key", "value 2")
58
+ expect(client.headers.fetch("key")).to eq("value 2")
59
+ end
60
+ end
61
+ end
@@ -1,86 +1,104 @@
1
1
  require 'flipper/adapters/http'
2
2
  require 'flipper/adapters/pstore'
3
- require 'rack/handler/webrick'
4
3
 
5
- FLIPPER_SPEC_API_PORT = ENV.fetch('FLIPPER_SPEC_API_PORT', 9001).to_i
6
-
7
- RSpec.describe Flipper::Adapters::Http do
8
- context 'adapter' do
9
- subject do
10
- described_class.new(url: "http://localhost:#{FLIPPER_SPEC_API_PORT}")
11
- end
12
-
13
- before :all do
14
- dir = FlipperRoot.join('tmp').tap(&:mkpath)
15
- log_path = dir.join('flipper_adapters_http_spec.log')
16
- @pstore_file = dir.join('flipper.pstore')
17
- @pstore_file.unlink if @pstore_file.exist?
18
-
19
- api_adapter = Flipper::Adapters::PStore.new(@pstore_file)
20
- flipper_api = Flipper.new(api_adapter)
21
- app = Flipper::Api.app(flipper_api)
22
- server_options = {
23
- Port: FLIPPER_SPEC_API_PORT,
24
- StartCallback: -> { @started = true },
25
- Logger: WEBrick::Log.new(log_path.to_s, WEBrick::Log::INFO),
26
- AccessLog: [
27
- [log_path.open('w'), WEBrick::AccessLog::COMBINED_LOG_FORMAT],
28
- ],
29
- }
30
- @server = WEBrick::HTTPServer.new(server_options)
31
- @server.mount '/', Rack::Handler::WEBrick, app
32
-
33
- Thread.new { @server.start }
34
- Timeout.timeout(1) { :wait until @started }
35
- end
36
-
37
- after :all do
38
- @server.shutdown if @server
39
- end
40
-
41
- before(:each) do
42
- @pstore_file.unlink if @pstore_file.exist?
43
- end
44
-
45
- it_should_behave_like 'a flipper adapter'
4
+ rack_handler = begin
5
+ # Rack 3+
6
+ require 'rackup/handler/webrick'
7
+ Rackup::Handler::WEBrick
8
+ rescue LoadError
9
+ require 'rack/handler/webrick'
10
+ Rack::Handler::WEBrick
11
+ end
46
12
 
47
- it "can enable and disable unregistered group" do
48
- flipper = Flipper.new(subject)
49
- expect(flipper[:search].enable_group(:some_made_up_group)).to be(true)
50
- expect(flipper[:search].groups_value).to eq(Set["some_made_up_group"])
51
13
 
52
- expect(flipper[:search].disable_group(:some_made_up_group)).to be(true)
53
- expect(flipper[:search].groups_value).to eq(Set.new)
54
- end
14
+ FLIPPER_SPEC_API_PORT = ENV.fetch('FLIPPER_SPEC_API_PORT', 9001).to_i
55
15
 
56
- it "can import" do
57
- adapter = Flipper::Adapters::Memory.new
58
- source_flipper = Flipper.new(adapter)
59
- source_flipper.enable_percentage_of_actors :search, 10
60
- source_flipper.enable_percentage_of_time :search, 15
61
- source_flipper.enable_actor :search, Flipper::Actor.new('User;1')
62
- source_flipper.enable_actor :search, Flipper::Actor.new('User;100')
63
- source_flipper.enable_group :search, :admins
64
- source_flipper.enable_group :search, :employees
65
- source_flipper.enable :plausible
66
- source_flipper.disable :google_analytics
67
-
68
- flipper = Flipper.new(subject)
69
- flipper.import(source_flipper)
70
- expect(flipper[:search].percentage_of_actors_value).to be(10)
71
- expect(flipper[:search].percentage_of_time_value).to be(15)
72
- expect(flipper[:search].actors_value).to eq(Set["User;1", "User;100"])
73
- expect(flipper[:search].groups_value).to eq(Set["admins", "employees"])
74
- expect(flipper[:plausible].boolean_value).to be(true)
75
- expect(flipper[:google_analytics].boolean_value).to be(false)
16
+ RSpec.describe Flipper::Adapters::Http do
17
+ default_options = {
18
+ url: "http://localhost:#{FLIPPER_SPEC_API_PORT}",
19
+ }
20
+
21
+ {
22
+ basic: default_options.dup,
23
+ gzip: default_options.dup.merge(headers: { 'accept-encoding': 'gzip' }),
24
+ }.each do |name, options|
25
+ context "adapter (#{name} #{options.inspect})" do
26
+ subject do
27
+ described_class.new(options)
28
+ end
29
+
30
+ before :all do
31
+ dir = FlipperRoot.join('tmp').tap(&:mkpath)
32
+ log_path = dir.join('flipper_adapters_http_spec.log')
33
+ @pstore_file = dir.join('flipper.pstore')
34
+ @pstore_file.unlink if @pstore_file.exist?
35
+
36
+ api_adapter = Flipper::Adapters::PStore.new(@pstore_file)
37
+ flipper_api = Flipper.new(api_adapter)
38
+ app = Flipper::Api.app(flipper_api)
39
+ server_options = {
40
+ Port: FLIPPER_SPEC_API_PORT,
41
+ StartCallback: -> { @started = true },
42
+ Logger: WEBrick::Log.new(log_path.to_s, WEBrick::Log::INFO),
43
+ AccessLog: [
44
+ [log_path.open('w'), WEBrick::AccessLog::COMBINED_LOG_FORMAT],
45
+ ],
46
+ }
47
+ @server = WEBrick::HTTPServer.new(server_options)
48
+ @server.mount '/', rack_handler, app
49
+
50
+ Thread.new { @server.start }
51
+ Timeout.timeout(1) { :wait until @started }
52
+ end
53
+
54
+ after :all do
55
+ @server.shutdown if @server
56
+ end
57
+
58
+ before(:each) do
59
+ @pstore_file.unlink if @pstore_file.exist?
60
+ end
61
+
62
+ it_should_behave_like 'a flipper adapter'
63
+
64
+ it "can enable and disable unregistered group" do
65
+ flipper = Flipper.new(subject)
66
+ expect(flipper[:search].enable_group(:some_made_up_group)).to be(true)
67
+ expect(flipper[:search].groups_value).to eq(Set["some_made_up_group"])
68
+
69
+ expect(flipper[:search].disable_group(:some_made_up_group)).to be(true)
70
+ expect(flipper[:search].groups_value).to eq(Set.new)
71
+ end
72
+
73
+ it "can import" do
74
+ adapter = Flipper::Adapters::Memory.new
75
+ source_flipper = Flipper.new(adapter)
76
+ source_flipper.enable_percentage_of_actors :search, 10
77
+ source_flipper.enable_percentage_of_time :search, 15
78
+ source_flipper.enable_actor :search, Flipper::Actor.new('User;1')
79
+ source_flipper.enable_actor :search, Flipper::Actor.new('User;100')
80
+ source_flipper.enable_group :search, :admins
81
+ source_flipper.enable_group :search, :employees
82
+ source_flipper.enable :plausible
83
+ source_flipper.disable :google_analytics
84
+
85
+ flipper = Flipper.new(subject)
86
+ flipper.import(source_flipper)
87
+ expect(flipper[:search].percentage_of_actors_value).to be(10)
88
+ expect(flipper[:search].percentage_of_time_value).to be(15)
89
+ expect(flipper[:search].actors_value).to eq(Set["User;1", "User;100"])
90
+ expect(flipper[:search].groups_value).to eq(Set["admins", "employees"])
91
+ expect(flipper[:plausible].boolean_value).to be(true)
92
+ expect(flipper[:google_analytics].boolean_value).to be(false)
93
+ end
76
94
  end
77
95
  end
78
96
 
79
97
  it "sends default headers" do
80
98
  headers = {
81
- 'Accept' => 'application/json',
82
- 'Content-Type' => 'application/json',
83
- 'User-Agent' => "Flipper HTTP Adapter v#{Flipper::VERSION}",
99
+ 'accept' => 'application/json',
100
+ 'content-type' => 'application/json',
101
+ 'user-agent' => "Flipper HTTP Adapter v#{Flipper::VERSION}",
84
102
  }
85
103
  stub_request(:get, "http://app.com/flipper/features/feature_panel")
86
104
  .with(headers: headers)
@@ -94,9 +112,17 @@ RSpec.describe Flipper::Adapters::Http do
94
112
  stub_const("Rails", double(version: "7.1.0"))
95
113
  stub_const("Sinatra::VERSION", "3.1.0")
96
114
  stub_const("Hanami::VERSION", "0.7.2")
115
+ stub_const("GoodJob::VERSION", "3.21.5")
116
+ stub_const("Sidekiq::VERSION", "7.2.0")
97
117
 
98
118
  headers = {
99
- "Client-Framework" => ["rails=7.1.0", "sinatra=3.1.0", "hanami=0.7.2"]
119
+ "client-framework" => [
120
+ "rails=7.1.0",
121
+ "sinatra=3.1.0",
122
+ "hanami=0.7.2",
123
+ "good_job=3.21.5",
124
+ "sidekiq=7.2.0",
125
+ ]
100
126
  }
101
127
 
102
128
  stub_request(:get, "http://app.com/flipper/features/feature_panel")
@@ -112,7 +138,7 @@ RSpec.describe Flipper::Adapters::Http do
112
138
  stub_const("Sinatra::VERSION", "3.1.0")
113
139
 
114
140
  headers = {
115
- "Client-Framework" => ["rails=7.1.0", "sinatra=3.1.0"]
141
+ "client-framework" => ["rails=7.1.0", "sinatra=3.1.0"]
116
142
  }
117
143
 
118
144
  stub_request(:get, "http://app.com/flipper/features/feature_panel")
@@ -280,7 +306,7 @@ RSpec.describe Flipper::Adapters::Http do
280
306
  let(:options) do
281
307
  {
282
308
  url: 'http://app.com/mount-point',
283
- headers: { 'X-Custom-Header' => 'foo' },
309
+ headers: { 'x-custom-header' => 'foo' },
284
310
  basic_auth_username: 'username',
285
311
  basic_auth_password: 'password',
286
312
  read_timeout: 100,
@@ -301,7 +327,7 @@ RSpec.describe Flipper::Adapters::Http do
301
327
  subject.get(feature)
302
328
  expect(
303
329
  a_request(:get, 'http://app.com/mount-point/features/feature_panel')
304
- .with(headers: { 'X-Custom-Header' => 'foo' })
330
+ .with(headers: { 'x-custom-header' => 'foo' })
305
331
  ).to have_been_made.once
306
332
  end
307
333
 
@@ -6,18 +6,20 @@ RSpec.describe Flipper::Adapters::Strict do
6
6
  subject { described_class.new(Flipper::Adapters::Memory.new, :noop) }
7
7
  end
8
8
 
9
- context "handler = :raise" do
10
- subject { described_class.new(Flipper::Adapters::Memory.new, :raise) }
9
+ [true, :raise].each do |handler|
10
+ context "handler = #{handler}" do
11
+ subject { described_class.new(Flipper::Adapters::Memory.new, handler) }
11
12
 
12
- context "#get" do
13
- it "raises an error for unknown feature" do
14
- expect { subject.get(feature) }.to raise_error(Flipper::Adapters::Strict::NotFound)
13
+ context "#get" do
14
+ it "raises an error for unknown feature" do
15
+ expect { subject.get(feature) }.to raise_error(Flipper::Adapters::Strict::NotFound)
16
+ end
15
17
  end
16
- end
17
18
 
18
- context "#get_multi" do
19
- it "raises an error for unknown feature" do
20
- expect { subject.get_multi([feature]) }.to raise_error(Flipper::Adapters::Strict::NotFound)
19
+ context "#get_multi" do
20
+ it "raises an error for unknown feature" do
21
+ expect { subject.get_multi([feature]) }.to raise_error(Flipper::Adapters::Strict::NotFound)
22
+ end
21
23
  end
22
24
  end
23
25
  end