flipper 0.16.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (285) hide show
  1. checksums.yaml +5 -5
  2. data/.codeclimate.yml +1 -0
  3. data/.github/FUNDING.yml +1 -0
  4. data/.github/dependabot.yml +6 -0
  5. data/.github/workflows/ci.yml +110 -0
  6. data/.github/workflows/examples.yml +105 -0
  7. data/.github/workflows/release.yml +54 -0
  8. data/.rspec +1 -0
  9. data/CLAUDE.md +87 -0
  10. data/Changelog.md +2 -215
  11. data/Dockerfile +1 -1
  12. data/Gemfile +28 -20
  13. data/README.md +72 -62
  14. data/Rakefile +13 -3
  15. data/benchmark/enabled_ips.rb +10 -0
  16. data/benchmark/enabled_multiple_actors_ips.rb +20 -0
  17. data/benchmark/enabled_profile.rb +20 -0
  18. data/benchmark/instrumentation_ips.rb +21 -0
  19. data/benchmark/typecast_ips.rb +27 -0
  20. data/docker-compose.yml +37 -34
  21. data/docs/DockerCompose.md +0 -1
  22. data/docs/README.md +1 -0
  23. data/docs/images/banner.jpg +0 -0
  24. data/docs/images/flipper_cloud.png +0 -0
  25. data/examples/api/basic.ru +18 -0
  26. data/examples/api/custom_memoized.ru +36 -0
  27. data/examples/api/memoized.ru +42 -0
  28. data/examples/basic.rb +1 -12
  29. data/examples/cloud/app.ru +12 -0
  30. data/examples/cloud/backoff_policy.rb +13 -0
  31. data/examples/cloud/basic.rb +22 -0
  32. data/examples/cloud/cloud_setup.rb +20 -0
  33. data/examples/cloud/forked.rb +36 -0
  34. data/examples/cloud/import.rb +17 -0
  35. data/examples/cloud/poll_interval/README.md +111 -0
  36. data/examples/cloud/poll_interval/client.rb +108 -0
  37. data/examples/cloud/poll_interval/server.rb +98 -0
  38. data/examples/cloud/threaded.rb +33 -0
  39. data/examples/configuring_default.rb +2 -5
  40. data/examples/dsl.rb +10 -35
  41. data/examples/enabled_for_actor.rb +10 -15
  42. data/examples/expressions.rb +237 -0
  43. data/examples/group.rb +3 -6
  44. data/examples/group_dynamic_lookup.rb +5 -19
  45. data/examples/group_with_members.rb +4 -14
  46. data/examples/importing.rb +1 -1
  47. data/examples/individual_actor.rb +2 -5
  48. data/examples/instrumentation.rb +2 -2
  49. data/examples/instrumentation_last_accessed_at.rb +38 -0
  50. data/examples/memoizing.rb +35 -0
  51. data/examples/mirroring.rb +59 -0
  52. data/examples/percentage_of_actors.rb +6 -16
  53. data/examples/percentage_of_actors_enabled_check.rb +7 -10
  54. data/examples/percentage_of_actors_group.rb +5 -18
  55. data/examples/percentage_of_time.rb +3 -6
  56. data/examples/strict.rb +18 -0
  57. data/exe/flipper +5 -0
  58. data/flipper-cloud.gemspec +19 -0
  59. data/flipper.gemspec +10 -7
  60. data/lib/flipper/actor.rb +10 -3
  61. data/lib/flipper/adapter.rb +50 -8
  62. data/lib/flipper/adapter_builder.rb +44 -0
  63. data/lib/flipper/adapters/actor_limit.rb +54 -0
  64. data/lib/flipper/adapters/cache_base.rb +161 -0
  65. data/lib/flipper/adapters/dual_write.rb +63 -0
  66. data/lib/flipper/adapters/failover.rb +85 -0
  67. data/lib/flipper/adapters/failsafe.rb +72 -0
  68. data/lib/flipper/adapters/http/client.rb +64 -7
  69. data/lib/flipper/adapters/http/error.rb +19 -1
  70. data/lib/flipper/adapters/http.rb +97 -43
  71. data/lib/flipper/adapters/instrumented.rb +47 -26
  72. data/lib/flipper/adapters/memoizable.rb +44 -40
  73. data/lib/flipper/adapters/memory.rb +75 -111
  74. data/lib/flipper/adapters/operation_logger.rb +22 -78
  75. data/lib/flipper/adapters/poll/poller.rb +2 -0
  76. data/lib/flipper/adapters/poll.rb +52 -0
  77. data/lib/flipper/adapters/pstore.rb +27 -17
  78. data/lib/flipper/adapters/read_only.rb +8 -41
  79. data/lib/flipper/adapters/strict.rb +45 -0
  80. data/lib/flipper/adapters/sync/feature_synchronizer.rb +14 -1
  81. data/lib/flipper/adapters/sync/interval_synchronizer.rb +2 -7
  82. data/lib/flipper/adapters/sync/synchronizer.rb +13 -6
  83. data/lib/flipper/adapters/sync.rb +23 -29
  84. data/lib/flipper/adapters/wrapper.rb +54 -0
  85. data/lib/flipper/cli.rb +314 -0
  86. data/lib/flipper/cloud/configuration.rb +271 -0
  87. data/lib/flipper/cloud/dsl.rb +27 -0
  88. data/lib/flipper/cloud/message_verifier.rb +95 -0
  89. data/lib/flipper/cloud/middleware.rb +63 -0
  90. data/lib/flipper/cloud/migrate.rb +71 -0
  91. data/lib/flipper/cloud/routes.rb +14 -0
  92. data/lib/flipper/cloud/telemetry/backoff_policy.rb +96 -0
  93. data/lib/flipper/cloud/telemetry/instrumenter.rb +22 -0
  94. data/lib/flipper/cloud/telemetry/metric.rb +39 -0
  95. data/lib/flipper/cloud/telemetry/metric_storage.rb +30 -0
  96. data/lib/flipper/cloud/telemetry/submitter.rb +100 -0
  97. data/lib/flipper/cloud/telemetry.rb +191 -0
  98. data/lib/flipper/cloud.rb +54 -0
  99. data/lib/flipper/configuration.rb +54 -7
  100. data/lib/flipper/dsl.rb +58 -47
  101. data/lib/flipper/engine.rb +102 -0
  102. data/lib/flipper/errors.rb +3 -21
  103. data/lib/flipper/export.rb +24 -0
  104. data/lib/flipper/exporter.rb +17 -0
  105. data/lib/flipper/exporters/json/export.rb +32 -0
  106. data/lib/flipper/exporters/json/v1.rb +33 -0
  107. data/lib/flipper/expression/builder.rb +73 -0
  108. data/lib/flipper/expression/constant.rb +25 -0
  109. data/lib/flipper/expression.rb +71 -0
  110. data/lib/flipper/expressions/all.rb +9 -0
  111. data/lib/flipper/expressions/any.rb +9 -0
  112. data/lib/flipper/expressions/boolean.rb +9 -0
  113. data/lib/flipper/expressions/comparable.rb +13 -0
  114. data/lib/flipper/expressions/equal.rb +9 -0
  115. data/lib/flipper/expressions/feature_enabled.rb +34 -0
  116. data/lib/flipper/expressions/greater_than.rb +9 -0
  117. data/lib/flipper/expressions/greater_than_or_equal_to.rb +9 -0
  118. data/lib/flipper/expressions/less_than.rb +9 -0
  119. data/lib/flipper/expressions/less_than_or_equal_to.rb +9 -0
  120. data/lib/flipper/expressions/not_equal.rb +9 -0
  121. data/lib/flipper/expressions/now.rb +9 -0
  122. data/lib/flipper/expressions/number.rb +9 -0
  123. data/lib/flipper/expressions/percentage.rb +9 -0
  124. data/lib/flipper/expressions/percentage_of_actors.rb +12 -0
  125. data/lib/flipper/expressions/property.rb +9 -0
  126. data/lib/flipper/expressions/random.rb +9 -0
  127. data/lib/flipper/expressions/string.rb +9 -0
  128. data/lib/flipper/expressions/time.rb +16 -0
  129. data/lib/flipper/feature.rb +95 -28
  130. data/lib/flipper/feature_check_context.rb +10 -6
  131. data/lib/flipper/gate.rb +13 -11
  132. data/lib/flipper/gate_values.rb +5 -18
  133. data/lib/flipper/gates/actor.rb +10 -17
  134. data/lib/flipper/gates/boolean.rb +1 -1
  135. data/lib/flipper/gates/expression.rb +75 -0
  136. data/lib/flipper/gates/group.rb +5 -7
  137. data/lib/flipper/gates/percentage_of_actors.rb +10 -13
  138. data/lib/flipper/gates/percentage_of_time.rb +1 -2
  139. data/lib/flipper/identifier.rb +17 -0
  140. data/lib/flipper/instrumentation/log_subscriber.rb +35 -8
  141. data/lib/flipper/instrumentation/statsd.rb +4 -2
  142. data/lib/flipper/instrumentation/statsd_subscriber.rb +2 -4
  143. data/lib/flipper/instrumentation/subscriber.rb +8 -5
  144. data/lib/flipper/instrumenters/memory.rb +6 -2
  145. data/lib/flipper/metadata.rb +8 -1
  146. data/lib/flipper/middleware/memoizer.rb +46 -27
  147. data/lib/flipper/middleware/setup_env.rb +13 -3
  148. data/lib/flipper/model/active_record.rb +23 -0
  149. data/lib/flipper/poller.rb +157 -0
  150. data/lib/flipper/serializers/gzip.rb +22 -0
  151. data/lib/flipper/serializers/json.rb +17 -0
  152. data/lib/flipper/spec/shared_adapter_specs.rb +122 -56
  153. data/lib/flipper/test/shared_adapter_test.rb +120 -52
  154. data/lib/flipper/test_help.rb +43 -0
  155. data/lib/flipper/typecast.rb +59 -18
  156. data/lib/flipper/types/actor.rb +19 -13
  157. data/lib/flipper/types/group.rb +12 -5
  158. data/lib/flipper/types/percentage.rb +1 -1
  159. data/lib/flipper/version.rb +11 -1
  160. data/lib/flipper.rb +71 -12
  161. data/lib/generators/flipper/setup_generator.rb +68 -0
  162. data/lib/generators/flipper/templates/initializer.rb +45 -0
  163. data/lib/generators/flipper/templates/update/migrations/01_create_flipper_tables.rb.erb +22 -0
  164. data/lib/generators/flipper/templates/update/migrations/02_change_flipper_gates_value_to_text.rb.erb +18 -0
  165. data/lib/generators/flipper/update_generator.rb +35 -0
  166. data/package-lock.json +41 -0
  167. data/package.json +10 -0
  168. data/spec/fixtures/environment.rb +1 -0
  169. data/spec/fixtures/flipper_pstore_1679087600.json +46 -0
  170. data/spec/flipper/actor_spec.rb +10 -2
  171. data/spec/flipper/adapter_builder_spec.rb +72 -0
  172. data/spec/flipper/adapter_spec.rb +52 -6
  173. data/spec/flipper/adapters/actor_limit_spec.rb +75 -0
  174. data/spec/flipper/adapters/dual_write_spec.rb +82 -0
  175. data/spec/flipper/adapters/failover_spec.rb +141 -0
  176. data/spec/flipper/adapters/failsafe_spec.rb +58 -0
  177. data/spec/flipper/adapters/http/client_spec.rb +61 -0
  178. data/spec/flipper/adapters/http_spec.rb +402 -65
  179. data/spec/flipper/adapters/instrumented_spec.rb +31 -13
  180. data/spec/flipper/adapters/memoizable_spec.rb +51 -33
  181. data/spec/flipper/adapters/memory_spec.rb +33 -5
  182. data/spec/flipper/adapters/operation_logger_spec.rb +38 -12
  183. data/spec/flipper/adapters/poll_spec.rb +41 -0
  184. data/spec/flipper/adapters/pstore_spec.rb +0 -2
  185. data/spec/flipper/adapters/read_only_spec.rb +32 -18
  186. data/spec/flipper/adapters/strict_spec.rb +64 -0
  187. data/spec/flipper/adapters/sync/feature_synchronizer_spec.rb +39 -1
  188. data/spec/flipper/adapters/sync/interval_synchronizer_spec.rb +4 -5
  189. data/spec/flipper/adapters/sync/synchronizer_spec.rb +87 -1
  190. data/spec/flipper/adapters/sync_spec.rb +17 -6
  191. data/spec/flipper/cli_spec.rb +217 -0
  192. data/spec/flipper/cloud/configuration_spec.rb +257 -0
  193. data/spec/flipper/cloud/dsl_spec.rb +90 -0
  194. data/spec/flipper/cloud/message_verifier_spec.rb +104 -0
  195. data/spec/flipper/cloud/middleware_spec.rb +307 -0
  196. data/spec/flipper/cloud/migrate_spec.rb +160 -0
  197. data/spec/flipper/cloud/telemetry/backoff_policy_spec.rb +107 -0
  198. data/spec/flipper/cloud/telemetry/metric_spec.rb +87 -0
  199. data/spec/flipper/cloud/telemetry/metric_storage_spec.rb +58 -0
  200. data/spec/flipper/cloud/telemetry/submitter_spec.rb +145 -0
  201. data/spec/flipper/cloud/telemetry_spec.rb +208 -0
  202. data/spec/flipper/cloud_spec.rb +186 -0
  203. data/spec/flipper/configuration_spec.rb +37 -3
  204. data/spec/flipper/dsl_spec.rb +67 -80
  205. data/spec/flipper/engine_spec.rb +374 -0
  206. data/spec/flipper/export_spec.rb +13 -0
  207. data/spec/flipper/exporter_spec.rb +16 -0
  208. data/spec/flipper/exporters/json/export_spec.rb +60 -0
  209. data/spec/flipper/exporters/json/v1_spec.rb +33 -0
  210. data/spec/flipper/expression/builder_spec.rb +248 -0
  211. data/spec/flipper/expression_spec.rb +188 -0
  212. data/spec/flipper/expressions/all_spec.rb +15 -0
  213. data/spec/flipper/expressions/any_spec.rb +15 -0
  214. data/spec/flipper/expressions/boolean_spec.rb +15 -0
  215. data/spec/flipper/expressions/equal_spec.rb +24 -0
  216. data/spec/flipper/expressions/greater_than_or_equal_to_spec.rb +28 -0
  217. data/spec/flipper/expressions/greater_than_spec.rb +28 -0
  218. data/spec/flipper/expressions/less_than_or_equal_to_spec.rb +28 -0
  219. data/spec/flipper/expressions/less_than_spec.rb +32 -0
  220. data/spec/flipper/expressions/not_equal_spec.rb +15 -0
  221. data/spec/flipper/expressions/now_spec.rb +11 -0
  222. data/spec/flipper/expressions/number_spec.rb +21 -0
  223. data/spec/flipper/expressions/percentage_of_actors_spec.rb +20 -0
  224. data/spec/flipper/expressions/percentage_spec.rb +15 -0
  225. data/spec/flipper/expressions/property_spec.rb +13 -0
  226. data/spec/flipper/expressions/random_spec.rb +9 -0
  227. data/spec/flipper/expressions/string_spec.rb +11 -0
  228. data/spec/flipper/expressions/time_spec.rb +29 -0
  229. data/spec/flipper/feature_check_context_spec.rb +18 -20
  230. data/spec/flipper/feature_spec.rb +461 -48
  231. data/spec/flipper/gate_spec.rb +0 -2
  232. data/spec/flipper/gate_values_spec.rb +2 -34
  233. data/spec/flipper/gates/actor_spec.rb +0 -2
  234. data/spec/flipper/gates/boolean_spec.rb +1 -3
  235. data/spec/flipper/gates/expression_spec.rb +190 -0
  236. data/spec/flipper/gates/group_spec.rb +2 -5
  237. data/spec/flipper/gates/percentage_of_actors_spec.rb +61 -7
  238. data/spec/flipper/gates/percentage_of_time_spec.rb +2 -4
  239. data/spec/flipper/identifier_spec.rb +12 -0
  240. data/spec/flipper/instrumentation/log_subscriber_spec.rb +24 -7
  241. data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +26 -3
  242. data/spec/flipper/instrumenters/memory_spec.rb +18 -1
  243. data/spec/flipper/instrumenters/noop_spec.rb +14 -8
  244. data/spec/flipper/middleware/memoizer_spec.rb +199 -62
  245. data/spec/flipper/middleware/setup_env_spec.rb +23 -5
  246. data/spec/flipper/model/active_record_spec.rb +72 -0
  247. data/spec/flipper/poller_spec.rb +390 -0
  248. data/spec/flipper/registry_spec.rb +0 -1
  249. data/spec/flipper/serializers/gzip_spec.rb +13 -0
  250. data/spec/flipper/serializers/json_spec.rb +13 -0
  251. data/spec/flipper/typecast_spec.rb +121 -7
  252. data/spec/flipper/types/actor_spec.rb +63 -47
  253. data/spec/flipper/types/boolean_spec.rb +0 -1
  254. data/spec/flipper/types/group_spec.rb +24 -3
  255. data/spec/flipper/types/percentage_of_actors_spec.rb +0 -1
  256. data/spec/flipper/types/percentage_of_time_spec.rb +0 -1
  257. data/spec/flipper/types/percentage_spec.rb +0 -1
  258. data/spec/{integration_spec.rb → flipper_integration_spec.rb} +301 -59
  259. data/spec/flipper_spec.rb +123 -29
  260. data/spec/{helper.rb → spec_helper.rb} +23 -21
  261. data/spec/support/actor_names.yml +1 -0
  262. data/spec/support/descriptions.yml +1 -0
  263. data/spec/support/fail_on_output.rb +8 -0
  264. data/spec/support/fake_backoff_policy.rb +15 -0
  265. data/spec/support/skippable.rb +18 -0
  266. data/spec/support/spec_helpers.rb +53 -6
  267. data/test/adapters/actor_limit_test.rb +20 -0
  268. data/test/test_helper.rb +2 -1
  269. data/test_rails/generators/flipper/setup_generator_test.rb +69 -0
  270. data/test_rails/generators/flipper/update_generator_test.rb +96 -0
  271. data/test_rails/helper.rb +31 -0
  272. data/test_rails/system/test_help_test.rb +52 -0
  273. metadata +200 -82
  274. data/.rubocop.yml +0 -54
  275. data/.rubocop_todo.yml +0 -199
  276. data/docs/Adapters.md +0 -124
  277. data/docs/Caveats.md +0 -4
  278. data/docs/Gates.md +0 -167
  279. data/docs/Instrumentation.md +0 -27
  280. data/docs/Optimization.md +0 -114
  281. data/docs/api/README.md +0 -849
  282. data/docs/http/README.md +0 -35
  283. data/docs/read-only/README.md +0 -21
  284. data/examples/example_setup.rb +0 -8
  285. data/test/helper.rb +0 -11
