flipper 0.26.0 → 1.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (228) hide show
  1. checksums.yaml +4 -4
  2. data/.github/FUNDING.yml +1 -0
  3. data/.github/workflows/ci.yml +61 -16
  4. data/.github/workflows/examples.yml +55 -18
  5. data/CLAUDE.md +74 -0
  6. data/Changelog.md +1 -486
  7. data/Gemfile +23 -11
  8. data/README.md +31 -27
  9. data/Rakefile +2 -2
  10. data/benchmark/enabled_ips.rb +10 -0
  11. data/benchmark/enabled_multiple_actors_ips.rb +20 -0
  12. data/benchmark/enabled_profile.rb +20 -0
  13. data/benchmark/instrumentation_ips.rb +21 -0
  14. data/benchmark/typecast_ips.rb +27 -0
  15. data/docs/images/banner.jpg +0 -0
  16. data/docs/images/flipper_cloud.png +0 -0
  17. data/examples/api/basic.ru +3 -4
  18. data/examples/api/custom_memoized.ru +3 -4
  19. data/examples/api/memoized.ru +3 -4
  20. data/examples/cloud/app.ru +12 -0
  21. data/examples/cloud/backoff_policy.rb +13 -0
  22. data/examples/cloud/basic.rb +22 -0
  23. data/examples/cloud/cloud_setup.rb +20 -0
  24. data/examples/cloud/forked.rb +36 -0
  25. data/examples/cloud/import.rb +17 -0
  26. data/examples/cloud/threaded.rb +33 -0
  27. data/examples/dsl.rb +1 -15
  28. data/examples/enabled_for_actor.rb +4 -2
  29. data/examples/expressions.rb +213 -0
  30. data/examples/mirroring.rb +59 -0
  31. data/examples/strict.rb +18 -0
  32. data/exe/flipper +5 -0
  33. data/flipper-cloud.gemspec +19 -0
  34. data/flipper.gemspec +8 -6
  35. data/lib/flipper/actor.rb +6 -3
  36. data/lib/flipper/adapter.rb +33 -7
  37. data/lib/flipper/adapter_builder.rb +44 -0
  38. data/lib/flipper/adapters/actor_limit.rb +28 -0
  39. data/lib/flipper/adapters/cache_base.rb +143 -0
  40. data/lib/flipper/adapters/dual_write.rb +1 -3
  41. data/lib/flipper/adapters/failover.rb +0 -4
  42. data/lib/flipper/adapters/failsafe.rb +0 -4
  43. data/lib/flipper/adapters/http/client.rb +40 -12
  44. data/lib/flipper/adapters/http/error.rb +2 -2
  45. data/lib/flipper/adapters/http.rb +30 -17
  46. data/lib/flipper/adapters/instrumented.rb +25 -6
  47. data/lib/flipper/adapters/memoizable.rb +33 -21
  48. data/lib/flipper/adapters/memory.rb +81 -46
  49. data/lib/flipper/adapters/operation_logger.rb +17 -78
  50. data/lib/flipper/adapters/poll/poller.rb +2 -125
  51. data/lib/flipper/adapters/poll.rb +20 -3
  52. data/lib/flipper/adapters/pstore.rb +17 -11
  53. data/lib/flipper/adapters/read_only.rb +8 -41
  54. data/lib/flipper/adapters/strict.rb +45 -0
  55. data/lib/flipper/adapters/sync/feature_synchronizer.rb +10 -1
  56. data/lib/flipper/adapters/sync.rb +0 -4
  57. data/lib/flipper/adapters/wrapper.rb +54 -0
  58. data/lib/flipper/cli.rb +263 -0
  59. data/lib/flipper/cloud/configuration.rb +266 -0
  60. data/lib/flipper/cloud/dsl.rb +27 -0
  61. data/lib/flipper/cloud/message_verifier.rb +95 -0
  62. data/lib/flipper/cloud/middleware.rb +63 -0
  63. data/lib/flipper/cloud/routes.rb +14 -0
  64. data/lib/flipper/cloud/telemetry/backoff_policy.rb +96 -0
  65. data/lib/flipper/cloud/telemetry/instrumenter.rb +22 -0
  66. data/lib/flipper/cloud/telemetry/metric.rb +39 -0
  67. data/lib/flipper/cloud/telemetry/metric_storage.rb +30 -0
  68. data/lib/flipper/cloud/telemetry/submitter.rb +100 -0
  69. data/lib/flipper/cloud/telemetry.rb +191 -0
  70. data/lib/flipper/cloud.rb +53 -0
  71. data/lib/flipper/configuration.rb +25 -4
  72. data/lib/flipper/dsl.rb +46 -45
  73. data/lib/flipper/engine.rb +102 -0
  74. data/lib/flipper/errors.rb +3 -3
  75. data/lib/flipper/export.rb +24 -0
  76. data/lib/flipper/exporter.rb +17 -0
  77. data/lib/flipper/exporters/json/export.rb +32 -0
  78. data/lib/flipper/exporters/json/v1.rb +33 -0
  79. data/lib/flipper/expression/builder.rb +73 -0
  80. data/lib/flipper/expression/constant.rb +25 -0
  81. data/lib/flipper/expression.rb +71 -0
  82. data/lib/flipper/expressions/all.rb +9 -0
  83. data/lib/flipper/expressions/any.rb +9 -0
  84. data/lib/flipper/expressions/boolean.rb +9 -0
  85. data/lib/flipper/expressions/comparable.rb +13 -0
  86. data/lib/flipper/expressions/duration.rb +28 -0
  87. data/lib/flipper/expressions/equal.rb +9 -0
  88. data/lib/flipper/expressions/greater_than.rb +9 -0
  89. data/lib/flipper/expressions/greater_than_or_equal_to.rb +9 -0
  90. data/lib/flipper/expressions/less_than.rb +9 -0
  91. data/lib/flipper/expressions/less_than_or_equal_to.rb +9 -0
  92. data/lib/flipper/expressions/not_equal.rb +9 -0
  93. data/lib/flipper/expressions/now.rb +9 -0
  94. data/lib/flipper/expressions/number.rb +9 -0
  95. data/lib/flipper/expressions/percentage.rb +9 -0
  96. data/lib/flipper/expressions/percentage_of_actors.rb +12 -0
  97. data/lib/flipper/expressions/property.rb +9 -0
  98. data/lib/flipper/expressions/random.rb +9 -0
  99. data/lib/flipper/expressions/string.rb +9 -0
  100. data/lib/flipper/expressions/time.rb +9 -0
  101. data/lib/flipper/feature.rb +94 -26
  102. data/lib/flipper/feature_check_context.rb +10 -6
  103. data/lib/flipper/gate.rb +13 -11
  104. data/lib/flipper/gate_values.rb +5 -18
  105. data/lib/flipper/gates/actor.rb +10 -17
  106. data/lib/flipper/gates/boolean.rb +1 -1
  107. data/lib/flipper/gates/expression.rb +75 -0
  108. data/lib/flipper/gates/group.rb +5 -7
  109. data/lib/flipper/gates/percentage_of_actors.rb +10 -13
  110. data/lib/flipper/gates/percentage_of_time.rb +1 -2
  111. data/lib/flipper/identifier.rb +2 -2
  112. data/lib/flipper/instrumentation/log_subscriber.rb +35 -8
  113. data/lib/flipper/instrumentation/statsd.rb +4 -2
  114. data/lib/flipper/instrumentation/statsd_subscriber.rb +2 -4
  115. data/lib/flipper/instrumentation/subscriber.rb +8 -5
  116. data/lib/flipper/metadata.rb +8 -1
  117. data/lib/flipper/middleware/memoizer.rb +30 -14
  118. data/lib/flipper/model/active_record.rb +23 -0
  119. data/lib/flipper/poller.rb +118 -0
  120. data/lib/flipper/serializers/gzip.rb +22 -0
  121. data/lib/flipper/serializers/json.rb +17 -0
  122. data/lib/flipper/spec/shared_adapter_specs.rb +105 -63
  123. data/lib/flipper/test/shared_adapter_test.rb +101 -58
  124. data/lib/flipper/test_help.rb +43 -0
  125. data/lib/flipper/typecast.rb +59 -18
  126. data/lib/flipper/types/actor.rb +13 -13
  127. data/lib/flipper/types/group.rb +4 -4
  128. data/lib/flipper/types/percentage.rb +1 -1
  129. data/lib/flipper/version.rb +11 -1
  130. data/lib/flipper.rb +50 -11
  131. data/lib/generators/flipper/setup_generator.rb +68 -0
  132. data/lib/generators/flipper/templates/initializer.rb +45 -0
  133. data/lib/generators/flipper/templates/update/migrations/01_create_flipper_tables.rb.erb +22 -0
  134. data/lib/generators/flipper/templates/update/migrations/02_change_flipper_gates_value_to_text.rb.erb +18 -0
  135. data/lib/generators/flipper/update_generator.rb +35 -0
  136. data/package-lock.json +41 -0
  137. data/package.json +10 -0
  138. data/spec/fixtures/environment.rb +1 -0
  139. data/spec/fixtures/flipper_pstore_1679087600.json +46 -0
  140. data/spec/flipper/adapter_builder_spec.rb +72 -0
  141. data/spec/flipper/adapter_spec.rb +30 -2
  142. data/spec/flipper/adapters/actor_limit_spec.rb +20 -0
  143. data/spec/flipper/adapters/dual_write_spec.rb +2 -2
  144. data/spec/flipper/adapters/http/client_spec.rb +61 -0
  145. data/spec/flipper/adapters/http_spec.rb +138 -55
  146. data/spec/flipper/adapters/instrumented_spec.rb +29 -11
  147. data/spec/flipper/adapters/memoizable_spec.rb +51 -31
  148. data/spec/flipper/adapters/memory_spec.rb +14 -3
  149. data/spec/flipper/adapters/operation_logger_spec.rb +31 -12
  150. data/spec/flipper/adapters/poll_spec.rb +41 -0
  151. data/spec/flipper/adapters/read_only_spec.rb +32 -17
  152. data/spec/flipper/adapters/strict_spec.rb +64 -0
  153. data/spec/flipper/adapters/sync/feature_synchronizer_spec.rb +27 -0
  154. data/spec/flipper/cli_spec.rb +166 -0
  155. data/spec/flipper/cloud/configuration_spec.rb +251 -0
  156. data/spec/flipper/cloud/dsl_spec.rb +82 -0
  157. data/spec/flipper/cloud/message_verifier_spec.rb +104 -0
  158. data/spec/flipper/cloud/middleware_spec.rb +289 -0
  159. data/spec/flipper/cloud/telemetry/backoff_policy_spec.rb +107 -0
  160. data/spec/flipper/cloud/telemetry/metric_spec.rb +87 -0
  161. data/spec/flipper/cloud/telemetry/metric_storage_spec.rb +58 -0
  162. data/spec/flipper/cloud/telemetry/submitter_spec.rb +145 -0
  163. data/spec/flipper/cloud/telemetry_spec.rb +208 -0
  164. data/spec/flipper/cloud_spec.rb +186 -0
  165. data/spec/flipper/configuration_spec.rb +17 -0
  166. data/spec/flipper/dsl_spec.rb +54 -76
  167. data/spec/flipper/engine_spec.rb +374 -0
  168. data/spec/flipper/export_spec.rb +13 -0
  169. data/spec/flipper/exporter_spec.rb +16 -0
  170. data/spec/flipper/exporters/json/export_spec.rb +60 -0
  171. data/spec/flipper/exporters/json/v1_spec.rb +33 -0
  172. data/spec/flipper/expression/builder_spec.rb +248 -0
  173. data/spec/flipper/expression_spec.rb +188 -0
  174. data/spec/flipper/expressions/all_spec.rb +15 -0
  175. data/spec/flipper/expressions/any_spec.rb +15 -0
  176. data/spec/flipper/expressions/boolean_spec.rb +15 -0
  177. data/spec/flipper/expressions/duration_spec.rb +43 -0
  178. data/spec/flipper/expressions/equal_spec.rb +24 -0
  179. data/spec/flipper/expressions/greater_than_or_equal_to_spec.rb +28 -0
  180. data/spec/flipper/expressions/greater_than_spec.rb +28 -0
  181. data/spec/flipper/expressions/less_than_or_equal_to_spec.rb +28 -0
  182. data/spec/flipper/expressions/less_than_spec.rb +32 -0
  183. data/spec/flipper/expressions/not_equal_spec.rb +15 -0
  184. data/spec/flipper/expressions/now_spec.rb +11 -0
  185. data/spec/flipper/expressions/number_spec.rb +21 -0
  186. data/spec/flipper/expressions/percentage_of_actors_spec.rb +20 -0
  187. data/spec/flipper/expressions/percentage_spec.rb +15 -0
  188. data/spec/flipper/expressions/property_spec.rb +13 -0
  189. data/spec/flipper/expressions/random_spec.rb +9 -0
  190. data/spec/flipper/expressions/string_spec.rb +11 -0
  191. data/spec/flipper/expressions/time_spec.rb +13 -0
  192. data/spec/flipper/feature_check_context_spec.rb +17 -17
  193. data/spec/flipper/feature_spec.rb +453 -39
  194. data/spec/flipper/gate_values_spec.rb +2 -33
  195. data/spec/flipper/gates/boolean_spec.rb +1 -1
  196. data/spec/flipper/gates/expression_spec.rb +108 -0
  197. data/spec/flipper/gates/group_spec.rb +2 -3
  198. data/spec/flipper/gates/percentage_of_actors_spec.rb +61 -5
  199. data/spec/flipper/gates/percentage_of_time_spec.rb +2 -2
  200. data/spec/flipper/identifier_spec.rb +4 -5
  201. data/spec/flipper/instrumentation/log_subscriber_spec.rb +24 -6
  202. data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +26 -2
  203. data/spec/flipper/middleware/memoizer_spec.rb +79 -10
  204. data/spec/flipper/model/active_record_spec.rb +72 -0
  205. data/spec/flipper/poller_spec.rb +47 -0
  206. data/spec/flipper/serializers/gzip_spec.rb +13 -0
  207. data/spec/flipper/serializers/json_spec.rb +13 -0
  208. data/spec/flipper/typecast_spec.rb +121 -6
  209. data/spec/flipper/types/actor_spec.rb +63 -46
  210. data/spec/flipper/types/group_spec.rb +2 -2
  211. data/spec/flipper_integration_spec.rb +168 -58
  212. data/spec/flipper_spec.rb +94 -30
  213. data/spec/spec_helper.rb +18 -18
  214. data/spec/support/actor_names.yml +1 -0
  215. data/spec/support/fail_on_output.rb +8 -0
  216. data/spec/support/fake_backoff_policy.rb +15 -0
  217. data/spec/support/skippable.rb +18 -0
  218. data/spec/support/spec_helpers.rb +34 -8
  219. data/test/adapters/actor_limit_test.rb +20 -0
  220. data/test_rails/generators/flipper/setup_generator_test.rb +69 -0
  221. data/test_rails/generators/flipper/update_generator_test.rb +96 -0
  222. data/test_rails/helper.rb +22 -2
  223. data/test_rails/system/test_help_test.rb +52 -0
  224. metadata +203 -20
  225. data/.github/workflows/release.yml +0 -44
  226. data/.tool-versions +0 -1
  227. data/lib/flipper/railtie.rb +0 -47
  228. data/spec/flipper/railtie_spec.rb +0 -109
