flipper 1.1.2 → 1.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +7 -1
  3. data/.github/workflows/examples.yml +7 -1
  4. data/Changelog.md +1 -647
  5. data/Gemfile +3 -1
  6. data/README.md +1 -1
  7. data/Rakefile +2 -2
  8. data/exe/flipper +5 -0
  9. data/flipper.gemspec +5 -1
  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 +38 -0
  21. data/lib/flipper/version.rb +11 -1
  22. data/lib/generators/flipper/setup_generator.rb +63 -0
  23. data/spec/fixtures/environment.rb +1 -0
  24. data/spec/flipper/adapter_builder_spec.rb +1 -2
  25. data/spec/flipper/adapters/http/client_spec.rb +61 -0
  26. data/spec/flipper/adapters/http_spec.rb +92 -75
  27. data/spec/flipper/adapters/strict_spec.rb +11 -9
  28. data/spec/flipper/cli_spec.rb +164 -0
  29. data/spec/flipper/cloud/configuration_spec.rb +9 -2
  30. data/spec/flipper/cloud/dsl_spec.rb +5 -5
  31. data/spec/flipper/cloud/middleware_spec.rb +8 -8
  32. data/spec/flipper/cloud/telemetry/submitter_spec.rb +24 -24
  33. data/spec/flipper/cloud/telemetry_spec.rb +1 -1
  34. data/spec/flipper/cloud_spec.rb +4 -4
  35. data/spec/flipper/engine_spec.rb +76 -11
  36. data/spec/flipper/instrumentation/log_subscriber_spec.rb +9 -2
  37. data/spec/flipper_spec.rb +1 -1
  38. data/spec/spec_helper.rb +1 -0
  39. data/spec/support/spec_helpers.rb +10 -4
  40. data/test_rails/generators/flipper/setup_generator_test.rb +64 -0
  41. data/test_rails/system/test_help_test.rb +46 -0
  42. metadata +20 -7
@@ -0,0 +1,38 @@
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) && RSpec.respond_to?(:configure)
20
+ RSpec.configure do |config|
21
+ config.include Flipper::TestHelp
22
+ config.before(:each) do
23
+ flipper_reset
24
+ flipper_configure
25
+ end
26
+ end
27
+ end
28
+
29
+ if defined?(ActiveSupport)
30
+ ActiveSupport.on_load(:active_support_test_case) do
31
+ ActiveSupport::TestCase.class_eval do
32
+ include Flipper::TestHelp
33
+
34
+ setup :flipper_configure
35
+ setup :flipper_reset
36
+ end
37
+ end
38
+ end
@@ -1,3 +1,13 @@
1
1
  module Flipper
