flipper 0.26.0 → 1.1.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 (199) hide show
  1. checksums.yaml +4 -4
  2. data/.github/FUNDING.yml +1 -0
  3. data/.github/workflows/ci.yml +19 -13
  4. data/.github/workflows/examples.yml +32 -15
  5. data/Changelog.md +294 -154
  6. data/Gemfile +15 -10
  7. data/README.md +13 -11
  8. data/benchmark/enabled_ips.rb +10 -0
  9. data/benchmark/enabled_multiple_actors_ips.rb +20 -0
  10. data/benchmark/enabled_profile.rb +20 -0
  11. data/benchmark/instrumentation_ips.rb +21 -0
  12. data/benchmark/typecast_ips.rb +27 -0
  13. data/docs/images/flipper_cloud.png +0 -0
  14. data/examples/api/basic.ru +3 -4
  15. data/examples/api/custom_memoized.ru +3 -4
  16. data/examples/api/memoized.ru +3 -4
  17. data/examples/cloud/app.ru +12 -0
  18. data/examples/cloud/backoff_policy.rb +13 -0
  19. data/examples/cloud/basic.rb +22 -0
  20. data/examples/cloud/cloud_setup.rb +20 -0
  21. data/examples/cloud/forked.rb +36 -0
  22. data/examples/cloud/import.rb +17 -0
  23. data/examples/cloud/threaded.rb +33 -0
  24. data/examples/dsl.rb +1 -15
  25. data/examples/enabled_for_actor.rb +4 -2
  26. data/examples/expressions.rb +213 -0
  27. data/examples/mirroring.rb +59 -0
  28. data/examples/strict.rb +18 -0
  29. data/flipper-cloud.gemspec +19 -0
  30. data/flipper.gemspec +3 -5
  31. data/lib/flipper/actor.rb +6 -3
  32. data/lib/flipper/adapter.rb +33 -7
  33. data/lib/flipper/adapter_builder.rb +44 -0
  34. data/lib/flipper/adapters/dual_write.rb +1 -3
  35. data/lib/flipper/adapters/failover.rb +0 -4
  36. data/lib/flipper/adapters/failsafe.rb +0 -4
  37. data/lib/flipper/adapters/http/client.rb +26 -7
  38. data/lib/flipper/adapters/http/error.rb +1 -1
  39. data/lib/flipper/adapters/http.rb +29 -16
  40. data/lib/flipper/adapters/instrumented.rb +25 -6
  41. data/lib/flipper/adapters/memoizable.rb +33 -21
  42. data/lib/flipper/adapters/memory.rb +81 -46
  43. data/lib/flipper/adapters/operation_logger.rb +16 -7
  44. data/lib/flipper/adapters/poll/poller.rb +2 -125
  45. data/lib/flipper/adapters/poll.rb +5 -3
  46. data/lib/flipper/adapters/pstore.rb +17 -11
  47. data/lib/flipper/adapters/read_only.rb +4 -4
  48. data/lib/flipper/adapters/strict.rb +47 -0
  49. data/lib/flipper/adapters/sync/feature_synchronizer.rb +10 -1
  50. data/lib/flipper/adapters/sync.rb +0 -4
  51. data/lib/flipper/cloud/configuration.rb +258 -0
  52. data/lib/flipper/cloud/dsl.rb +27 -0
  53. data/lib/flipper/cloud/message_verifier.rb +95 -0
  54. data/lib/flipper/cloud/middleware.rb +63 -0
  55. data/lib/flipper/cloud/routes.rb +14 -0
  56. data/lib/flipper/cloud/telemetry/backoff_policy.rb +93 -0
  57. data/lib/flipper/cloud/telemetry/instrumenter.rb +26 -0
  58. data/lib/flipper/cloud/telemetry/metric.rb +39 -0
  59. data/lib/flipper/cloud/telemetry/metric_storage.rb +30 -0
  60. data/lib/flipper/cloud/telemetry/submitter.rb +98 -0
  61. data/lib/flipper/cloud/telemetry.rb +183 -0
  62. data/lib/flipper/cloud.rb +53 -0
  63. data/lib/flipper/configuration.rb +25 -4
  64. data/lib/flipper/dsl.rb +46 -45
  65. data/lib/flipper/engine.rb +88 -0
  66. data/lib/flipper/errors.rb +3 -3
  67. data/lib/flipper/export.rb +26 -0
  68. data/lib/flipper/exporter.rb +17 -0
  69. data/lib/flipper/exporters/json/export.rb +32 -0
  70. data/lib/flipper/exporters/json/v1.rb +33 -0
  71. data/lib/flipper/expression/builder.rb +73 -0
  72. data/lib/flipper/expression/constant.rb +25 -0
  73. data/lib/flipper/expression.rb +71 -0
  74. data/lib/flipper/expressions/all.rb +11 -0
  75. data/lib/flipper/expressions/any.rb +9 -0
  76. data/lib/flipper/expressions/boolean.rb +9 -0
  77. data/lib/flipper/expressions/comparable.rb +13 -0
  78. data/lib/flipper/expressions/duration.rb +28 -0
  79. data/lib/flipper/expressions/equal.rb +9 -0
  80. data/lib/flipper/expressions/greater_than.rb +9 -0
  81. data/lib/flipper/expressions/greater_than_or_equal_to.rb +9 -0
  82. data/lib/flipper/expressions/less_than.rb +9 -0
  83. data/lib/flipper/expressions/less_than_or_equal_to.rb +9 -0
  84. data/lib/flipper/expressions/not_equal.rb +9 -0
  85. data/lib/flipper/expressions/now.rb +9 -0
  86. data/lib/flipper/expressions/number.rb +9 -0
  87. data/lib/flipper/expressions/percentage.rb +9 -0
  88. data/lib/flipper/expressions/percentage_of_actors.rb +12 -0
  89. data/lib/flipper/expressions/property.rb +9 -0
  90. data/lib/flipper/expressions/random.rb +9 -0
  91. data/lib/flipper/expressions/string.rb +9 -0
  92. data/lib/flipper/expressions/time.rb +9 -0
  93. data/lib/flipper/feature.rb +87 -26
  94. data/lib/flipper/feature_check_context.rb +10 -6
  95. data/lib/flipper/gate.rb +13 -11
  96. data/lib/flipper/gate_values.rb +5 -18
  97. data/lib/flipper/gates/actor.rb +10 -17
  98. data/lib/flipper/gates/boolean.rb +1 -1
  99. data/lib/flipper/gates/expression.rb +75 -0
  100. data/lib/flipper/gates/group.rb +5 -7
  101. data/lib/flipper/gates/percentage_of_actors.rb +10 -13
  102. data/lib/flipper/gates/percentage_of_time.rb +1 -2
  103. data/lib/flipper/identifier.rb +2 -2
  104. data/lib/flipper/instrumentation/log_subscriber.rb +24 -5
  105. data/lib/flipper/instrumentation/statsd_subscriber.rb +2 -4
  106. data/lib/flipper/instrumentation/subscriber.rb +8 -1
  107. data/lib/flipper/metadata.rb +5 -1
  108. data/lib/flipper/middleware/memoizer.rb +30 -14
  109. data/lib/flipper/poller.rb +117 -0
  110. data/lib/flipper/serializers/gzip.rb +24 -0
  111. data/lib/flipper/serializers/json.rb +19 -0
  112. data/lib/flipper/spec/shared_adapter_specs.rb +95 -54
  113. data/lib/flipper/test/shared_adapter_test.rb +91 -48
  114. data/lib/flipper/typecast.rb +56 -15
  115. data/lib/flipper/types/actor.rb +13 -13
  116. data/lib/flipper/types/group.rb +4 -4
  117. data/lib/flipper/types/percentage.rb +1 -1
  118. data/lib/flipper/version.rb +1 -1
  119. data/lib/flipper.rb +47 -10
  120. data/spec/fixtures/flipper_pstore_1679087600.json +46 -0
  121. data/spec/flipper/adapter_builder_spec.rb +73 -0
  122. data/spec/flipper/adapter_spec.rb +30 -2
  123. data/spec/flipper/adapters/dual_write_spec.rb +2 -2
  124. data/spec/flipper/adapters/http_spec.rb +64 -8
  125. data/spec/flipper/adapters/instrumented_spec.rb +29 -11
  126. data/spec/flipper/adapters/memoizable_spec.rb +51 -31
  127. data/spec/flipper/adapters/memory_spec.rb +14 -3
  128. data/spec/flipper/adapters/operation_logger_spec.rb +31 -12
  129. data/spec/flipper/adapters/read_only_spec.rb +32 -17
  130. data/spec/flipper/adapters/strict_spec.rb +62 -0
  131. data/spec/flipper/adapters/sync/feature_synchronizer_spec.rb +27 -0
  132. data/spec/flipper/cloud/configuration_spec.rb +252 -0
  133. data/spec/flipper/cloud/dsl_spec.rb +82 -0
  134. data/spec/flipper/cloud/message_verifier_spec.rb +104 -0
  135. data/spec/flipper/cloud/middleware_spec.rb +289 -0
  136. data/spec/flipper/cloud/telemetry/backoff_policy_spec.rb +108 -0
  137. data/spec/flipper/cloud/telemetry/metric_spec.rb +87 -0
  138. data/spec/flipper/cloud/telemetry/metric_storage_spec.rb +58 -0
  139. data/spec/flipper/cloud/telemetry/submitter_spec.rb +145 -0
  140. data/spec/flipper/cloud/telemetry_spec.rb +156 -0
  141. data/spec/flipper/cloud_spec.rb +180 -0
  142. data/spec/flipper/configuration_spec.rb +17 -0
  143. data/spec/flipper/dsl_spec.rb +54 -73
  144. data/spec/flipper/engine_spec.rb +291 -0
  145. data/spec/flipper/export_spec.rb +13 -0
  146. data/spec/flipper/exporter_spec.rb +16 -0
  147. data/spec/flipper/exporters/json/export_spec.rb +60 -0
  148. data/spec/flipper/exporters/json/v1_spec.rb +33 -0
  149. data/spec/flipper/expression/builder_spec.rb +248 -0
  150. data/spec/flipper/expression_spec.rb +188 -0
  151. data/spec/flipper/expressions/all_spec.rb +15 -0
  152. data/spec/flipper/expressions/any_spec.rb +15 -0
  153. data/spec/flipper/expressions/boolean_spec.rb +15 -0
  154. data/spec/flipper/expressions/duration_spec.rb +43 -0
  155. data/spec/flipper/expressions/equal_spec.rb +24 -0
  156. data/spec/flipper/expressions/greater_than_or_equal_to_spec.rb +28 -0
  157. data/spec/flipper/expressions/greater_than_spec.rb +28 -0
  158. data/spec/flipper/expressions/less_than_or_equal_to_spec.rb +28 -0
  159. data/spec/flipper/expressions/less_than_spec.rb +32 -0
  160. data/spec/flipper/expressions/not_equal_spec.rb +15 -0
  161. data/spec/flipper/expressions/now_spec.rb +11 -0
  162. data/spec/flipper/expressions/number_spec.rb +21 -0
  163. data/spec/flipper/expressions/percentage_of_actors_spec.rb +20 -0
  164. data/spec/flipper/expressions/percentage_spec.rb +15 -0
  165. data/spec/flipper/expressions/property_spec.rb +13 -0
  166. data/spec/flipper/expressions/random_spec.rb +9 -0
  167. data/spec/flipper/expressions/string_spec.rb +11 -0
  168. data/spec/flipper/expressions/time_spec.rb +13 -0
  169. data/spec/flipper/feature_check_context_spec.rb +17 -17
  170. data/spec/flipper/feature_spec.rb +436 -33
  171. data/spec/flipper/gate_values_spec.rb +2 -33
  172. data/spec/flipper/gates/boolean_spec.rb +1 -1
  173. data/spec/flipper/gates/expression_spec.rb +108 -0
  174. data/spec/flipper/gates/group_spec.rb +2 -3
  175. data/spec/flipper/gates/percentage_of_actors_spec.rb +61 -5
  176. data/spec/flipper/gates/percentage_of_time_spec.rb +2 -2
  177. data/spec/flipper/identifier_spec.rb +4 -5
  178. data/spec/flipper/instrumentation/log_subscriber_spec.rb +15 -5
  179. data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +25 -1
  180. data/spec/flipper/middleware/memoizer_spec.rb +67 -0
  181. data/spec/flipper/poller_spec.rb +47 -0
  182. data/spec/flipper/serializers/gzip_spec.rb +13 -0
  183. data/spec/flipper/serializers/json_spec.rb +13 -0
  184. data/spec/flipper/typecast_spec.rb +121 -6
  185. data/spec/flipper/types/actor_spec.rb +63 -46
  186. data/spec/flipper/types/group_spec.rb +2 -2
  187. data/spec/flipper_integration_spec.rb +168 -58
  188. data/spec/flipper_spec.rb +92 -28
  189. data/spec/spec_helper.rb +6 -13
  190. data/spec/support/actor_names.yml +1 -0
  191. data/spec/support/climate_control.rb +7 -0
  192. data/spec/support/fake_backoff_policy.rb +15 -0
  193. data/spec/support/skippable.rb +18 -0
  194. data/spec/support/spec_helpers.rb +11 -3
  195. metadata +166 -13
  196. data/.github/workflows/release.yml +0 -44
  197. data/.tool-versions +0 -1
  198. data/lib/flipper/railtie.rb +0 -47
  199. data/spec/flipper/railtie_spec.rb +0 -109
