flipper 0.26.2 → 0.27.1

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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/Changelog.md +8 -0
  3. data/Gemfile +2 -3
  4. data/Rakefile +3 -3
  5. data/examples/api/basic.ru +3 -4
  6. data/examples/api/custom_memoized.ru +3 -4
  7. data/examples/api/memoized.ru +3 -4
  8. data/lib/flipper/adapter.rb +23 -7
  9. data/lib/flipper/adapters/http.rb +11 -3
  10. data/lib/flipper/adapters/instrumented.rb +25 -2
  11. data/lib/flipper/adapters/memoizable.rb +19 -2
  12. data/lib/flipper/adapters/memory.rb +8 -4
  13. data/lib/flipper/adapters/operation_logger.rb +16 -3
  14. data/lib/flipper/dsl.rb +1 -5
  15. data/lib/flipper/export.rb +26 -0
  16. data/lib/flipper/exporter.rb +17 -0
  17. data/lib/flipper/exporters/json/export.rb +32 -0
  18. data/lib/flipper/exporters/json/v1.rb +33 -0
  19. data/lib/flipper/instrumentation/subscriber.rb +8 -0
  20. data/lib/flipper/spec/shared_adapter_specs.rb +23 -0
  21. data/lib/flipper/test/shared_adapter_test.rb +24 -0
  22. data/lib/flipper/typecast.rb +17 -0
  23. data/lib/flipper/version.rb +1 -1
  24. data/lib/flipper.rb +2 -1
  25. data/spec/fixtures/flipper_pstore_1679087600.json +46 -0
  26. data/spec/flipper/adapter_spec.rb +29 -2
  27. data/spec/flipper/adapters/http_spec.rb +25 -3
  28. data/spec/flipper/adapters/instrumented_spec.rb +28 -10
  29. data/spec/flipper/adapters/memoizable_spec.rb +30 -10
  30. data/spec/flipper/adapters/operation_logger_spec.rb +29 -10
  31. data/spec/flipper/dsl_spec.rb +20 -3
  32. data/spec/flipper/export_spec.rb +13 -0
  33. data/spec/flipper/exporter_spec.rb +16 -0
  34. data/spec/flipper/exporters/json/export_spec.rb +60 -0
  35. data/spec/flipper/exporters/json/v1_spec.rb +33 -0
  36. data/spec/flipper/instrumentation/log_subscriber_spec.rb +10 -0
  37. data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +10 -0
  38. data/spec/flipper/typecast_spec.rb +79 -0
  39. data/spec/flipper_spec.rb +7 -1
  40. data/spec/support/skippable.rb +18 -0
  41. metadata +18 -2
@@ -0,0 +1,46 @@
1
+ {
2
+ "version": 1,
3
+ "features": {
4
+ "search": {
5
+ "boolean": null,
6
+ "actors": [
7
+ "john",
8
+ "another",
9
+ "testing"
10
+ ],
11
+ "percentage_of_actors": null,
12
+ "percentage_of_time": null,
13
+ "groups": [
14
+ "admins"
15
+ ]
16
+ },
17
+ "new_pricing": {
18
+ "boolean": "true",
19
+ "actors": [],
20
+ "percentage_of_actors": null,
21
+ "percentage_of_time": null,
22
+ "groups": []
23
+ },
24
+ "google_analytics_tag": {
25
+ "boolean": null,
26
+ "actors": [],
27
+ "percentage_of_actors": "100",
28
+ "percentage_of_time": null,
29
+ "groups": []
30
+ },
31
+ "help_scout_tag": {
32
+ "boolean": null,
33
+ "actors": [],
34
+ "percentage_of_actors": null,
35
+ "percentage_of_time": "50",
36
+ "groups": []
37
+ },
38
+ "nope": {
39
+ "boolean": null,
40
+ "actors": [],
41
+ "percentage_of_actors": null,
42
+ "percentage_of_time": null,
43
+ "groups": []
44
+ }
45
+ }
46
+ }
@@ -30,9 +30,9 @@ RSpec.describe Flipper::Adapter do
30
30
  end
31
31
 
32
32
  describe '#import' do
33
- it 'returns nothing' do
33
+ it 'returns true' do
34
34
  result = destination_flipper.import(source_flipper)
35
- expect(result).to be(nil)
35
+ expect(result).to be(true)
36
36
  end
37
37
 
38
38
  it 'can import from one adapter to another' do