@@ -5,11 +5,12 @@ RSpec.describe Flipper::Adapters::ReadOnly do
5
5
  let(:flipper) { Flipper.new(subject) }
6
6
  let(:feature) { flipper[:stats] }
7
7
 
8
- let(:boolean_gate) { feature.gate(:boolean) }
9
- let(:group_gate) { feature.gate(:group) }
10
- let(:actor_gate) { feature.gate(:actor) }
11
- let(:actors_gate) { feature.gate(:percentage_of_actors) }
12
- let(:time_gate) { feature.gate(:percentage_of_time) }
8
+ let(:boolean_gate) { feature.gate(:boolean) }
9
+ let(:group_gate) { feature.gate(:group) }
10
+ let(:actor_gate) { feature.gate(:actor) }
11
+ let(:expression_gate) { feature.gate(:expression) }
12
+ let(:actors_gate) { feature.gate(:percentage_of_actors) }
13
+ let(:time_gate) { feature.gate(:percentage_of_time) }
13
14
 
14
15
  subject { described_class.new(adapter) }
15
16
 
@@ -41,18 +42,28 @@ RSpec.describe Flipper::Adapters::ReadOnly do
41
42
  end
42
43
 
43
44
  it 'can get feature' do
45
+ expression = Flipper.property(:plan).eq("basic")
44
46
  actor22 = Flipper::Actor.new('22')
