flipper 0.28.0 → 1.3.6

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 (195) hide show
  1. checksums.yaml +4 -4
  2. data/.github/FUNDING.yml +1 -0
  3. data/.github/workflows/ci.yml +50 -7
  4. data/.github/workflows/examples.yml +52 -10
  5. data/CLAUDE.md +74 -0
  6. data/Changelog.md +1 -526
  7. data/Gemfile +17 -8
  8. data/README.md +31 -27
  9. data/Rakefile +2 -2
  10. data/benchmark/typecast_ips.rb +8 -0
  11. data/docs/images/banner.jpg +0 -0
  12. data/docs/images/flipper_cloud.png +0 -0
  13. data/examples/cloud/app.ru +12 -0
  14. data/examples/cloud/backoff_policy.rb +13 -0
  15. data/examples/cloud/basic.rb +22 -0
  16. data/examples/cloud/cloud_setup.rb +20 -0
  17. data/examples/cloud/forked.rb +36 -0
  18. data/examples/cloud/import.rb +17 -0
  19. data/examples/cloud/threaded.rb +33 -0
  20. data/examples/dsl.rb +0 -14
  21. data/examples/expressions.rb +213 -0
  22. data/examples/mirroring.rb +59 -0
  23. data/examples/strict.rb +18 -0
  24. data/exe/flipper +5 -0
  25. data/flipper-cloud.gemspec +19 -0
  26. data/flipper.gemspec +8 -4
  27. data/lib/flipper/actor.rb +6 -3
  28. data/lib/flipper/adapter.rb +10 -0
  29. data/lib/flipper/adapter_builder.rb +44 -0
  30. data/lib/flipper/adapters/actor_limit.rb +28 -0
  31. data/lib/flipper/adapters/cache_base.rb +143 -0
  32. data/lib/flipper/adapters/dual_write.rb +1 -3
  33. data/lib/flipper/adapters/failover.rb +0 -4
  34. data/lib/flipper/adapters/failsafe.rb +0 -4
  35. data/lib/flipper/adapters/http/client.rb +40 -12
  36. data/lib/flipper/adapters/http/error.rb +2 -2
  37. data/lib/flipper/adapters/http.rb +19 -14
  38. data/lib/flipper/adapters/instrumented.rb +0 -4
  39. data/lib/flipper/adapters/memoizable.rb +14 -19
  40. data/lib/flipper/adapters/memory.rb +37 -19
  41. data/lib/flipper/adapters/operation_logger.rb +18 -92
  42. data/lib/flipper/adapters/poll.rb +16 -3
  43. data/lib/flipper/adapters/pstore.rb +17 -11
  44. data/lib/flipper/adapters/read_only.rb +8 -41
  45. data/lib/flipper/adapters/strict.rb +45 -0
  46. data/lib/flipper/adapters/sync/feature_synchronizer.rb +10 -1
  47. data/lib/flipper/adapters/sync.rb +0 -4
  48. data/lib/flipper/adapters/wrapper.rb +54 -0
  49. data/lib/flipper/cli.rb +263 -0
  50. data/lib/flipper/cloud/configuration.rb +266 -0
  51. data/lib/flipper/cloud/dsl.rb +27 -0
  52. data/lib/flipper/cloud/message_verifier.rb +95 -0
  53. data/lib/flipper/cloud/middleware.rb +63 -0
  54. data/lib/flipper/cloud/routes.rb +14 -0
  55. data/lib/flipper/cloud/telemetry/backoff_policy.rb +96 -0
  56. data/lib/flipper/cloud/telemetry/instrumenter.rb +22 -0
  57. data/lib/flipper/cloud/telemetry/metric.rb +39 -0
  58. data/lib/flipper/cloud/telemetry/metric_storage.rb +30 -0
  59. data/lib/flipper/cloud/telemetry/submitter.rb +100 -0
  60. data/lib/flipper/cloud/telemetry.rb +191 -0
  61. data/lib/flipper/cloud.rb +53 -0
  62. data/lib/flipper/configuration.rb +25 -4
  63. data/lib/flipper/dsl.rb +47 -42
  64. data/lib/flipper/engine.rb +102 -0
  65. data/lib/flipper/export.rb +0 -2
  66. data/lib/flipper/exporters/json/export.rb +1 -1
  67. data/lib/flipper/exporters/json/v1.rb +1 -1
  68. data/lib/flipper/expression/builder.rb +73 -0
  69. data/lib/flipper/expression/constant.rb +25 -0
  70. data/lib/flipper/expression.rb +71 -0
  71. data/lib/flipper/expressions/all.rb +9 -0
  72. data/lib/flipper/expressions/any.rb +9 -0
  73. data/lib/flipper/expressions/boolean.rb +9 -0
  74. data/lib/flipper/expressions/comparable.rb +13 -0
  75. data/lib/flipper/expressions/duration.rb +28 -0
  76. data/lib/flipper/expressions/equal.rb +9 -0
  77. data/lib/flipper/expressions/greater_than.rb +9 -0
  78. data/lib/flipper/expressions/greater_than_or_equal_to.rb +9 -0
  79. data/lib/flipper/expressions/less_than.rb +9 -0
  80. data/lib/flipper/expressions/less_than_or_equal_to.rb +9 -0
  81. data/lib/flipper/expressions/not_equal.rb +9 -0
  82. data/lib/flipper/expressions/now.rb +9 -0
  83. data/lib/flipper/expressions/number.rb +9 -0
  84. data/lib/flipper/expressions/percentage.rb +9 -0
  85. data/lib/flipper/expressions/percentage_of_actors.rb +12 -0
  86. data/lib/flipper/expressions/property.rb +9 -0
  87. data/lib/flipper/expressions/random.rb +9 -0
  88. data/lib/flipper/expressions/string.rb +9 -0
  89. data/lib/flipper/expressions/time.rb +9 -0
  90. data/lib/flipper/feature.rb +63 -1
  91. data/lib/flipper/gate.rb +2 -1
  92. data/lib/flipper/gate_values.rb +5 -2
  93. data/lib/flipper/gates/expression.rb +75 -0
  94. data/lib/flipper/instrumentation/log_subscriber.rb +28 -5
  95. data/lib/flipper/instrumentation/statsd.rb +4 -2
  96. data/lib/flipper/instrumentation/statsd_subscriber.rb +2 -4
  97. data/lib/flipper/instrumentation/subscriber.rb +0 -4
  98. data/lib/flipper/metadata.rb +8 -1
  99. data/lib/flipper/middleware/memoizer.rb +30 -14
  100. data/lib/flipper/model/active_record.rb +23 -0
  101. data/lib/flipper/poller.rb +10 -9
  102. data/lib/flipper/serializers/gzip.rb +22 -0
  103. data/lib/flipper/serializers/json.rb +17 -0
  104. data/lib/flipper/spec/shared_adapter_specs.rb +82 -63
  105. data/lib/flipper/test/shared_adapter_test.rb +77 -58
  106. data/lib/flipper/test_help.rb +43 -0
  107. data/lib/flipper/typecast.rb +37 -9
  108. data/lib/flipper/types/percentage.rb +1 -1
  109. data/lib/flipper/version.rb +11 -1
  110. data/lib/flipper.rb +44 -7
  111. data/lib/generators/flipper/setup_generator.rb +68 -0
  112. data/lib/generators/flipper/templates/initializer.rb +45 -0
  113. data/lib/generators/flipper/templates/update/migrations/01_create_flipper_tables.rb.erb +22 -0
  114. data/lib/generators/flipper/templates/update/migrations/02_change_flipper_gates_value_to_text.rb.erb +18 -0
  115. data/lib/generators/flipper/update_generator.rb +35 -0
  116. data/package-lock.json +41 -0
  117. data/package.json +10 -0
  118. data/spec/fixtures/environment.rb +1 -0
  119. data/spec/flipper/adapter_builder_spec.rb +72 -0
  120. data/spec/flipper/adapter_spec.rb +1 -0
  121. data/spec/flipper/adapters/actor_limit_spec.rb +20 -0
  122. data/spec/flipper/adapters/dual_write_spec.rb +2 -2
  123. data/spec/flipper/adapters/http/client_spec.rb +61 -0
  124. data/spec/flipper/adapters/http_spec.rb +135 -74
  125. data/spec/flipper/adapters/instrumented_spec.rb +1 -1
  126. data/spec/flipper/adapters/memoizable_spec.rb +21 -21
  127. data/spec/flipper/adapters/memory_spec.rb +11 -2
  128. data/spec/flipper/adapters/operation_logger_spec.rb +2 -2
  129. data/spec/flipper/adapters/poll_spec.rb +41 -0
  130. data/spec/flipper/adapters/read_only_spec.rb +32 -17
  131. data/spec/flipper/adapters/strict_spec.rb +64 -0
  132. data/spec/flipper/adapters/sync/feature_synchronizer_spec.rb +27 -0
  133. data/spec/flipper/cli_spec.rb +166 -0
  134. data/spec/flipper/cloud/configuration_spec.rb +251 -0
  135. data/spec/flipper/cloud/dsl_spec.rb +82 -0
  136. data/spec/flipper/cloud/message_verifier_spec.rb +104 -0
  137. data/spec/flipper/cloud/middleware_spec.rb +289 -0
  138. data/spec/flipper/cloud/telemetry/backoff_policy_spec.rb +107 -0
  139. data/spec/flipper/cloud/telemetry/metric_spec.rb +87 -0
  140. data/spec/flipper/cloud/telemetry/metric_storage_spec.rb +58 -0
  141. data/spec/flipper/cloud/telemetry/submitter_spec.rb +145 -0
  142. data/spec/flipper/cloud/telemetry_spec.rb +208 -0
  143. data/spec/flipper/cloud_spec.rb +186 -0
  144. data/spec/flipper/configuration_spec.rb +17 -0
  145. data/spec/flipper/dsl_spec.rb +34 -73
  146. data/spec/flipper/engine_spec.rb +374 -0
  147. data/spec/flipper/exporters/json/v1_spec.rb +3 -3
  148. data/spec/flipper/expression/builder_spec.rb +248 -0
  149. data/spec/flipper/expression_spec.rb +188 -0
  150. data/spec/flipper/expressions/all_spec.rb +15 -0
  151. data/spec/flipper/expressions/any_spec.rb +15 -0
  152. data/spec/flipper/expressions/boolean_spec.rb +15 -0
  153. data/spec/flipper/expressions/duration_spec.rb +43 -0
  154. data/spec/flipper/expressions/equal_spec.rb +24 -0
  155. data/spec/flipper/expressions/greater_than_or_equal_to_spec.rb +28 -0
  156. data/spec/flipper/expressions/greater_than_spec.rb +28 -0
  157. data/spec/flipper/expressions/less_than_or_equal_to_spec.rb +28 -0
  158. data/spec/flipper/expressions/less_than_spec.rb +32 -0
  159. data/spec/flipper/expressions/not_equal_spec.rb +15 -0
  160. data/spec/flipper/expressions/now_spec.rb +11 -0
  161. data/spec/flipper/expressions/number_spec.rb +21 -0
  162. data/spec/flipper/expressions/percentage_of_actors_spec.rb +20 -0
  163. data/spec/flipper/expressions/percentage_spec.rb +15 -0
  164. data/spec/flipper/expressions/property_spec.rb +13 -0
  165. data/spec/flipper/expressions/random_spec.rb +9 -0
  166. data/spec/flipper/expressions/string_spec.rb +11 -0
  167. data/spec/flipper/expressions/time_spec.rb +13 -0
  168. data/spec/flipper/feature_spec.rb +380 -10
  169. data/spec/flipper/gate_values_spec.rb +2 -2
  170. data/spec/flipper/gates/expression_spec.rb +108 -0
  171. data/spec/flipper/identifier_spec.rb +4 -5
  172. data/spec/flipper/instrumentation/log_subscriber_spec.rb +10 -2
  173. data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +16 -2
  174. data/spec/flipper/middleware/memoizer_spec.rb +79 -10
  175. data/spec/flipper/model/active_record_spec.rb +72 -0
  176. data/spec/flipper/serializers/gzip_spec.rb +13 -0
  177. data/spec/flipper/serializers/json_spec.rb +13 -0
  178. data/spec/flipper/typecast_spec.rb +43 -7
  179. data/spec/flipper/types/actor_spec.rb +18 -1
  180. data/spec/flipper_integration_spec.rb +114 -16
  181. data/spec/flipper_spec.rb +87 -29
  182. data/spec/spec_helper.rb +17 -17
  183. data/spec/support/actor_names.yml +1 -0
  184. data/spec/support/fail_on_output.rb +8 -0
  185. data/spec/support/fake_backoff_policy.rb +15 -0
  186. data/spec/support/spec_helpers.rb +34 -8
  187. data/test/adapters/actor_limit_test.rb +20 -0
  188. data/test_rails/generators/flipper/setup_generator_test.rb +69 -0
  189. data/test_rails/generators/flipper/update_generator_test.rb +96 -0
  190. data/test_rails/helper.rb +22 -2
  191. data/test_rails/system/test_help_test.rb +52 -0
  192. metadata +179 -19
  193. data/.tool-versions +0 -1
  194. data/lib/flipper/railtie.rb +0 -47
  195. data/spec/flipper/railtie_spec.rb +0 -109