data/spec/flipper_spec.rb CHANGED
@@ -1,4 +1,4 @@
1
- require 'helper'
1
+ require 'flipper/cloud'
2
2
 
3
3
  RSpec.describe Flipper do
4
4
  describe '.new' do
@@ -64,7 +64,12 @@ RSpec.describe Flipper do
64
64
 
65
65
  describe "delegation to instance" do
66
66
  let(:group) { Flipper::Types::Group.new(:admins) }
67
- let(:actor) { Flipper::Actor.new("1") }
67
+ let(:actor) {
68
+ Flipper::Actor.new("1", {
69
+ "plan" => "basic",
70
+ })
71
+ }
72
+ let(:expression) { Flipper.property(:plan).eq("basic") }
68
73
 
69
74
  before do
70
75
  described_class.configure do |config|
@@ -88,12 +93,35 @@ RSpec.describe Flipper do
88
93
  expect(described_class.instance.enabled?(:search)).to be(false)
89
94
  end
90
95
 
91
- it 'delegates bool to instance' do
92
- expect(described_class.bool).to eq(described_class.instance.bool)
96
+ it 'delegates expression to instance' do
97
+ expect(described_class.expression(:search)).to be(nil)
98
+
99
+ expression = Flipper.property(:plan).eq("basic")
100
+ Flipper.instance.enable_expression :search, expression
101
+
102
+ expect(described_class.expression(:search)).to eq(expression)
103
+ end
104
+
105
+ it 'delegates enable_expression to instance' do
106
+ described_class.enable_expression(:search, expression)
107
+ expect(described_class.instance.enabled?(:search, actor)).to be(true)
93
108
  end
