flipper 1.1.1 → 1.2.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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +25 -1
  3. data/.github/workflows/examples.yml +7 -1
  4. data/Changelog.md +1 -638
  5. data/Gemfile +5 -1
  6. data/README.md +21 -21
  7. data/Rakefile +2 -2
  8. data/exe/flipper +5 -0
  9. data/flipper.gemspec +6 -2
  10. data/lib/flipper/adapters/http/client.rb +25 -16
  11. data/lib/flipper/adapters/strict.rb +11 -8
  12. data/lib/flipper/cli.rb +240 -0
  13. data/lib/flipper/cloud/configuration.rb +7 -1
  14. data/lib/flipper/cloud/middleware.rb +5 -5
  15. data/lib/flipper/cloud/telemetry/submitter.rb +2 -2
  16. data/lib/flipper/cloud.rb +1 -1
  17. data/lib/flipper/engine.rb +32 -17
  18. data/lib/flipper/instrumentation/log_subscriber.rb +12 -3
  19. data/lib/flipper/metadata.rb +3 -1
  20. data/lib/flipper/test_help.rb +36 -0
  21. data/lib/flipper/version.rb +11 -1
  22. data/lib/generators/flipper/setup_generator.rb +63 -0
  23. data/lib/generators/flipper/templates/update/migrations/01_create_flipper_tables.rb.erb +22 -0
  24. data/lib/generators/flipper/templates/update/migrations/02_change_flipper_gates_value_to_text.rb.erb +18 -0
  25. data/lib/generators/flipper/update_generator.rb +35 -0
  26. data/spec/fixtures/environment.rb +1 -0
  27. data/spec/flipper/adapter_builder_spec.rb +1 -2
  28. data/spec/flipper/adapters/http/client_spec.rb +61 -0
  29. data/spec/flipper/adapters/http_spec.rb +92 -75
  30. data/spec/flipper/adapters/strict_spec.rb +11 -9
  31. data/spec/flipper/cli_spec.rb +164 -0
  32. data/spec/flipper/cloud/configuration_spec.rb +9 -2
  33. data/spec/flipper/cloud/dsl_spec.rb +5 -5
  34. data/spec/flipper/cloud/middleware_spec.rb +8 -8
  35. data/spec/flipper/cloud/telemetry/submitter_spec.rb +24 -24
  36. data/spec/flipper/cloud/telemetry_spec.rb +1 -1
  37. data/spec/flipper/cloud_spec.rb +4 -4
  38. data/spec/flipper/engine_spec.rb +76 -11
  39. data/spec/flipper/instrumentation/log_subscriber_spec.rb +9 -2
  40. data/spec/flipper_spec.rb +1 -1
  41. data/spec/spec_helper.rb +1 -0
  42. data/spec/support/spec_helpers.rb +10 -4
  43. data/test_rails/generators/flipper/setup_generator_test.rb +64 -0
  44. data/test_rails/generators/flipper/update_generator_test.rb +96 -0
  45. data/test_rails/helper.rb +19 -2
  46. data/test_rails/system/test_help_test.rb +46 -0
  47. metadata +25 -8
@@ -1,4 +1,5 @@
1
1
  require 'securerandom'
2
+ require 'active_support/gem_version'
2
3
  require 'active_support/notifications'
3
4
  require 'active_support/log_subscriber'
4
5
 
@@ -71,11 +72,19 @@ module Flipper
71
72
  self.class.logger
72
73
  end
73
74
 
75
+ def self.attach
76
+ attach_to InstrumentationNamespace
77
+ end
78
+
79
+ def self.detach
80
+ # Rails 5.2 doesn't support this, that's fine
81
+ detach_from InstrumentationNamespace if respond_to?(:detach_from)
82
+ end
83
+
74
84
  private
75
85
 
76
86
  # Rails 7.1 changed the signature of this function.
77
- # Checking if > 7.0.99 rather than >= 7.1 so that 7.1 pre-release versions are included.
78
- COLOR_OPTIONS = if Rails.gem_version > Gem::Version.new('7.0.99')
87
+ COLOR_OPTIONS = if Gem::Requirement.new(">=7.1").satisfied_by?(ActiveSupport.gem_version)
79
88
  { bold: true }.freeze
