flipper 0.22.0 → 0.28.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.
Files changed (139) hide show
  1. checksums.yaml +4 -4
  2. data/.codeclimate.yml +1 -0
  3. data/.github/dependabot.yml +6 -0
  4. data/.github/workflows/ci.yml +26 -20
  5. data/.github/workflows/examples.yml +62 -0
  6. data/.rspec +1 -0
  7. data/.tool-versions +1 -0
  8. data/Changelog.md +152 -3
  9. data/Dockerfile +1 -1
  10. data/Gemfile +9 -6
  11. data/README.md +15 -67
  12. data/Rakefile +4 -2
  13. data/benchmark/enabled_ips.rb +10 -0
  14. data/benchmark/enabled_multiple_actors_ips.rb +20 -0
  15. data/benchmark/enabled_profile.rb +20 -0
  16. data/benchmark/instrumentation_ips.rb +21 -0
  17. data/benchmark/typecast_ips.rb +19 -0
  18. data/docker-compose.yml +37 -34
  19. data/docs/README.md +1 -0
  20. data/docs/images/banner.jpg +0 -0
  21. data/examples/api/basic.ru +3 -4
  22. data/examples/api/custom_memoized.ru +3 -4
  23. data/examples/api/memoized.ru +3 -4
  24. data/examples/dsl.rb +3 -3
  25. data/examples/enabled_for_actor.rb +4 -2
  26. data/examples/instrumentation.rb +1 -0
  27. data/examples/instrumentation_last_accessed_at.rb +38 -0
  28. data/flipper.gemspec +2 -2
  29. data/lib/flipper/actor.rb +4 -0
  30. data/lib/flipper/adapter.rb +23 -7
  31. data/lib/flipper/adapters/dual_write.rb +10 -16
  32. data/lib/flipper/adapters/failover.rb +83 -0
  33. data/lib/flipper/adapters/failsafe.rb +76 -0
  34. data/lib/flipper/adapters/http/client.rb +18 -12
  35. data/lib/flipper/adapters/http/error.rb +19 -1
  36. data/lib/flipper/adapters/http.rb +14 -4
  37. data/lib/flipper/adapters/instrumented.rb +25 -2
  38. data/lib/flipper/adapters/memoizable.rb +27 -18
  39. data/lib/flipper/adapters/memory.rb +56 -39
  40. data/lib/flipper/adapters/operation_logger.rb +16 -3
  41. data/lib/flipper/adapters/poll/poller.rb +2 -0
  42. data/lib/flipper/adapters/poll.rb +39 -0
  43. data/lib/flipper/adapters/pstore.rb +2 -5
  44. data/lib/flipper/adapters/sync/interval_synchronizer.rb +1 -6
  45. data/lib/flipper/adapters/sync/synchronizer.rb +2 -1
  46. data/lib/flipper/adapters/sync.rb +9 -15
  47. data/lib/flipper/dsl.rb +9 -11
  48. data/lib/flipper/errors.rb +3 -20
  49. data/lib/flipper/export.rb +26 -0
  50. data/lib/flipper/exporter.rb +17 -0
  51. data/lib/flipper/exporters/json/export.rb +32 -0
  52. data/lib/flipper/exporters/json/v1.rb +33 -0
  53. data/lib/flipper/feature.rb +32 -26
  54. data/lib/flipper/feature_check_context.rb +10 -6
  55. data/lib/flipper/gate.rb +12 -11
  56. data/lib/flipper/gate_values.rb +0 -16
  57. data/lib/flipper/gates/actor.rb +10 -17
  58. data/lib/flipper/gates/boolean.rb +1 -1
  59. data/lib/flipper/gates/group.rb +5 -7
  60. data/lib/flipper/gates/percentage_of_actors.rb +10 -13
  61. data/lib/flipper/gates/percentage_of_time.rb +1 -2
  62. data/lib/flipper/identifier.rb +2 -2
  63. data/lib/flipper/instrumentation/log_subscriber.rb +7 -3
  64. data/lib/flipper/instrumentation/subscriber.rb +8 -1
  65. data/lib/flipper/instrumenters/memory.rb +6 -2
  66. data/lib/flipper/metadata.rb +1 -1
  67. data/lib/flipper/middleware/memoizer.rb +2 -12
  68. data/lib/flipper/poller.rb +117 -0
  69. data/lib/flipper/railtie.rb +23 -22
  70. data/lib/flipper/spec/shared_adapter_specs.rb +23 -0
  71. data/lib/flipper/test/shared_adapter_test.rb +24 -0
  72. data/lib/flipper/typecast.rb +28 -15
  73. data/lib/flipper/types/actor.rb +19 -13
  74. data/lib/flipper/types/group.rb +12 -5
  75. data/lib/flipper/version.rb +1 -1
  76. data/lib/flipper.rb +6 -4
  77. data/spec/fixtures/flipper_pstore_1679087600.json +46 -0
  78. data/spec/flipper/actor_spec.rb +10 -2
  79. data/spec/flipper/adapter_spec.rb +29 -4
  80. data/spec/flipper/adapters/dual_write_spec.rb +0 -2
  81. data/spec/flipper/adapters/failover_spec.rb +129 -0
  82. data/spec/flipper/adapters/failsafe_spec.rb +58 -0
  83. data/spec/flipper/adapters/http_spec.rb +64 -6
  84. data/spec/flipper/adapters/instrumented_spec.rb +28 -12
  85. data/spec/flipper/adapters/memoizable_spec.rb +30 -12
  86. data/spec/flipper/adapters/memory_spec.rb +3 -4
  87. data/spec/flipper/adapters/operation_logger_spec.rb +29 -12
  88. data/spec/flipper/adapters/pstore_spec.rb +0 -2
  89. data/spec/flipper/adapters/read_only_spec.rb +0 -1
  90. data/spec/flipper/adapters/sync/feature_synchronizer_spec.rb +0 -1
  91. data/spec/flipper/adapters/sync/interval_synchronizer_spec.rb +4 -5
  92. data/spec/flipper/adapters/sync/synchronizer_spec.rb +0 -1
  93. data/spec/flipper/adapters/sync_spec.rb +0 -2
  94. data/spec/flipper/configuration_spec.rb +0 -1
  95. data/spec/flipper/dsl_spec.rb +38 -12
  96. data/spec/flipper/export_spec.rb +13 -0
  97. data/spec/flipper/exporter_spec.rb +16 -0
  98. data/spec/flipper/exporters/json/export_spec.rb +60 -0
  99. data/spec/flipper/exporters/json/v1_spec.rb +33 -0
  100. data/spec/flipper/feature_check_context_spec.rb +17 -19
  101. data/spec/flipper/feature_spec.rb +76 -33
  102. data/spec/flipper/gate_spec.rb +0 -2
  103. data/spec/flipper/gate_values_spec.rb +2 -34
  104. data/spec/flipper/gates/actor_spec.rb +0 -2
  105. data/spec/flipper/gates/boolean_spec.rb +1 -3
  106. data/spec/flipper/gates/group_spec.rb +2 -5
  107. data/spec/flipper/gates/percentage_of_actors_spec.rb +61 -7
  108. data/spec/flipper/gates/percentage_of_time_spec.rb +2 -4
  109. data/spec/flipper/identifier_spec.rb +0 -1
  110. data/spec/flipper/instrumentation/log_subscriber_spec.rb +15 -6
  111. data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +10 -1
  112. data/spec/flipper/instrumenters/memory_spec.rb +18 -1
  113. data/spec/flipper/instrumenters/noop_spec.rb +14 -8
  114. data/spec/flipper/middleware/memoizer_spec.rb +0 -23
  115. data/spec/flipper/middleware/setup_env_spec.rb +0 -2
  116. data/spec/flipper/poller_spec.rb +47 -0
  117. data/spec/flipper/railtie_spec.rb +73 -33
  118. data/spec/flipper/registry_spec.rb +0 -1
  119. data/spec/flipper/typecast_spec.rb +82 -4
  120. data/spec/flipper/types/actor_spec.rb +45 -46
  121. data/spec/flipper/types/boolean_spec.rb +0 -1
  122. data/spec/flipper/types/group_spec.rb +2 -3
  123. data/spec/flipper/types/percentage_of_actors_spec.rb +0 -1
  124. data/spec/flipper/types/percentage_of_time_spec.rb +0 -1
  125. data/spec/flipper/types/percentage_spec.rb +0 -1
  126. data/spec/flipper_integration_spec.rb +62 -51
  127. data/spec/flipper_spec.rb +7 -2
  128. data/spec/{helper.rb → spec_helper.rb} +4 -2
  129. data/spec/support/skippable.rb +18 -0
  130. data/spec/support/spec_helpers.rb +2 -6
  131. metadata +63 -19
  132. data/docs/Adapters.md +0 -124
  133. data/docs/Caveats.md +0 -4
  134. data/docs/Gates.md +0 -167
  135. data/docs/Instrumentation.md +0 -27
  136. data/docs/Optimization.md +0 -137
  137. data/docs/api/README.md +0 -884
  138. data/docs/http/README.md +0 -36
  139. data/docs/read-only/README.md +0 -24