45
- adapter.enable(feature, boolean_gate, flipper.boolean)
47
+ adapter.enable(feature, boolean_gate, Flipper::Types::Boolean.new)
46
48
  adapter.enable(feature, group_gate, flipper.group(:admins))
47
- adapter.enable(feature, actor_gate, flipper.actor(actor22))
48
- adapter.enable(feature, actors_gate, flipper.actors(25))
49
- adapter.enable(feature, time_gate, flipper.time(45))
50
-
51
- expect(subject.get(feature)).to eq(boolean: 'true',
52
- groups: Set['admins'],
53
- actors: Set['22'],
54
- percentage_of_actors: '25',
55
- percentage_of_time: '45')
49
+ adapter.enable(feature, actor_gate, Flipper::Types::Actor.new(actor22))
50
+ adapter.enable(feature, actors_gate, Flipper::Types::PercentageOfActors.new(25))
51
+ adapter.enable(feature, time_gate, Flipper::Types::PercentageOfTime.new(45))
52
+ adapter.enable(feature, expression_gate, expression)
53
+
54
+ expect(subject.get(feature)).to eq({
55
+ boolean: 'true',
56
+ groups: Set['admins'],
57
+ actors: Set['22'],
58
+ expression: {
59
+ "Equal" => [
60
+ {"Property" => ["plan"]},
61
+ "basic",
62
+ ]
63
+ },
64
+ percentage_of_actors: '25',
65
+ percentage_of_time: '45',
66
+ })
56
67
  end