data/spec/flipper_spec.rb CHANGED
@@ -64,7 +64,12 @@ RSpec.describe Flipper do
64
64
 
65
65
  describe "delegation to instance" do
66
66
  let(:group) { Flipper::Types::Group.new(:admins) }
67
- let(:actor) { Flipper::Actor.new("1") }
67
+ let(:actor) {
68
+ Flipper::Actor.new("1", {
69
+ "plan" => "basic",
70
+ })
71
+ }
72
+ let(:expression) { Flipper.property(:plan).eq("basic") }
68
73
 
69
74
  before do
70
75
  described_class.configure do |config|
@@ -88,12 +93,35 @@ RSpec.describe Flipper do
88
93
  expect(described_class.instance.enabled?(:search)).to be(false)
89
94
  end
90
95
 
91
- it 'delegates bool to instance' do
92
- expect(described_class.bool).to eq(described_class.instance.bool)
96
+ it 'delegates expression to instance' do
97
+ expect(described_class.expression(:search)).to be(nil)
98
+
99
+ expression = Flipper.property(:plan).eq("basic")
100
+ Flipper.instance.enable_expression :search, expression
101
+
102
+ expect(described_class.expression(:search)).to eq(expression)
103
+ end
104
+
105
+ it 'delegates enable_expression to instance' do
106
+ described_class.enable_expression(:search, expression)
107
+ expect(described_class.instance.enabled?(:search, actor)).to be(true)
108
+ end
109
+
110
+ it 'delegates disable_expression to instance' do
111
+ described_class.disable_expression(:search)
112
+ expect(described_class.instance.enabled?(:search, actor)).to be(false)
113
+ end
114
+
115
+ it 'delegates add_expression to instance' do
116
+ described_class.add_expression(:search, expression)
117
+ expect(described_class.instance.enabled?(:search, actor)).to be(true)
93
118
  end
