flipper 0.26.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 (228) hide show
  1. checksums.yaml +4 -4
  2. data/.github/FUNDING.yml +1 -0
  3. data/.github/workflows/ci.yml +61 -16
  4. data/.github/workflows/examples.yml +55 -18
  5. data/CLAUDE.md +74 -0
  6. data/Changelog.md +1 -486
  7. data/Gemfile +23 -11
  8. data/README.md +31 -27
  9. data/Rakefile +2 -2
  10. data/benchmark/enabled_ips.rb +10 -0
  11. data/benchmark/enabled_multiple_actors_ips.rb +20 -0
  12. data/benchmark/enabled_profile.rb +20 -0
  13. data/benchmark/instrumentation_ips.rb +21 -0
  14. data/benchmark/typecast_ips.rb +27 -0
  15. data/docs/images/banner.jpg +0 -0
  16. data/docs/images/flipper_cloud.png +0 -0
  17. data/examples/api/basic.ru +3 -4
  18. data/examples/api/custom_memoized.ru +3 -4
  19. data/examples/api/memoized.ru +3 -4
  20. data/examples/cloud/app.ru +12 -0
  21. data/examples/cloud/backoff_policy.rb +13 -0
  22. data/examples/cloud/basic.rb +22 -0
  23. data/examples/cloud/cloud_setup.rb +20 -0
  24. data/examples/cloud/forked.rb +36 -0
  25. data/examples/cloud/import.rb +17 -0
  26. data/examples/cloud/threaded.rb +33 -0
  27. data/examples/dsl.rb +1 -15
  28. data/examples/enabled_for_actor.rb +4 -2
  29. data/examples/expressions.rb +213 -0
  30. data/examples/mirroring.rb +59 -0
  31. data/examples/strict.rb +18 -0
  32. data/exe/flipper +5 -0
  33. data/flipper-cloud.gemspec +19 -0
  34. data/flipper.gemspec +8 -6
  35. data/lib/flipper/actor.rb +6 -3
  36. data/lib/flipper/adapter.rb +33 -7
  37. data/lib/flipper/adapter_builder.rb +44 -0
  38. data/lib/flipper/adapters/actor_limit.rb +28 -0
  39. data/lib/flipper/adapters/cache_base.rb +143 -0
  40. data/lib/flipper/adapters/dual_write.rb +1 -3
  41. data/lib/flipper/adapters/failover.rb +0 -4
  42. data/lib/flipper/adapters/failsafe.rb +0 -4
  43. data/lib/flipper/adapters/http/client.rb +40 -12
  44. data/lib/flipper/adapters/http/error.rb +2 -2
  45. data/lib/flipper/adapters/http.rb +30 -17
  46. data/lib/flipper/adapters/instrumented.rb +25 -6
  47. data/lib/flipper/adapters/memoizable.rb +33 -21
  48. data/lib/flipper/adapters/memory.rb +81 -46
  49. data/lib/flipper/adapters/operation_logger.rb +17 -78
  50. data/lib/flipper/adapters/poll/poller.rb +2 -125
  51. data/lib/flipper/adapters/poll.rb +20 -3
  52. data/lib/flipper/adapters/pstore.rb +17 -11
  53. data/lib/flipper/adapters/read_only.rb +8 -41
  54. data/lib/flipper/adapters/strict.rb +45 -0
  55. data/lib/flipper/adapters/sync/feature_synchronizer.rb +10 -1
  56. data/lib/flipper/adapters/sync.rb +0 -4
  57. data/lib/flipper/adapters/wrapper.rb +54 -0
  58. data/lib/flipper/cli.rb +263 -0
  59. data/lib/flipper/cloud/configuration.rb +266 -0
  60. data/lib/flipper/cloud/dsl.rb +27 -0
  61. data/lib/flipper/cloud/message_verifier.rb +95 -0
  62. data/lib/flipper/cloud/middleware.rb +63 -0
  63. data/lib/flipper/cloud/routes.rb +14 -0
  64. data/lib/flipper/cloud/telemetry/backoff_policy.rb +96 -0
  65. data/lib/flipper/cloud/telemetry/instrumenter.rb +22 -0
  66. data/lib/flipper/cloud/telemetry/metric.rb +39 -0
  67. data/lib/flipper/cloud/telemetry/metric_storage.rb +30 -0
  68. data/lib/flipper/cloud/telemetry/submitter.rb +100 -0
  69. data/lib/flipper/cloud/telemetry.rb +191 -0
  70. data/lib/flipper/cloud.rb +53 -0
  71. data/lib/flipper/configuration.rb +25 -4
  72. data/lib/flipper/dsl.rb +46 -45
  73. data/lib/flipper/engine.rb +102 -0
  74. data/lib/flipper/errors.rb +3 -3
  75. data/lib/flipper/export.rb +24 -0
  76. data/lib/flipper/exporter.rb +17 -0
  77. data/lib/flipper/exporters/json/export.rb +32 -0
  78. data/lib/flipper/exporters/json/v1.rb +33 -0
  79. data/lib/flipper/expression/builder.rb +73 -0
  80. data/lib/flipper/expression/constant.rb +25 -0
  81. data/lib/flipper/expression.rb +71 -0
  82. data/lib/flipper/expressions/all.rb +9 -0
  83. data/lib/flipper/expressions/any.rb +9 -0
  84. data/lib/flipper/expressions/boolean.rb +9 -0
  85. data/lib/flipper/expressions/comparable.rb +13 -0
  86. data/lib/flipper/expressions/duration.rb +28 -0
  87. data/lib/flipper/expressions/equal.rb +9 -0
  88. data/lib/flipper/expressions/greater_than.rb +9 -0
  89. data/lib/flipper/expressions/greater_than_or_equal_to.rb +9 -0
  90. data/lib/flipper/expressions/less_than.rb +9 -0
  91. data/lib/flipper/expressions/less_than_or_equal_to.rb +9 -0
  92. data/lib/flipper/expressions/not_equal.rb +9 -0
  93. data/lib/flipper/expressions/now.rb +9 -0
  94. data/lib/flipper/expressions/number.rb +9 -0
  95. data/lib/flipper/expressions/percentage.rb +9 -0
  96. data/lib/flipper/expressions/percentage_of_actors.rb +12 -0
  97. data/lib/flipper/expressions/property.rb +9 -0
  98. data/lib/flipper/expressions/random.rb +9 -0
  99. data/lib/flipper/expressions/string.rb +9 -0
  100. data/lib/flipper/expressions/time.rb +9 -0
  101. data/lib/flipper/feature.rb +94 -26
  102. data/lib/flipper/feature_check_context.rb +10 -6
  103. data/lib/flipper/gate.rb +13 -11
  104. data/lib/flipper/gate_values.rb +5 -18
  105. data/lib/flipper/gates/actor.rb +10 -17
  106. data/lib/flipper/gates/boolean.rb +1 -1
  107. data/lib/flipper/gates/expression.rb +75 -0
  108. data/lib/flipper/gates/group.rb +5 -7
  109. data/lib/flipper/gates/percentage_of_actors.rb +10 -13
  110. data/lib/flipper/gates/percentage_of_time.rb +1 -2
  111. data/lib/flipper/identifier.rb +2 -2
  112. data/lib/flipper/instrumentation/log_subscriber.rb +35 -8
  113. data/lib/flipper/instrumentation/statsd.rb +4 -2
  114. data/lib/flipper/instrumentation/statsd_subscriber.rb +2 -4
  115. data/lib/flipper/instrumentation/subscriber.rb +8 -5
  116. data/lib/flipper/metadata.rb +8 -1
  117. data/lib/flipper/middleware/memoizer.rb +30 -14
  118. data/lib/flipper/model/active_record.rb +23 -0
  119. data/lib/flipper/poller.rb +118 -0
  120. data/lib/flipper/serializers/gzip.rb +22 -0
  121. data/lib/flipper/serializers/json.rb +17 -0
  122. data/lib/flipper/spec/shared_adapter_specs.rb +105 -63
  123. data/lib/flipper/test/shared_adapter_test.rb +101 -58
  124. data/lib/flipper/test_help.rb +43 -0
  125. data/lib/flipper/typecast.rb +59 -18
  126. data/lib/flipper/types/actor.rb +13 -13
  127. data/lib/flipper/types/group.rb +4 -4
  128. data/lib/flipper/types/percentage.rb +1 -1
  129. data/lib/flipper/version.rb +11 -1
  130. data/lib/flipper.rb +50 -11
  131. data/lib/generators/flipper/setup_generator.rb +68 -0
  132. data/lib/generators/flipper/templates/initializer.rb +45 -0
  133. data/lib/generators/flipper/templates/update/migrations/01_create_flipper_tables.rb.erb +22 -0
  134. data/lib/generators/flipper/templates/update/migrations/02_change_flipper_gates_value_to_text.rb.erb +18 -0
  135. data/lib/generators/flipper/update_generator.rb +35 -0
  136. data/package-lock.json +41 -0
  137. data/package.json +10 -0
  138. data/spec/fixtures/environment.rb +1 -0
  139. data/spec/fixtures/flipper_pstore_1679087600.json +46 -0
  140. data/spec/flipper/adapter_builder_spec.rb +72 -0
  141. data/spec/flipper/adapter_spec.rb +30 -2
  142. data/spec/flipper/adapters/actor_limit_spec.rb +20 -0
  143. data/spec/flipper/adapters/dual_write_spec.rb +2 -2
  144. data/spec/flipper/adapters/http/client_spec.rb +61 -0
  145. data/spec/flipper/adapters/http_spec.rb +138 -55
  146. data/spec/flipper/adapters/instrumented_spec.rb +29 -11
  147. data/spec/flipper/adapters/memoizable_spec.rb +51 -31
  148. data/spec/flipper/adapters/memory_spec.rb +14 -3
  149. data/spec/flipper/adapters/operation_logger_spec.rb +31 -12
  150. data/spec/flipper/adapters/poll_spec.rb +41 -0
  151. data/spec/flipper/adapters/read_only_spec.rb +32 -17
  152. data/spec/flipper/adapters/strict_spec.rb +64 -0
  153. data/spec/flipper/adapters/sync/feature_synchronizer_spec.rb +27 -0
  154. data/spec/flipper/cli_spec.rb +166 -0
  155. data/spec/flipper/cloud/configuration_spec.rb +251 -0
  156. data/spec/flipper/cloud/dsl_spec.rb +82 -0
  157. data/spec/flipper/cloud/message_verifier_spec.rb +104 -0
  158. data/spec/flipper/cloud/middleware_spec.rb +289 -0
  159. data/spec/flipper/cloud/telemetry/backoff_policy_spec.rb +107 -0
  160. data/spec/flipper/cloud/telemetry/metric_spec.rb +87 -0
  161. data/spec/flipper/cloud/telemetry/metric_storage_spec.rb +58 -0
  162. data/spec/flipper/cloud/telemetry/submitter_spec.rb +145 -0
  163. data/spec/flipper/cloud/telemetry_spec.rb +208 -0
  164. data/spec/flipper/cloud_spec.rb +186 -0
  165. data/spec/flipper/configuration_spec.rb +17 -0
  166. data/spec/flipper/dsl_spec.rb +54 -76
  167. data/spec/flipper/engine_spec.rb +374 -0
  168. data/spec/flipper/export_spec.rb +13 -0
  169. data/spec/flipper/exporter_spec.rb +16 -0
  170. data/spec/flipper/exporters/json/export_spec.rb +60 -0
  171. data/spec/flipper/exporters/json/v1_spec.rb +33 -0
  172. data/spec/flipper/expression/builder_spec.rb +248 -0
  173. data/spec/flipper/expression_spec.rb +188 -0
  174. data/spec/flipper/expressions/all_spec.rb +15 -0
  175. data/spec/flipper/expressions/any_spec.rb +15 -0
  176. data/spec/flipper/expressions/boolean_spec.rb +15 -0
  177. data/spec/flipper/expressions/duration_spec.rb +43 -0
  178. data/spec/flipper/expressions/equal_spec.rb +24 -0
  179. data/spec/flipper/expressions/greater_than_or_equal_to_spec.rb +28 -0
  180. data/spec/flipper/expressions/greater_than_spec.rb +28 -0
  181. data/spec/flipper/expressions/less_than_or_equal_to_spec.rb +28 -0
  182. data/spec/flipper/expressions/less_than_spec.rb +32 -0
  183. data/spec/flipper/expressions/not_equal_spec.rb +15 -0
  184. data/spec/flipper/expressions/now_spec.rb +11 -0
  185. data/spec/flipper/expressions/number_spec.rb +21 -0
  186. data/spec/flipper/expressions/percentage_of_actors_spec.rb +20 -0
  187. data/spec/flipper/expressions/percentage_spec.rb +15 -0
  188. data/spec/flipper/expressions/property_spec.rb +13 -0
  189. data/spec/flipper/expressions/random_spec.rb +9 -0
  190. data/spec/flipper/expressions/string_spec.rb +11 -0
  191. data/spec/flipper/expressions/time_spec.rb +13 -0
  192. data/spec/flipper/feature_check_context_spec.rb +17 -17
  193. data/spec/flipper/feature_spec.rb +453 -39
  194. data/spec/flipper/gate_values_spec.rb +2 -33
  195. data/spec/flipper/gates/boolean_spec.rb +1 -1
  196. data/spec/flipper/gates/expression_spec.rb +108 -0
  197. data/spec/flipper/gates/group_spec.rb +2 -3
  198. data/spec/flipper/gates/percentage_of_actors_spec.rb +61 -5
  199. data/spec/flipper/gates/percentage_of_time_spec.rb +2 -2
  200. data/spec/flipper/identifier_spec.rb +4 -5
  201. data/spec/flipper/instrumentation/log_subscriber_spec.rb +24 -6
  202. data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +26 -2
  203. data/spec/flipper/middleware/memoizer_spec.rb +79 -10
  204. data/spec/flipper/model/active_record_spec.rb +72 -0
  205. data/spec/flipper/poller_spec.rb +47 -0
  206. data/spec/flipper/serializers/gzip_spec.rb +13 -0
  207. data/spec/flipper/serializers/json_spec.rb +13 -0
  208. data/spec/flipper/typecast_spec.rb +121 -6
  209. data/spec/flipper/types/actor_spec.rb +63 -46
  210. data/spec/flipper/types/group_spec.rb +2 -2
  211. data/spec/flipper_integration_spec.rb +168 -58
  212. data/spec/flipper_spec.rb +94 -30
  213. data/spec/spec_helper.rb +18 -18
  214. data/spec/support/actor_names.yml +1 -0
  215. data/spec/support/fail_on_output.rb +8 -0
  216. data/spec/support/fake_backoff_policy.rb +15 -0
  217. data/spec/support/skippable.rb +18 -0
  218. data/spec/support/spec_helpers.rb +34 -8
  219. data/test/adapters/actor_limit_test.rb +20 -0
  220. data/test_rails/generators/flipper/setup_generator_test.rb +69 -0
  221. data/test_rails/generators/flipper/update_generator_test.rb +96 -0
  222. data/test_rails/helper.rb +22 -2
  223. data/test_rails/system/test_help_test.rb +52 -0
  224. metadata +203 -20
  225. data/.github/workflows/release.yml +0 -44
  226. data/.tool-versions +0 -1
  227. data/lib/flipper/railtie.rb +0 -47
  228. data/spec/flipper/railtie_spec.rb +0 -109
