flipper 0.26.2 → 0.27.1

Sign up to get free protection for your applications and to get access to all the features.
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