57
68
 
58
69
  it 'can get features' do
@@ -61,6 +72,10 @@ RSpec.describe Flipper::Adapters::ReadOnly do
61
72
  expect(subject.features).to eq(Set['stats'])
62
73
  end
63
74
 
75
+ it 'is configured as read only' do
76
+ expect(subject.read_only?).to eq(true)
77
+ end
78
+
64
79
  it 'raises error on add' do
65
80
  expect { subject.add(feature) }.to raise_error(Flipper::Adapters::ReadOnly::WriteAttempted)
66
81
  end
@@ -74,12 +89,12 @@ RSpec.describe Flipper::Adapters::ReadOnly do
74
89
  end
75
90
 
76
91
  it 'raises error on enable' do
77
- expect { subject.enable(feature, boolean_gate, flipper.boolean) }
92
+ expect { subject.enable(feature, boolean_gate, Flipper::Types::Boolean.new) }
78
93
  .to raise_error(Flipper::Adapters::ReadOnly::WriteAttempted)
79
94
  end
80
95
 
81
96
  it 'raises error on disable' do
82
- expect { subject.disable(feature, boolean_gate, flipper.boolean) }
97
+ expect { subject.disable(feature, boolean_gate, Flipper::Types::Boolean.new) }
83
98
  .to raise_error(Flipper::Adapters::ReadOnly::WriteAttempted)