94
119
 
95
- it 'delegates boolean to instance' do
96
- expect(described_class.boolean).to eq(described_class.instance.boolean)
120
+ it 'delegates remove_expression to instance' do
121
+ described_class.enable_expression(:search, Flipper.any(expression))
122
+ expect(described_class.instance.enabled?(:search, actor)).to be(true)
123
+ described_class.remove_expression(:search, expression)
124
+ expect(described_class.instance.enabled?(:search, actor)).to be(false)
97
125
  end
98
126
 
99
127
  it 'delegates enable_actor to instance' do
@@ -106,10 +134,6 @@ RSpec.describe Flipper do
106
134
  expect(described_class.instance.enabled?(:search, actor)).to be(false)
107
135
  end
108
136
 
109
- it 'delegates actor to instance' do
110
- expect(described_class.actor(actor)).to eq(described_class.instance.actor(actor))
111
- end
112
-
113
137
  it 'delegates enable_group to instance' do
114
138
  described_class.enable_group(:search, group)
115
139
  expect(described_class.instance[:search].enabled_groups).to include(group)
@@ -130,15 +154,6 @@ RSpec.describe Flipper do
130
154
  expect(described_class.instance[:search].percentage_of_actors_value).to be(0)
131
155
  end
132
156
 
133
- it 'delegates actors to instance' do
134
- expect(described_class.actors(5)).to eq(described_class.instance.actors(5))
135
- end
136
-
137
- it 'delegates percentage_of_actors to instance' do
138
- expected = described_class.instance.percentage_of_actors(5)
139
- expect(described_class.percentage_of_actors(5)).to eq(expected)
140
- end
141
-
142
157
  it 'delegates enable_percentage_of_time to instance' do