94
109
 
95
- it 'delegates boolean to instance' do
96
- expect(described_class.boolean).to eq(described_class.instance.boolean)
110
+ it 'delegates disable_expression to instance' do
111
+ described_class.disable_expression(:search)
112
+ expect(described_class.instance.enabled?(:search, actor)).to be(false)
113
+ end
114
+
115
+ it 'delegates add_expression to instance' do
116
+ described_class.add_expression(:search, expression)
117
+ expect(described_class.instance.enabled?(:search, actor)).to be(true)
118
+ end
119
+
120
+ it 'delegates remove_expression to instance' do
121
+ described_class.enable_expression(:search, Flipper.any(expression))
122
+ expect(described_class.instance.enabled?(:search, actor)).to be(true)
123
+ described_class.remove_expression(:search, expression)
124
+ expect(described_class.instance.enabled?(:search, actor)).to be(false)
97
125
  end
98
126
 
99
127
  it 'delegates enable_actor to instance' do
@@ -106,10 +134,6 @@ RSpec.describe Flipper do
106
134
  expect(described_class.instance.enabled?(:search, actor)).to be(false)
107
135
  end
108
136
 
109
- it 'delegates actor to instance' do
110
- expect(described_class.actor(actor)).to eq(described_class.instance.actor(actor))
111
- end
112
-
113
137
  it 'delegates enable_group to instance' do