@@ -34,7 +34,16 @@ module Flipper
34
34
  assert_includes @adapter.class.ancestors, Flipper::Adapter
35
35
  end
36
36
 
37
+ def test_knows_how_to_get_adapter_from_source
38
+ adapter = Flipper::Adapters::Memory.new
39
+ flipper = Flipper.new(adapter)
40
+
41
+ assert_includes adapter.class.from(adapter).class.ancestors, Flipper::Adapter
42
+ assert_includes adapter.class.from(flipper).class.ancestors, Flipper::Adapter
43
+ end
44
+
37
45
  def test_returns_correct_default_values_for_gates_if_none_are_enabled
46
+ assert_equal @adapter.class.default_config, @adapter.get(@feature)
38
47
  assert_equal @adapter.default_config, @adapter.get(@feature)
39
48
  end
40
49
 
@@ -300,6 +309,21 @@ module Flipper
300
309
  assert_equal true, @adapter.enable(@feature, @boolean_gate, @flipper.boolean(true))
301
310
  assert_equal @adapter.default_config.merge(boolean: "true"), @adapter.get(@feature)
302
311
  end
312
+
313
+ def test_can_import_and_export
314
+ adapter = Flipper::Adapters::Memory.new
315
+ source_flipper = Flipper.new(adapter)
316
+ source_flipper.enable(:stats)
317
+ export = adapter.export
318
+
319
+ # some adapters cannot import so if they return false lets assert it
320
+ # didn't happen
321
+ if @adapter.import(export)
322
+ assert @flipper[:stats].enabled?
323
+ else
324
+ refute @flipper[:stats].enabled?
325
+ end
326
+ end
303
327
  end