@@ -123,18 +123,6 @@ RSpec.describe Flipper::DSL do
123
123
  end
124
124
  end
125
125
 
126
- describe '#boolean' do
127
- it_should_behave_like 'a DSL boolean method' do
128
- let(:method_name) { :boolean }
129
- end
130
- end
131
-
132
- describe '#bool' do
133
- it_should_behave_like 'a DSL boolean method' do
134
- let(:method_name) { :bool }
135
- end
136
- end
137
-
138
126
  describe '#group' do
139
127
  context 'for registered group' do
140
128
  before do
@@ -148,66 +136,15 @@ RSpec.describe Flipper::DSL do
148
136
  end
149
137
  end
150
138
 
151
- describe '#actor' do
152
- context 'for a thing' do
153
- it 'returns actor instance' do
154
- thing = Flipper::Actor.new(33)
155
- actor = subject.actor(thing)
156
- expect(actor).to be_instance_of(Flipper::Types::Actor)
157
- expect(actor.value).to eq('33')
158
- end
159
- end
160
-
161
- context 'for nil' do
162
- it 'raises argument error' do
163
- expect do
164
- subject.actor(nil)
165
- end.to raise_error(ArgumentError)
166
- end
167
- end
168
-
169
- context 'for something that is not actor wrappable' do
170
- it 'raises argument error' do
171
- expect do
172
- subject.actor(Object.new)
173
- end.to raise_error(ArgumentError)
174
- end
175
- end
176
- end
177
-
178
- describe '#time' do
179
- before do
180
- @result = subject.time(5)
181
- end
182
-
183
- it 'returns percentage of time' do
184
- expect(@result).to be_instance_of(Flipper::Types::PercentageOfTime)
185
- end
186
-
187
- it 'sets value' do
188
- expect(@result.value).to eq(5)
189
- end
190
-
191
- it 'is aliased to percentage_of_time' do
192
- expect(@result).to eq(subject.percentage_of_time(@result.value))
193
- end
194
- end
195
-
196
- describe '#actors' do
197
- before do
198
- @result = subject.actors(17)
199
- end
200
-
201
- it 'returns percentage of actors' do
202
- expect(@result).to be_instance_of(Flipper::Types::PercentageOfActors)
139
+ describe '#expression' do
140
+ it "returns nil if feature has no expression" do
141
+ expect(subject.expression(:stats)).to be(nil)
203
142
  end
