flipper 0.24.1 → 1.3.0

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