304
328
  end
305
329
  end
@@ -21,11 +21,9 @@ module Flipper
21
21
  # Returns an Integer representation of the value.
22
22
  # Raises ArgumentError if conversion is not possible.
23
23
  def self.to_integer(value)
24
- if value.respond_to?(:to_i)
25
- value.to_i
26
- else
27
- raise ArgumentError, "#{value.inspect} cannot be converted to an integer"
28
- end
24
+ value.to_i
25
+ rescue NoMethodError
26
+ raise ArgumentError, "#{value.inspect} cannot be converted to an integer"
29
27
  end
30
28
 
31
29
  # Internal: Convert value to a float.
@@ -33,11 +31,9 @@ module Flipper
33
31
  # Returns a Float representation of the value.
34
32
  # Raises ArgumentError if conversion is not possible.
35
33
  def self.to_float(value)
36
- if value.respond_to?(:to_f)
37
- value.to_f
38
- else
39
- raise ArgumentError, "#{value.inspect} cannot be converted to a float"
40
- end
34
+ value.to_f
35
+ rescue NoMethodError
36
+ raise ArgumentError, "#{value.inspect} cannot be converted to a float"
41
37
  end
42
38
 
43
39
  # Internal: Convert value to a percentage.
@@ -45,11 +41,11 @@ module Flipper
45
41
  # Returns a Integer or Float representation of the value.
46
42
  # Raises ArgumentError if conversion is not possible.