114
138
  described_class.enable_group(:search, group)
115
139
  expect(described_class.instance[:search].enabled_groups).to include(group)
@@ -130,15 +154,6 @@ RSpec.describe Flipper do
130
154
  expect(described_class.instance[:search].percentage_of_actors_value).to be(0)
131
155
  end
132
156
 
133
- it 'delegates actors to instance' do
134
- expect(described_class.actors(5)).to eq(described_class.instance.actors(5))
135
- end
136
-
137
- it 'delegates percentage_of_actors to instance' do
138
- expected = described_class.instance.percentage_of_actors(5)
139
- expect(described_class.percentage_of_actors(5)).to eq(expected)
140
- end
141
-
142
157
  it 'delegates enable_percentage_of_time to instance' do
143
158
  described_class.enable_percentage_of_time(:search, 5)
144
159
  expect(described_class.instance[:search].percentage_of_time_value).to be(5)
@@ -149,15 +164,6 @@ RSpec.describe Flipper do
149
164
  expect(described_class.instance[:search].percentage_of_time_value).to be(0)
150
165
  end
151
166
 
152
- it 'delegates time to instance' do
153
- expect(described_class.time(56)).to eq(described_class.instance.time(56))
154
- end
155
-
156
- it 'delegates percentage_of_time to instance' do
157
- expected = described_class.instance.percentage_of_time(56)
158
- expect(described_class.percentage_of_time(56)).to eq(expected)
159
- end
160
-
161
167
  it 'delegates features to instance' do