2
- VERSION = '1.1.2'.freeze
2
+ VERSION = '1.2.1'.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 @@
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
@@ -0,0 +1,164 @@
1
+ require "flipper/cli"
2
+
3
+ RSpec.describe Flipper::CLI do
4
+ # Infer the command from the description
5
+ subject(:argv) do
6
+ descriptions = self.class.parent_groups.map {|g| g.metadata[:description_args] }.reverse.flatten.drop(1)
7
+ descriptions.map { |arg| Shellwords.split(arg) }.flatten
8
+ end
9
+
10
+ subject { run argv }
11
+
12
+ before do
13
+ ENV["FLIPPER_REQUIRE"] = "./spec/fixtures/environment"
14
+ end
15
+
16
+ describe "enable" do
17
+ describe "feature" do
18
+ it do
19
+ expect(subject).to have_attributes(status: 0, stdout: /feature.*enabled/)
20
+ expect(Flipper).to be_enabled(:feature)
21
+ end
22
+ end
23
+
24
+ describe "-a User;1 feature" do
25
+ it do
26
+ expect(subject).to have_attributes(status: 0, stdout: /feature.*enabled.*User;1/m)
27
+ expect(Flipper).to be_enabled(:feature, Flipper::Actor.new("User;1"))
28
+ end
29
+ end
30
+
31
+ describe "feature -g admins" do
32
+ it do
33
+ expect(subject).to have_attributes(status: 0, stdout: /feature.*enabled.*admins/m)
34
+ expect(Flipper.feature('feature').enabled_groups.map(&:name)).to eq([:admins])
35
+ end
36
+ end
37
+
38
+ describe "feature -p 30" do
39
+ it do
40
+ expect(subject).to have_attributes(status: 0, stdout: /feature.*enabled.*30% of actors/m)
41
+ expect(Flipper.feature('feature').percentage_of_actors_value).to eq(30)
42
+ end
43
+ end
44
+
45
+ describe "feature -t 50" do
46
+ it do
47
+ expect(subject).to have_attributes(status: 0, stdout: /feature.*enabled.*50% of time/m)
48
+ expect(Flipper.feature('feature').percentage_of_time_value).to eq(50)
49
+ end
50
+ end
51
+
52
+ describe %|feature -x '{"Equal":[{"Property":"flipper_id"},"User;1"]}'| do
53
+ it do
54
+ expect(subject).to have_attributes(status: 0, stdout: /feature.*enabled.*User;1/m)
55
+ expect(Flipper.feature('feature').expression.value).to eq({ "Equal" => [ { "Property" => ["flipper_id"] }, "User;1" ] })
56
+ end
57
+ end
58
+
59
+ describe %|feature -x invalid_json| do
60
+ it do
61
+ expect(subject).to have_attributes(status: 1, stderr: /JSON parse error/m)
62
+ end
63
+ end
64
+
65
+ describe %|feature -x '{}'| do
66
+ it do
67
+ expect(subject).to have_attributes(status: 1, stderr: /Invalid expression/m)
68
+ end
69
+ end
70
+ end
71
+
72
+ describe "disable" do
73
+ describe "feature" do
74
+ before { Flipper.enable :feature }
75
+
76
+ it do
77
+ expect(subject).to have_attributes(status: 0, stdout: /feature.*disabled/)
78
+ expect(Flipper).not_to be_enabled(:feature)
79
+ end
80
+ end
81
+
82
+ describe "feature -g admins" do
83
+ before { Flipper.enable_group(:feature, :admins) }
84
+
85
+ it do
86
+ expect(subject).to have_attributes(status: 0, stdout: /feature.*disabled/)
87
+ expect(Flipper.feature('feature').enabled_groups).to be_empty
88
+ end
89
+ end
90
+ end
91
+
92
+ describe "list" do
93
+ before do
94
+ Flipper.enable :foo
95
+ Flipper.disable :bar
96
+ end
97
+
98
+ it "lists features" do
99
+ expect(subject).to have_attributes(status: 0, stdout: /foo.*enabled/)
100
+ expect(subject).to have_attributes(status: 0, stdout: /bar.*disabled/)
101
+ end
102
+ end
103
+
104
+ ["-h", "--help", "help"].each do |arg|
105
+ describe arg do
106
+ it { should have_attributes(status: 0, stdout: /Usage: flipper/) }
107
+
108
+ it "should list subcommands" do
109
+ %w(enable disable list).each do |subcommand|
110
+ expect(subject.stdout).to match(/#{subcommand}/)
111
+ end
112
+ end
113
+ end
114
+ end
115
+
116
+ describe "help enable" do
117
+ it { should have_attributes(status: 0, stdout: /Usage: flipper enable \[options\] <feature>/) }
118
+ end
119
+
120
+ describe "nope" do
121
+ it { should have_attributes(status: 1, stderr: /Unknown command: nope/) }
122
+ end
123
+
124
+ describe "--nope" do
125
+ it { should have_attributes(status: 1, stderr: /invalid option: --nope/) }
126
+ end
127
+
128
+ describe "show foo" do
129
+ context "boolean" do
130
+ before { Flipper.enable :foo }
131
+ it { should have_attributes(status: 0, stdout: /foo.*enabled/) }
132
+ end
133
+
134
+ context "actors" do
135
+ before { Flipper.enable_actor :foo, Flipper::Actor.new("User;1") }
136
+ it { should have_attributes(status: 0, stdout: /User;1/) }
137
+ end
138
+
139
+ context "groups" do
140
+ before { Flipper.enable_group :foo, :admins }
141
+ it { should have_attributes(status: 0, stdout: /enabled.*admins/m) }
142
+ end
143
+ end
144
+
145
+ def run(argv)
146
+ original_stdout = $stdout
147
+ original_stderr = $stderr
148
+
149
+ $stdout = StringIO.new
150
+ $stderr = StringIO.new
151
+ status = 0
152
+
153
+ begin
154
+ Flipper::CLI.run(argv)
155
+ rescue SystemExit => e
156
+ status = e.status
157
+ end
158
+
159
+ OpenStruct.new(status: status, stdout: $stdout.string, stderr: $stderr.string)
160
+ ensure
161
+ $stdout = original_stdout
162
+ $stderr = original_stderr
163
+ end
164
+ end
@@ -86,6 +86,13 @@ RSpec.describe Flipper::Cloud::Configuration do
86
86
  expect(instance.debug_output).to eq(STDOUT)
87
87
  end
88
88
 
89
+ it "defaults debug_output to STDOUT if FLIPPER_CLOUD_DEBUG_OUTPUT_STDOUT set to true" do
90
+ with_env "FLIPPER_CLOUD_DEBUG_OUTPUT_STDOUT" => "true" do
91
+ instance = described_class.new(required_options)
92
+ expect(instance.debug_output).to eq(STDOUT)
93
+ end
94
+ end
95
+
89
96
  it "defaults adapter block" do
90
97
  # The initial sync of http to local invokes this web request.
91
98
  stub_request(:get, /flippercloud\.io/).to_return(status: 200, body: "{}")
@@ -233,9 +240,9 @@ RSpec.describe Flipper::Cloud::Configuration do
233
240
  stub = stub_request(:get, "https://www.flippercloud.io/adapter/features?exclude_gate_names=true").
234
241
  with({
235
242
  headers: {
236
- 'Flipper-Cloud-Token'=>'asdf',
243
+ 'flipper-cloud-token'=>'asdf',
237
244
  },
238
- }).to_return(status: 200, body: body, headers: {})
245
+ }).to_return(status: 200, body: body)
239
246
  instance = described_class.new(required_options)