84
99
  end
85
100
  end
@@ -0,0 +1,64 @@
1
+ RSpec.describe Flipper::Adapters::Strict do
2
+ let(:flipper) { Flipper.new(subject) }
3
+ let(:feature) { flipper[:unknown] }
4
+
5
+ it_should_behave_like 'a flipper adapter' do
6
+ subject { described_class.new(Flipper::Adapters::Memory.new, :noop) }
7
+ end
8
+
9
+ [true, :raise].each do |handler|
10
+ context "handler = #{handler}" do
11
+ subject { described_class.new(Flipper::Adapters::Memory.new, handler) }
12
+
13
+ context "#get" do
14
+ it "raises an error for unknown feature" do
15
+ expect { subject.get(feature) }.to raise_error(Flipper::Adapters::Strict::NotFound)
16
+ end
17
+ end
18
+
19
+ context "#get_multi" do
20
+ it "raises an error for unknown feature" do
21
+ expect { subject.get_multi([feature]) }.to raise_error(Flipper::Adapters::Strict::NotFound)
22
+ end
23
+ end
24
+ end
25
+ end
26
+
27
+ context "handler = :warn" do
28
+ subject { described_class.new(Flipper::Adapters::Memory.new, :warn) }
29
+
30
+ context "#get" do
31
+ it "raises an error for unknown feature" do
32
+ expect(capture_output { subject.get(feature) }).to match(/Could not find feature "unknown"/)
33
+ end
34
+ end
35
+
36
+ context "#get_multi" do
37
+ it "raises an error for unknown feature" do
38
+ expect(capture_output { subject.get_multi([feature]) }).to match(/Could not find feature "unknown"/)
39
+ end
40
+ end
41
+ end
42
+
43
+ context "handler = Block" do
44
+ let(:unknown_features) { [] }
45
+ subject do
46
+ described_class.new(Flipper::Adapters::Memory.new) { |feature| unknown_features << feature.key}
47
+ end
48
+
49
+
50
+ context "#get" do
51
+ it "raises an error for unknown feature" do
52
+ subject.get(feature)
53
+ expect(unknown_features).to eq(["unknown"])
54
+ end
55
+ end
56
+
57
+ context "#get_multi" do
58
+ it "raises an error for unknown feature" do
59
+ subject.get_multi([flipper[:foo], flipper[:bar]])
60
+ expect(unknown_features).to eq(["foo", "bar"])
61
+ end
62
+ end
63
+ end
64
+ end
@@ -8,6 +8,8 @@ RSpec.describe Flipper::Adapters::Sync::FeatureSynchronizer do
8
8
  end