143
158
  described_class.enable_percentage_of_time(:search, 5)
144
159
  expect(described_class.instance[:search].percentage_of_time_value).to be(5)
@@ -149,15 +164,6 @@ RSpec.describe Flipper do
149
164
  expect(described_class.instance[:search].percentage_of_time_value).to be(0)
150
165
  end
151
166
 
152
- it 'delegates time to instance' do
153
- expect(described_class.time(56)).to eq(described_class.instance.time(56))
154
- end
155
-
156
- it 'delegates percentage_of_time to instance' do
157
- expected = described_class.instance.percentage_of_time(56)
158
- expect(described_class.percentage_of_time(56)).to eq(expected)
159
- end
160
-
161
167
  it 'delegates features to instance' do
162
168
  described_class.instance.add(:search)
163
169
  expect(described_class.features).to eq(described_class.instance.features)
@@ -222,6 +228,10 @@ RSpec.describe Flipper do
222
228
  expect(described_class.memoizing?).to eq(described_class.adapter.memoizing?)
223
229
  end
224
230
 
231
+ it 'delegates read_only? to instance' do
232
+ expect(described_class.read_only?).to eq(described_class.adapter.read_only?)
233
+ end
234
+
225
235
  it 'delegates sync stuff to instance and does nothing' do
226
236
  expect(described_class.sync).to be(nil)
227
237
  expect(described_class.sync_secret).to be(nil)
@@ -231,7 +241,7 @@ RSpec.describe Flipper do
231
241
  stub = stub_request(:get, "https://www.flippercloud.io/adapter/features?exclude_gate_names=true").
232
242
  with({
233
243
  headers: {
234
- 'Flipper-Cloud-Token'=>'asdf',
244
+ 'flipper-cloud-token'=>'asdf',
235
245
  },
236
246
  }).to_return(status: 200, body: '{"features": {}}', headers: {})