162
168
  described_class.instance.add(:search)
163
169
  expect(described_class.features).to eq(described_class.instance.features)
@@ -202,10 +208,21 @@ RSpec.describe Flipper do
202
208
  expect(described_class.enabled?(:search)).to be(true)
203
209
  end
204
210
 
211
+ it 'delegates export to instance' do
212
+ described_class.enable(:search)
213
+ expect(described_class.export).to eq(described_class.adapter.export)
214
+ expect(described_class.export(format: :json)).to eq(described_class.adapter.export(format: :json))
215
+ end
216
+
205
217
  it 'delegates adapter to instance' do
206
218
  expect(described_class.adapter).to eq(described_class.instance.adapter)
207
219
  end
208
220
 
221
+ it 'delegates adapter_stack to instance' do
222
+ expect(described_class.adapter_stack).to eq(described_class.instance.adapter_stack)
223
+ expect(described_class.adapter_stack).to eq("memoizable -> memory")
224
+ end
225
+
209
226
  it 'delegates memoize= to instance' do
210
227
  expect(described_class.adapter.memoizing?).to be(false)
211
228
  described_class.memoize = true
@@ -215,6 +232,35 @@ RSpec.describe Flipper do
215
232
  it 'delegates memoizing? to instance' do
216
233
  expect(described_class.memoizing?).to eq(described_class.adapter.memoizing?)
217
234
  end
235
+
236
+ it 'delegates read_only? to instance' do
237
+ expect(described_class.read_only?).to eq(described_class.adapter.read_only?)
238
+ end
239
+
240
+ it 'delegates sync stuff to instance and does nothing' do
241
+ expect(described_class.sync).to be(nil)
242
+ expect(described_class.sync_secret).to be(nil)
243
+ end
244
+
245
+ it 'delegates sync stuff to instance for Flipper::Cloud' do
246
+ stub = stub_request(:get, "https://www.flippercloud.io/adapter/features?exclude_gate_names=true").
247
+ with({
248
+ headers: {
249
+ 'flipper-cloud-token'=>'asdf',
250
+ },
251
+ }).to_return(status: 200, body: '{"features": {}}', headers: {})
252
+ cloud_configuration = Flipper::Cloud::Configuration.new({
253
+ token: "asdf",
254
+ sync_secret: "tasty",
255
+ })
256
+
257
+ described_class.configure do |config|
258
+ config.default { Flipper::Cloud::DSL.new(cloud_configuration) }
259
+ end
260
+ described_class.sync
261
+ expect(described_class.sync_secret).to eq("tasty")
262
+ expect(stub).to have_been_made.at_least_once
263
+ end
218
264
  end
219
265
 
220
266
  describe '.register' do
@@ -272,7 +318,7 @@ RSpec.describe Flipper do
272
318
 
273
319
  describe '.group_exists' do
274
320
  it 'returns true if the group is already created' do
275
- group = described_class.register('admins', &:admin?)
321
+ described_class.register('admins', &:admin?)
276
322
  expect(described_class.group_exists?(:admins)).to eq(true)
277
323
  end
278
324
 
@@ -325,4 +371,52 @@ RSpec.describe Flipper do
325
371
  expect(described_class.instance_variable_get('@groups_registry')).to eq(registry)
326
372
  end
327
373
  end
374
+
375
+ describe ".constant" do
376
+ it "returns Flipper::Expression::Constant instance" do
377
+ expect(described_class.constant(false)).to eq(Flipper::Expression::Constant.new(false))
378
+ expect(described_class.constant("string")).to eq(Flipper::Expression::Constant.new("string"))
379
+ end
380
+ end
381
+
382
+ describe ".property" do
383
+ it "returns Flipper::Expressions::Property expression" do
384
+ expect(Flipper.property("name")).to eq(Flipper::Expression.build(Property: "name"))
385
+ end
386
+ end
387
+
388
+ describe ".boolean" do
389
+ it "returns Flipper::Expressions::Boolean expression" do
390
+ expect(described_class.boolean(true)).to eq(Flipper::Expression.build(Boolean: true))
391
+ expect(described_class.boolean(false)).to eq(Flipper::Expression.build(Boolean: false))
392
+ end
393
+ end
394
+
395
+ describe ".random" do
396
+ it "returns Flipper::Expressions::Random expression" do
397
+ expect(Flipper.random(100)).to eq(Flipper::Expression.build(Random: 100))
398
+ end
399
+ end
400
+
401
+ describe ".any" do
402
+ let(:age_expression) { Flipper.property(:age).gte(21) }
403
+ let(:plan_expression) { Flipper.property(:plan).eq("basic") }
404
+
405
+ it "returns Flipper::Expressions::Any instance" do
406
+ expect(Flipper.any(age_expression, plan_expression)).to eq(
407
+ Flipper::Expression.build({Any: [age_expression, plan_expression]})
408
+ )
409
+ end
410
+ end
411
+
412
+ describe ".all" do
413
+ let(:age_expression) { Flipper.property(:age).gte(21) }
414
+ let(:plan_expression) { Flipper.property(:plan).eq("basic") }
415
+
416
+ it "returns Flipper::Expressions::All instance" do
417
+ expect(Flipper.all(age_expression, plan_expression)).to eq(
418
+ Flipper::Expression.build({All: [age_expression, plan_expression]})
419
+ )
420
+ end
421
+ end
328
422
  end