240
247
  instance.sync
241
248
 
@@ -18,7 +18,7 @@ RSpec.describe Flipper::Cloud::DSL do
18
18
  stub = stub_request(:get, "https://www.flippercloud.io/adapter/features?exclude_gate_names=true").
19
19
  with({
20
20
  headers: {
21
- 'Flipper-Cloud-Token'=>'asdf',
21
+ 'flipper-cloud-token'=>'asdf',
22
22
  },
23
23
  }).to_return(status: 200, body: '{"features": {}}', headers: {})
24
24
  cloud_configuration = Flipper::Cloud::Configuration.new({
@@ -65,11 +65,11 @@ RSpec.describe Flipper::Cloud::DSL do
65
65
 
66
66
  it "sends writes to cloud and local" do
67
67
  add_stub = stub_request(:post, "https://www.flippercloud.io/adapter/features").
68
- with({headers: {'Flipper-Cloud-Token'=>'asdf'}}).
69
- to_return(status: 200, body: '{}', headers: {})
68
+ with({headers: {'flipper-cloud-token'=>'asdf'}}).
69
+ to_return(status: 200, body: '{}')
70
70
  enable_stub = stub_request(:post, "https://www.flippercloud.io/adapter/features/foo/boolean").
71
- with(headers: {'Flipper-Cloud-Token'=>'asdf'}).
72
- to_return(status: 200, body: '{}', headers: {})
71
+ with(headers: {'flipper-cloud-token'=>'asdf'}).
72
+ to_return(status: 200, body: '{}')
73
73
 
74
74
  subject.enable(:foo)
75
75