80
89
  else
81
90
  true
@@ -88,5 +97,5 @@ module Flipper
88
97
  end
89
98
  end
90
99
 
91
- Instrumentation::LogSubscriber.attach_to InstrumentationNamespace
100
+ Instrumentation::LogSubscriber.attach
92
101
  end
@@ -1,9 +1,11 @@
1
+ require_relative './version'
2
+
1
3
  module Flipper
2
4
  METADATA = {
3
5
  "documentation_uri" => "https://www.flippercloud.io/docs",
4
6
  "homepage_uri" => "https://www.flippercloud.io",
5
7
  "source_code_uri" => "https://github.com/flippercloud/flipper",
6
8
  "bug_tracker_uri" => "https://github.com/flippercloud/flipper/issues",
7
- "changelog_uri" => "https://github.com/flippercloud/flipper/blob/main/Changelog.md",
9
+ "changelog_uri" => "https://github.com/flippercloud/flipper/releases/tag/v#{Flipper::VERSION}",
8
10
  }.freeze
9
11
  end
@@ -0,0 +1,36 @@
1
+ module Flipper
2
+ module TestHelp
3
+ def flipper_configure
4
+ # Create a single shared memory adapter instance for each test
5
+ @flipper_adapter = Flipper::Adapters::Memory.new
6
+
7
+ Flipper.configure do |config|
8
+ config.adapter { @flipper_adapter }
9
+ config.default { Flipper.new(config.adapter) }
10
+ end
11
+ end
12
+
13
+ def flipper_reset
14
+ Flipper.instance = nil # Reset previous flipper instance
15
+ end
16
+ end
17
+ end
18
+
19
+ if defined?(RSpec)
20
+ RSpec.configure do |config|
21
+ config.include Flipper::TestHelp
22
+ config.before(:all) { flipper_configure }
23
+ config.before(:each) { flipper_reset }
24
+ end
25
+ end
26
+
27
+ if defined?(ActiveSupport)
28
+ ActiveSupport.on_load(:active_support_test_case) do
29
+ ActiveSupport::TestCase.class_eval do
30
+ include Flipper::TestHelp
31
+
32
+ setup :flipper_configure
33
+ setup :flipper_reset
34
+ end
35
+ end
36
+ end
@@ -1,3 +1,13 @@
1
1
  module Flipper
2
- VERSION = '1.1.1'.freeze
2
+ VERSION = '1.2.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
@@ -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
@@ -0,0 +1,22 @@
1
+ class CreateFlipperTables < ActiveRecord::Migration<%= migration_version %>
2
+ def up
3
+ create_table :flipper_features do |t|
4
+ t.string :key, null: false
5
+ t.timestamps null: false
6
+ end
7
+ add_index :flipper_features, :key, unique: true
8
+
9
+ create_table :flipper_gates do |t|
10
+ t.string :feature_key, null: false
11
+ t.string :key, null: false
12
+ t.string :value
13
+ t.timestamps null: false
14
+ end
15
+ add_index :flipper_gates, [:feature_key, :key, :value], unique: true
16
+ end
17
+
18
+ def down
19
+ drop_table :flipper_gates
20
+ drop_table :flipper_features
21
+ end
22
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ChangeFlipperGatesValueToText < ActiveRecord::Migration<%= migration_version %>
4
+ def up
5
+ # Ensure this incremental update migration is idempotent
6
+ return unless connection.column_exists? :flipper_gates, :value, :string
7
+
8
+ if index_exists? :flipper_gates, [:feature_key, :key, :value]
9
+ remove_index :flipper_gates, [:feature_key, :key, :value]
10
+ end
11
+ change_column :flipper_gates, :value, :text
12
+ add_index :flipper_gates, [:feature_key, :key, :value], unique: true, length: { value: 255 }
13
+ end
14
+
15
+ def down
16
+ change_column :flipper_gates, :value, :string
17
+ end
18
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+ require 'rails/generators/active_record'
5
+
6
+ module Flipper
7
+ module Generators
8
+ #
9
+ # Rails generator used for updating Flipper in a Rails application.
10
+ # Run it with +bin/rails g flipper:update+ in your console.
11
+ #
12
+ class UpdateGenerator < Rails::Generators::Base
13
+ include ActiveRecord::Generators::Migration
14
+
15
+ TEMPLATES = File.join(File.dirname(__FILE__), 'templates/update')
16
+ source_paths << TEMPLATES
17
+
18
+ # Generates incremental migration files unless they already exist.
19
+ # All migrations should be idempotent e.g. +add_index+ is guarded with +if_index_exists?+
20
+ def update_migration_files
21
+ migration_templates = Dir.children(File.join(TEMPLATES, 'migrations')).sort
22
+ migration_templates.each do |template_file|
23
+ destination_file = template_file.match(/^\d*_(.*\.rb)/)[1] # 01_create_flipper_tables.rb.erb => create_flipper_tables.rb
24
+ migration_template "migrations/#{template_file}", File.join(db_migrate_path, destination_file), skip: true
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def migration_version
31
+ "[#{ActiveRecord::VERSION::STRING.to_f}]"
32
+ end
33
+ end
34
+ end
35
+ end
@@ -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,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
@@ -5,82 +5,91 @@ require 'rack/handler/webrick'
5
5
  FLIPPER_SPEC_API_PORT = ENV.fetch('FLIPPER_SPEC_API_PORT', 9001).to_i