@@ -2,25 +2,39 @@ $LOAD_PATH.unshift(File.expand_path('../../lib', __FILE__))
2
2
 
3
3
  require 'pp'
4
4
  require 'pathname'
5
- FlipperRoot = Pathname(__FILE__).dirname.join('..').expand_path
6
-
7
- require 'rubygems'
8
- require 'bundler'
5
+ require 'bundler/setup'
9
6
 
10
- Bundler.setup(:default)
7
+ require 'warning'
8
+ Warning.ignore(/lib\/statsd/)
9
+ Warning.ignore(/lib\/debug\//)
10
+ Warning.ignore(/lib\/ice_age\//)
11
+ Warning.ignore(/lib\/moneta\//)
12
+ Warning.ignore(/lib\/mongo\//)
11
13
 
12
- require 'pry'
14
+ require 'debug'
15
+ require 'statsd'
13
16
  require 'webmock/rspec'
14
17
  WebMock.disable_net_connect!(allow_localhost: true)
15
18
 
16
19
  require 'flipper'
17
- require 'flipper-ui'
18
- require 'flipper-api'
20
+ require 'flipper/api'
21
+ require 'flipper/spec/shared_adapter_specs'
22
+ require 'flipper/ui'
23
+ require 'flipper/test_help'
24
+
25
+ FlipperRoot = Pathname(__FILE__).dirname.join('..').expand_path
26
+ Dir[FlipperRoot.join('spec/support/**/*.rb')].sort.each { |f| require f }
19
27
 
20
- Dir[FlipperRoot.join('spec/support/**/*.rb')].each { |f| require f }
28
+ # Disable telemetry logging in specs.
29
+ ENV["FLIPPER_CLOUD_LOGGING_ENABLED"] = "false"
21
30
 
22
31
  RSpec.configure do |config|
23
32
  config.before(:example) do
33
+ # default stub for telemetry
34
+ stub_request(:post, "https://www.flippercloud.io/adapter/telemetry").
35
+ to_return(status: 200, body: "", headers: {})
36
+ Flipper::Cloud::Telemetry.reset if defined?(Flipper::Cloud::Telemetry) && Flipper::Cloud::Telemetry.respond_to?(:reset)
37
+ Flipper::Poller.reset if defined?(Flipper::Poller)
24
38
  Flipper.unregister_groups
25
39
  Flipper.configuration = nil
26
40
  end
@@ -89,15 +103,3 @@ RSpec.shared_examples_for 'a DSL feature' do
89
103
  end.to raise_error(ArgumentError, /must be a String or Symbol/)
90
104
  end
91
105
  end
92
-
93
- RSpec.shared_examples_for 'a DSL boolean method' do
94
- it 'returns boolean with value set' do
95
- result = subject.send(method_name, true)
96
- expect(result).to be_instance_of(Flipper::Types::Boolean)
97
- expect(result.value).to be(true)
98
-
99
- result = subject.send(method_name, false)
100
- expect(result).to be_instance_of(Flipper::Types::Boolean)
101
- expect(result.value).to be(false)
102
- end
103
- end
@@ -0,0 +1 @@
1
+ actor_name_1: 'Actor #1'
@@ -0,0 +1 @@
1
+ some_awesome_feature: 'Awesome feature description'
@@ -0,0 +1,8 @@
1
+ if ENV["CI"] || ENV["FAIL_ON_OUTPUT"]
2
+ RSpec.configure do |config|
3
+ config.around do |example|
4
+ output = capture_output { example.run }
5
+ fail "Use `silence { }` to avoid printing to STDOUT/STDERR\n#{output}" unless output.empty?
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,15 @@
1
+ class FakeBackoffPolicy
2
+ def initialize
3
+ @retries = 0
4
+ end
5
+
6
+ attr_reader :retries
7
+
8
+ def next_interval
9
+ @retries += 1
10
+ 0
11
+ end
12
+
13
+ def reset
14
+ end
15
+ end
@@ -0,0 +1,18 @@
1
+ RSpec.configure do |config|
2
+ config.before(:all) do
3
+ $skip = false
4
+ end
5
+
6
+ def skip_on_error(error, message, &block)
7
+ # Premptively skip if we've already skipped
8
+ skip(message) if $skip
9
+ block.call
10
+ rescue error
11
+ if ENV["CI"]
12
+ raise
13
+ else
14
+ $skip = true
15
+ skip(message)
16
+ end
17
+ end
18
+ end
@@ -1,7 +1,11 @@
1
+ require 'ice_age'
1
2
  require 'json'
2
3
  require 'rack/test'
4
+ require 'rack/session'
3
5
 
4
6
  module SpecHelpers
7
+ extend self
8
+
5
9
  def self.included(base)
6
10
  base.let(:flipper) { build_flipper }
7
11
  base.let(:app) { build_app(flipper) }
@@ -9,7 +13,7 @@ module SpecHelpers
9
13
 
10
14
  def build_app(flipper, options = {})
11
15
  Flipper::UI.app(flipper, options) do |builder|
12
- builder.use Rack::Session::Cookie, secret: 'test'
16
+ builder.use Rack::Session::Cookie, secret: 'test' * 16 # Rack 3+ wants a 64-character secret
13
17
  end
14
18
  end
15
19
 
@@ -26,11 +30,15 @@ module SpecHelpers
26
30
  end
27
31
 
28
32
  def json_response
29
- JSON.parse(last_response.body)
33
+ body = last_response.body
34
+ if last_response["content-encoding"] == 'gzip'
35
+ body = Flipper::Typecast.from_gzip(body)
36
+ end
37
+ JSON.parse(body)
30
38
  end
31
39
 
32
40
  def api_error_code_reference_url
33
- 'https://github.com/jnunemaker/flipper/tree/master/docs/api#error-code-reference'
41
+ 'https://flippercloud.io/docs/api#error-code-reference'
34
42
  end
35
43
 
36
44
  def api_not_found_response
@@ -41,6 +49,14 @@ module SpecHelpers
41
49
  }
42
50
  end
43
51
 
52
+ def api_positive_percentage_error_response
53
+ {
54
+ 'code' => 3,
55
+ 'message' => 'Percentage must be a positive number less than or equal to 100.',
56
+ 'more_info' => api_error_code_reference_url,
57
+ }
58
+ end
59
+
44
60
  def api_flipper_id_is_missing_response
45
61
  {
46
62
  'code' => 4,
@@ -49,16 +65,47 @@ module SpecHelpers
49
65
  }
50
66
  end
51
67
 
52
- def api_positive_percentage_error_response
68
+ def api_expression_invalid_response
53
69
  {
54
- 'code' => 3,
55
- 'message' => 'Percentage must be a positive number less than or equal to 100.',
70
+ 'code' => 7,
71
+ 'message' => 'The provided expression was not valid.',
56
72
  'more_info' => api_error_code_reference_url,
57
73
  }
58
74
  end
75
+
76
+ def silence
77
+ # Store the original stderr and stdout in order to restore them later
78
+ original_stderr = $stderr
79
+ original_stdout = $stdout
80
+
81
+ # Redirect stderr and stdout
82
+ $stderr = $stdout = StringIO.new
83
+
84
+ yield
85
+ ensure
86
+ $stderr = original_stderr
87
+ $stdout = original_stdout
88
+ end
89
+
90
+ def capture_output
91
+ original_stderr = $stderr
92
+ original_stdout = $stdout
93
+
94
+ output = $stdout = $stderr = StringIO.new
95
+
96
+ yield
97
+
98
+ output.string
99
+ ensure
100
+ $stderr = original_stderr
101
+ $stdout = original_stdout
102
+ end
59
103
  end
60
104
 
61
105
  RSpec.configure do |config|
106
+ config.order = :random
107
+ Kernel.srand config.seed
108
+
62
109
  config.include Rack::Test::Methods
63
110
  config.include SpecHelpers
64
111
  end
@@ -0,0 +1,20 @@
1
+ require "test_helper"
2
+ require "flipper/test/shared_adapter_test"
3
+ require "flipper/adapters/actor_limit"
4
+
5
+ class Flipper::Adapters::ActorLimitTest < MiniTest::Test
6
+ prepend Flipper::Test::SharedAdapterTests
7
+
8
+ def setup
9
+ @memory = Flipper::Adapters::Memory.new
10
+ @adapter = Flipper::Adapters::ActorLimit.new(@memory, 5)
11
+ end
12
+
13
+ def test_enable_fails_when_limit_exceeded
14
+ 5.times { |i| @feature.enable Flipper::Actor.new("User;#{i}") }
15
+
16
+ assert_raises Flipper::Adapters::ActorLimit::LimitExceeded do
17
+ @feature.enable Flipper::Actor.new("User;6")
18
+ end
19
+ end
20
+ end
data/test/test_helper.rb CHANGED
@@ -1,6 +1,7 @@
1
+ require 'bundler/setup'
1
2
  require 'flipper'
2
3
  require 'minitest/autorun'
3
4
  require 'minitest/unit'
4
- Dir['./lib/flipper/test/*.rb'].each { |f| require(f) }
5
+ Dir['./lib/flipper/test/*.rb'].sort.each { |f| require(f) }
5
6
 
6
7
  FlipperRoot = Pathname(__FILE__).dirname.join('..').expand_path
@@ -0,0 +1,69 @@
1
+ require "helper"
2
+ require "generators/flipper/setup_generator"
3
+
4
+ class SetupGeneratorTest < Rails::Generators::TestCase
5
+ tests Flipper::Generators::SetupGenerator
6
+ ROOT = File.expand_path("../../../tmp/generators", __dir__)
7
+ destination ROOT
8
+ setup :prepare_destination
9
+
10
+ test "invokes flipper:active_record generator if ActiveRecord adapter defined" do
11
+ begin
12
+ load 'flipper/adapters/active_record.rb'
13
+ run_generator
14
+ assert_migration "db/migrate/create_flipper_tables.rb"
15
+ ensure
16
+ Flipper::Adapters.send(:remove_const, :ActiveRecord)
17
+ end
18
+ end
19
+
20
+ test "generates an initializer" do
21
+ run_generator
22
+ assert_file 'config/initializers/flipper.rb', /Flipper\.configure/
23
+ end
24
+
25
+ test "does not invoke flipper:active_record generator if ActiveRecord adapter not defined" do
26
+ # Ensure adapter not defined
27
+ Flipper::Adapters.send(:remove_const, :ActiveRecord) rescue nil
28
+
29
+ run_generator
30
+ assert_no_migration "db/migrate/create_flipper_tables.rb"
31
+ end
32
+
33
+ %w(.env.development .env.local .env).each do |file|
34
+ test "configures Flipper Cloud token in #{file} if it exists" do
35
+ File.write("#{ROOT}/#{file}", "")
36
+ run_generator %w(--token abc123)
37
+ assert_file file, /^FLIPPER_CLOUD_TOKEN=abc123$/m
38
+ end
39
+ end
40
+
41
+ test "configures Flipper Cloud token in .env.development before .env" do
42
+ File.write("#{ROOT}/.env.development", "")
43
+ File.write("#{ROOT}/.env", "")
44
+
45
+ run_generator %w(--token abc123)
46
+ assert_file ".env.development", /^FLIPPER_CLOUD_TOKEN=abc123$/m
47
+ assert_file ".env", ""
48
+ end
49
+
50
+ test "does not write to .env if no token provided" do
51
+ File.write("#{ROOT}/.env", "")
52
+ run_generator
53
+ assert_file ".env", ""
54
+ end
55
+
56
+ test "configures Flipper Cloud token in config/credentials.yml.enc if credentials.yml.enc exist" do
57
+ Dir.chdir(ROOT) do
58
+ FileUtils.mkdir_p("config")
59
+ ENV["RAILS_MASTER_KEY"] = "a" * 32
60
+ Rails.application = Class.new(Rails::Application)
61
+ Rails.application.credentials.write("")
62
+
63
+ run_generator %w(--token abc123)
64
+ assert_file "config/credentials.yml.enc"
65
+ expected_config = { flipper: { cloud_token: "abc123" } }
66
+ assert_equal expected_config, Rails.application.credentials.config
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,96 @@
1
+ require "helper"
2
+ require "generators/flipper/update_generator"
3
+
4
+ class UpdateGeneratorTest < Rails::Generators::TestCase
5
+ tests Flipper::Generators::UpdateGenerator
6
+ ROOT = File.expand_path("../../../../tmp/generators", __FILE__)
7
+ destination ROOT
8
+ setup :prepare_destination
9
+
10
+ setup do
11
+ ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:')
12
+ end
13
+
14
+ teardown do
15
+ ActiveRecord::Base.connection.close
16
+ end
17
+
18
+ test "generates migrations" do
19
+ run_generator
20
+
21
+ assert_migration "db/migrate/create_flipper_tables.rb" do |migration|
22
+ assert_method :up, migration do |up|
23
+ assert_match(/create_table :flipper_features/, up)
24
+ assert_match(/create_table :flipper_gates/, up)
25
+ end
26
+
27
+ assert_method :down, migration do |down|
28
+ assert_match(/drop_table :flipper_features/, down)
29
+ assert_match(/drop_table :flipper_gates/, down)
30
+ end
31
+ end
32
+
33
+ assert_migration "db/migrate/change_flipper_gates_value_to_text.rb" do |migration|
34
+ [:up, :down].each do |dir|
35
+ assert_method :up, migration do |method|
36
+ assert_match(/change_column/, method)
37
+ end
38
+ end
39
+ end
40
+
41
+ require_migrations
42
+
43
+ silence { CreateFlipperTables.migrate(:up) }
44
+ assert ActiveRecord::Base.connection.table_exists?(:flipper_features)
45
+ assert ActiveRecord::Base.connection.table_exists?(:flipper_gates)
46
+
47
+ assert ActiveRecord::Base.connection.column_exists?(:flipper_gates, :value, :string)
48
+ silence { ChangeFlipperGatesValueToText.migrate(:up) }
49
+ assert ActiveRecord::Base.connection.column_exists?(:flipper_gates, :value, :text)
50
+
51
+ silence { ChangeFlipperGatesValueToText.migrate(:down) }
52
+ assert ActiveRecord::Base.connection.column_exists?(:flipper_gates, :value, :string)
53
+
54
+ silence { CreateFlipperTables.migrate(:down) }
55
+ refute ActiveRecord::Base.connection.table_exists?(:flipper_features)
56
+ refute ActiveRecord::Base.connection.table_exists?(:flipper_gates)
57
+ end
58
+
59
+ test "ChangeFlipperGatesValueToText is a noop if value is already text" do
60
+ self.class.generator_class = Flipper::Generators::ActiveRecordGenerator
61
+ run_generator
62
+
63
+ self.class.generator_class = Flipper::Generators::UpdateGenerator
64
+ run_generator
65
+
66
+ assert_migration "db/migrate/create_flipper_tables.rb" do |migration|
67
+ assert_method :up, migration do |up|
68
+ assert_match(/text :value/, up)
69
+ end
70
+ end
71
+
72
+ assert_migration "db/migrate/change_flipper_gates_value_to_text.rb"
73
+
74
+ require_migrations
75
+
76
+ silence { CreateFlipperTables.migrate(:up) }
77
+ assert ActiveRecord::Base.connection.column_exists?(:flipper_gates, :value, :text)
78
+
79
+ assert_nothing_raised do
80
+ silence { ChangeFlipperGatesValueToText.migrate(:up) }
81
+ end
82
+ assert ActiveRecord::Base.connection.column_exists?(:flipper_gates, :value, :text)
83
+ end
84
+
85
+ def require_migrations
86
+ # If these are not reloaded, then test order can cause failures
87
+ Object.send(:remove_const, :CreateFlipperTables) if defined?(::CreateFlipperTables)
88
+ Object.send(:remove_const, :ChangeFlipperGatesValueToText) if defined?(::ChangeFlipperGatesValueToText)
89
+
90
+ Dir.glob("#{ROOT}/db/migrate/*.rb").each do |file|
91
+ assert_nothing_raised do
92
+ load file
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,31 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+ require 'minitest/autorun'
4
+ require 'rails'
5
+ require 'rails/test_help'
6
+
7
+ require 'warning'
8
+ Warning.ignore(/lib\/capybara\//)
9
+
10
+ begin
11
+ ActiveSupport::TestCase.test_order = :random
12
+ rescue NoMethodError
13
+ # no biggie, means we are on older version of AS that doesn't have this option
14
+ end
15
+
16
+ def silence
17
+ # Store the original stderr and stdout in order to restore them later
18
+ original_stderr = $stderr
19
+ original_stdout = $stdout
20
+
21
+ # Redirect stderr and stdout
22
+ output = $stderr = $stdout = StringIO.new
23
+
24
+ yield
25
+
26
+ $stderr = original_stderr
27
+ $stdout = original_stdout
28
+
29
+ # Return output
30
+ output.string
31
+ end