47
43
  def self.to_percentage(value)
48
- if value.to_s.include?('.'.freeze)
49
- to_float(value)
50
- else
51
- to_integer(value)
52
- end
44
+ result_to_f = value.to_f
45
+ result_to_i = result_to_f.to_i
46
+ result_to_f == result_to_i ? result_to_i : result_to_f
47
+ rescue NoMethodError
48
+ raise ArgumentError, "#{value.inspect} cannot be converted to a percentage"
53
49
  end
54
50
 
55
51
  # Internal: Convert value to a set.
@@ -66,5 +62,22 @@ module Flipper
66
62
  raise ArgumentError, "#{value.inspect} cannot be converted to a set"
67
63
  end
68
64
  end
65
+
66
+ def self.features_hash(source)
67
+ normalized_source = {}
68
+ (source || {}).each do |feature_key, gates|
69
+ normalized_source[feature_key] ||= {}
70
+ gates.each do |gate_key, value|
71
+ normalized_value = case value
72
+ when Array, Set
73
+ value.to_set
74
+ else
75
+ value ? value.to_s : value
76
+ end
77
+ normalized_source[feature_key][gate_key.to_sym] = normalized_value
78
+ end
79
+ end
80
+ normalized_source
81
+ end
69
82
  end
70
83
  end
@@ -1,30 +1,36 @@
1
1
  module Flipper
2
2
  module Types
3
3
  class Actor < Type
4
- def self.wrappable?(thing)
5
- return false if thing.nil?
6
- thing.respond_to?(:flipper_id)
4
+ def self.wrappable?(actor)
5
+ return false if actor.nil?
6
+ actor.respond_to?(:flipper_id)
7
7
  end
8
8
 
9
- attr_reader :thing
9
+ attr_reader :actor
10
10
 
11
- def initialize(thing)
12
- raise ArgumentError, 'thing cannot be nil' if thing.nil?
11
+ def initialize(actor)
12
+ raise ArgumentError, 'actor cannot be nil' if actor.nil?
13
13
 
14
- unless thing.respond_to?(:flipper_id)
15
- raise ArgumentError, "#{thing.inspect} must respond to flipper_id, but does not"
14
+ unless actor.respond_to?(:flipper_id)
15
+ raise ArgumentError, "#{actor.inspect} must respond to flipper_id, but does not"
16
16
  end
17
17
 
18
- @thing = thing
19
- @value = thing.flipper_id.to_s
18
+ @actor = actor
19
+ @value = actor.flipper_id.to_s
20
20
  end
21
21
 
22
22
  def respond_to?(*args)
23
- super || @thing.respond_to?(*args)
23
+ super || @actor.respond_to?(*args)
24
24
  end
25
25
 
26
- def method_missing(name, *args, &block)
27
- @thing.send name, *args, &block
26
+ if RUBY_VERSION >= '3.0'
27
+ def method_missing(name, *args, **kwargs, &block)
28
+ @actor.send name, *args, **kwargs, &block
29
+ end
30
+ else
31
+ def method_missing(name, *args, &block)
32
+ @actor.send name, *args, &block
33
+ end
28
34
  end
29
35
  end
30
36
  end
@@ -14,20 +14,27 @@ module Flipper
14
14
 
15
15
  if block_given?
16
16
  @block = block
17
- @single_argument = @block.arity.abs == 1
17
+ @single_argument = call_with_no_context?(@block)
18
18
  else
19
- @block = ->(_thing, _context) { false }
19
+ @block = ->(actor, context) { false }
20
20
  @single_argument = false
21
21
  end
22
22
  end
23
23
 
24
- def match?(thing, context)
24
+ def match?(actor, context)
25
25
  if @single_argument
26
- @block.call(thing)
26
+ @block.call(actor)
27
27
  else
28
- @block.call(thing, context)
28
+ @block.call(actor, context)
29
29
  end
30
30
  end
31
+
32
+ NO_PARAMS_IN_RUBY_3 = [[:req], [:rest]]
33
+ def call_with_no_context?(block)
34
+ return true if block.parameters == NO_PARAMS_IN_RUBY_3
35
+
36
+ block.arity.abs == 1
37
+ end
31
38
  end