6
6
 
7
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'
46
-
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
-
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
55
-
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)
8
+ default_options = {
9
+ url: "http://localhost:#{FLIPPER_SPEC_API_PORT}",
10
+ }
11
+
12
+ {
13
+ basic: default_options.dup,
14
+ gzip: default_options.dup.merge(headers: { 'accept-encoding': 'gzip' }),
15
+ }.each do |name, options|
16
+ context "adapter (#{name} #{options.inspect})" do
17
+ subject do
18
+ described_class.new(options)
19
+ end
20
+
21
+ before :all do
22
+ dir = FlipperRoot.join('tmp').tap(&:mkpath)
23
+ log_path = dir.join('flipper_adapters_http_spec.log')
24
+ @pstore_file = dir.join('flipper.pstore')
25
+ @pstore_file.unlink if @pstore_file.exist?
26
+
27
+ api_adapter = Flipper::Adapters::PStore.new(@pstore_file)
28
+ flipper_api = Flipper.new(api_adapter)
29
+ app = Flipper::Api.app(flipper_api)
30
+ server_options = {
31
+ Port: FLIPPER_SPEC_API_PORT,
32
+ StartCallback: -> { @started = true },
33
+ Logger: WEBrick::Log.new(log_path.to_s, WEBrick::Log::INFO),
34
+ AccessLog: [
35
+ [log_path.open('w'), WEBrick::AccessLog::COMBINED_LOG_FORMAT],
36
+ ],
37
+ }
38
+ @server = WEBrick::HTTPServer.new(server_options)
39
+ @server.mount '/', Rack::Handler::WEBrick, app
40
+
41
+ Thread.new { @server.start }
42
+ Timeout.timeout(1) { :wait until @started }
43
+ end
44
+
45
+ after :all do
46
+ @server.shutdown if @server
47
+ end
48
+
49
+ before(:each) do
50
+ @pstore_file.unlink if @pstore_file.exist?
51
+ end
52
+
53
+ it_should_behave_like 'a flipper adapter'
54
+
55
+ it "can enable and disable unregistered group" do
56
+ flipper = Flipper.new(subject)
57
+ expect(flipper[:search].enable_group(:some_made_up_group)).to be(true)
58
+ expect(flipper[:search].groups_value).to eq(Set["some_made_up_group"])
59
+
60
+ expect(flipper[:search].disable_group(:some_made_up_group)).to be(true)
61
+ expect(flipper[:search].groups_value).to eq(Set.new)
62
+ end
63
+
64
+ it "can import" do
65
+ adapter = Flipper::Adapters::Memory.new
66
+ source_flipper = Flipper.new(adapter)
67
+ source_flipper.enable_percentage_of_actors :search, 10
68
+ source_flipper.enable_percentage_of_time :search, 15
69
+ source_flipper.enable_actor :search, Flipper::Actor.new('User;1')
70
+ source_flipper.enable_actor :search, Flipper::Actor.new('User;100')
71
+ source_flipper.enable_group :search, :admins
72
+ source_flipper.enable_group :search, :employees
73
+ source_flipper.enable :plausible
74
+ source_flipper.disable :google_analytics
75
+
76
+ flipper = Flipper.new(subject)
77
+ flipper.import(source_flipper)
78
+ expect(flipper[:search].percentage_of_actors_value).to be(10)
79
+ expect(flipper[:search].percentage_of_time_value).to be(15)
80
+ expect(flipper[:search].actors_value).to eq(Set["User;1", "User;100"])
81
+ expect(flipper[:search].groups_value).to eq(Set["admins", "employees"])
82
+ expect(flipper[:plausible].boolean_value).to be(true)
83
+ expect(flipper[:google_analytics].boolean_value).to be(false)
84
+ end
76
85
  end