@@ -114,5 +114,32 @@ RSpec.describe Flipper::Adapter do
114
114
  destination_flipper.import(source_flipper)
115
115
  expect(destination_flipper.features.map(&:key)).to eq([])
116
116
  end
117
+
118
+ it 'can import an export' do
119
+ source_flipper.enable(:search)
120
+ source_flipper.enable(:google_analytics, Flipper::Actor.new("User;1"))
121
+
122
+ destination_flipper.import(source_flipper.export)
123
+
124
+ feature = destination_flipper[:search]
125
+ expect(feature.boolean_value).to be(true)
126
+
127
+ feature = destination_flipper[:google_analytics]
128
+ expect(feature.actors_value).to eq(Set["User;1"])
129
+ end
130
+ end
131
+
132
+ describe "#export" do
133
+ it "exports features" do
134
+ source_flipper.enable(:search)
135
+ export = source_flipper.export
136
+ expect(export.features.dig("search", :boolean)).to eq("true")
137
+ end
138
+
139
+ it "exports with arguments" do
140
+ source_flipper.enable(:search)
141
+ export = source_flipper.export(format: :json, version: 1)
142
+ expect(export.features.dig("search", :boolean)).to eq("true")
143
+ end
117
144
  end
118
145
  end
@@ -52,6 +52,28 @@ RSpec.describe Flipper::Adapters::Http do
52
52
  expect(flipper[:search].disable_group(:some_made_up_group)).to be(true)
53
53
  expect(flipper[:search].groups_value).to eq(Set.new)
54
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)
76
+ end
55
77
  end
56
78
 
57
79
  it "sends default headers" do
@@ -82,7 +104,7 @@ RSpec.describe Flipper::Adapters::Http do
82
104
 
83
105
  describe "#get_multi" do
84
106
  it "raises error when not successful response" do
85
- stub_request(:get, "http://app.com/flipper/features?keys=feature_panel")
107
+ stub_request(:get, "http://app.com/flipper/features?keys=feature_panel&exclude_gate_names=true")
86
108
  .to_return(status: 503, body: "", headers: {})
87
109
 
88
110
  adapter = described_class.new(url: 'http://app.com/flipper')
@@ -94,7 +116,7 @@ RSpec.describe Flipper::Adapters::Http do
94
116
 
95
117
  describe "#get_all" do
96
118
  it "raises error when not successful response" do
97
- stub_request(:get, "http://app.com/flipper/features")
119
+ stub_request(:get, "http://app.com/flipper/features?exclude_gate_names=true")
98
120
  .to_return(status: 503, body: "", headers: {})
99
121
 
100
122
  adapter = described_class.new(url: 'http://app.com/flipper')
@@ -106,7 +128,7 @@ RSpec.describe Flipper::Adapters::Http do
106
128
 
107
129
  describe "#features" do
108
130
  it "raises error when not successful response" do
109
- stub_request(:get, "http://app.com/flipper/features")
131
+ stub_request(:get, "http://app.com/flipper/features?exclude_gate_names=true")
110
132
  .to_return(status: 503, body: "", headers: {})
111
133
 
112
134
  adapter = described_class.new(url: 'http://app.com/flipper')
@@ -16,16 +16,6 @@ RSpec.describe Flipper::Adapters::Instrumented do
16
16
 
17
17
  it_should_behave_like 'a flipper adapter'
18
18
 
19
- it 'forwards missing methods to underlying adapter' do
20
- adapter = Class.new do
21
- def foo
22
- :foo
23
- end
24
- end.new
25
- instrumented = described_class.new(adapter)
26
- expect(instrumented.foo).to eq(:foo)
27
- end
28
-
29
19
  describe '#name' do
30
20
  it 'is instrumented' do
31
21
  expect(subject.name).to be(:instrumented)
@@ -146,4 +136,32 @@ RSpec.describe Flipper::Adapters::Instrumented do
146
136
  expect(event.payload[:result]).to be(result)
147
137
  end
148
138
  end
