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
@@ -1,5 +1,5 @@
1
- require "helper"
2
1
  require "flipper/adapters/memory"
2
+ require "flipper/adapters/actor_limit"
3
3
  require "flipper/instrumenters/memory"
4
4
  require "flipper/adapters/sync/synchronizer"
5
5
 
@@ -85,5 +85,91 @@ RSpec.describe Flipper::Adapters::Sync::Synchronizer do
85
85
 
86
86
  expect(local_flipper.features.map(&:key)).to eq([])
87
87
  end
88
+
89
+ it 'emits feature_operation.flipper events when syncing' do
90
+ remote_flipper.enable(:search)
91
+
92
+ subject.call
93
+
94
+ events = instrumenter.events_by_name("feature_operation.flipper")
95
+ enable_events = events.select { |e| e.payload[:operation] == :enable }
96
+ expect(enable_events).not_to be_empty
97
+
98
+ feature_names = enable_events.map { |e| e.payload[:feature_name].to_s }
99
+ expect(feature_names).to include("search")
100
+ end
101
+
102
+ it 'emits feature_operation.flipper events when adding features' do
103
+ remote_flipper.add(:new_feature)
104
+
105
+ subject.call
106
+
107
+ events = instrumenter.events_by_name("feature_operation.flipper")
108
+ add_events = events.select { |e| e.payload[:operation] == :add }
109
+ expect(add_events).not_to be_empty
110
+
111
+ feature_names = add_events.map { |e| e.payload[:feature_name].to_s }
112
+ expect(feature_names).to include("new_feature")
113
+ end
114
+
115
+ it 'emits feature_operation.flipper events when removing features' do
116
+ local_flipper.add(:old_feature)
117
+
118
+ subject.call
119
+
120
+ events = instrumenter.events_by_name("feature_operation.flipper")
121
+ remove_events = events.select { |e| e.payload[:operation] == :remove }
122
+ expect(remove_events).not_to be_empty
123
+
124
+ feature_names = remove_events.map { |e| e.payload[:feature_name].to_s }
125
+ expect(feature_names).to include("old_feature")
126
+ end
127
+ end
128
+
129
+ context 'with ActorLimit adapter wrapping local' do
130
+ let(:limit) { 10 }
131
+ let(:limited_local) { Flipper::Adapters::ActorLimit.new(local, limit) }
132
+ let(:limited_local_flipper) { Flipper.new(limited_local) }
133
+
134
+ subject { described_class.new(limited_local, remote, instrumenter: instrumenter) }
135
+
136
+ it 'syncs actors even when remote has more actors than local limit' do
137
+ # Remote has more actors than local limit allows
138
+ 20.times { |i| remote_flipper[:search].enable_actor Flipper::Actor.new("User;#{i}") }
139
+
140
+ # This should NOT raise - sync should bypass actor limits
141
+ expect { subject.call }.not_to raise_error
142
+
143
+ # All actors should be synced
144
+ expect(limited_local_flipper[:search].actors_value.size).to eq(20)
145
+ end
146
+
147
+ it 'syncs new actors added to remote after initial sync' do
148
+ # Initial state: remote has 20 actors, local limit is 10
149
+ 20.times { |i| remote_flipper[:search].enable_actor Flipper::Actor.new("User;#{i}") }
150
+
151
+ # First sync - should work despite exceeding limit
152
+ subject.call
153
+ expect(limited_local_flipper[:search].actors_value.size).to eq(20)
154
+
155
+ # Add a 21st actor to remote (simulating Cloud adding a new actor)
156
+ remote_flipper[:search].enable_actor Flipper::Actor.new("User;20")
157
+
158
+ # Sync again - should pick up the new actor
159
+ expect { subject.call }.not_to raise_error
160
+ expect(limited_local_flipper[:search].actors_value.size).to eq(21)
161
+ expect(limited_local_flipper[:search].actors_value).to include("User;20")
162
+ end
163
+
164
+ it 'still enforces limit for direct enable operations' do
165
+ # First sync 20 actors from remote
166
+ 20.times { |i| remote_flipper[:search].enable_actor Flipper::Actor.new("User;#{i}") }
167
+ subject.call
168
+
169
+ # Direct enable should still fail because we're over limit
170
+ expect {
171
+ limited_local_flipper[:search].enable_actor Flipper::Actor.new("User;new")
172
+ }.to raise_error(Flipper::Adapters::ActorLimit::LimitExceeded)
173
+ end
88
174
  end
