flipper 1.1.2 → 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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +7 -1
- data/.github/workflows/examples.yml +7 -1
- data/Changelog.md +1 -647
- data/Gemfile +3 -1
- data/README.md +1 -1
- data/Rakefile +2 -2
- data/exe/flipper +5 -0
- data/flipper.gemspec +5 -1
- data/lib/flipper/adapters/http/client.rb +25 -16
- data/lib/flipper/adapters/strict.rb +11 -8
- data/lib/flipper/cli.rb +240 -0
- data/lib/flipper/cloud/configuration.rb +7 -1
- data/lib/flipper/cloud/middleware.rb +5 -5
- data/lib/flipper/cloud/telemetry/submitter.rb +2 -2
- data/lib/flipper/cloud.rb +1 -1
- data/lib/flipper/engine.rb +32 -17
- data/lib/flipper/instrumentation/log_subscriber.rb +12 -3
- data/lib/flipper/metadata.rb +3 -1
- data/lib/flipper/test_help.rb +36 -0
- data/lib/flipper/version.rb +11 -1
- data/lib/generators/flipper/setup_generator.rb +63 -0
- data/spec/fixtures/environment.rb +1 -0
- data/spec/flipper/adapter_builder_spec.rb +1 -2
- data/spec/flipper/adapters/http/client_spec.rb +61 -0
- data/spec/flipper/adapters/http_spec.rb +92 -75
- data/spec/flipper/adapters/strict_spec.rb +11 -9
- data/spec/flipper/cli_spec.rb +164 -0
- data/spec/flipper/cloud/configuration_spec.rb +9 -2
- data/spec/flipper/cloud/dsl_spec.rb +5 -5
- data/spec/flipper/cloud/middleware_spec.rb +8 -8
- data/spec/flipper/cloud/telemetry/submitter_spec.rb +24 -24
- data/spec/flipper/cloud/telemetry_spec.rb +1 -1
- data/spec/flipper/cloud_spec.rb +4 -4
- data/spec/flipper/engine_spec.rb +76 -11
- data/spec/flipper/instrumentation/log_subscriber_spec.rb +9 -2
- data/spec/flipper_spec.rb +1 -1
- data/spec/spec_helper.rb +1 -0
- data/spec/support/spec_helpers.rb +10 -4
- data/test_rails/generators/flipper/setup_generator_test.rb +64 -0
- data/test_rails/system/test_help_test.rb +46 -0
- metadata +20 -7
data/lib/flipper/version.rb
CHANGED
@@ -1,3 +1,13 @@
|
|
1
1
|
module Flipper
|
2
|
-
VERSION = '1.
|
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 @@
|
|
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(
|
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
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
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
|
-
'
|
82
|
-
'
|
83
|
-
'
|
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
|
-
"
|
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
|
-
"
|
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: { '
|
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: { '
|
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
|
-
|
10
|
-
|
9
|
+
[true, :raise].each do |handler|
|
10
|
+
context "handler = #{handler}" do
|
11
|
+
subject { described_class.new(Flipper::Adapters::Memory.new, handler) }
|
11
12
|
|
12
|
-
|
13
|
-
|
14
|
-
|
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
|
-
|
19
|
-
|
20
|
-
|
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
|
-
'
|
243
|
+
'flipper-cloud-token'=>'asdf',
|
237
244
|
},
|
238
|
-
}).to_return(status: 200, body: body
|
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
|
-
'
|
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: {'
|
69
|
-
to_return(status: 200, body: '{}'
|
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: {'
|
72
|
-
to_return(status: 200, body: '{}'
|
71
|
+
with(headers: {'flipper-cloud-token'=>'asdf'}).
|
72
|
+
to_return(status: 200, body: '{}')
|
73
73
|
|
74
74
|
subject.enable(:foo)
|
75
75
|
|
@@ -101,8 +101,8 @@ RSpec.describe Flipper::Cloud::Middleware do
|
|
101
101
|
post '/', request_body, env
|
102
102
|
|
103
103
|
expect(last_response.status).to eq(402)
|
104
|
-
expect(last_response.headers["
|
105
|
-
expect(last_response.headers["
|
104
|
+
expect(last_response.headers["flipper-cloud-response-error-class"]).to eq("Flipper::Adapters::Http::Error")
|
105
|
+
expect(last_response.headers["flipper-cloud-response-error-message"]).to include("Failed with status: 402")
|
106
106
|
expect(stub).to have_been_requested
|
107
107
|
end
|
108
108
|
end
|
@@ -124,8 +124,8 @@ RSpec.describe Flipper::Cloud::Middleware do
|
|
124
124
|
post '/', request_body, env
|
125
125
|
|
126
126
|
expect(last_response.status).to eq(500)
|
127
|
-
expect(last_response.headers["
|
128
|
-
expect(last_response.headers["
|
127
|
+
expect(last_response.headers["flipper-cloud-response-error-class"]).to eq("Flipper::Adapters::Http::Error")
|
128
|
+
expect(last_response.headers["flipper-cloud-response-error-message"]).to include("Failed with status: 503")
|
129
129
|
expect(stub).to have_been_requested
|
130
130
|
end
|
131
131
|
end
|
@@ -147,8 +147,8 @@ RSpec.describe Flipper::Cloud::Middleware do
|
|
147
147
|
post '/', request_body, env
|
148
148
|
|
149
149
|
expect(last_response.status).to eq(500)
|
150
|
-
expect(last_response.headers["
|
151
|
-
expect(last_response.headers["
|
150
|
+
expect(last_response.headers["flipper-cloud-response-error-class"]).to eq("Net::OpenTimeout")
|
151
|
+
expect(last_response.headers["flipper-cloud-response-error-message"]).to eq("execution expired")
|
152
152
|
expect(stub).to have_been_requested
|
153
153
|
end
|
154
154
|
end
|
@@ -277,13 +277,13 @@ RSpec.describe Flipper::Cloud::Middleware do
|
|
277
277
|
stub = stub_request(:get, "https://www.flippercloud.io/adapter/features?exclude_gate_names=true").
|
278
278
|
with({
|
279
279
|
headers: {
|
280
|
-
'
|
280
|
+
'flipper-cloud-token' => token,
|
281
281
|
},
|
282
282
|
})
|
283
283
|
if status == :timeout
|
284
284
|
stub.to_timeout
|
285
285
|
else
|
286
|
-
stub.to_return(status: status, body: response_body
|
286
|
+
stub.to_return(status: status, body: response_body)
|
287
287
|
end
|
288
288
|
end
|
289
289
|
end
|