77
86
  end
78
87
 
79
88
  it "sends default headers" do
80
89
  headers = {
81
- 'Accept' => 'application/json',
82
- 'Content-Type' => 'application/json',
83
- 'User-Agent' => "Flipper HTTP Adapter v#{Flipper::VERSION}",
90
+ 'accept' => 'application/json',
91
+ 'content-type' => 'application/json',
92
+ 'user-agent' => "Flipper HTTP Adapter v#{Flipper::VERSION}",
84
93
  }
85
94
  stub_request(:get, "http://app.com/flipper/features/feature_panel")
86
95
  .with(headers: headers)
@@ -94,9 +103,17 @@ RSpec.describe Flipper::Adapters::Http do
94
103
  stub_const("Rails", double(version: "7.1.0"))
95
104
  stub_const("Sinatra::VERSION", "3.1.0")
96
105
  stub_const("Hanami::VERSION", "0.7.2")
106
+ stub_const("GoodJob::VERSION", "3.21.5")
107
+ stub_const("Sidekiq::VERSION", "7.2.0")
97
108
 
98
109
  headers = {
99
- "Client-Framework" => ["rails=7.1.0", "sinatra=3.1.0", "hanami=0.7.2"]
110
+ "client-framework" => [
111
+ "rails=7.1.0",
112
+ "sinatra=3.1.0",
113
+ "hanami=0.7.2",
114
+ "good_job=3.21.5",
115
+ "sidekiq=7.2.0",
116
+ ]
100
117
  }
101
118
 
102
119
  stub_request(:get, "http://app.com/flipper/features/feature_panel")
@@ -112,7 +129,7 @@ RSpec.describe Flipper::Adapters::Http do
112
129
  stub_const("Sinatra::VERSION", "3.1.0")
113
130
 
114
131
  headers = {
115
- "Client-Framework" => ["rails=7.1.0", "sinatra=3.1.0"]
132
+ "client-framework" => ["rails=7.1.0", "sinatra=3.1.0"]
116
133
  }
117
134
 
118
135
  stub_request(:get, "http://app.com/flipper/features/feature_panel")
@@ -280,7 +297,7 @@ RSpec.describe Flipper::Adapters::Http do
280
297
  let(:options) do
281
298
  {
282
299
  url: 'http://app.com/mount-point',
283
- headers: { 'X-Custom-Header' => 'foo' },
300
+ headers: { 'x-custom-header' => 'foo' },
284
301
  basic_auth_username: 'username',
285
302
  basic_auth_password: 'password',
286
303
  read_timeout: 100,
@@ -301,7 +318,7 @@ RSpec.describe Flipper::Adapters::Http do
301
318
  subject.get(feature)
302
319
  expect(
303
320
  a_request(:get, 'http://app.com/mount-point/features/feature_panel')
304
- .with(headers: { 'X-Custom-Header' => 'foo' })
321
+ .with(headers: { 'x-custom-header' => 'foo' })
305
322
  ).to have_been_made.once
306
323
  end
307
324
 
@@ -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