32
39
  end
33
40
  end
@@ -1,3 +1,3 @@
1
1
  module Flipper
2
- VERSION = '0.22.0'.freeze
2
+ VERSION = '0.28.0'.freeze
3
3
  end
data/lib/flipper.rb CHANGED
@@ -64,7 +64,7 @@ module Flipper
64
64
  :enable_percentage_of_time, :disable_percentage_of_time,
65
65
  :time, :percentage_of_time,
66
66
  :features, :feature, :[], :preload, :preload_all,
67
- :adapter, :add, :exist?, :remove, :import,
67
+ :adapter, :add, :exist?, :remove, :import, :export,
68
68
  :memoize=, :memoizing?,
69
69
  :sync, :sync_secret # For Flipper::Cloud. Will error for OSS Flipper.
70
70
 
@@ -72,12 +72,12 @@ module Flipper
72
72
  #
73
73
  # name - The Symbol name of the group.
74
74
  # block - The block that should be used to determine if the group matches a
75
- # given thing.
75
+ # given actor.
76
76
  #
77
77
  # Examples
78
78
  #
79
- # Flipper.register(:admins) { |thing|
80
- # thing.respond_to?(:admin?) && thing.admin?
79
+ # Flipper.register(:admins) { |actor|
80
+ # actor.respond_to?(:admin?) && actor.admin?
81
81
  # }
82
82
  #
83
83
  # Returns a Flipper::Group.
@@ -155,6 +155,7 @@ require 'flipper/instrumenters/noop'
155
155
  require 'flipper/identifier'
156
156
  require 'flipper/middleware/memoizer'
157
157
  require 'flipper/middleware/setup_env'
158
+ require 'flipper/poller'
158
159
  require 'flipper/registry'
159
160
  require 'flipper/type'
160
161
  require 'flipper/types/actor'
@@ -164,5 +165,6 @@ require 'flipper/types/percentage'
164
165
  require 'flipper/types/percentage_of_actors'
165
166
  require 'flipper/types/percentage_of_time'
166
167
  require 'flipper/typecast'
168
+ require 'flipper/version'
167
169
 
168
170
  require "flipper/railtie" if defined?(Rails::Railtie)
@@ -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
+ }
@@ -1,5 +1,3 @@
1
- require 'helper'
2
-
3
1
  RSpec.describe Flipper::Actor do
4
2
  it 'initializes with and knows flipper_id' do
5
3
  actor = described_class.new("User;235")
@@ -45,4 +43,14 @@ RSpec.describe Flipper::Actor do
45
43
  expect(actor1.==(actor2)).to be(false)
46
44
  end
47
45
  end
46
+
47
+ describe '#hash' do
48
+ it 'returns a hash-value based on the flipper id' do
49
+ h = {
50
+ described_class.new("User;123") => true
51
+ }
52
+ expect(h).to have_key(described_class.new("User;123"))
53
+ expect(h).not_to have_key(described_class.new("User;456"))
54
+ end
55
+ end
48
56
  end
@@ -1,5 +1,3 @@
1
- require 'helper'
2
-
3
1
  RSpec.describe Flipper::Adapter do
4
2
  let(:source_flipper) { build_flipper }
5
3
  let(:destination_flipper) { build_flipper }
@@ -32,9 +30,9 @@ RSpec.describe Flipper::Adapter do
32
30
  end
33
31
 
34
32
  describe '#import' do
35
- it 'returns nothing' do
33
+ it 'returns true' do
36
34
  result = destination_flipper.import(source_flipper)
37
- expect(result).to be(nil)
35
+ expect(result).to be(true)
38
36
  end
39
37
 
40
38
  it 'can import from one adapter to another' do
@@ -116,5 +114,32 @@ RSpec.describe Flipper::Adapter do
116
114
  destination_flipper.import(source_flipper)
117
115
  expect(destination_flipper.features.map(&:key)).to eq([])
118
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
119
144
  end
120
145
  end
@@ -1,7 +1,5 @@
1
- require 'helper'
2
1
  require 'flipper/adapters/dual_write'