139
+
140
+ describe '#import' do
141
+ it 'records instrumentation' do
142
+ result = subject.import(Flipper::Adapters::Memory.new)
143
+
144
+ event = instrumenter.events.last
145
+ expect(event).not_to be_nil
146
+ expect(event.name).to eq('adapter_operation.flipper')
147
+ expect(event.payload[:operation]).to eq(:import)
148
+ expect(event.payload[:adapter_name]).to eq(:memory)
149
+ expect(event.payload[:result]).to be(result)
150
+ end
151
+ end
152
+
153
+ describe '#export' do
154
+ it 'records instrumentation' do
155
+ result = subject.export(format: :json, version: 1)
156
+
157
+ event = instrumenter.events.last
158
+ expect(event).not_to be_nil
159
+ expect(event.name).to eq('adapter_operation.flipper')
160
+ expect(event.payload[:operation]).to eq(:export)
161
+ expect(event.payload[:adapter_name]).to eq(:memory)
162
+ expect(event.payload[:format]).to be(:json)
163
+ expect(event.payload[:version]).to be(1)
164
+ expect(event.payload[:result]).to be(result)
165
+ end
166
+ end
149
167
  end
@@ -11,16 +11,6 @@ RSpec.describe Flipper::Adapters::Memoizable do
11
11
 
12
12
  it_should_behave_like 'a flipper adapter'
13
13
 
14
- it 'forwards missing methods to underlying adapter' do
15
- adapter = Class.new do
16
- def foo
17
- :foo
18
- end
19
- end.new
20
- memoizable = described_class.new(adapter)
21
- expect(memoizable.foo).to eq(:foo)
22
- end
23
-
24
14
  describe '#name' do
25
15
  it 'is instrumented' do
26
16
  expect(subject.name).to be(:memoizable)
@@ -249,6 +239,36 @@ RSpec.describe Flipper::Adapters::Memoizable do
249
239
  end
250
240
  end
251
241
 
242
+ describe "#import" do
243
+ context "with memoization enabled" do
244
+ before do
245
+ subject.memoize = true
246
+ end
247
+
248
+ it "unmemoizes features" do
249
+ cache[:foo] = "bar"
250
+ flipper[:stats].enable
251
+ flipper[:search].disable
252
+ subject.import(Flipper::Adapters::Memory.new)
253
+ expect(cache).to be_empty
254
+ end
255
+ end
256
+
257
+ context "with memoization disabled" do
258
+ before do
259
+ subject.memoize = false
260
+ end
261
+
262
+ it "does not unmemoize features" do
263
+ cache[:foo] = "bar"
264
+ flipper[:stats].enable
265
+ flipper[:search].disable
266
+ subject.import(Flipper::Adapters::Memory.new)
267
+ expect(cache).not_to be_empty
268
+ end
269
+ end
270
+ end
271
+
252
272
  describe '#features' do
253
273
  context 'with memoization enabled' do
254
274
  before do