204
143
 
205
- it 'sets value' do
206
- expect(@result.value).to eq(17)
207
- end
208
-
209
- it 'is aliased to percentage_of_actors' do
210
- expect(@result).to eq(subject.percentage_of_actors(@result.value))
144
+ it "returns expression if feature has expression" do
145
+ expression = Flipper.property(:plan).eq("basic")
146
+ subject[:stats].enable_expression expression
147
+ expect(subject.expression(:stats)).to eq(expression)
211
148
  end
212
149
  end
213
150
 
@@ -246,6 +183,33 @@ RSpec.describe Flipper::DSL do
246
183
  end
247
184
  end
248
185
 
186
+ describe '#enable_expression/disable_expression' do
187
+ it 'enables and disables the feature for the expression' do
188
+ expression = Flipper.property(:plan).eq("basic")
189
+
190
+ expect(subject[:stats].expression).to be(nil)
191
+ subject.enable_expression(:stats, expression)
192
+ expect(subject[:stats].expression).to eq(expression)
193
+
194
+ subject.disable_expression(:stats)
195
+ expect(subject[:stats].expression).to be(nil)
196
+ end
197
+ end
198
+
199
+ describe '#add_expression/remove_expression' do
200
+ it 'enables and disables the feature for the expression' do
201
+ expression = Flipper.property(:plan).eq("basic")
202
+ any_expression = Flipper.any(expression)
203
+
204
+ expect(subject[:stats].expression).to be(nil)
205
+ subject.add_expression(:stats, any_expression)
206
+ expect(subject[:stats].expression).to eq(any_expression)
207
+
208
+ subject.remove_expression(:stats, expression)
209
+ expect(subject[:stats].expression).to eq(Flipper.any)
210
+ end
211
+ end
212
+
249
213
  describe '#enable_actor/disable_actor' do
