flipper 1.1.2 → 1.3.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.
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