@@ -18,16 +18,6 @@ RSpec.describe Flipper::Adapters::OperationLogger do
18
18
  expect(output).to match(/@adapter=#<Flipper::Adapters::Memory/)
19
19
  end
20
20
 
21
- it 'forwards missing methods to underlying adapter' do
22
- adapter = Class.new do
23
- def foo
24
- :foo
25
- end
26
- end.new
27
- operation_logger = described_class.new(adapter)
28
- expect(operation_logger.foo).to eq(:foo)
29
- end
30
-
31
21
  describe '#get' do
32
22
  before do
33
23
  @feature = flipper[:stats]
@@ -106,4 +96,33 @@ RSpec.describe Flipper::Adapters::OperationLogger do
106
96
  expect(@result).to eq(adapter.add(@feature))
107
97
  end
108
98
  end
99
+
100
+ describe '#import' do
101
+ before do
102
+ @source = Flipper::Adapters::Memory.new
103
+ @result = subject.import(@source)
104
+ end
105
+
106
+ it 'logs operation' do
107
+ expect(subject.count(:import)).to be(1)
108
+ end
109
+
110
+ it 'returns result' do
111
+ expect(@result).to eq(adapter.import(@source))
112
+ end
113
+ end
114
+
115
+ describe '#export' do
116
+ before do
117
+ @result = subject.export(format: :json, version: 1)
118
+ end
119
+
120
+ it 'logs operation' do
121
+ expect(subject.count(:export)).to be(1)
122
+ end
123
+
124
+ it 'returns result' do
125
+ expect(@result).to eq(adapter.export(format: :json, version: 1))
126
+ end
127
+ end
109
128
  end
@@ -342,10 +342,27 @@ RSpec.describe Flipper::DSL do
342
342
  end
343
343
 
344
344
  describe '#import' do
345
+ context "with flipper instance" do
346
+ it 'delegates to adapter' do
347
+ destination_flipper = build_flipper
348
+ expect(subject.adapter).to receive(:import).with(destination_flipper)
349
+ subject.import(destination_flipper)
350
+ end
351
+ end
352
+
353
+ context "with flipper adapter" do
354
+ it 'delegates to adapter' do
355
+ destination_flipper = build_flipper
356
+ expect(subject.adapter).to receive(:import).with(destination_flipper.adapter)
357
+ subject.import(destination_flipper.adapter)
358
+ end
359
+ end
360
+ end
361
+
362
+ describe "#export" do
345
363
  it 'delegates to adapter' do
346
- destination_flipper = build_flipper
347
- expect(subject.adapter).to receive(:import).with(destination_flipper.adapter)
348
- subject.import(destination_flipper)
364
+ expect(subject.export).to eq(subject.adapter.export)
365
+ expect(subject.export(format: :json)).to eq(subject.adapter.export(format: :json))
349
366
  end
350
367
  end
351
368
 
@@ -0,0 +1,13 @@
1
+ RSpec.describe Flipper::Export do
2
+ it "can initialize" do
3
+ export = described_class.new(contents: "{}", format: :json, version: 1)
4
+ expect(export.contents).to eq("{}")
5
+ expect(export.format).to eq(:json)
6
+ expect(export.version).to eq(1)
7
+ end
8
+
9
+ it "raises not implemented for features" do
10
+ export = described_class.new(contents: "{}", format: :json, version: 1)
11
+ expect { export.features }.to raise_error(NotImplementedError)
12
+ end
13
+ end
@@ -0,0 +1,16 @@
1
+ RSpec.describe Flipper::Exporter do
2
+ describe ".build" do
3
+ it "builds instance of exporter" do
4
+ exporter = described_class.build(format: :json, version: 1)
5
+ expect(exporter).to be_instance_of(Flipper::Exporters::Json::V1)
6
+ end
7
+
8
+ it "raises if format not found" do
9
+ expect { described_class.build(format: :nope, version: 1) }.to raise_error(KeyError)
10
+ end
11
+
12
+ it "raises if version not found" do
13
+ expect { described_class.build(format: :json, version: 0) }.to raise_error(KeyError)
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,60 @@
1
+ require 'flipper/exporters/json/v1'
2
+
3
+ RSpec.describe Flipper::Exporters::Json::Export do
4
+ let(:contents) {
5
+ <<~JSON
6
+ {
7
+ "version":1,
8
+ "features":{
9
+ "search":{"boolean":null,"groups":["admins","employees"],"actors":["User;1","User;100"],"percentage_of_actors":"10","percentage_of_time":"15"},
10
+ "plausible":{"boolean":"true","groups":[],"actors":[],"percentage_of_actors":null,"percentage_of_time":null},
11
+ "google_analytics":{"boolean":null,"groups":[],"actors":[],"percentage_of_actors":null,"percentage_of_time":null}
12
+ }
13
+ }
14
+ JSON
15
+ }
16
+
17
+ it "can initialize" do
18
+ export = described_class.new(contents: contents)
19
+ expect(export.format).to eq(:json)
20
+ expect(export.version).to be(1)
21
+ end
22
+
23
+ it "can initialize with version" do
24
+ export = described_class.new(contents: contents, version: 1)
25
+ expect(export.version).to be(1)
26
+ end
27
+
28
+ it "can build features from contents" do
29
+ export = Flipper::Exporters::Json::Export.new(contents: contents)
30
+ expect(export.features).to eq({
31
+ "search" => {actors: Set["User;1", "User;100"], boolean: nil, groups: Set["admins", "employees"], percentage_of_actors: "10", percentage_of_time: "15"},
32
+ "plausible" => {actors: Set.new, boolean: "true", groups: Set.new, percentage_of_actors: nil, percentage_of_time: nil},
33
+ "google_analytics" => {actors: Set.new, boolean: nil, groups: Set.new, percentage_of_actors: nil, percentage_of_time: nil},
34
+ })
35
+ end
36
+
37
+ it "can build an adapter from features" do
38
+ export = Flipper::Exporters::Json::Export.new(contents: contents)
39
+ expect(export.adapter).to be_instance_of(Flipper::Adapters::Memory)
40
+ expect(export.adapter.get_all).to eq({
41
+ "plausible" => {actors: Set.new, boolean: "true", groups: Set.new, percentage_of_actors: nil, percentage_of_time: nil},
42
+ "search" => {actors: Set["User;1", "User;100"], boolean: nil, groups: Set["admins", "employees"], percentage_of_actors: "10", percentage_of_time: "15"},
43
+ "google_analytics" => {actors: Set.new, boolean: nil, groups: Set.new, percentage_of_actors: nil, percentage_of_time: nil},
44
+ })
45
+ end
46
+
47
+ it "raises for invalid json" do
48
+ export = described_class.new(contents: "bad contents")
49
+ expect {
50
+ export.features
51
+ }.to raise_error(Flipper::Exporters::Json::JsonError)
52
+ end
53
+
54
+ it "raises for missing features key" do
55
+ export = described_class.new(contents: "{}")
56
+ expect {
57
+ export.features
58
+ }.to raise_error(Flipper::Exporters::Json::InvalidError)
59
+ end
60
+ end
@@ -0,0 +1,33 @@
1
+ require 'flipper/exporters/json/v1'
2
+
3
+ RSpec.describe Flipper::Exporters::Json::V1 do
4
+ subject { described_class.new }
5
+
6
+ it "has a version number" do
7
+ adapter = Flipper::Adapters::Memory.new
8
+ export = subject.call(adapter)
9
+ data = JSON.parse(export.contents)
10
+ expect(data["version"]).to eq(1)
11
+ end
12
+
13
+ it "exports features and gates" do
14
+ adapter = Flipper::Adapters::Memory.new
15
+ flipper = Flipper.new(adapter)
16
+ flipper.enable_percentage_of_actors :search, 10
17
+ flipper.enable_percentage_of_time :search, 15
18
+ flipper.enable_actor :search, Flipper::Actor.new('User;1')
19
+ flipper.enable_actor :search, Flipper::Actor.new('User;100')
20
+ flipper.enable_group :search, :admins
21
+ flipper.enable_group :search, :employees
22
+ flipper.enable :plausible
23
+ flipper.disable :google_analytics
24
+
25
+ export = subject.call(adapter)
26
+
27
+ expect(export.features).to eq({
28
+ "google_analytics" => {actors: Set.new, boolean: nil, groups: Set.new, percentage_of_actors: nil, percentage_of_time: nil},
29
+ "plausible" => {actors: Set.new, boolean: "true", groups: Set.new, percentage_of_actors: nil, percentage_of_time: nil},
30
+ "search" => {actors: Set["User;1", "User;100"], boolean: nil, groups: Set["admins", "employees"], percentage_of_actors: "10", percentage_of_time: "15"},
31
+ })
32
+ end
33
+ end
@@ -2,6 +2,12 @@ require 'logger'
2
2
  require 'flipper/adapters/instrumented'
3
3
  require 'flipper/instrumentation/log_subscriber'
4
4
 
5
+ begin
6
+ require 'active_support/isolated_execution_state'
7
+ rescue LoadError
8
+ # ActiveSupport::IsolatedExecutionState is only available in Rails 5.2+
9
+ end
10
+
5
11
  RSpec.describe Flipper::Instrumentation::LogSubscriber do
6
12
  let(:adapter) do
7
13
  memory = Flipper::Adapters::Memory.new
@@ -26,6 +32,10 @@ RSpec.describe Flipper::Instrumentation::LogSubscriber do
26
32
  described_class.logger = nil
27
33
  end
28
34
 
35
+ after(:all) do
36
+ ActiveSupport::Notifications.unsubscribe("flipper")
37
+ end
38
+
29
39
  let(:log) { @io.string }
30
40
 
31
41
  context 'feature enabled checks' do
@@ -2,6 +2,12 @@ require 'flipper/adapters/instrumented'
2
2
  require 'flipper/instrumentation/statsd'
3
3
  require 'statsd'
4
4
 
5
+ begin
6
+ require 'active_support/isolated_execution_state'
7
+ rescue LoadError
8
+ # ActiveSupport::IsolatedExecutionState is only available in Rails 5.2+
9
+ end
10
+
5
11
  RSpec.describe Flipper::Instrumentation::StatsdSubscriber do
6
12
  let(:statsd_client) { Statsd.new }
7
13
  let(:socket) { FakeUDPSocket.new }
@@ -25,6 +31,10 @@ RSpec.describe Flipper::Instrumentation::StatsdSubscriber do
25
31
  Thread.current[:statsd_socket] = nil
26
32
  end
27
33
 
34
+ after(:all) do
35
+ ActiveSupport::Notifications.unsubscribe("flipper")
36
+ end
37
+
28
38
  def assert_timer(metric)
29
39
  regex = /#{Regexp.escape metric}\:\d+\|ms/
30
40
  result = socket.buffer.detect { |op| op.first =~ regex }
@@ -114,4 +114,83 @@ RSpec.describe Flipper::Typecast do
114
114
  described_class.to_set('asdf')
115
115
  end.to raise_error(ArgumentError, %("asdf" cannot be converted to a set))
116
116
  end
117
+
118
+ describe "#features_hash" do
119
+ it "returns new hash" do
120
+ hash = {
121
+ "search" => {
122
+ boolean: nil,
123
+ }
124
+ }
125
+ result = described_class.features_hash(hash)
126
+ expect(result).not_to be(hash)
127
+ expect(result["search"]).not_to be(hash["search"])
128
+ end
129
+
130
+ it "converts gate value arrays to sets" do
131
+ hash = {
132
+ "search" => {
133
+ boolean: nil,
134
+ groups: ['a', 'b'],
135
+ actors: ['User;1'],
136
+ percentage_of_actors: nil,
137
+ percentage_of_time: nil,
138
+ },
139
+ }
140
+ result = described_class.features_hash(hash)
141
+ expect(result).to eq({
142
+ "search" => {
143
+ boolean: nil,
144
+ groups: Set['a', 'b'],
145
+ actors: Set['User;1'],
146
+ percentage_of_actors: nil,
147
+ percentage_of_time: nil,
148
+ },
149
+ })
150
+ end
151
+
152
+ it "converts gate value boolean and integers to strings" do
153
+ hash = {
154
+ "search" => {
155
+ boolean: true,
156
+ groups: Set.new,
157
+ actors: Set.new,
158
+ percentage_of_actors: 10,
159
+ percentage_of_time: 15,
160
+ },
161
+ }
162
+ result = described_class.features_hash(hash)
163
+ expect(result).to eq({
164
+ "search" => {
165
+ boolean: "true",
166
+ groups: Set.new,
167
+ actors: Set.new,
168
+ percentage_of_actors: "10",
169
+ percentage_of_time: "15",
170
+ },
171
+ })
172
+ end
173
+
174
+ it "converts string gate keys to symbols" do
175
+ hash = {
176
+ "search" => {
177
+ "boolean" => nil,
178
+ "groups" => Set.new,
179
+ "actors" => Set.new,
180
+ "percentage_of_actors" => nil,
181
+ "percentage_of_time" => nil,
182
+ },
183
+ }
184
+ result = described_class.features_hash(hash)
185
+ expect(result).to eq({
186
+ "search" => {
187
+ boolean: nil,
188
+ groups: Set.new,
189
+ actors: Set.new,
190
+ percentage_of_actors: nil,
191
+ percentage_of_time: nil,
192
+ },
193
+ })
194
+ end
195
+ end
117
196
  end
data/spec/flipper_spec.rb CHANGED
@@ -202,6 +202,12 @@ RSpec.describe Flipper do
202
202
  expect(described_class.enabled?(:search)).to be(true)
203
203
  end
204
204
 
205
+ it 'delegates export to instance' do
206
+ described_class.enable(:search)
207
+ expect(described_class.export).to eq(described_class.adapter.export)
208
+ expect(described_class.export(format: :json)).to eq(described_class.adapter.export(format: :json))
209
+ end
210
+
205
211
  it 'delegates adapter to instance' do
206
212
  expect(described_class.adapter).to eq(described_class.instance.adapter)
207
213
  end
@@ -222,7 +228,7 @@ RSpec.describe Flipper do
222
228
  end
223
229
 
224
230
  it 'delegates sync stuff to instance for Flipper::Cloud' do
225
- stub = stub_request(:get, "https://www.flippercloud.io/adapter/features").
231
+ stub = stub_request(:get, "https://www.flippercloud.io/adapter/features?exclude_gate_names=true").
226
232
  with({
227
233
  headers: {
228
234
  'Flipper-Cloud-Token'=>'asdf',
@@ -0,0 +1,18 @@
1
+ RSpec.configure do |config|
2
+ config.before(:all) do
3
+ $skip = false
4
+ end
5
+
6
+ def skip_on_error(error, message, &block)
7
+ # Premptively skip if we've already skipped
8
+ skip(message) if $skip
9
+ block.call
10
+ rescue error
11
+ if ENV["CI"]
12
+ raise
13
+ else
14
+ $skip = true
15
+ skip(message)
16
+ end
17
+ end
18
+ end