3
2
  require 'flipper/adapters/operation_logger'
4
- require 'flipper/spec/shared_adapter_specs'
5
3
  require 'active_support/notifications'
6
4
 
7
5
  RSpec.describe Flipper::Adapters::DualWrite do
@@ -0,0 +1,129 @@
1
+ require 'dalli'
2
+ require 'flipper/adapters/failover'
3
+ require 'net/http'
4
+ require 'pstore'
5
+ require 'redis'
6
+
7
+ RSpec.describe Flipper::Adapters::Failover do
8
+ subject { described_class.new(primary, secondary, options) }
9
+
10
+ let(:primary) { Flipper::Adapters::Memory.new }
11
+ let(:secondary) { Flipper::Adapters::Memory.new }
12
+ let(:options) { {} }
13
+ let(:flipper) { Flipper.new(subject) }
14
+
15
+ context 'when the primary is a functioning adapter' do
16
+ it_should_behave_like 'a flipper adapter'
17
+
18
+ it 'should not call the secondary' do
19
+ expect(secondary).not_to receive(:features)
20
+ subject.features
21
+ end
22
+
23
+ it 'should not write to secondary' do
24
+ expect(secondary).not_to receive(:add)
25
+ expect(secondary).not_to receive(:enable)
26
+
27
+ flipper[:flag].enable
28
+ end
29
+
30
+ context 'when dual_write is enabled' do
31
+ let(:options) { { dual_write: true } }
32
+
33
+ it_should_behave_like 'a flipper adapter'
34
+
35
+ it 'writes to both primary and secondary' do
36
+ expect(primary).to receive(:add).and_call_original
37
+ expect(primary).to receive(:enable).and_call_original
38
+
39
+ expect(secondary).to receive(:add)
40
+ expect(secondary).to receive(:enable)
41
+
42
+ flipper[:flag].enable
43
+ end
44
+ end
45
+ end
46
+
47
+ context 'when primary fails during read operations' do
48
+ before do
49
+ allow(primary).to receive(:features).and_raise(Redis::ConnectionError)
50
+ allow(primary).to receive(:get).and_raise(Dalli::NetworkError)
51
+ end
52
+
53
+ it 'fails over to the secondary adapter for reads' do
54
+ expect(secondary).to receive(:features)
55
+ subject.features
56
+
57
+ flipper[:flag].enable
58
+ expect(secondary).to receive(:get).and_call_original
59
+ flipper[:flag].enabled?
60
+ end
61
+
62
+ context 'when dual_write is enabled' do
63
+ let(:options) { { dual_write: true } }
64
+
65
+ it_should_behave_like 'a flipper adapter'
66
+ end
67
+ end
68
+
69
+ context 'when primary fails during write operations' do
70
+ before do
71
+ allow(primary).to receive(:add).and_raise(PStore::Error)
72
+ end
73
+
74
+ let(:options) { { dual_write: true } }
75
+
76
+ it 'fails and does not write to secondary adapter' do
77
+ expect(secondary).not_to receive(:add)
78
+ expect(secondary).not_to receive(:enable)
79
+
80
+ expect { flipper[:flag].enable }.to raise_error(PStore::Error)
81
+ end
82
+ end
83
+
84
+ context 'when primary is instrumented and fails' do
85
+ before do
86
+ allow(memory_adapter).to receive(:get).and_raise(Net::ReadTimeout)
87
+ end
88
+
89
+ let(:memory_adapter) { Flipper::Adapters::Memory.new }
90
+ let(:primary) do
91
+ Flipper::Adapters::Instrumented.new(
92
+ memory_adapter,
93
+ instrumenter: instrumenter,
94
+ )
95
+ end
96
+ let(:instrumenter) { Flipper::Instrumenters::Memory.new }
97
+
98
+ it 'logs the raised exception' do
99
+ flipper[:flag].enabled?
100
+
101
+ expect(instrumenter.events.count).to be 1
102
+
103
+ payload = instrumenter.events[0].payload
104
+ expect(payload.keys).to include(:exception, :exception_object)
105
+ expect(payload[:exception_object]).to be_a Net::ReadTimeout
106
+ end
107
+ end
108
+
109
+ context 'when adapter raises a SyntaxError' do
110
+ before do
111
+ allow(primary).to receive(:features).and_raise(SyntaxError)
112
+ end
113
+
114
+ it 'does not rescue this type by default' do
115
+ expect {
116
+ subject.features
117
+ }.to raise_error(SyntaxError)
118
+ end
119
+
120
+ context 'when Failover adapter is configured to catch SyntaxError' do
121
+ let(:options) { { errors: [ SyntaxError ] } }
122
+
123
+ it 'fails over to secondary adapter' do
124
+ expect(secondary).to receive(:features)
125
+ subject.features
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,58 @@
1
+ require 'flipper/adapters/failsafe'
2
+
3
+ RSpec.describe Flipper::Adapters::Failsafe do
4
+ subject { described_class.new(memory_adapter, options) }
5
+
6
+ let(:memory_adapter) { Flipper::Adapters::Memory.new }
7
+ let(:options) { {} }
8
+ let(:flipper) { Flipper.new(subject) }
9
+
10
+ it_should_behave_like 'a flipper adapter'
11
+
12
+ context 'when disaster strikes' do
13
+ before do
14
+ expect(flipper[feature.name].enable).to be(true)
15
+
16
+ (subject.methods - Object.methods).each do |method_name|
17
+ allow(memory_adapter).to receive(method_name).and_raise(IOError)
18
+ end
19
+ end
20
+
21
+ let(:feature) { Flipper::Feature.new(:my_feature, subject) }
22
+
23
+ it { expect(subject.features).to eq(Set.new) }
24
+ it { expect(feature.add).to eq(false) }
25
+ it { expect(feature.remove).to eq(false) }
26
+ it { expect(feature.clear).to eq(false) }
27
+ it { expect(subject.get(feature)).to eq({}) }
28
+ it { expect(subject.get_multi([feature])).to eq({}) }
29
+ it { expect(subject.get_all).to eq({}) }
30
+ it { expect(feature.enable).to eq(false) }
31
+ it { expect(feature.disable).to eq(false) }
32
+
33
+ context 'when used via Flipper' do
34
+ it { expect(flipper.features).to eq(Set.new) }
35
+ it { expect(flipper[feature.name].enabled?).to eq(false) }
36
+ it { expect(flipper[feature.name].enable).to eq(false) }
37
+ it { expect(flipper[feature.name].disable).to eq(false) }
38
+ end
39
+
40
+ context 'when there is a syntax error' do
41
+ let(:test) { flipper[feature.name].enabled? }
42
+
43
+ before do
44
+ expect(memory_adapter).to receive(:get).and_raise(SyntaxError)
45
+ end
46
+
47
+ it 'does not catch this type of error' do
48
+ expect { test }.to raise_error(SyntaxError)
49
+ end
50
+
51
+ context 'when configured to catch SyntaxError' do
52
+ let(:options) { { errors: [SyntaxError] } }
53
+
54
+ it { expect(test).to eq(false) }
55
+ end
56
+ end
57
+ end
58
+ end
@@ -1,7 +1,5 @@
1
- require 'helper'
2
1
  require 'flipper/adapters/http'