9
9
  let(:flipper) { Flipper.new(adapter) }
10
10
  let(:feature) { flipper[:search] }
11
+ let(:plan_expression) { Flipper.property(:plan).eq("basic") }
12
+ let(:age_expression) { Flipper.property(:age).gte(21) }
11
13
 
12
14
  context "when remote disabled" do
13
15
  let(:remote) { Flipper::GateValues.new({}) }
@@ -63,6 +65,7 @@ RSpec.describe Flipper::Adapters::Sync::FeatureSynchronizer do
63
65
  boolean: nil,
64
66
  actors: Set["1"],
65
67
  groups: Set["staff"],
68
+ expression: plan_expression.value,
66
69
  percentage_of_time: 10,
67
70
  percentage_of_actors: 15,
68
71
  }
@@ -74,10 +77,34 @@ RSpec.describe Flipper::Adapters::Sync::FeatureSynchronizer do
74
77
  expect(local_gate_values_hash.fetch(:boolean)).to be(nil)
75
78
  expect(local_gate_values_hash.fetch(:actors)).to eq(Set["1"])
76
79
  expect(local_gate_values_hash.fetch(:groups)).to eq(Set["staff"])
80
+ expect(local_gate_values_hash.fetch(:expression)).to eq(plan_expression.value)
77
81
  expect(local_gate_values_hash.fetch(:percentage_of_time)).to eq("10")
78
82
  expect(local_gate_values_hash.fetch(:percentage_of_actors)).to eq("15")
79
83
  end
80
84
 
85
+ it "updates expression when remote is updated" do
86
+ any_expression = Flipper.any(plan_expression, age_expression)
87
+ remote = Flipper::GateValues.new(expression: any_expression.value)
88
+ feature.enable_expression(age_expression)
89
+ adapter.reset
90
+
91
+ described_class.new(feature, feature.gate_values, remote).call
92
+
93
+ expect(feature.expression_value).to eq(any_expression.value)
94
+ expect_only_enable
95
+ end
96
+
97
+ it "does nothing to expression if in sync" do
98
+ remote = Flipper::GateValues.new(expression: plan_expression.value)
99
+ feature.enable_expression(plan_expression)
100
+ adapter.reset
101
+
102
+ described_class.new(feature, feature.gate_values, remote).call
103
+
104
+ expect(feature.expression_value).to eq(plan_expression.value)
105
+ expect_no_enable_or_disable
106
+ end
107
+
81
108
  it "adds remotely added actors" do