250
214
  it 'enables and disables the feature for actor' do
251
215
  actor = Flipper::Actor.new(5)
@@ -261,9 +225,6 @@ RSpec.describe Flipper::DSL do
261
225
 
262
226
  describe '#enable_group/disable_group' do
263
227
  it 'enables and disables the feature for group' do
264
- actor = Flipper::Actor.new(5)
265
- group = Flipper.register(:fives) { |actor| actor.flipper_id == 5 }
266
-
267
228
  expect(subject[:stats].groups_value).to be_empty
268
229
  subject.enable_group(:stats, :fives)
269
230
  expect(subject[:stats].groups_value).to eq(Set['fives'])
@@ -342,10 +303,27 @@ RSpec.describe Flipper::DSL do
342
303
  end
343
304
 
344
305
  describe '#import' do
306
+ context "with flipper instance" do
307
+ it 'delegates to adapter' do
308
+ destination_flipper = build_flipper
309
+ expect(subject.adapter).to receive(:import).with(destination_flipper)
310
+ subject.import(destination_flipper)
311
+ end
312
+ end
313
+
314
+ context "with flipper adapter" do
315
+ it 'delegates to adapter' do
316
+ destination_flipper = build_flipper
317
+ expect(subject.adapter).to receive(:import).with(destination_flipper.adapter)
318
+ subject.import(destination_flipper.adapter)
319
+ end
320
+ end
321
+ end
322
+
323
+ describe "#export" do
345
324
  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)