89
175
  end
@@ -1,7 +1,5 @@
1
- require 'helper'
2
1
  require 'flipper/adapters/sync'
3
2
  require 'flipper/adapters/operation_logger'
4
- require 'flipper/spec/shared_adapter_specs'
5
3
  require 'active_support/notifications'
6
4
 
7
5
  RSpec.describe Flipper::Adapters::Sync do
@@ -175,22 +173,22 @@ RSpec.describe Flipper::Adapters::Sync do
175
173
  end
176
174
 
177
175
  it 'synchronizes for #features' do
178
- expect(subject).to receive(:sync)
176
+ expect(subject).to receive(:synchronize)
179
177
  subject.features
180
178
  end
181
179
 
182
180
  it 'synchronizes for #get' do
183
- expect(subject).to receive(:sync)
181
+ expect(subject).to receive(:synchronize)
184
182
  subject.get sync[:search]
185
183
  end
186
184
 
187
185
  it 'synchronizes for #get_multi' do
188
- expect(subject).to receive(:sync)
186
+ expect(subject).to receive(:synchronize)
189
187
  subject.get_multi [sync[:search]]
190
188
  end
191
189
 
192
190
  it 'synchronizes for #get_all' do
193
- expect(subject).to receive(:sync)
191
+ expect(subject).to receive(:synchronize)
194
192
  subject.get_all
195
193
  end
196
194
 
@@ -199,4 +197,17 @@ RSpec.describe Flipper::Adapters::Sync do
199
197
  expect(remote_adapter).to receive(:get_all).and_raise(exception)
200
198
  expect { subject.get_all }.not_to raise_error
201
199
  end
200
+
201
+ describe '#adapter_stack' do
202
+ it 'returns the tree representation' do
203
+ expect(subject.adapter_stack).to eq("sync(local: operation_logger -> memory, remote: operation_logger -> memory)")
204
+ end
205
+
206
+ it 'shows nested adapters in the tree' do
207
+ memory = Flipper::Adapters::Memory.new
208
+ strict = Flipper::Adapters::Strict.new(Flipper::Adapters::Memory.new)
209
+ adapter = described_class.new(memory, strict, interval: 1)
210
+ expect(adapter.adapter_stack).to eq("sync(local: memory, remote: strict -> memory)")
211
+ end
212
+ end
202
213
  end