3
2
  require 'flipper/adapters/pstore'
4
- require 'flipper/spec/shared_adapter_specs'
5
3
  require 'rack/handler/webrick'
6
4
 
7
5
  FLIPPER_SPEC_API_PORT = ENV.fetch('FLIPPER_SPEC_API_PORT', 9001).to_i
@@ -54,6 +52,28 @@ RSpec.describe Flipper::Adapters::Http do
54
52
  expect(flipper[:search].disable_group(:some_made_up_group)).to be(true)
55
53
  expect(flipper[:search].groups_value).to eq(Set.new)
56
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
57
77
  end
58
78
 
59
79
  it "sends default headers" do
@@ -84,7 +104,7 @@ RSpec.describe Flipper::Adapters::Http do
84
104
 
85
105
  describe "#get_multi" do
86
106
  it "raises error when not successful response" do
87
- 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")
88
108
  .to_return(status: 503, body: "", headers: {})
89
109
 
90
110
  adapter = described_class.new(url: 'http://app.com/flipper')
@@ -96,7 +116,7 @@ RSpec.describe Flipper::Adapters::Http do
96
116
 
97
117
  describe "#get_all" do
98
118
  it "raises error when not successful response" do
99
- stub_request(:get, "http://app.com/flipper/features")
119
+ stub_request(:get, "http://app.com/flipper/features?exclude_gate_names=true")
100
120
  .to_return(status: 503, body: "", headers: {})