325
+ expect(subject.export).to eq(subject.adapter.export)
326
+ expect(subject.export(format: :json)).to eq(subject.adapter.export(format: :json))
349
327
  end
350
328
  end
351
329
 
@@ -0,0 +1,374 @@
1
+ require 'rails'
2
+ require 'flipper/engine'
3
+
4
+ RSpec.describe Flipper::Engine do
5
+ let(:application) do
6
+ Class.new(Rails::Application) do
7
+ config.load_defaults Rails::VERSION::STRING.to_f
8
+ config.eager_load = false
9
+ config.logger = ActiveSupport::Logger.new($stdout)
10
+ config.active_support.remove_deprecated_time_with_zone_name = false
11
+ end.instance
12
+ end
13
+
14
+ before do
15
+ stub_request(:get, /flippercloud\.io/).to_return(status: 200, body: "{}")
16
+ Rails.application = nil
17
+ ActiveSupport::Dependencies.autoload_paths = ActiveSupport::Dependencies.autoload_paths.dup
18
+ ActiveSupport::Dependencies.autoload_once_paths = ActiveSupport::Dependencies.autoload_once_paths.dup
19
+ end
20
+
21
+ # Reset Rails.env around each example
22
+ around do |example|
23
+ begin
24
+ env = Rails.env.to_s
25
+ example.run
26
+ ensure
27
+ Rails.env = env
28
+ end
29
+ end
30
+
31
+ let(:config) { application.config.flipper }
32
+
33
+ subject { SpecHelpers.silence { application.initialize! } }
34
+
35
+ shared_examples 'config.strict' do
36
+ let(:adapter) { Flipper.adapter.adapter }
37
+
38
+ it 'can set strict=true from ENV' do
39
+ ENV['FLIPPER_STRICT'] = 'true'
40
+ subject
41
+ expect(config.strict).to eq(:raise)
42
+ expect(adapter).to be_instance_of(Flipper::Adapters::Strict)
43
+ end
44
+
45
+ it 'can set strict=warn from ENV' do
46
+ ENV['FLIPPER_STRICT'] = 'warn'
47
+ subject
48
+ expect(config.strict).to eq(:warn)
49
+ expect(adapter).to be_instance_of(Flipper::Adapters::Strict)
50
+ expect(adapter.handler).to be(:warn)
51
+ end
52
+
53
+ it 'can set strict=false from ENV' do
54
+ ENV['FLIPPER_STRICT'] = 'false'
55
+ subject
56
+ expect(config.strict).to eq(false)
57
+ expect(adapter).not_to be_instance_of(Flipper::Adapters::Strict)
58
+ end
59
+
60
+ [true, :raise, :warn].each do |value|
61
+ it "can set strict=#{value.inspect} in initializer" do
62
+ initializer { config.strict = value }
63
+ subject
64
+ expect(adapter).to be_instance_of(Flipper::Adapters::Strict)
65
+ expect(adapter.handler).to be(value)
66
+ end
67
+ end
68
+
69
+ it "can set strict=false in initializer" do
70
+ initializer { config.strict = false }
71
+ subject
72
+ expect(config.strict).to eq(false)
73
+ expect(adapter).not_to be_instance_of(Flipper::Adapters::Strict)
74
+ end
75
+
76
+ it "defaults to strict=:warn in RAILS_ENV=development" do
77
+ Rails.env = "development"
78
+ subject
79
+ expect(config.strict).to eq(:warn)
80
+ expect(adapter).to be_instance_of(Flipper::Adapters::Strict)
81
+ end
82
+
83
+ %w(production test).each do |env|
84
+ it "defaults to strict=warn in RAILS_ENV=#{env}" do
85
+ Rails.env = env
86
+ expect(Rails.env).to eq(env)
87
+ subject
88
+ expect(config.strict).to eq(false)
89
+ expect(adapter).not_to be_instance_of(Flipper::Adapters::Strict)
90
+ end
91
+ end
92
+
93
+ it "defaults to strict=warn in RAILS_ENV=development" do
94
+ Rails.env = "development"
95
+ expect(Rails.env).to eq("development")
96
+ subject
97
+ expect(config.strict).to eq(:warn)
98
+ expect(adapter).to be_instance_of(Flipper::Adapters::Strict)
99
+ expect(adapter.handler).to be(:warn)
100
+ end
101
+ end
102
+
103
+ context 'cloudless' do
104
+ it_behaves_like 'config.strict'
105
+
106
+ it 'can set env_key from ENV' do
107
+ ENV['FLIPPER_ENV_KEY'] = 'flopper'
108
+ subject
109
+ expect(config.env_key).to eq('flopper')
110
+ end
111
+
112
+ it 'can set memoize from ENV' do
113
+ ENV['FLIPPER_MEMOIZE'] = 'false'
114
+ subject
115
+ expect(config.memoize).to eq(false)
116
+ end
117
+
118
+ it 'can set preload from ENV' do
119
+ ENV['FLIPPER_PRELOAD'] = 'false'
120
+ subject
121
+ expect(config.preload).to eq(false)
122
+ end
123
+
124
+ it 'can set instrumenter from ENV' do
125
+ stub_const('My::Cool::Instrumenter', Class.new)
126
+ ENV['FLIPPER_INSTRUMENTER'] = 'My::Cool::Instrumenter'
127
+ subject
128
+ expect(config.instrumenter).to eq(My::Cool::Instrumenter)
129
+ end
130
+
131
+ it 'can set log from ENV' do
132
+ ENV['FLIPPER_LOG'] = 'false'
133
+ subject
134
+ expect(config.log).to eq(false)
135
+ end
136
+
137
+ it 'sets defaults' do
138
+ subject # initialize
139
+ expect(config.env_key).to eq("flipper")
140
+ expect(config.memoize).to be(true)
141
+ expect(config.preload).to be(true)
142
+ end
143
+
144
+ it "configures instrumentor on default instance" do
145
+ subject # initialize
146
+ expect(Flipper.instance.instrumenter).to eq(ActiveSupport::Notifications)
147
+ end
148
+
149
+ it 'uses Memoizer middleware if config.memoize = true' do
150
+ initializer { config.memoize = true }
151
+ expect(subject.middleware).to include(Flipper::Middleware::Memoizer)
152
+ end
153
+
154
+ it 'does not use Memoizer middleware if config.memoize = false' do
155
+ initializer { config.memoize = false }
156
+ expect(subject.middleware).not_to include(Flipper::Middleware::Memoizer)
157
+ end
158
+
159
+ it 'passes config to memoizer' do
160
+ initializer do
161
+ config.update(
162
+ env_key: 'my_flipper',
163
+ preload: [:stats, :search]
164
+ )
165
+ end
166
+
167
+ expect(subject.middleware).to include(Flipper::Middleware::Memoizer)
168
+ middleware = subject.middleware.detect { |m| m.klass == Flipper::Middleware::Memoizer }
169
+ expect(middleware.args[0]).to eq({
170
+ env_key: config.env_key,
171
+ preload: config.preload,
172
+ if: nil
173
+ })
174
+ end
175
+
176
+ context "test_help" do
177
+ it "is loaded if RAILS_ENV=test" do
178
+ Rails.env = "test"
179
+ allow(Flipper::Engine.instance).to receive(:require).and_call_original
180
+ expect(Flipper::Engine.instance).to receive(:require).with("flipper/test_help")
181
+ subject
182
+ expect(config.test_help).to eq(true)
183
+ end
184
+
185
+ it "is loaded if FLIPPER_TEST_HELP=true" do
186
+ ENV["FLIPPER_TEST_HELP"] = "true"
187
+ allow(Flipper::Engine.instance).to receive(:require).and_call_original
188
+ expect(Flipper::Engine.instance).to receive(:require).with("flipper/test_help")
189
+ subject
190
+ expect(config.test_help).to eq(true)
191
+ end
192
+
193
+ it "is loaded if config.flipper.test_help = true" do
194
+ initializer { config.test_help = true }
195
+ allow(Flipper::Engine.instance).to receive(:require).and_call_original
196
+ expect(Flipper::Engine.instance).to receive(:require).with("flipper/test_help")
197
+ subject
198
+ end
199
+
200
+ it "is not loaded if FLIPPER_TEST_HELP=false" do
201
+ ENV["FLIPPER_TEST_HELP"] = "false"
202
+ allow(Flipper::Engine.instance).to receive(:require).and_call_original
203
+ expect(Flipper::Engine.instance).to receive(:require).with("flipper/test_help").never
204
+ subject
205
+ end
206
+
207
+ it "is not loaded if config.flipper.test_help = false" do
208
+ Rails.env = "true"
209
+ initializer { config.test_help = false }
210
+ allow(Flipper::Engine.instance).to receive(:require).and_call_original
211
+ expect(Flipper::Engine.instance).to receive(:require).with("flipper/test_help").never
212
+ subject
213
+ end
214
+ end
215
+ end
216
+
217
+ context 'with cloud' do
218
+ before do
219
+ ENV["FLIPPER_CLOUD_TOKEN"] = "test-token"
220
+ end
221
+
222
+ # App for Rack::Test
223
+ let(:app) { application.routes }
224
+
225
+ it_behaves_like 'config.strict' do
226
+ let(:adapter) do
227
+ memoizable = Flipper.adapter
228
+ dual_write = memoizable.adapter
229
+ poll = dual_write.local
230
+ poll.adapter
231
+ end
232
+ end
233
+
234
+ it "initializes cloud configuration" do
235
+ stub_request(:get, /flippercloud\.io/).to_return(status: 200, body: "{}")
236
+
237
+ silence { application.initialize! }
238
+
239
+ expect(Flipper.instance).to be_a(Flipper::Cloud::DSL)
240
+ expect(Flipper.instance.instrumenter).to be_a(Flipper::Cloud::Telemetry::Instrumenter)
241
+ expect(Flipper.instance.instrumenter.instrumenter).to be(ActiveSupport::Notifications)
242
+ end
243
+
244
+ context "with CLOUD_SYNC_SECRET" do
245
+ before do
246
+ ENV["FLIPPER_CLOUD_SYNC_SECRET"] = "test-secret"
247
+ end
248
+
249
+ let(:request_body) do
250
+ JSON.generate({
251
+ "environment_id" => 1,
252
+ "webhook_id" => 1,
253
+ "delivery_id" => SecureRandom.uuid,
254
+ "action" => "sync",
255
+ })
256
+ end
257
+ let(:timestamp) { Time.now }
258
+
259
+ let(:signature) {
260
+ Flipper::Cloud::MessageVerifier.new(secret: ENV["FLIPPER_CLOUD_SYNC_SECRET"]).generate(request_body, timestamp)
261
+ }
262
+ let(:signature_header_value) {
263
+ Flipper::Cloud::MessageVerifier.new(secret: "").header(signature, timestamp)
264
+ }
265
+
266
+ it "configures webhook app" do
267
+ silence { application.initialize! }
268
+
269
+ stub = stub_request(:get, "https://www.flippercloud.io/adapter/features?exclude_gate_names=true").with({
270
+ headers: { "flipper-cloud-token" => ENV["FLIPPER_CLOUD_TOKEN"] },
271
+ }).to_return(status: 200, body: JSON.generate({ features: {} }), headers: {})
272
+
273
+ post "/_flipper", request_body, { "HTTP_FLIPPER_CLOUD_SIGNATURE" => signature_header_value }
274
+
275
+ expect(last_response.status).to eq(200)
276
+ expect(stub).to have_been_requested
277
+ end
278
+ end
279
+
280
+ context "without CLOUD_SYNC_SECRET" do
281
+ it "does not configure webhook app" do
282
+ silence { application.initialize! }
283
+
284
+ post "/_flipper"
285
+ expect(last_response.status).to eq(404)
286
+ end
287
+ end
288
+
289
+ context "without FLIPPER_CLOUD_TOKEN" do
290
+ it "gracefully skips configuring webhook app" do
291
+ ENV["FLIPPER_CLOUD_TOKEN"] = nil
292
+ silence { application.initialize! }
293
+ expect(Flipper.instance).to be_a(Flipper::DSL)
294
+
295
+ post "/_flipper"
296
+ expect(last_response.status).to eq(404)
297
+ end
298
+ end
299
+ end
300
+
301
+ context 'with cloud secrets in Rails.credentials' do
302
+ around do |example|
303
+ # Create temporary directory for Rails.root to write credentials to
304
+ # Once Rails 5.2 support is dropped, this can all be replaced with
305
+ # `config.credentials.content_path = Tempfile.new.path`
306
+ Dir.mktmpdir do |dir|
307
+ Dir.chdir(dir) do
308
+ Dir.mkdir("#{dir}/config")
309
+
310
+ example.run
311
+ end
312
+ end
313
+ end
314
+
315
+ before do
316
+ # Set master key which is needed to write credentials
317
+ ENV["RAILS_MASTER_KEY"] = "a" * 32
318
+
319
+ application.credentials.write(YAML.dump({
320
+ flipper: {
321
+ cloud_token: "credentials-token",
322
+ cloud_sync_secret: "credentials-secret",
323
+ }
324
+ }))
325
+ end
326
+
327
+ it "enables cloud" do
328
+ silence { application.initialize! }
329
+ expect(ENV["FLIPPER_CLOUD_TOKEN"]).to eq("credentials-token")
330
+ expect(ENV["FLIPPER_CLOUD_SYNC_SECRET"]).to eq("credentials-secret")
331
+ expect(Flipper.instance).to be_a(Flipper::Cloud::DSL)
332
+ end
333
+ end
334
+
335
+ it "includes model methods" do
336
+ subject
337
+ require 'active_record'
338
+ expect(ActiveRecord::Base.ancestors).to include(Flipper::Model::ActiveRecord)
339
+ end
340
+
341
+ describe "config.actor_limit" do
342
+ let(:adapter) do
343
+ silence { application.initialize! }
344
+ Flipper.adapter.adapter.adapter
345
+ end
346
+
347
+ it "defaults to 100" do
348
+ expect(adapter).to be_instance_of(Flipper::Adapters::ActorLimit)
349
+ expect(adapter.limit).to eq(100)
350
+ end
351
+
352
+ it "can be set from FLIPPER_ACTOR_LIMIT env" do
353
+ ENV["FLIPPER_ACTOR_LIMIT"] = "500"
354
+ expect(adapter.limit).to eq(500)
355
+ end
356
+
357
+ it "can be set from an initializer" do
358
+ initializer { config.actor_limit = 99 }
359
+ expect(adapter.limit).to eq(99)
360
+ end
361
+
362
+ it "can be disabled from an initializer" do
363
+ initializer { config.actor_limit = false }
364
+ expect(adapter).not_to be_instance_of(Flipper::Adapters::ActorLimit)
365
+ end
366
+ end
367
+
368
+ # Add app initializer in the same order as config/initializers/*
369
+ def initializer(&block)
370
+ application.initializer 'spec', before: :load_config_initializers do
371
+ block.call
372
+ end
373
+ end
374
+ end
@@ -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, expression: nil, groups: Set.new, percentage_of_actors: nil, percentage_of_time: nil},
29
+ "plausible" => {actors: Set.new, boolean: "true", expression: nil, groups: Set.new, percentage_of_actors: nil, percentage_of_time: nil},
30
+ "search" => {actors: Set["User;1", "User;100"], boolean: nil, expression: nil, groups: Set["admins", "employees"], percentage_of_actors: "10", percentage_of_time: "15"},
31
+ })
32
+ end
33
+ end