flipper 1.1.2 → 1.2.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) 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 -2
  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 +244 -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 +43 -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 +189 -0
  29. data/spec/flipper/cloud/configuration_spec.rb +33 -35
  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/backoff_policy_spec.rb +8 -9
  33. data/spec/flipper/cloud/telemetry/submitter_spec.rb +24 -24
  34. data/spec/flipper/cloud/telemetry_spec.rb +1 -1
  35. data/spec/flipper/cloud_spec.rb +6 -7
  36. data/spec/flipper/engine_spec.rb +109 -57
  37. data/spec/flipper/instrumentation/log_subscriber_spec.rb +9 -2
  38. data/spec/flipper_spec.rb +1 -1
  39. data/spec/spec_helper.rb +1 -0
  40. data/spec/support/spec_helpers.rb +10 -4
  41. data/test_rails/generators/flipper/setup_generator_test.rb +64 -0
  42. data/test_rails/system/test_help_test.rb +51 -0
  43. metadata +20 -9
  44. data/spec/support/climate_control.rb +0 -7
@@ -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
@@ -1,3 +1,13 @@
1
1
  module Flipper
2
- VERSION = '1.1.2'.freeze
2
+ VERSION = '1.2.2'.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,189 @@
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.*\e\[32m.*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.*\e\[33m.*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
+ context "bundler is not installed" do
146
+ let(:argv) { "list" }
147
+
148
+ around do |example|
149
+ original_bundler = Bundler
150
+ begin
151
+ Object.send(:remove_const, :Bundler)
152
+ example.run
153
+ ensure
154
+ Object.const_set(:Bundler, original_bundler)
155
+ end
156
+ end
157
+
158
+ it "should not raise an error" do
159
+ Flipper.enable(:enabled_feature)
160
+ Flipper.enable_group(:enabled_groups, :admins)
161
+ Flipper.add(:disabled_feature)
162
+
163
+ expect(subject).to have_attributes(status: 0, stdout: /enabled_feature.*enabled_groups.*disabled_feature/m)
164
+ end
165
+ end
166
+
167
+ def run(argv)
168
+ original_stdout = $stdout
169
+ original_stderr = $stderr
170
+
171
+ $stdout = StringIO.new
172
+ $stderr = StringIO.new
173
+ status = 0
174
+
175
+ # Prentend this a TTY so we can test colorization
176
+ allow($stdout).to receive(:tty?).and_return(true)
177
+
178
+ begin
179
+ Flipper::CLI.run(argv)
180
+ rescue SystemExit => e
181
+ status = e.status
182
+ end
183
+
184
+ OpenStruct.new(status: status, stdout: $stdout.string, stderr: $stderr.string)
185
+ ensure
186
+ $stdout = original_stdout
187
+ $stderr = original_stderr
188
+ end
189
+ end