82
109
  remote = Flipper::GateValues.new(actors: Set["1", "2"])
83
110
  feature.enable_actor(Flipper::Actor.new("1"))
@@ -0,0 +1,166 @@
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 "show foo" do
151
+ context "boolean" do
152
+ before { Flipper.enable :foo }
153
+ it { should have_attributes(status: 0, stdout: /foo.*enabled/) }
154
+ end
155
+
156
+ context "actors" do
157
+ before { Flipper.enable_actor :foo, Flipper::Actor.new("User;1") }
158
+ it { should have_attributes(status: 0, stdout: /User;1/) }
159
+ end
160
+
161
+ context "groups" do
162
+ before { Flipper.enable_group :foo, :admins }
163
+ it { should have_attributes(status: 0, stdout: /enabled.*admins/m) }
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,251 @@
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
+ instance = described_class.new(required_options.merge({
139
+ sync_secret: "secret",
140
+ }))
141
+
142
+ expect(instance.sync_method).to eq(:webhook)
143
+ expect(instance.adapter).to be_instance_of(Flipper::Adapters::DualWrite)
144
+ end
145
+
146
+ it "sets sync_method to :webhook if FLIPPER_CLOUD_SYNC_SECRET set" do
147
+ ENV["FLIPPER_CLOUD_SYNC_SECRET"] = "abc"
148
+ instance = described_class.new(required_options)
149
+
150
+ expect(instance.sync_method).to eq(:webhook)
151
+ expect(instance.adapter).to be_instance_of(Flipper::Adapters::DualWrite)
152
+ end
153
+
154
+ it "can set sync_secret" do
155
+ instance = described_class.new(required_options.merge(sync_secret: "from_config"))
156
+ expect(instance.sync_secret).to eq("from_config")
157
+ end
158
+
159
+ it "can override sync_secret using ENV var" do
160
+ ENV["FLIPPER_CLOUD_SYNC_SECRET"] = "from_env"
161
+ instance = described_class.new(required_options.reject { |k, v| k == :sync_secret })
162
+ expect(instance.sync_secret).to eq("from_env")
163
+ end
164
+
165
+ it "can sync with cloud" do
166
+ body = JSON.generate({
167
+ "features": [
168
+ {
169
+ "key": "search",
170
+ "state": "on",
171
+ "gates": [
172
+ {
173
+ "key": "boolean",
174
+ "name": "boolean",
175
+ "value": true
176
+ },
177
+ {
178
+ "key": "groups",
179
+ "name": "group",
180
+ "value": []
181
+ },
182
+ {
183
+ "key": "actors",
184
+ "name": "actor",
185
+ "value": []
186
+ },
187
+ {
188
+ "key": "percentage_of_actors",
189
+ "name": "percentage_of_actors",
190
+ "value": 0
191
+ },
192
+ {
193
+ "key": "percentage_of_time",
194
+ "name": "percentage_of_time",
195
+ "value": 0
196
+ }
197
+ ]
198
+ },
199
+ {
200
+ "key": "history",
201
+ "state": "off",
202
+ "gates": [
203
+ {
204
+ "key": "boolean",
205
+ "name": "boolean",
206
+ "value": false
207
+ },
208
+ {
209
+ "key": "groups",
210
+ "name": "group",
211
+ "value": []
212
+ },
213
+ {
214
+ "key": "actors",
215
+ "name": "actor",
216
+ "value": []
217
+ },
218
+ {
219
+ "key": "percentage_of_actors",
220
+ "name": "percentage_of_actors",
221
+ "value": 0
222
+ },
223
+ {
224
+ "key": "percentage_of_time",
225
+ "name": "percentage_of_time",
226
+ "value": 0
227
+ }
228
+ ]
229
+ }
230
+ ]
231
+ })
232
+ stub = stub_request(:get, "https://www.flippercloud.io/adapter/features?exclude_gate_names=true").
233
+ with({
234
+ headers: {
235
+ 'flipper-cloud-token'=>'asdf',
236
+ },
237
+ }).to_return(status: 200, body: body)
238
+ instance = described_class.new(required_options)
239
+ instance.sync
240
+
241
+ # Check that remote was fetched.
242
+ expect(stub).to have_been_requested
243
+
244
+ # Check that local adapter really did sync.
245
+ local_adapter = instance.local_adapter
246
+ all = local_adapter.get_all
247
+ expect(all.keys).to eq(["search", "history"])
248
+ expect(all["search"][:boolean]).to eq("true")
249
+ expect(all["history"][:boolean]).to eq(nil)
250
+ end
251
+ end
@@ -0,0 +1,82 @@
1
+ require 'flipper/cloud/configuration'
2
+ require 'flipper/cloud/dsl'
3
+ require 'flipper/adapters/operation_logger'
4
+ require 'flipper/adapters/instrumented'
5
+
6
+ RSpec.describe Flipper::Cloud::DSL do
7
+ it 'delegates everything to flipper instance' do
8
+ cloud_configuration = Flipper::Cloud::Configuration.new({
9
+ token: "asdf",
10
+ sync_secret: "tasty",
11
+ })
12
+ dsl = described_class.new(cloud_configuration)
13
+ expect(dsl.features).to eq(Set.new)
14
+ expect(dsl.enabled?(:foo)).to be(false)
15
+ end
16
+
17
+ it 'delegates sync to cloud configuration' do
18
+ stub = stub_request(:get, "https://www.flippercloud.io/adapter/features?exclude_gate_names=true").
19
+ with({
20
+ headers: {
21
+ 'flipper-cloud-token'=>'asdf',
22
+ },
23
+ }).to_return(status: 200, body: '{"features": {}}', headers: {})
24
+ cloud_configuration = Flipper::Cloud::Configuration.new({
25
+ token: "asdf",
26
+ sync_secret: "tasty",
27
+ })
28
+ dsl = described_class.new(cloud_configuration)
29
+ dsl.sync
30
+ expect(stub).to have_been_requested
31
+ end
32
+
33
+ it 'delegates sync_secret to cloud configuration' do
34
+ cloud_configuration = Flipper::Cloud::Configuration.new({
35
+ token: "asdf",
36
+ sync_secret: "tasty",
37
+ })
38
+ dsl = described_class.new(cloud_configuration)
39
+ expect(dsl.sync_secret).to eq("tasty")
40
+ end
41
+
42
+ context "when sync_method is webhook" do
43
+ let(:local_adapter) do
44
+ Flipper::Adapters::OperationLogger.new Flipper::Adapters::Memory.new
45
+ end
46
+
47
+ let(:cloud_configuration) do
48
+ Flipper::Cloud::Configuration.new({
49
+ token: "asdf",
50
+ sync_secret: "tasty",
51
+ local_adapter: local_adapter
52
+ })
53
+ end
54
+
55
+ subject do
56
+ described_class.new(cloud_configuration)
57
+ end
58
+
59
+ it "sends reads to local adapter" do
60
+ subject.features
61
+ subject.enabled?(:foo)
62
+ expect(local_adapter.count(:features)).to be(1)
63
+ expect(local_adapter.count(:get)).to be(1)
64
+ end
65
+
66
+ it "sends writes to cloud and local" do
67
+ add_stub = stub_request(:post, "https://www.flippercloud.io/adapter/features").
68
+ with({headers: {'flipper-cloud-token'=>'asdf'}}).
69
+ to_return(status: 200, body: '{}')
70
+ enable_stub = stub_request(:post, "https://www.flippercloud.io/adapter/features/foo/boolean").
71
+ with(headers: {'flipper-cloud-token'=>'asdf'}).
72
+ to_return(status: 200, body: '{}')
73
+
74
+ subject.enable(:foo)
75
+
76
+ expect(local_adapter.count(:add)).to be(1)
77
+ expect(local_adapter.count(:enable)).to be(1)
78
+ expect(add_stub).to have_been_requested
79
+ expect(enable_stub).to have_been_requested
80
+ end
81
+ end
82
+ end