237
247
  cloud_configuration = Flipper::Cloud::Configuration.new({
@@ -303,7 +313,7 @@ RSpec.describe Flipper do
303
313
 
304
314
  describe '.group_exists' do
305
315
  it 'returns true if the group is already created' do
306
- group = described_class.register('admins', &:admin?)
316
+ described_class.register('admins', &:admin?)
307
317
  expect(described_class.group_exists?(:admins)).to eq(true)
308
318
  end
309
319
 
@@ -356,4 +366,52 @@ RSpec.describe Flipper do
356
366
  expect(described_class.instance_variable_get('@groups_registry')).to eq(registry)
357
367
  end
358
368
  end
369
+
370
+ describe ".constant" do
371
+ it "returns Flipper::Expression::Constant instance" do
372
+ expect(described_class.constant(false)).to eq(Flipper::Expression::Constant.new(false))
373
+ expect(described_class.constant("string")).to eq(Flipper::Expression::Constant.new("string"))
374
+ end
375
+ end
376
+
377
+ describe ".property" do
378
+ it "returns Flipper::Expressions::Property expression" do
379
+ expect(Flipper.property("name")).to eq(Flipper::Expression.build(Property: "name"))
380
+ end
381
+ end
382
+
383
+ describe ".boolean" do
384
+ it "returns Flipper::Expressions::Boolean expression" do
385
+ expect(described_class.boolean(true)).to eq(Flipper::Expression.build(Boolean: true))
386
+ expect(described_class.boolean(false)).to eq(Flipper::Expression.build(Boolean: false))
387
+ end
388
+ end
389
+
390
+ describe ".random" do
391
+ it "returns Flipper::Expressions::Random expression" do
392
+ expect(Flipper.random(100)).to eq(Flipper::Expression.build(Random: 100))
393
+ end
394
+ end
395
+
396
+ describe ".any" do
397
+ let(:age_expression) { Flipper.property(:age).gte(21) }
398
+ let(:plan_expression) { Flipper.property(:plan).eq("basic") }
399
+
400
+ it "returns Flipper::Expressions::Any instance" do
401
+ expect(Flipper.any(age_expression, plan_expression)).to eq(
402
+ Flipper::Expression.build({Any: [age_expression, plan_expression]})
403
+ )
404
+ end
405
+ end
406
+
407
+ describe ".all" do
408
+ let(:age_expression) { Flipper.property(:age).gte(21) }
409
+ let(:plan_expression) { Flipper.property(:plan).eq("basic") }
410
+
411
+ it "returns Flipper::Expressions::All instance" do
412
+ expect(Flipper.all(age_expression, plan_expression)).to eq(
413
+ Flipper::Expression.build({All: [age_expression, plan_expression]})
414
+ )
415
+ end
416
+ end
359
417
  end
data/spec/spec_helper.rb CHANGED
@@ -2,14 +2,17 @@ $LOAD_PATH.unshift(File.expand_path('../../lib', __FILE__))
2
2
 
3
3
  require 'pp'
4
4
  require 'pathname'
5
- FlipperRoot = Pathname(__FILE__).dirname.join('..').expand_path
6
-
7
- require 'rubygems'
8
- require 'bundler'
5
+ require 'bundler/setup'
9
6
 
10
- Bundler.setup(:default)
7
+ require 'warning'
8
+ Warning.ignore(/lib\/statsd/)
9
+ Warning.ignore(/lib\/debug\//)
10
+ Warning.ignore(/lib\/ice_age\//)
11
+ Warning.ignore(/lib\/moneta\//)
12
+ Warning.ignore(/lib\/mongo\//)
11
13
 
12
14
  require 'debug'
15
+ require 'statsd'
13
16
  require 'webmock/rspec'
14
17
  WebMock.disable_net_connect!(allow_localhost: true)
15
18
 
@@ -17,11 +20,20 @@ require 'flipper'
17
20
  require 'flipper/api'
18
21
  require 'flipper/spec/shared_adapter_specs'
19
22
  require 'flipper/ui'
23
+ require 'flipper/test_help'
20
24
 
25
+ FlipperRoot = Pathname(__FILE__).dirname.join('..').expand_path
21
26
  Dir[FlipperRoot.join('spec/support/**/*.rb')].sort.each { |f| require f }
22
27
 
28
+ # Disable telemetry logging in specs.
29
+ ENV["FLIPPER_CLOUD_LOGGING_ENABLED"] = "false"
30
+
23
31
  RSpec.configure do |config|
24
32
  config.before(:example) do
33
+ # default stub for telemetry
34
+ stub_request(:post, "https://www.flippercloud.io/adapter/telemetry").
35
+ to_return(status: 200, body: "", headers: {})
36
+ Flipper::Cloud::Telemetry.reset if defined?(Flipper::Cloud::Telemetry) && Flipper::Cloud::Telemetry.respond_to?(:reset)
25
37
  Flipper::Poller.reset if defined?(Flipper::Poller)
26
38
  Flipper.unregister_groups
27
39
  Flipper.configuration = nil
@@ -91,15 +103,3 @@ RSpec.shared_examples_for 'a DSL feature' do
91
103
  end.to raise_error(ArgumentError, /must be a String or Symbol/)
92
104
  end
93
105
  end
94
-
95
- RSpec.shared_examples_for 'a DSL boolean method' do
96
- it 'returns boolean with value set' do
97
- result = subject.send(method_name, true)
98
- expect(result).to be_instance_of(Flipper::Types::Boolean)
99
- expect(result.value).to be(true)
100
-
101
- result = subject.send(method_name, false)
102
- expect(result).to be_instance_of(Flipper::Types::Boolean)
103
- expect(result.value).to be(false)
104
- end
105
- end
@@ -0,0 +1 @@
1
+ actor_name_1: 'Actor #1'
@@ -0,0 +1,8 @@
1
+ if ENV["CI"] || ENV["FAIL_ON_OUTPUT"]
2
+ RSpec.configure do |config|
3
+ config.around do |example|
4
+ output = capture_output { example.run }
5
+ fail "Use `silence { }` to avoid printing to STDOUT/STDERR\n#{output}" unless output.empty?
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,15 @@
1
+ class FakeBackoffPolicy
2
+ def initialize
3
+ @retries = 0
4
+ end
5
+
6
+ attr_reader :retries
7
+
8
+ def next_interval
9
+ @retries += 1
10
+ 0
11
+ end
12
+
13
+ def reset
14
+ end
15
+ end
@@ -1,8 +1,11 @@
1
1
  require 'ice_age'
2
2
  require 'json'
3
3
  require 'rack/test'
4
+ require 'rack/session'
4
5
 
5
6
  module SpecHelpers
7
+ extend self
8
+
6
9
  def self.included(base)
7
10
  base.let(:flipper) { build_flipper }
8
11
  base.let(:app) { build_app(flipper) }
@@ -10,7 +13,7 @@ module SpecHelpers
10
13
 
11
14
  def build_app(flipper, options = {})
12
15
  Flipper::UI.app(flipper, options) do |builder|
13
- builder.use Rack::Session::Cookie, secret: 'test'
16
+ builder.use Rack::Session::Cookie, secret: 'test' * 16 # Rack 3+ wants a 64-character secret
14
17
  end
15
18
  end
16
19
 
@@ -27,7 +30,11 @@ module SpecHelpers
27
30
  end
28
31
 
29
32
  def json_response
30
- JSON.parse(last_response.body)
33
+ body = last_response.body
34
+ if last_response["content-encoding"] == 'gzip'
35
+ body = Flipper::Typecast.from_gzip(body)
36
+ end
37
+ JSON.parse(body)
31
38
  end
32
39
 
33
40
  def api_error_code_reference_url
@@ -42,6 +49,14 @@ module SpecHelpers
42
49
  }
43
50
  end
44
51
 
52
+ def api_positive_percentage_error_response
53
+ {
54
+ 'code' => 3,
55
+ 'message' => 'Percentage must be a positive number less than or equal to 100.',
56
+ 'more_info' => api_error_code_reference_url,
57
+ }
58
+ end
59
+
45
60
  def api_flipper_id_is_missing_response
46
61
  {
47
62
  'code' => 4,
@@ -50,10 +65,10 @@ module SpecHelpers
50
65
  }
51
66
  end
52
67
 
53
- def api_positive_percentage_error_response
68
+ def api_expression_invalid_response
54
69
  {
55
- 'code' => 3,
56
- 'message' => 'Percentage must be a positive number less than or equal to 100.',
70
+ 'code' => 7,
71
+ 'message' => 'The provided expression was not valid.',
57
72
  'more_info' => api_error_code_reference_url,
58
73
  }
59
74
  end
@@ -64,15 +79,26 @@ module SpecHelpers
64
79
  original_stdout = $stdout
65
80
 
66
81
  # Redirect stderr and stdout
67
- output = $stderr = $stdout = StringIO.new
82
+ $stderr = $stdout = StringIO.new
68
83
 
69
84
  yield
70
-
85
+ ensure
71
86
  $stderr = original_stderr
72
87
  $stdout = original_stdout
88
+ end
89
+
90
+ def capture_output
91
+ original_stderr = $stderr
92
+ original_stdout = $stdout
93
+
94
+ output = $stdout = $stderr = StringIO.new
95
+
96
+ yield
73
97
 
74
- # Return output
75
98
  output.string
99
+ ensure
100
+ $stderr = original_stderr
101
+ $stdout = original_stdout
76
102
  end
77
103
  end
78
104
 
@@ -0,0 +1,20 @@
1
+ require "test_helper"
2
+ require "flipper/test/shared_adapter_test"
3
+ require "flipper/adapters/actor_limit"
4
+
5
+ class Flipper::Adapters::ActorLimitTest < MiniTest::Test
6
+ prepend Flipper::Test::SharedAdapterTests
7
+
8
+ def setup
9
+ @memory = Flipper::Adapters::Memory.new
10
+ @adapter = Flipper::Adapters::ActorLimit.new(@memory, 5)
11
+ end
12
+
13
+ def test_enable_fails_when_limit_exceeded
14
+ 5.times { |i| @feature.enable Flipper::Actor.new("User;#{i}") }
15
+
16
+ assert_raises Flipper::Adapters::ActorLimit::LimitExceeded do
17
+ @feature.enable Flipper::Actor.new("User;6")
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,69 @@
1
+ require "helper"
2
+ require "generators/flipper/setup_generator"
3
+
4
+ class SetupGeneratorTest < Rails::Generators::TestCase
5
+ tests Flipper::Generators::SetupGenerator
6
+ ROOT = File.expand_path("../../../tmp/generators", __dir__)
7
+ destination ROOT
8
+ setup :prepare_destination
9
+
10
+ test "invokes flipper:active_record generator if ActiveRecord adapter defined" do
11
+ begin
12
+ load 'flipper/adapters/active_record.rb'
13
+ run_generator
14
+ assert_migration "db/migrate/create_flipper_tables.rb"
15
+ ensure
16
+ Flipper::Adapters.send(:remove_const, :ActiveRecord)
17
+ end
18
+ end
19
+
20
+ test "generates an initializer" do
21
+ run_generator
22
+ assert_file 'config/initializers/flipper.rb', /Flipper\.configure/
23
+ end
24
+
25
+ test "does not invoke flipper:active_record generator if ActiveRecord adapter not defined" do
26
+ # Ensure adapter not defined
27
+ Flipper::Adapters.send(:remove_const, :ActiveRecord) rescue nil
28
+
29
+ run_generator
30
+ assert_no_migration "db/migrate/create_flipper_tables.rb"
31
+ end
32
+
33
+ %w(.env.development .env.local .env).each do |file|
34
+ test "configures Flipper Cloud token in #{file} if it exists" do
35
+ File.write("#{ROOT}/#{file}", "")
36
+ run_generator %w(--token abc123)
37
+ assert_file file, /^FLIPPER_CLOUD_TOKEN=abc123$/m
38
+ end
39
+ end
40
+
41
+ test "configures Flipper Cloud token in .env.development before .env" do
42
+ File.write("#{ROOT}/.env.development", "")
43
+ File.write("#{ROOT}/.env", "")
44
+
45
+ run_generator %w(--token abc123)
46
+ assert_file ".env.development", /^FLIPPER_CLOUD_TOKEN=abc123$/m
47
+ assert_file ".env", ""
48
+ end
49
+
50
+ test "does not write to .env if no token provided" do
51
+ File.write("#{ROOT}/.env", "")
52
+ run_generator
53
+ assert_file ".env", ""
54
+ end
55
+
56
+ test "configures Flipper Cloud token in config/credentials.yml.enc if credentials.yml.enc exist" do
57
+ Dir.chdir(ROOT) do
58
+ FileUtils.mkdir_p("config")
59
+ ENV["RAILS_MASTER_KEY"] = "a" * 32
60
+ Rails.application = Class.new(Rails::Application)
61
+ Rails.application.credentials.write("")
62
+
63
+ run_generator %w(--token abc123)
64
+ assert_file "config/credentials.yml.enc"
65
+ expected_config = { flipper: { cloud_token: "abc123" } }
66
+ assert_equal expected_config, Rails.application.credentials.config
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,96 @@
1
+ require "helper"
2
+ require "generators/flipper/update_generator"
3
+
4
+ class UpdateGeneratorTest < Rails::Generators::TestCase
5
+ tests Flipper::Generators::UpdateGenerator
6
+ ROOT = File.expand_path("../../../../tmp/generators", __FILE__)
7
+ destination ROOT
8
+ setup :prepare_destination
9
+
10
+ setup do
11
+ ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:')
12
+ end
13
+
14
+ teardown do
15
+ ActiveRecord::Base.connection.close
16
+ end
17
+
18
+ test "generates migrations" do
19
+ run_generator
20
+
21
+ assert_migration "db/migrate/create_flipper_tables.rb" do |migration|
22
+ assert_method :up, migration do |up|
23
+ assert_match(/create_table :flipper_features/, up)
24
+ assert_match(/create_table :flipper_gates/, up)
25
+ end
26
+
27
+ assert_method :down, migration do |down|
28
+ assert_match(/drop_table :flipper_features/, down)
29
+ assert_match(/drop_table :flipper_gates/, down)
30
+ end
31
+ end
32
+
33
+ assert_migration "db/migrate/change_flipper_gates_value_to_text.rb" do |migration|
34
+ [:up, :down].each do |dir|
35
+ assert_method :up, migration do |method|
36
+ assert_match(/change_column/, method)
37
+ end
38
+ end
39
+ end
40
+
41
+ require_migrations
42
+
43
+ silence { CreateFlipperTables.migrate(:up) }
44
+ assert ActiveRecord::Base.connection.table_exists?(:flipper_features)
45
+ assert ActiveRecord::Base.connection.table_exists?(:flipper_gates)
46
+
47
+ assert ActiveRecord::Base.connection.column_exists?(:flipper_gates, :value, :string)
48
+ silence { ChangeFlipperGatesValueToText.migrate(:up) }
49
+ assert ActiveRecord::Base.connection.column_exists?(:flipper_gates, :value, :text)
50
+
51
+ silence { ChangeFlipperGatesValueToText.migrate(:down) }
52
+ assert ActiveRecord::Base.connection.column_exists?(:flipper_gates, :value, :string)
53
+
54
+ silence { CreateFlipperTables.migrate(:down) }
55
+ refute ActiveRecord::Base.connection.table_exists?(:flipper_features)
56
+ refute ActiveRecord::Base.connection.table_exists?(:flipper_gates)
57
+ end
58
+
59
+ test "ChangeFlipperGatesValueToText is a noop if value is already text" do
60
+ self.class.generator_class = Flipper::Generators::ActiveRecordGenerator
61
+ run_generator
62
+
63
+ self.class.generator_class = Flipper::Generators::UpdateGenerator
64
+ run_generator
65
+
66
+ assert_migration "db/migrate/create_flipper_tables.rb" do |migration|
67
+ assert_method :up, migration do |up|
68
+ assert_match(/text :value/, up)
69
+ end
70
+ end
71
+
72
+ assert_migration "db/migrate/change_flipper_gates_value_to_text.rb"
73
+
74
+ require_migrations
75
+
76
+ silence { CreateFlipperTables.migrate(:up) }
77
+ assert ActiveRecord::Base.connection.column_exists?(:flipper_gates, :value, :text)
78
+
79
+ assert_nothing_raised do
80
+ silence { ChangeFlipperGatesValueToText.migrate(:up) }
81
+ end
82
+ assert ActiveRecord::Base.connection.column_exists?(:flipper_gates, :value, :text)
83
+ end
84
+
85
+ def require_migrations
86
+ # If these are not reloaded, then test order can cause failures
87
+ Object.send(:remove_const, :CreateFlipperTables) if defined?(::CreateFlipperTables)
88
+ Object.send(:remove_const, :ChangeFlipperGatesValueToText) if defined?(::ChangeFlipperGatesValueToText)
89
+
90
+ Dir.glob("#{ROOT}/db/migrate/*.rb").each do |file|
91
+ assert_nothing_raised do
92
+ load file
93
+ end
94
+ end
95
+ end
96
+ end
data/test_rails/helper.rb CHANGED
@@ -1,11 +1,31 @@
1
1
  require 'rubygems'
2
- require 'bundler'
3
- Bundler.setup(:default)
2
+ require 'bundler/setup'
3
+ require 'minitest/autorun'
4
4
  require 'rails'
5
5
  require 'rails/test_help'
6
6
 
7
+ require 'warning'
8
+ Warning.ignore(/lib\/capybara\//)
9
+
7
10
  begin
8
11
  ActiveSupport::TestCase.test_order = :random
9
12
  rescue NoMethodError
10
13
  # no biggie, means we are on older version of AS that doesn't have this option
11
14
  end
15
+
16
+ def silence
17
+ # Store the original stderr and stdout in order to restore them later
18
+ original_stderr = $stderr
19
+ original_stdout = $stdout
20
+
21
+ # Redirect stderr and stdout
22
+ output = $stderr = $stdout = StringIO.new
23
+
24
+ yield
25
+
26
+ $stderr = original_stderr
27
+ $stdout = original_stdout
28
+
29
+ # Return output
30
+ output.string
31
+ end
@@ -0,0 +1,52 @@
1
+ require_relative "../helper"
2
+
3
+ # Not worth trying to test on old Rails versions
4
+ return unless Rails::VERSION::MAJOR >= 7
5
+
6
+ require "capybara/cuprite"
7
+ require "flipper"
8
+ require "flipper/test_help"
9
+ require "action_controller/railtie"
10
+
11
+ require 'action_dispatch/system_testing/server'
12
+ ActionDispatch::SystemTesting::Server.silence_puma = true
13
+
14
+ class TestApp < Rails::Application
15
+ config.load_defaults "#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}"
16
+ config.eager_load = false
17
+ config.logger = ActiveSupport::Logger.new(StringIO.new)
18
+ routes.append do
19
+ root to: "features#index"
20
+ end
21
+ end
22
+
23
+ TestApp.initialize!
24
+
25
+ class FeaturesController < ActionController::Base
26
+ def index
27
+ render json: Flipper.enabled?(:test) ? "Enabled" : "Disabled"
28
+ end
29
+ end
30
+
31
+ class TestHelpTest < ActionDispatch::SystemTestCase
32
+ # Any driver that runs the app in a separate thread will test what we want here.
33
+ driven_by :cuprite, options: { process_timeout: 60 }
34
+
35
+ setup do
36
+ # Reconfigure Flipper since other tests change the adapter.
37
+ flipper_configure
38
+
39
+ # Ensure this test uses this app instance
40
+ Rails.application = TestApp.instance
41
+ end
42
+
43
+ test "configures a shared adapter between tests and app" do
44
+ Flipper.disable(:test)
45
+ visit "/"
46
+ assert_selector "*", text: "Disabled"
47
+
48
+ Flipper.enable(:test)
49
+ visit "/"
50
+ assert_selector "*", text: "Enabled"
51
+ end
52
+ end