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
@@ -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
@@ -25,9 +25,9 @@ RSpec.describe Flipper::Exporters::Json::V1 do
25
25
  export = subject.call(adapter)
26
26
 
27
27
  expect(export.features).to eq({
28
- "google_analytics" => {actors: Set.new, boolean: nil, groups: Set.new, percentage_of_actors: nil, percentage_of_time: nil},
29
- "plausible" => {actors: Set.new, boolean: "true", groups: Set.new, percentage_of_actors: nil, percentage_of_time: nil},
30
- "search" => {actors: Set["User;1", "User;100"], boolean: nil, groups: Set["admins", "employees"], percentage_of_actors: "10", percentage_of_time: "15"},
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
31
  })
32
32
  end
33
33
  end
@@ -0,0 +1,248 @@
1
+ RSpec.describe Flipper::Expression::Builder do
2
+ def build(object)
3
+ Flipper::Expression.build(object)
4
+ end
5
+
6
+ describe "#add" do
7
+ it "converts to Any and adds new expressions" do
8
+ expression = build("something")
9
+ first = Flipper.boolean(true).eq(true)
10
+ second = Flipper.boolean(false).eq(false)
11
+ new_expression = expression.add(first, second)
12
+ expect(new_expression).to eq(build({ Any: ["something", first, second] }))
13
+ end
14
+ end
15
+
16
+ describe "#remove" do
17
+ it "converts to Any and removes any expressions that match" do
18
+ expression = build("something")
19
+ first = Flipper.boolean(true).eq(true)
20
+ second = Flipper.boolean(false).eq(false)
21
+ new_expression = expression.remove(build("something"), first, second)
22
+ expect(new_expression).to eq(build(Any: []))
23
+ end
24
+ end
25
+
26
+ it "can convert to Any" do
27
+ expression = build("something")
28
+ converted = expression.any
29
+ expect(converted).to be_instance_of(Flipper::Expression)
30
+ expect(converted.function).to be(Flipper::Expressions::Any)
31
+ expect(converted.args).to eq([expression])
32
+ end
33
+
34
+ it "can convert to All" do
35
+ expression = build("something")
36
+ converted = expression.all
37
+ expect(converted).to eq(build(All: ["something"]))
38
+ end
39
+
40
+ context "Any" do
41
+ describe "#any" do
42
+ it "returns self" do
43
+ expression = build(Any: [
44
+ Flipper.boolean(true),
45
+ Flipper.string("yep").eq("yep"),
46
+ ])
47
+ expect(expression.any).to be(expression)
48
+ end
49
+ end
50
+
51
+ describe "#add" do
52
+ it "returns new instance with expression added" do
53
+ expression = Flipper.boolean(true)
54
+ other = Flipper.string("yep").eq("yep")
55
+
56
+ result = expression.add(other)
57
+ expect(result.args).to eq([
58
+ Flipper.boolean(true),
59
+ Flipper.string("yep").eq("yep"),
60
+ ])
61
+ end
62
+
63
+ it "returns new instance with many expressions added" do
64
+ expression = Flipper.boolean(true)
65
+ second = Flipper.string("yep").eq("yep")
66
+ third = Flipper.number(1).lte(20)
67
+
68
+ result = expression.add(second, third)
69
+ expect(result.args).to eq([
70
+ Flipper.boolean(true),
71
+ Flipper.string("yep").eq("yep"),
72
+ Flipper.number(1).lte(20),
73
+ ])
74
+ end
75
+
76
+ it "returns new instance with array of expressions added" do
77
+ expression = Flipper.boolean(true)
78
+ second = Flipper.string("yep").eq("yep")
79
+ third = Flipper.number(1).lte(20)
80
+
81
+ result = expression.add([second, third])
82
+ expect(result.args).to eq([
83
+ Flipper.boolean(true),
84
+ Flipper.string("yep").eq("yep"),
85
+ Flipper.number(1).lte(20),
86
+ ])
87
+ end
88
+ end
89
+
90
+ describe "#remove" do
91
+ it "returns new instance with expression removed" do
92
+ first = Flipper.boolean(true)
93
+ second = Flipper.string("yep").eq("yep")
94
+ third = Flipper.number(1).lte(20)
95
+ expression = Flipper.any([first, second, third])
96
+
97
+ result = expression.remove(second)
98
+ expect(expression.args).to eq([first, second, third])
99
+ expect(result.args).to eq([first, third])
100
+ end
101
+
102
+ it "returns new instance with many expressions removed" do
103
+ first = Flipper.boolean(true)
104
+ second = Flipper.string("yep").eq("yep")
105
+ third = Flipper.number(1).lte(20)
106
+ expression = Flipper.any([first, second, third])
107
+
108
+ result = expression.remove(second, third)
109
+ expect(expression.args).to eq([first, second, third])
110
+ expect(result.args).to eq([first])
111
+ end
112
+
113
+ it "returns new instance with array of expressions removed" do
114
+ first = Flipper.boolean(true)
115
+ second = Flipper.string("yep").eq("yep")
116
+ third = Flipper.number(1).lte(20)
117
+ expression = Flipper.any([first, second, third])
118
+
119
+ result = expression.remove([second, third])
120
+ expect(expression.args).to eq([first, second, third])
121
+ expect(result.args).to eq([first])
122
+ end
123
+ end
124
+ end
125
+
126
+ [
127
+ [2, 3, "equal", "eq", :Equal],
128
+ [2, 3, "not_equal", "neq", :NotEqual],
129
+ [2, 3, "greater_than", "gt", :GreaterThan],
130
+ [2, 3, "greater_than_or_equal_to", "gte", :GreaterThanOrEqualTo],
131
+ [2, 3, "greater_than_or_equal_to", "greater_than_or_equal", :GreaterThanOrEqualTo],
132
+ [2, 3, "less_than", "lt", :LessThan],
133
+ [2, 3, "less_than_or_equal_to", "lte", :LessThanOrEqualTo],
134
+ [2, 3, "less_than_or_equal_to", "less_than_or_equal", :LessThanOrEqualTo],
135
+ ].each do |(left, right, method_name, shortcut_name, function)|
136
+ it "can convert to #{function}" do
137
+ expression = build(left)
138
+ other = build(right)
139
+ converted = expression.send(method_name, other)
140
+ expect(converted).to eq(build({ function => [ left, right] }))
141
+ end
142
+
143
+ it "can convert to #{function} using #{shortcut_name}" do
144
+ expression = build(left)
145
+ other = build(right)
146
+ converted = expression.send(shortcut_name, other)
147
+ expect(converted).to eq(build({ function => [ left, right] }))
148
+ end
149
+
150
+ it "builds args into expressions when converting to #{function}" do
151
+ expression = build(left)
152
+ other = Flipper.property(:age)
153
+ converted = expression.send(method_name, other.value)
154
+ expect(converted).to eq(build({ function => [ left, other.value] }))
155
+ end
156
+ end
157
+
158
+ it "can convert to PercentageOfActors" do
159
+ expression = Flipper.constant("User;1").percentage_of_actors(40)
160
+ expect(expression).to eq(build({ PercentageOfActors: [ "User;1", 40 ] }))
161
+ end
162
+
163
+ context "All" do
164
+ describe "#all" do
165
+ it "returns self" do
166
+ expression = Flipper.all([
167
+ Flipper.boolean(true),
168
+ Flipper.string("yep").eq("yep"),
169
+ ])
170
+ expect(expression.all).to be(expression)
171
+ end
172
+ end
173
+
174
+ describe "#add" do
175
+ it "returns new instance with expression added" do
176
+ expression = Flipper.all([Flipper.boolean(true)])
177
+ other = Flipper.string("yep").eq("yep")
178
+
179
+ result = expression.add(other)
180
+ expect(result.args).to eq([
181
+ Flipper.boolean(true),
182
+ Flipper.string("yep").eq("yep"),
183
+ ])
184
+ end
185
+
186
+ it "returns new instance with many expressions added" do
187
+ expression = Flipper.all([Flipper.boolean(true)])
188
+ second = Flipper.string("yep").eq("yep")
189
+ third = Flipper.number(1).lte(20)
190
+
191
+ result = expression.add(second, third)
192
+ expect(result.args).to eq([
193
+ Flipper.boolean(true),
194
+ Flipper.string("yep").eq("yep"),
195
+ Flipper.number(1).lte(20),
196
+ ])
197
+ end
198
+
199
+ it "returns new instance with array of expressions added" do
200
+ expression = Flipper.all([Flipper.boolean(true)])
201
+ second = Flipper.string("yep").eq("yep")
202
+ third = Flipper.number(1).lte(20)
203
+
204
+ result = expression.add([second, third])
205
+ expect(result.args).to eq([
206
+ Flipper.boolean(true),
207
+ Flipper.string("yep").eq("yep"),
208
+ Flipper.number(1).lte(20),
209
+ ])
210
+ end
211
+ end
212
+
213
+ describe "#remove" do
214
+ it "returns new instance with expression removed" do
215
+ first = Flipper.boolean(true)
216
+ second = Flipper.string("yep").eq("yep")
217
+ third = Flipper.number(1).lte(20)
218
+ expression = Flipper.all([first, second, third])
219
+
220
+ result = expression.remove(second)
221
+ expect(expression.args).to eq([first, second, third])
222
+ expect(result.args).to eq([first, third])
223
+ end
224
+
225
+ it "returns new instance with many expressions removed" do
226
+ first = Flipper.boolean(true)
227
+ second = Flipper.string("yep").eq("yep")
228
+ third = Flipper.number(1).lte(20)
229
+ expression = Flipper.all([first, second, third])
230
+
231
+ result = expression.remove(second, third)
232
+ expect(expression.args).to eq([first, second, third])
233
+ expect(result.args).to eq([first])
234
+ end
235
+
236
+ it "returns new instance with array of expressions removed" do
237
+ first = Flipper.boolean(true)
238
+ second = Flipper.string("yep").eq("yep")
239
+ third = Flipper.number(1).lte(20)
240
+ expression = Flipper.all([first, second, third])
241
+
242
+ result = expression.remove([second, third])
243
+ expect(expression.args).to eq([first, second, third])
244
+ expect(result.args).to eq([first])
245
+ end
246
+ end
247
+ end
248
+ end