@@ -0,0 +1,62 @@
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
+ context "handler = :raise" do
10
+ subject { described_class.new(Flipper::Adapters::Memory.new, :raise) }
11
+
12
+ context "#get" do
13
+ it "raises an error for unknown feature" do
14
+ expect { subject.get(feature) }.to raise_error(Flipper::Adapters::Strict::NotFound)
15
+ end
16
+ end
17
+
18
+ context "#get_multi" do
19
+ it "raises an error for unknown feature" do
20
+ expect { subject.get_multi([feature]) }.to raise_error(Flipper::Adapters::Strict::NotFound)
21
+ end
22
+ end
23
+ end
24
+
25
+ context "handler = :warn" do
26
+ subject { described_class.new(Flipper::Adapters::Memory.new, :warn) }
27
+
28
+ context "#get" do
29
+ it "raises an error for unknown feature" do
30
+ expect(silence { subject.get(feature) }).to match(/Could not find feature "unknown"/)
31
+ end
32
+ end
33
+
34
+ context "#get_multi" do
35
+ it "raises an error for unknown feature" do
36
+ expect(silence { subject.get_multi([feature]) }).to match(/Could not find feature "unknown"/)
37
+ end
38
+ end
39
+ end
40
+
41
+ context "handler = Block" do
42
+ let(:unknown_features) { [] }
43
+ subject do
44
+ described_class.new(Flipper::Adapters::Memory.new) { |feature| unknown_features << feature.key}
45
+ end
46
+
47
+
48
+ context "#get" do
49
+ it "raises an error for unknown feature" do
50
+ subject.get(feature)
51
+ expect(unknown_features).to eq(["unknown"])
52
+ end
53
+ end
54
+
55
+ context "#get_multi" do
56
+ it "raises an error for unknown feature" do
57
+ subject.get_multi([flipper[:foo], flipper[:bar]])
58
+ expect(unknown_features).to eq(["foo", "bar"])
59
+ end
60
+ end
61
+ end
62
+ 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,252 @@
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
+ with_env "FLIPPER_CLOUD_TOKEN" => "from_env" do
16
+ instance = described_class.new(required_options.reject { |k, v| k == :token })
17
+ expect(instance.token).to eq("from_env")
18
+ end
19
+ end
20
+
21
+ it "can set instrumenter" do
22
+ instrumenter = Object.new
23
+ instance = described_class.new(required_options.merge(instrumenter: instrumenter))
24
+ expect(instance.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
+ with_env "FLIPPER_CLOUD_READ_TIMEOUT" => "9" do
34
+ instance = described_class.new(required_options.reject { |k, v| k == :read_timeout })
35
+ expect(instance.read_timeout).to eq(9)
36
+ end
37
+ end
38
+
39
+ it "can set open_timeout" do
40
+ instance = described_class.new(required_options.merge(open_timeout: 5))
41
+ expect(instance.open_timeout).to eq(5)
42
+ end
43
+
44
+ it "can set open_timeout from ENV var" do
45
+ with_env "FLIPPER_CLOUD_OPEN_TIMEOUT" => "9" do
46
+ instance = described_class.new(required_options.reject { |k, v| k == :open_timeout })
47
+ expect(instance.open_timeout).to eq(9)
48
+ end
49
+ end
50
+
51
+ it "can set write_timeout" do
52
+ instance = described_class.new(required_options.merge(write_timeout: 5))
53
+ expect(instance.write_timeout).to eq(5)
54
+ end
55
+
56
+ it "can set write_timeout from ENV var" do
57
+ with_env "FLIPPER_CLOUD_WRITE_TIMEOUT" => "9" do
58
+ instance = described_class.new(required_options.reject { |k, v| k == :write_timeout })
59
+ expect(instance.write_timeout).to eq(9)
60
+ end
61
+ end
62
+
63
+ it "can set sync_interval" do
64
+ instance = described_class.new(required_options.merge(sync_interval: 15))
65
+ expect(instance.sync_interval).to eq(15)
66
+ end
67
+
68
+ it "can set sync_interval from ENV var" do
69
+ with_env "FLIPPER_CLOUD_SYNC_INTERVAL" => "15" do
70
+ instance = described_class.new(required_options.reject { |k, v| k == :sync_interval })
71
+ expect(instance.sync_interval).to eq(15)
72
+ end
73
+ end
74
+
75
+ it "passes sync_interval into sync adapter" do
76
+ # The initial sync of http to local invokes this web request.
77
+ stub_request(:get, /flippercloud\.io/).to_return(status: 200, body: "{}")
78
+
79
+ instance = described_class.new(required_options.merge(sync_interval: 20))
80
+ poller = instance.send(:poller)
81
+ expect(poller.interval).to eq(20)
82
+ end
83
+
84
+ it "can set debug_output" do
85
+ instance = described_class.new(required_options.merge(debug_output: STDOUT))
86
+ expect(instance.debug_output).to eq(STDOUT)
87
+ end
88
+
89
+ it "defaults adapter block" do
90
+ # The initial sync of http to local invokes this web request.
91
+ stub_request(:get, /flippercloud\.io/).to_return(status: 200, body: "{}")
92
+
93
+ instance = described_class.new(required_options)
94
+ expect(instance.adapter).to be_instance_of(Flipper::Adapters::DualWrite)
95
+ end
96
+
97
+ it "can override adapter block" do
98
+ # The initial sync of http to local invokes this web request.
99
+ stub_request(:get, /flippercloud\.io/).to_return(status: 200, body: "{}")
100
+
101
+ instance = described_class.new(required_options)
102
+ instance.adapter do |adapter|
103
+ Flipper::Adapters::Instrumented.new(adapter)
104
+ end
105
+ expect(instance.adapter).to be_instance_of(Flipper::Adapters::Instrumented)
106
+ end
107
+
108
+ it "defaults url" do
109
+ instance = described_class.new(required_options.reject { |k, v| k == :url })
110
+ expect(instance.url).to eq("https://www.flippercloud.io/adapter")
111
+ end
112
+
113
+ it "can override url using options" do
114
+ options = required_options.merge(url: "http://localhost:5000/adapter")
115
+ instance = described_class.new(options)
116
+ expect(instance.url).to eq("http://localhost:5000/adapter")
117
+
118
+ instance = described_class.new(required_options)
119
+ instance.url = "http://localhost:5000/adapter"
120
+ expect(instance.url).to eq("http://localhost:5000/adapter")
121
+ end
122
+
123
+ it "can override URL using ENV var" do
124
+ with_env "FLIPPER_CLOUD_URL" => "https://example.com" do
125
+ instance = described_class.new(required_options.reject { |k, v| k == :url })
126
+ expect(instance.url).to eq("https://example.com")
127
+ end
128
+ end
129
+
130
+ it "defaults sync_method to :poll" do
131
+ instance = described_class.new(required_options)
132
+
133
+ expect(instance.sync_method).to eq(:poll)
134
+ end
135
+
136
+ it "sets sync_method to :webhook if sync_secret provided" do
137
+ instance = described_class.new(required_options.merge({
138
+ sync_secret: "secret",
139
+ }))
140
+
141
+ expect(instance.sync_method).to eq(:webhook)
142
+ expect(instance.adapter).to be_instance_of(Flipper::Adapters::DualWrite)
143
+ end
144
+
145
+ it "sets sync_method to :webhook if FLIPPER_CLOUD_SYNC_SECRET set" do
146
+ with_env "FLIPPER_CLOUD_SYNC_SECRET" => "abc" do
147
+ instance = described_class.new(required_options)
148
+
149
+ expect(instance.sync_method).to eq(:webhook)
150
+ expect(instance.adapter).to be_instance_of(Flipper::Adapters::DualWrite)
151
+ end
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
+ with_env "FLIPPER_CLOUD_SYNC_SECRET" => "from_env" do
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
+ end
165
+
166
+ it "can sync with cloud" do
167
+ body = JSON.generate({
168
+ "features": [
169
+ {
170
+ "key": "search",
171
+ "state": "on",
172
+ "gates": [
173
+ {
174
+ "key": "boolean",
175
+ "name": "boolean",
176
+ "value": true
177
+ },
178
+ {
179
+ "key": "groups",
180
+ "name": "group",
181
+ "value": []
182
+ },
183
+ {
184
+ "key": "actors",
185
+ "name": "actor",
186
+ "value": []
187
+ },
188
+ {
189
+ "key": "percentage_of_actors",
190
+ "name": "percentage_of_actors",
191
+ "value": 0
192
+ },
193
+ {
194
+ "key": "percentage_of_time",
195
+ "name": "percentage_of_time",
196
+ "value": 0
197
+ }
198
+ ]
199
+ },
200
+ {
201
+ "key": "history",
202
+ "state": "off",
203
+ "gates": [
204
+ {
205
+ "key": "boolean",
206
+ "name": "boolean",
207
+ "value": false
208
+ },
209
+ {
210
+ "key": "groups",
211
+ "name": "group",
212
+ "value": []
213
+ },
214
+ {
215
+ "key": "actors",
216
+ "name": "actor",
217
+ "value": []
218
+ },
219
+ {
220
+ "key": "percentage_of_actors",
221
+ "name": "percentage_of_actors",
222
+ "value": 0
223
+ },
224
+ {
225
+ "key": "percentage_of_time",
226
+ "name": "percentage_of_time",
227
+ "value": 0
228
+ }
229
+ ]
230
+ }
231
+ ]
232
+ })
233
+ stub = stub_request(:get, "https://www.flippercloud.io/adapter/features?exclude_gate_names=true").
234
+ with({
235
+ headers: {
236
+ 'Flipper-Cloud-Token'=>'asdf',
237
+ },
238
+ }).to_return(status: 200, body: body, headers: {})
239
+ instance = described_class.new(required_options)
240
+ instance.sync
241
+
242
+ # Check that remote was fetched.
243
+ expect(stub).to have_been_requested
244
+
245
+ # Check that local adapter really did sync.
246
+ local_adapter = instance.local_adapter
247
+ all = local_adapter.get_all
248
+ expect(all.keys).to eq(["search", "history"])
249
+ expect(all["search"][:boolean]).to eq("true")
250
+ expect(all["history"][:boolean]).to eq(nil)
251
+ end
252
+ 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
+ cloud_configuration = 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: '{}', headers: {})
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: '{}', headers: {})
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
@@ -0,0 +1,104 @@
1
+ require 'flipper/cloud/message_verifier'
2
+
3
+ RSpec.describe Flipper::Cloud::MessageVerifier do
4
+ let(:payload) { "some payload" }
5
+ let(:secret) { "secret" }
6
+ let(:timestamp) { Time.now }
7
+
8
+ describe "#generate" do
9
+ it "generates signature that can be verified" do
10
+ message_verifier = Flipper::Cloud::MessageVerifier.new(secret: secret)
11
+ signature = message_verifier.generate(payload, timestamp)
12
+ header = generate_header(timestamp: timestamp, signature: signature)
13
+ expect(message_verifier.verify(payload, header)).to be(true)
14
+ end
15
+ end
16
+
17
+ describe "#header" do
18
+ it "generates a header in valid format" do
19
+ version = "v1"
20
+ message_verifier = Flipper::Cloud::MessageVerifier.new(secret: secret, version: version)
21
+ signature = message_verifier.generate(payload, timestamp)
22
+ header = message_verifier.header(signature, timestamp)
23
+ expect(header).to eq("t=#{timestamp.to_i},#{version}=#{signature}")
24
+ end
25
+ end
26
+
27
+ describe ".header" do
28
+ it "generates a header in valid format" do
29
+ version = "v1"
30
+ message_verifier = Flipper::Cloud::MessageVerifier.new(secret: secret, version: version)
31
+ signature = message_verifier.generate(payload, timestamp)
32
+
33
+ header = Flipper::Cloud::MessageVerifier.header(signature, timestamp, version)
34
+ expect(header).to eq("t=#{timestamp.to_i},#{version}=#{signature}")
35
+ end
36
+ end
37
+
38
+ describe "#verify" do
39
+ it "raises a InvalidSignature when the header does not have the expected format" do
40
+ header = "i'm not even a real signature header"
41
+ expect {
42
+ message_verifier = Flipper::Cloud::MessageVerifier.new(secret: "secret")
43
+ message_verifier.verify(payload, header)
44
+ }.to raise_error(Flipper::Cloud::MessageVerifier::InvalidSignature, "Unable to extract timestamp and signatures from header")
45
+ end
46
+
47
+ it "raises a InvalidSignature when there are no signatures with the expected version" do
48
+ header = generate_header(version: "v0")
49
+ expect {
50
+ message_verifier = Flipper::Cloud::MessageVerifier.new(secret: "secret")
51
+ message_verifier.verify(payload, header)
52
+ }.to raise_error(Flipper::Cloud::MessageVerifier::InvalidSignature, /No signatures found with expected version/)
53
+ end
54
+
55
+ it "raises a InvalidSignature when there are no valid signatures for the payload" do
56
+ header = generate_header(signature: "bad_signature")
57
+ expect {
58
+ message_verifier = Flipper::Cloud::MessageVerifier.new(secret: "secret")
59
+ message_verifier.verify(payload, header)
60
+ }.to raise_error(Flipper::Cloud::MessageVerifier::InvalidSignature, "No signatures found matching the expected signature for payload")
61
+ end
62
+
63
+ it "raises a InvalidSignature when the timestamp is not within the tolerance" do
64
+ header = generate_header(timestamp: Time.now - 15)
65
+ expect {
66
+ message_verifier = Flipper::Cloud::MessageVerifier.new(secret: secret)
67
+ message_verifier.verify(payload, header, tolerance: 10)
68
+ }.to raise_error(Flipper::Cloud::MessageVerifier::InvalidSignature, /Timestamp outside the tolerance zone/)
69
+ end
70
+
71
+ it "returns true when the header contains a valid signature and the timestamp is within the tolerance" do
72
+ header = generate_header
73
+ message_verifier = Flipper::Cloud::MessageVerifier.new(secret: "secret")
74
+ expect(message_verifier.verify(payload, header, tolerance: 10)).to be(true)
75
+ end
76
+
77
+ it "returns true when the header contains at least one valid signature" do
78
+ header = generate_header + ",v1=bad_signature"
79
+ message_verifier = Flipper::Cloud::MessageVerifier.new(secret: secret)
80
+ expect(message_verifier.verify(payload, header, tolerance: 10)).to be(true)
81
+ end
82
+
83
+ it "returns true when the header contains a valid signature and the timestamp is off but no tolerance is provided" do
84
+ header = generate_header(timestamp: Time.at(12_345))
85
+ message_verifier = Flipper::Cloud::MessageVerifier.new(secret: secret)
86
+ expect(message_verifier.verify(payload, header)).to be(true)
87
+ end
88
+ end
89
+
90
+ private
91
+
92
+ def generate_header(options = {})
93
+ options[:secret] ||= secret
94
+ options[:version] ||= "v1"
95
+
96
+ message_verifier = Flipper::Cloud::MessageVerifier.new(secret: options[:secret], version: options[:version])
97
+
98
+ options[:timestamp] ||= timestamp
99
+ options[:payload] ||= payload
100
+ options[:signature] ||= message_verifier.generate(options[:payload], options[:timestamp])
101
+
102
+ Flipper::Cloud::MessageVerifier.header(options[:signature], options[:timestamp], options[:version])
103
+ end
104
+ end