101
121
 
102
122
  adapter = described_class.new(url: 'http://app.com/flipper')
@@ -108,7 +128,7 @@ RSpec.describe Flipper::Adapters::Http do
108
128
 
109
129
  describe "#features" do
110
130
  it "raises error when not successful response" do
111
- stub_request(:get, "http://app.com/flipper/features")
131
+ stub_request(:get, "http://app.com/flipper/features?exclude_gate_names=true")
112
132
  .to_return(status: 503, body: "", headers: {})
113
133
 
114
134
  adapter = described_class.new(url: 'http://app.com/flipper')
@@ -159,6 +179,19 @@ RSpec.describe Flipper::Adapters::Http do
159
179
  stub_request(:post, /app.com/)
160
180
  .to_return(status: 503, body: "{}", headers: {})
161
181
 
182
+ adapter = described_class.new(url: 'http://app.com/flipper')
183
+ feature = Flipper::Feature.new(:search, adapter)
184
+ gate = feature.gate(:boolean)
185
+ thing = gate.wrap(true)
186
+ expect {
187
+ adapter.enable(feature, gate, thing)
188
+ }.to raise_error(Flipper::Adapters::Http::Error, "Failed with status: 503")
189
+ end
190
+
191
+ it "doesn't raise json error if body cannot be parsed" do
192
+ stub_request(:post, /app.com/)
193
+ .to_return(status: 503, body: "barf", headers: {})
194
+
162
195
  adapter = described_class.new(url: 'http://app.com/flipper')
163
196
  feature = Flipper::Feature.new(:search, adapter)
164
197
  gate = feature.gate(:boolean)
@@ -167,6 +200,30 @@ RSpec.describe Flipper::Adapters::Http do
167
200
  adapter.enable(feature, gate, thing)
168
201
  }.to raise_error(Flipper::Adapters::Http::Error)
169
202
  end
203
+
204
+ it "includes response information if available when raising error" do
205
+ api_response = {
206
+ "code" => "error",
207
+ "message" => "This feature has reached the limit to the number of " +
208
+ "actors per feature. Check out groups as a more flexible " +
209
+ "way to enable many actors.",
210
+ "more_info" => "https://www.flippercloud.io/docs",
211
+ }
212
+ stub_request(:post, /app.com/)
213
+ .to_return(status: 503, body: JSON.dump(api_response), headers: {})
214
+
215
+ adapter = described_class.new(url: 'http://app.com/flipper')
216
+ feature = Flipper::Feature.new(:search, adapter)
217
+ gate = feature.gate(:boolean)
218
+ thing = gate.wrap(true)
219
+ error_message = "Failed with status: 503\n\nThis feature has reached the " +
220
+ "limit to the number of actors per feature. Check out " +
221
+ "groups as a more flexible way to enable many actors.\n" +
222
+ "https://www.flippercloud.io/docs"
223
+ expect {
224
+ adapter.enable(feature, gate, thing)
225
+ }.to raise_error(Flipper::Adapters::Http::Error, error_message)
226
+ end
170
227
  end
171
228
 
172
229
  describe "#disable" do
@@ -202,7 +259,8 @@ RSpec.describe Flipper::Adapters::Http do
202
259
  let(:feature) { flipper[:feature_panel] }
203
260
 
204
261
  before do
205
- stub_request(:get, %r{\Ahttp://app.com*}).to_return(body: fixture_file('feature.json'))
262
+ stub_request(:get, %r{\Ahttp://app.com*}).
263
+ to_return(body: fixture_file('feature.json'))
206
264
  end
207
265
 
208
266
  it 'allows client to set request headers' do