@@ -0,0 +1,217 @@
1
+ require "flipper/cli"
2
+
3
+ RSpec.describe Flipper::CLI do
4
+ let(:stdout) { StringIO.new }
5
+ let(:stderr) { StringIO.new }
6
+ let(:cli) { Flipper::CLI.new(stdout: stdout, stderr: stderr) }
7
+
8
+ Result = Struct.new(:status, :stdout, :stderr, keyword_init: true)
9
+
10
+ before do
11
+ # Prentend stdout/stderr a TTY to test colorization
12
+ allow(stdout).to receive(:tty?).and_return(true)
13
+ allow(stderr).to receive(:tty?).and_return(true)
14
+ end
15
+
16
+ # Infer the command from the description
17
+ let(:argv) do
18
+ descriptions = self.class.parent_groups.map {|g| g.metadata[:description_args] }.reverse.flatten.drop(1)
19
+ descriptions.map { |arg| Shellwords.split(arg) }.flatten
20
+ end
21
+
22
+ subject do
23
+ status = 0
24
+
25
+ begin
26
+ cli.run(argv)
27
+ rescue SystemExit => e
28
+ status = e.status
29
+ end
30
+
31
+ Result.new(status: status, stdout: stdout.string, stderr: stderr.string)
32
+ end
33
+
34
+ before do
35
+ ENV["FLIPPER_REQUIRE"] = "./spec/fixtures/environment"
36
+ end
37
+
38
+ describe "enable" do
39
+ describe "feature" do
40
+ it do
41
+ expect(subject).to have_attributes(status: 0, stdout: /feature.*\e\[32m.*enabled/)
42
+ expect(Flipper).to be_enabled(:feature)
43
+ end
44
+ end
45
+
46
+ describe "-a User;1 feature" do
47
+ it do
48
+ expect(subject).to have_attributes(status: 0, stdout: /feature.*\e\[33m.*enabled.*User;1/m)
49
+ expect(Flipper).to be_enabled(:feature, Flipper::Actor.new("User;1"))
50
+ end
51
+ end
52
+
53
+ describe "feature -g admins" do
54
+ it do
55
+ expect(subject).to have_attributes(status: 0, stdout: /feature.*enabled.*admins/m)
56
+ expect(Flipper.feature('feature').enabled_groups.map(&:name)).to eq([:admins])
57
+ end
58
+ end
59
+
60
+ describe "feature -p 30" do
61
+ it do
62
+ expect(subject).to have_attributes(status: 0, stdout: /feature.*enabled.*30% of actors/m)
63
+ expect(Flipper.feature('feature').percentage_of_actors_value).to eq(30)
64
+ end
65
+ end
66
+
67
+ describe "feature -t 50" do
68
+ it do
69
+ expect(subject).to have_attributes(status: 0, stdout: /feature.*enabled.*50% of time/m)
70
+ expect(Flipper.feature('feature').percentage_of_time_value).to eq(50)
71
+ end
72
+ end
73
+
74
+ describe %|feature -x '{"Equal":[{"Property":"flipper_id"},"User;1"]}'| do
75
+ it do
76
+ expect(subject).to have_attributes(status: 0, stdout: /feature.*enabled.*User;1/m)
77
+ expect(Flipper.feature('feature').expression.value).to eq({ "Equal" => [ { "Property" => ["flipper_id"] }, "User;1" ] })
78
+ end
79
+ end
80
+
81
+ describe %|feature -x invalid_json| do
82
+ it do
83
+ expect(subject).to have_attributes(status: 1, stderr: /JSON parse error/m)
84
+ end
85
+ end
86
+
87
+ describe %|feature -x '{}'| do
88
+ it do
89
+ expect(subject).to have_attributes(status: 1, stderr: /Invalid expression/m)
90
+ end
91
+ end
92
+ end
93
+
94
+ describe "disable" do
95
+ describe "feature" do
96
+ before { Flipper.enable :feature }
97
+
98
+ it do
99
+ expect(subject).to have_attributes(status: 0, stdout: /feature.*disabled/)
100
+ expect(Flipper).not_to be_enabled(:feature)
101
+ end
102
+ end
103
+
104
+ describe "feature -g admins" do
105
+ before { Flipper.enable_group(:feature, :admins) }
106
+
107
+ it do
108
+ expect(subject).to have_attributes(status: 0, stdout: /feature.*disabled/)
109
+ expect(Flipper.feature('feature').enabled_groups).to be_empty
110
+ end
111
+ end
112
+ end
113
+
114
+ describe "list" do
115
+ before do
116
+ Flipper.enable :foo
117
+ Flipper.disable :bar
118
+ end
119
+
120
+ it "lists features" do
121
+ expect(subject).to have_attributes(status: 0, stdout: /foo.*enabled/)
122
+ expect(subject).to have_attributes(status: 0, stdout: /bar.*disabled/)
123
+ end
124
+ end
125
+
126
+ ["-h", "--help", "help"].each do |arg|
127
+ describe arg do
128
+ it { should have_attributes(status: 0, stdout: /Usage: flipper/) }
129
+
130
+ it "should list subcommands" do
131
+ %w(enable disable list).each do |subcommand|
132
+ expect(subject.stdout).to match(/#{subcommand}/)
133
+ end
134
+ end
135
+ end
136
+ end
137
+
138
+ describe "help enable" do
139
+ it { should have_attributes(status: 0, stdout: /Usage: flipper enable \[options\] <feature>/) }
140
+ end
141
+
142
+ describe "nope" do
143
+ it { should have_attributes(status: 1, stderr: /Unknown command: nope/) }
144
+ end
145
+
146
+ describe "--nope" do
147
+ it { should have_attributes(status: 1, stderr: /invalid option: --nope/) }
148
+ end
149
+
150
+ describe "export" do
151
+ before do
152
+ Flipper.enable :search
153
+ Flipper.disable :analytics
154
+ end
155
+
156
+ it "outputs valid JSON export" do
157
+ expect(subject).to have_attributes(status: 0)
158
+ data = JSON.parse(subject.stdout)
159
+ expect(data["version"]).to eq(1)
160
+ expect(data["features"]).to have_key("search")
161
+ expect(data["features"]).to have_key("analytics")
162
+ end
163
+ end
164
+
165
+ describe "cloud" do
166
+ it "shows help when no subcommand given" do
167
+ expect(subject).to have_attributes(status: 0, stdout: /migrate/)
168
+ expect(subject.stdout).to match(/push/)
169
+ end
170
+ end
171
+
172
+ describe "cloud migrate" do
173
+ before do
174
+ Flipper.enable :search
175
+ require 'flipper/cloud/migrate'
176
+ allow(Flipper::Cloud).to receive(:migrate).and_return(
177
+ Flipper::Cloud::MigrateResult.new(code: 200, url: "https://www.flippercloud.io/cloud/setup/abc123")
178
+ )
179
+ allow(cli).to receive(:system)
180
+ end
181
+
182
+ it "prints the cloud URL" do
183
+ expect(subject).to have_attributes(status: 0, stdout: /flippercloud\.io/)
184
+ end
185
+ end
186
+
187
+ describe "cloud push test-token" do
188
+ before do
189
+ Flipper.enable :search
190
+ require 'flipper/cloud/migrate'
191
+ allow(Flipper::Cloud).to receive(:push).and_return(
192
+ Flipper::Cloud::MigrateResult.new(code: 204, url: nil)
193
+ )
194
+ end
195
+
196
+ it "prints success message" do
197
+ expect(subject).to have_attributes(status: 0, stdout: /Successfully pushed/)
198
+ end
199
+ end
200
+
201
+ describe "show foo" do
202
+ context "boolean" do
203
+ before { Flipper.enable :foo }
204
+ it { should have_attributes(status: 0, stdout: /foo.*enabled/) }
205
+ end
206
+
207
+ context "actors" do
208
+ before { Flipper.enable_actor :foo, Flipper::Actor.new("User;1") }
209
+ it { should have_attributes(status: 0, stdout: /User;1/) }
210
+ end
211
+
212
+ context "groups" do
213
+ before { Flipper.enable_group :foo, :admins }
214
+ it { should have_attributes(status: 0, stdout: /enabled.*admins/m) }
215
+ end
216
+ end
217
+ end
@@ -0,0 +1,257 @@
1
+ require 'flipper/cloud/configuration'
2
+ require 'flipper/adapters/instrumented'
3
+
4
+ RSpec.describe Flipper::Cloud::Configuration do
5
+ let(:required_options) do
6
+ { token: "asdf" }
7
+ end
8
+
9
+ it "can set token" do
10
+ instance = described_class.new(required_options)
11
+ expect(instance.token).to eq(required_options[:token])
12
+ end
13
+
14
+ it "can set token from ENV var" do
15
+ ENV["FLIPPER_CLOUD_TOKEN"] = "from_env"
16
+ instance = described_class.new(required_options.reject { |k, v| k == :token })
17
+ expect(instance.token).to eq("from_env")
18
+ end
19
+
20
+ it "can set instrumenter" do
21
+ instrumenter = Object.new
22
+ instance = described_class.new(required_options.merge(instrumenter: instrumenter))
23
+ expect(instance.instrumenter).to be_a(Flipper::Cloud::Telemetry::Instrumenter)
24
+ expect(instance.instrumenter.instrumenter).to be(instrumenter)
25
+ end
26
+
27
+ it "can set read_timeout" do
28
+ instance = described_class.new(required_options.merge(read_timeout: 5))
29
+ expect(instance.read_timeout).to eq(5)
30
+ end
31
+
32
+ it "can set read_timeout from ENV var" do
33
+ ENV["FLIPPER_CLOUD_READ_TIMEOUT"] = "9"
34
+ instance = described_class.new(required_options.reject { |k, v| k == :read_timeout })
35
+ expect(instance.read_timeout).to eq(9)
36
+ end
37
+
38
+ it "can set open_timeout" do
39
+ instance = described_class.new(required_options.merge(open_timeout: 5))
40
+ expect(instance.open_timeout).to eq(5)
41
+ end
42
+
43
+ it "can set open_timeout from ENV var" do
44
+ ENV["FLIPPER_CLOUD_OPEN_TIMEOUT"] = "9"
45
+ instance = described_class.new(required_options.reject { |k, v| k == :open_timeout })
46
+ expect(instance.open_timeout).to eq(9)
47
+ end
48
+
49
+ it "can set write_timeout" do
50
+ instance = described_class.new(required_options.merge(write_timeout: 5))
51
+ expect(instance.write_timeout).to eq(5)
52
+ end
53
+
54
+ it "can set write_timeout from ENV var" do
55
+ ENV["FLIPPER_CLOUD_WRITE_TIMEOUT"] = "9"
56
+ instance = described_class.new(required_options.reject { |k, v| k == :write_timeout })
57
+ expect(instance.write_timeout).to eq(9)
58
+ end
59
+
60
+ it "can set sync_interval" do
61
+ instance = described_class.new(required_options.merge(sync_interval: 15))
62
+ expect(instance.sync_interval).to eq(15)
63
+ end
64
+
65
+ it "can set sync_interval from ENV var" do
66
+ ENV["FLIPPER_CLOUD_SYNC_INTERVAL"] = "15"
67
+ instance = described_class.new(required_options.reject { |k, v| k == :sync_interval })
68
+ expect(instance.sync_interval).to eq(15)
69
+ end
70
+
71
+ it "passes sync_interval into sync adapter" do
72
+ # The initial sync of http to local invokes this web request.
73
+ stub_request(:get, /flippercloud\.io/).to_return(status: 200, body: "{}")
74
+
75
+ instance = described_class.new(required_options.merge(sync_interval: 20))
76
+ poller = instance.send(:poller)
77
+ expect(poller.interval).to eq(20)
78
+ end
79
+
80
+ it "can set debug_output" do
81
+ instance = described_class.new(required_options.merge(debug_output: STDOUT))
82
+ expect(instance.debug_output).to eq(STDOUT)
83
+ end
84
+
85
+ it "defaults debug_output to STDOUT if FLIPPER_CLOUD_DEBUG_OUTPUT_STDOUT set to true" do
86
+ ENV["FLIPPER_CLOUD_DEBUG_OUTPUT_STDOUT"] = "true"
87
+ instance = described_class.new(required_options)
88
+ expect(instance.debug_output).to eq(STDOUT)
89
+ end
90
+
91
+ it "defaults adapter block" do
92
+ # The initial sync of http to local invokes this web request.
93
+ stub_request(:get, /flippercloud\.io/).to_return(status: 200, body: "{}")
94
+
95
+ instance = described_class.new(required_options)
96
+ expect(instance.adapter).to be_instance_of(Flipper::Adapters::DualWrite)
97
+ end
98
+
99
+ it "can override adapter block" do
100
+ # The initial sync of http to local invokes this web request.
101
+ stub_request(:get, /flippercloud\.io/).to_return(status: 200, body: "{}")
102
+
103
+ instance = described_class.new(required_options)
104
+ instance.adapter do |adapter|
105
+ Flipper::Adapters::Instrumented.new(adapter)
106
+ end
107
+ expect(instance.adapter).to be_instance_of(Flipper::Adapters::Instrumented)
108
+ end
109
+
110
+ it "defaults url" do
111
+ instance = described_class.new(required_options.reject { |k, v| k == :url })
112
+ expect(instance.url).to eq("https://www.flippercloud.io/adapter")
113
+ end
114
+
115
+ it "can override url using options" do
116
+ options = required_options.merge(url: "http://localhost:5000/adapter")
117
+ instance = described_class.new(options)
118
+ expect(instance.url).to eq("http://localhost:5000/adapter")
119
+
120
+ instance = described_class.new(required_options)
121
+ instance.url = "http://localhost:5000/adapter"
122
+ expect(instance.url).to eq("http://localhost:5000/adapter")
123
+ end
124
+
125
+ it "can override URL using ENV var" do
126
+ ENV["FLIPPER_CLOUD_URL"] = "https://example.com"
127
+ instance = described_class.new(required_options.reject { |k, v| k == :url })
128
+ expect(instance.url).to eq("https://example.com")
129
+ end
130
+
131
+ it "defaults sync_method to :poll" do
132
+ instance = described_class.new(required_options)
133
+
134
+ expect(instance.sync_method).to eq(:poll)
135
+ end
136
+
137
+ it "sets sync_method to :webhook if sync_secret provided" do
138
+ # The initial sync of http to local invokes this web request.
139
+ stub_request(:get, /flippercloud\.io/).to_return(status: 200, body: "{}")
140
+
141
+ instance = described_class.new(required_options.merge({
142
+ sync_secret: "secret",
143
+ }))
144
+
145
+ expect(instance.sync_method).to eq(:webhook)
146
+ expect(instance.adapter).to be_instance_of(Flipper::Adapters::DualWrite)
147
+ end
148
+
149
+ it "sets sync_method to :webhook if FLIPPER_CLOUD_SYNC_SECRET set" do
150
+ # The initial sync of http to local invokes this web request.
151
+ stub_request(:get, /flippercloud\.io/).to_return(status: 200, body: "{}")
152
+
153
+ ENV["FLIPPER_CLOUD_SYNC_SECRET"] = "abc"
154
+ instance = described_class.new(required_options)
155
+
156
+ expect(instance.sync_method).to eq(:webhook)
157
+ expect(instance.adapter).to be_instance_of(Flipper::Adapters::DualWrite)
158
+ end
159
+
160
+ it "can set sync_secret" do
161
+ instance = described_class.new(required_options.merge(sync_secret: "from_config"))
162
+ expect(instance.sync_secret).to eq("from_config")
163
+ end
164
+
165
+ it "can override sync_secret using ENV var" do
166
+ ENV["FLIPPER_CLOUD_SYNC_SECRET"] = "from_env"
167
+ instance = described_class.new(required_options.reject { |k, v| k == :sync_secret })
168
+ expect(instance.sync_secret).to eq("from_env")
169
+ end
170
+
171
+ it "can sync with cloud" do
172
+ body = JSON.generate({
173
+ "features": [
174
+ {
175
+ "key": "search",
176
+ "state": "on",
177
+ "gates": [
178
+ {
179
+ "key": "boolean",
180
+ "name": "boolean",
181
+ "value": true
182
+ },
183
+ {
184
+ "key": "groups",
185
+ "name": "group",
186
+ "value": []
187
+ },
188
+ {
189
+ "key": "actors",
190
+ "name": "actor",
191
+ "value": []
192
+ },
193
+ {
194
+ "key": "percentage_of_actors",
195
+ "name": "percentage_of_actors",
196
+ "value": 0
197
+ },
198
+ {
199
+ "key": "percentage_of_time",
200
+ "name": "percentage_of_time",
201
+ "value": 0
202
+ }
203
+ ]
204
+ },
205
+ {
206
+ "key": "history",
207
+ "state": "off",
208
+ "gates": [
209
+ {
210
+ "key": "boolean",
211
+ "name": "boolean",
212
+ "value": false
213
+ },
214
+ {
215
+ "key": "groups",
216
+ "name": "group",
217
+ "value": []
218
+ },
219
+ {
220
+ "key": "actors",
221
+ "name": "actor",
222
+ "value": []
223
+ },
224
+ {
225
+ "key": "percentage_of_actors",
226
+ "name": "percentage_of_actors",
227
+ "value": 0
228
+ },
229
+ {
230
+ "key": "percentage_of_time",
231
+ "name": "percentage_of_time",
232
+ "value": 0
233
+ }
234
+ ]
235
+ }
236
+ ]
237
+ })
238
+ stub = stub_request(:get, "https://www.flippercloud.io/adapter/features?exclude_gate_names=true").
239
+ with({
240
+ headers: {
241
+ 'flipper-cloud-token'=>'asdf',
242
+ },
243
+ }).to_return(status: 200, body: body)
244
+ instance = described_class.new(required_options)
245
+ instance.sync
246
+
247
+ # Check that remote was fetched.
248
+ expect(stub).to have_been_requested
249
+
250
+ # Check that local adapter really did sync.
251
+ local_adapter = instance.local_adapter
252
+ all = local_adapter.get_all
253
+ expect(all.keys).to eq(["search", "history"])
254
+ expect(all["search"][:boolean]).to eq("true")
255
+ expect(all["history"][:boolean]).to eq(nil)
256
+ end
257
+ end