flipper 0.28.3 → 1.4.2

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 (204) hide show
  1. checksums.yaml +4 -4
  2. data/.github/FUNDING.yml +1 -0
  3. data/.github/workflows/ci.yml +56 -10
  4. data/.github/workflows/examples.yml +54 -11
  5. data/.github/workflows/release.yml +54 -0
  6. data/.superset/config.json +4 -0
  7. data/CLAUDE.md +93 -0
  8. data/Changelog.md +1 -559
  9. data/Gemfile +17 -8
  10. data/README.md +34 -29
  11. data/Rakefile +2 -2
  12. data/benchmark/typecast_ips.rb +8 -0
  13. data/docs/images/banner.jpg +0 -0
  14. data/docs/images/flipper_cloud.png +0 -0
  15. data/examples/cloud/app.ru +12 -0
  16. data/examples/cloud/backoff_policy.rb +13 -0
  17. data/examples/cloud/basic.rb +22 -0
  18. data/examples/cloud/cloud_setup.rb +20 -0
  19. data/examples/cloud/forked.rb +36 -0
  20. data/examples/cloud/import.rb +17 -0
  21. data/examples/cloud/poll_interval/README.md +111 -0
  22. data/examples/cloud/poll_interval/client.rb +108 -0
  23. data/examples/cloud/poll_interval/server.rb +98 -0
  24. data/examples/cloud/threaded.rb +33 -0
  25. data/examples/dsl.rb +0 -14
  26. data/examples/expressions.rb +237 -0
  27. data/examples/strict.rb +18 -0
  28. data/exe/flipper +5 -0
  29. data/flipper-cloud.gemspec +19 -0
  30. data/flipper.gemspec +8 -4
  31. data/lib/flipper/actor.rb +6 -3
  32. data/lib/flipper/adapter.rb +27 -1
  33. data/lib/flipper/adapter_builder.rb +44 -0
  34. data/lib/flipper/adapters/actor_limit.rb +54 -0
  35. data/lib/flipper/adapters/cache_base.rb +161 -0
  36. data/lib/flipper/adapters/dual_write.rb +7 -5
  37. data/lib/flipper/adapters/failover.rb +9 -7
  38. data/lib/flipper/adapters/failsafe.rb +2 -6
  39. data/lib/flipper/adapters/http/client.rb +53 -14
  40. data/lib/flipper/adapters/http/error.rb +2 -2
  41. data/lib/flipper/adapters/http.rb +56 -16
  42. data/lib/flipper/adapters/instrumented.rb +2 -6
  43. data/lib/flipper/adapters/memoizable.rb +17 -22
  44. data/lib/flipper/adapters/memory.rb +5 -7
  45. data/lib/flipper/adapters/operation_logger.rb +18 -92
  46. data/lib/flipper/adapters/poll.rb +16 -3
  47. data/lib/flipper/adapters/pstore.rb +18 -12
  48. data/lib/flipper/adapters/read_only.rb +8 -41
  49. data/lib/flipper/adapters/strict.rb +75 -0
  50. data/lib/flipper/adapters/sync/feature_synchronizer.rb +14 -1
  51. data/lib/flipper/adapters/sync/synchronizer.rb +13 -5
  52. data/lib/flipper/adapters/sync.rb +7 -7
  53. data/lib/flipper/adapters/wrapper.rb +54 -0
  54. data/lib/flipper/cli.rb +314 -0
  55. data/lib/flipper/cloud/configuration.rb +267 -0
  56. data/lib/flipper/cloud/dsl.rb +27 -0
  57. data/lib/flipper/cloud/message_verifier.rb +95 -0
  58. data/lib/flipper/cloud/middleware.rb +63 -0
  59. data/lib/flipper/cloud/migrate.rb +71 -0
  60. data/lib/flipper/cloud/routes.rb +14 -0
  61. data/lib/flipper/cloud/telemetry/backoff_policy.rb +96 -0
  62. data/lib/flipper/cloud/telemetry/instrumenter.rb +22 -0
  63. data/lib/flipper/cloud/telemetry/metric.rb +39 -0
  64. data/lib/flipper/cloud/telemetry/metric_storage.rb +30 -0
  65. data/lib/flipper/cloud/telemetry/submitter.rb +100 -0
  66. data/lib/flipper/cloud/telemetry.rb +191 -0
  67. data/lib/flipper/cloud.rb +54 -0
  68. data/lib/flipper/configuration.rb +25 -4
  69. data/lib/flipper/dsl.rb +48 -43
  70. data/lib/flipper/engine.rb +102 -0
  71. data/lib/flipper/export.rb +0 -2
  72. data/lib/flipper/exporters/json/export.rb +1 -1
  73. data/lib/flipper/exporters/json/v1.rb +1 -1
  74. data/lib/flipper/expression/builder.rb +73 -0
  75. data/lib/flipper/expression/constant.rb +25 -0
  76. data/lib/flipper/expression.rb +71 -0
  77. data/lib/flipper/expressions/all.rb +9 -0
  78. data/lib/flipper/expressions/any.rb +9 -0
  79. data/lib/flipper/expressions/boolean.rb +9 -0
  80. data/lib/flipper/expressions/comparable.rb +13 -0
  81. data/lib/flipper/expressions/equal.rb +9 -0
  82. data/lib/flipper/expressions/feature_enabled.rb +34 -0
  83. data/lib/flipper/expressions/greater_than.rb +9 -0
  84. data/lib/flipper/expressions/greater_than_or_equal_to.rb +9 -0
  85. data/lib/flipper/expressions/less_than.rb +9 -0
  86. data/lib/flipper/expressions/less_than_or_equal_to.rb +9 -0
  87. data/lib/flipper/expressions/not_equal.rb +9 -0
  88. data/lib/flipper/expressions/now.rb +9 -0
  89. data/lib/flipper/expressions/number.rb +9 -0
  90. data/lib/flipper/expressions/percentage.rb +9 -0
  91. data/lib/flipper/expressions/percentage_of_actors.rb +12 -0
  92. data/lib/flipper/expressions/property.rb +9 -0
  93. data/lib/flipper/expressions/random.rb +9 -0
  94. data/lib/flipper/expressions/string.rb +9 -0
  95. data/lib/flipper/expressions/time.rb +16 -0
  96. data/lib/flipper/feature.rb +63 -1
  97. data/lib/flipper/gate.rb +2 -1
  98. data/lib/flipper/gate_values.rb +5 -2
  99. data/lib/flipper/gates/expression.rb +75 -0
  100. data/lib/flipper/instrumentation/log_subscriber.rb +13 -5
  101. data/lib/flipper/instrumentation/statsd.rb +4 -2
  102. data/lib/flipper/instrumentation/statsd_subscriber.rb +2 -4
  103. data/lib/flipper/instrumentation/subscriber.rb +0 -4
  104. data/lib/flipper/metadata.rb +8 -1
  105. data/lib/flipper/middleware/memoizer.rb +30 -14
  106. data/lib/flipper/model/active_record.rb +23 -0
  107. data/lib/flipper/poller.rb +54 -12
  108. data/lib/flipper/serializers/gzip.rb +22 -0
  109. data/lib/flipper/serializers/json.rb +17 -0
  110. data/lib/flipper/spec/shared_adapter_specs.rb +82 -63
  111. data/lib/flipper/test/shared_adapter_test.rb +77 -58
  112. data/lib/flipper/test_help.rb +43 -0
  113. data/lib/flipper/typecast.rb +37 -9
  114. data/lib/flipper/types/percentage.rb +1 -1
  115. data/lib/flipper/version.rb +11 -1
  116. data/lib/flipper.rb +61 -8
  117. data/lib/generators/flipper/setup_generator.rb +68 -0
  118. data/lib/generators/flipper/templates/initializer.rb +45 -0
  119. data/lib/generators/flipper/templates/update/migrations/01_create_flipper_tables.rb.erb +22 -0
  120. data/lib/generators/flipper/templates/update/migrations/02_change_flipper_gates_value_to_text.rb.erb +18 -0
  121. data/lib/generators/flipper/update_generator.rb +35 -0
  122. data/package-lock.json +41 -0
  123. data/package.json +10 -0
  124. data/spec/fixtures/environment.rb +1 -0
  125. data/spec/flipper/adapter_builder_spec.rb +72 -0
  126. data/spec/flipper/adapter_spec.rb +21 -0
  127. data/spec/flipper/adapters/actor_limit_spec.rb +75 -0
  128. data/spec/flipper/adapters/dual_write_spec.rb +15 -2
  129. data/spec/flipper/adapters/failover_spec.rb +12 -0
  130. data/spec/flipper/adapters/http/client_spec.rb +61 -0
  131. data/spec/flipper/adapters/http_spec.rb +375 -74
  132. data/spec/flipper/adapters/instrumented_spec.rb +1 -1
  133. data/spec/flipper/adapters/memoizable_spec.rb +21 -21
  134. data/spec/flipper/adapters/operation_logger_spec.rb +2 -2
  135. data/spec/flipper/adapters/poll_spec.rb +41 -0
  136. data/spec/flipper/adapters/read_only_spec.rb +32 -17
  137. data/spec/flipper/adapters/strict_spec.rb +122 -0
  138. data/spec/flipper/adapters/sync/feature_synchronizer_spec.rb +39 -0
  139. data/spec/flipper/adapters/sync/synchronizer_spec.rb +87 -0
  140. data/spec/flipper/adapters/sync_spec.rb +13 -0
  141. data/spec/flipper/cli_spec.rb +217 -0
  142. data/spec/flipper/cloud/configuration_spec.rb +251 -0
  143. data/spec/flipper/cloud/dsl_spec.rb +82 -0
  144. data/spec/flipper/cloud/message_verifier_spec.rb +104 -0
  145. data/spec/flipper/cloud/middleware_spec.rb +289 -0
  146. data/spec/flipper/cloud/migrate_spec.rb +160 -0
  147. data/spec/flipper/cloud/telemetry/backoff_policy_spec.rb +107 -0
  148. data/spec/flipper/cloud/telemetry/metric_spec.rb +87 -0
  149. data/spec/flipper/cloud/telemetry/metric_storage_spec.rb +58 -0
  150. data/spec/flipper/cloud/telemetry/submitter_spec.rb +145 -0
  151. data/spec/flipper/cloud/telemetry_spec.rb +208 -0
  152. data/spec/flipper/cloud_spec.rb +186 -0
  153. data/spec/flipper/configuration_spec.rb +17 -0
  154. data/spec/flipper/dsl_spec.rb +34 -73
  155. data/spec/flipper/engine_spec.rb +374 -0
  156. data/spec/flipper/exporters/json/v1_spec.rb +3 -3
  157. data/spec/flipper/expression/builder_spec.rb +248 -0
  158. data/spec/flipper/expression_spec.rb +188 -0
  159. data/spec/flipper/expressions/all_spec.rb +15 -0
  160. data/spec/flipper/expressions/any_spec.rb +15 -0
  161. data/spec/flipper/expressions/boolean_spec.rb +15 -0
  162. data/spec/flipper/expressions/equal_spec.rb +24 -0
  163. data/spec/flipper/expressions/greater_than_or_equal_to_spec.rb +28 -0
  164. data/spec/flipper/expressions/greater_than_spec.rb +28 -0
  165. data/spec/flipper/expressions/less_than_or_equal_to_spec.rb +28 -0
  166. data/spec/flipper/expressions/less_than_spec.rb +32 -0
  167. data/spec/flipper/expressions/not_equal_spec.rb +15 -0
  168. data/spec/flipper/expressions/now_spec.rb +11 -0
  169. data/spec/flipper/expressions/number_spec.rb +21 -0
  170. data/spec/flipper/expressions/percentage_of_actors_spec.rb +20 -0
  171. data/spec/flipper/expressions/percentage_spec.rb +15 -0
  172. data/spec/flipper/expressions/property_spec.rb +13 -0
  173. data/spec/flipper/expressions/random_spec.rb +9 -0
  174. data/spec/flipper/expressions/string_spec.rb +11 -0
  175. data/spec/flipper/expressions/time_spec.rb +29 -0
  176. data/spec/flipper/feature_spec.rb +380 -10
  177. data/spec/flipper/gate_values_spec.rb +2 -2
  178. data/spec/flipper/gates/expression_spec.rb +190 -0
  179. data/spec/flipper/identifier_spec.rb +4 -5
  180. data/spec/flipper/instrumentation/log_subscriber_spec.rb +10 -2
  181. data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +16 -2
  182. data/spec/flipper/middleware/memoizer_spec.rb +107 -7
  183. data/spec/flipper/model/active_record_spec.rb +72 -0
  184. data/spec/flipper/poller_spec.rb +347 -4
  185. data/spec/flipper/serializers/gzip_spec.rb +13 -0
  186. data/spec/flipper/serializers/json_spec.rb +13 -0
  187. data/spec/flipper/typecast_spec.rb +43 -7
  188. data/spec/flipper/types/actor_spec.rb +18 -1
  189. data/spec/flipper_integration_spec.rb +247 -16
  190. data/spec/flipper_spec.rb +92 -29
  191. data/spec/spec_helper.rb +24 -17
  192. data/spec/support/actor_names.yml +1 -0
  193. data/spec/support/fail_on_output.rb +8 -0
  194. data/spec/support/fake_backoff_policy.rb +15 -0
  195. data/spec/support/spec_helpers.rb +34 -8
  196. data/test/adapters/actor_limit_test.rb +20 -0
  197. data/test_rails/generators/flipper/setup_generator_test.rb +69 -0
  198. data/test_rails/generators/flipper/update_generator_test.rb +96 -0
  199. data/test_rails/helper.rb +22 -2
  200. data/test_rails/system/test_help_test.rb +52 -0
  201. metadata +139 -75
  202. data/.tool-versions +0 -1
  203. data/lib/flipper/railtie.rb +0 -47
  204. data/spec/flipper/railtie_spec.rb +0 -109
@@ -0,0 +1,190 @@
1
+ RSpec.describe Flipper::Gates::Expression do
2
+ let(:feature_name) { :search }
3
+
4
+ subject do
5
+ described_class.new
6
+ end
7
+
8
+ def context(expression, properties: {})
9
+ Flipper::FeatureCheckContext.new(
10
+ feature_name: feature_name,
11
+ values: Flipper::GateValues.new(expression: expression),
12
+ actors: [Flipper::Types::Actor.new(Flipper::Actor.new(1, properties))]
13
+ )
14
+ end
15
+
16
+ describe '#enabled?' do
17
+ context 'for nil value' do
18
+ it 'returns false' do
19
+ expect(subject.enabled?(nil)).to eq(false)
20
+ end
21
+ end
22
+
23
+ context 'for empty value' do
24
+ it 'returns false' do
25
+ expect(subject.enabled?({})).to eq(false)
26
+ end
27
+ end
28
+
29
+ context "for not empty value" do
30
+ it 'returns true' do
31
+ expect(subject.enabled?({"Boolean" => [true]})).to eq(true)
32
+ end
33
+ end
34
+ end
35
+
36
+ describe '#open?' do
37
+ context 'for expression that evaluates to true' do
38
+ it 'returns true' do
39
+ expression = Flipper.boolean(true).eq(true)
40
+ expect(subject.open?(context(expression.value))).to be(true)
41
+ end
42
+ end
43
+
44
+ context 'for expression that evaluates to false' do
45
+ it 'returns false' do
46
+ expression = Flipper.boolean(true).eq(false)
47
+ expect(subject.open?(context(expression.value))).to be(false)
48
+ end
49
+ end
50
+
51
+ context 'for properties that have string keys' do
52
+ it 'returns true when expression evalutes to true' do
53
+ expression = Flipper.property(:type).eq("User")
54
+ context = context(expression.value, properties: {"type" => "User"})
55
+ expect(subject.open?(context)).to be(true)
56
+ end
57
+
58
+ it 'returns false when expression evaluates to false' do
59
+ expression = Flipper.property(:type).eq("User")
60
+ context = context(expression.value, properties: {"type" => "Org"})
61
+ expect(subject.open?(context)).to be(false)
62
+ end
63
+ end
64
+
65
+ context 'for actor in context' do
66
+ it 'passes actor to expression context' do
67
+ actor = Flipper::Actor.new("User;1", {type: "User"})
68
+ wrapped_actor = Flipper::Types::Actor.new(actor)
69
+ expression = Flipper.property(:flipper_id).eq("User;1")
70
+ ctx = Flipper::FeatureCheckContext.new(
71
+ feature_name: feature_name,
72
+ values: Flipper::GateValues.new(expression: expression.value),
73
+ actors: [wrapped_actor]
74
+ )
75
+ expect(subject.open?(ctx)).to be(true)
76
+ end
77
+
78
+ it 'passes nil actor when no actors provided' do
79
+ expression = Flipper.boolean(true).eq(true)
80
+ ctx = Flipper::FeatureCheckContext.new(
81
+ feature_name: feature_name,
82
+ values: Flipper::GateValues.new(expression: expression.value),
83
+ actors: nil
84
+ )
85
+ expect(subject.open?(ctx)).to be(true)
86
+ end
87
+ end
88
+
89
+ context 'for properties that have symbol keys' do
90
+ it 'returns true when expression evalutes to true' do
91
+ expression = Flipper.property(:type).eq("User")
92
+ context = context(expression.value, properties: {type: "User"})
93
+ expect(subject.open?(context)).to be(true)
94
+ end
95
+
96
+ it 'returns false when expression evaluates to false' do
97
+ expression = Flipper.property(:type).eq("User")
98
+ context = context(expression.value, properties: {type: "Org"})
99
+ expect(subject.open?(context)).to be(false)
100
+ end
101
+ end
102
+
103
+ context 'for time-based expressions' do
104
+ it 'enables when now is past a scheduled epoch' do
105
+ past_epoch = Time.now.to_i - 86_400
106
+ expression = Flipper.now.gte(Flipper.time(past_epoch))
107
+ expect(subject.open?(context(expression.value))).to be(true)
108
+ end
109
+
110
+ it 'does not enable when now is before a future epoch' do
111
+ future_epoch = Time.now.to_i + 86_400
112
+ expression = Flipper.now.gte(Flipper.time(future_epoch))
113
+ expect(subject.open?(context(expression.value))).to be(false)
114
+ end
115
+
116
+ it 'enables when now is past a scheduled datetime' do
117
+ past_time = (Time.now.utc - 86_400).iso8601
118
+ expression = Flipper.now.gte(Flipper.time(past_time))
119
+ expect(subject.open?(context(expression.value))).to be(true)
120
+ end
121
+
122
+ it 'does not enable when now is before a future datetime' do
123
+ future_time = (Time.now.utc + 86_400).iso8601
124
+ expression = Flipper.now.gte(Flipper.time(future_time))
125
+ expect(subject.open?(context(expression.value))).to be(false)
126
+ end
127
+
128
+ it 'enables expiring features with lt' do
129
+ future_time = (Time.now.utc + 86_400).iso8601
130
+ expression = Flipper.now.lt(Flipper.time(future_time))
131
+ expect(subject.open?(context(expression.value))).to be(true)
132
+ end
133
+
134
+ it 'disables expired features with lt' do
135
+ past_time = (Time.now.utc - 86_400).iso8601
136
+ expression = Flipper.now.lt(Flipper.time(past_time))
137
+ expect(subject.open?(context(expression.value))).to be(false)
138
+ end
139
+
140
+ it 'enables within a time window using all' do
141
+ start_time = (Time.now.utc - 86_400).iso8601
142
+ end_time = (Time.now.utc + 86_400).iso8601
143
+ expression = Flipper.all(
144
+ Flipper.now.gte(Flipper.time(start_time)),
145
+ Flipper.now.lt(Flipper.time(end_time))
146
+ )
147
+ expect(subject.open?(context(expression.value))).to be(true)
148
+ end
149
+
150
+ it 'does not enable outside a time window' do
151
+ start_time = (Time.now.utc + 86_400).iso8601
152
+ end_time = (Time.now.utc + 172_800).iso8601
153
+ expression = Flipper.all(
154
+ Flipper.now.gte(Flipper.time(start_time)),
155
+ Flipper.now.lt(Flipper.time(end_time))
156
+ )
157
+ expect(subject.open?(context(expression.value))).to be(false)
158
+ end
159
+ end
160
+ end
161
+
162
+ describe '#protects?' do
163
+ it 'returns true for Flipper::Expression' do
164
+ expression = Flipper.number(20).eq(20)
165
+ expect(subject.protects?(expression)).to be(true)
166
+ end
167
+
168
+ it 'returns true for Hash' do
169
+ expression = Flipper.number(20).eq(20)
170
+ expect(subject.protects?(expression.value)).to be(true)
171
+ end
172
+
173
+ it 'returns false for other things' do
174
+ expect(subject.protects?(false)).to be(false)
175
+ end
176
+ end
177
+
178
+ describe '#wrap' do
179
+ it 'returns self for Flipper::Expression' do
180
+ expression = Flipper.number(20).eq(20)
181
+ expect(subject.wrap(expression)).to be(expression)
182
+ end
183
+
184
+ it 'returns Flipper::Expression for Hash' do
185
+ expression = Flipper.number(20).eq(20)
186
+ expect(subject.wrap(expression.value)).to be_instance_of(Flipper::Expression)
187
+ expect(subject.wrap(expression.value)).to eq(expression)
188
+ end
189
+ end
190
+ end
@@ -2,12 +2,11 @@ require 'flipper/identifier'
2
2
 
3
3
  RSpec.describe Flipper::Identifier do
4
4
  describe '#flipper_id' do
5
- class User < Struct.new(:id)
6
- include Flipper::Identifier
7
- end
8
-
9
5
  it 'uses class name and id' do
10
- expect(User.new(5).flipper_id).to eq('User;5')
6
+ class BlahBlah < Struct.new(:id)
7
+ include Flipper::Identifier
8
+ end
9
+ expect(BlahBlah.new(5).flipper_id).to eq('BlahBlah;5')
11
10
  end
12
11
  end
13
12
  end
@@ -1,6 +1,7 @@
1
1
  require 'logger'
2
- require 'flipper/adapters/instrumented'
2
+ require 'active_support/core_ext/object/blank'
3
3
  require 'flipper/instrumentation/log_subscriber'
4
+ require 'flipper/adapters/instrumented'
4
5
 
5
6
  begin
6
7
  require 'active_support/isolated_execution_state'
@@ -8,6 +9,9 @@ rescue LoadError
8
9
  # ActiveSupport::IsolatedExecutionState is only available in Rails 5.2+
9
10
  end
10
11
 
12
+ # Don't log in other tests, we'll manually re-attach when this one starts
13
+ Flipper::Instrumentation::LogSubscriber.detach
14
+
11
15
  RSpec.describe Flipper::Instrumentation::LogSubscriber do
12
16
  let(:adapter) do
13
17
  memory = Flipper::Adapters::Memory.new
@@ -32,8 +36,12 @@ RSpec.describe Flipper::Instrumentation::LogSubscriber do
32
36
  described_class.logger = nil
33
37
  end
34
38
 
39
+ before(:all) do
40
+ described_class.attach
41
+ end
42
+
35
43
  after(:all) do
36
- ActiveSupport::Notifications.unsubscribe("flipper")
44
+ described_class.detach
37
45
  end
38
46
 
39
47
  let(:log) { @io.string }
@@ -1,6 +1,5 @@
1
1
  require 'flipper/adapters/instrumented'
2
2
  require 'flipper/instrumentation/statsd'
3
- require 'statsd'
4
3
 
5
4
  begin
6
5
  require 'active_support/isolated_execution_state'
@@ -19,7 +18,7 @@ RSpec.describe Flipper::Instrumentation::StatsdSubscriber do
19
18
  Flipper.new(adapter, instrumenter: ActiveSupport::Notifications)
20
19
  end
21
20
 
22
- let(:user) { user = Flipper::Actor.new('1') }
21
+ let(:user) { Flipper::Actor.new('1') }
23
22
 
24
23
  before do
25
24
  described_class.client = statsd_client
@@ -78,4 +77,19 @@ RSpec.describe Flipper::Instrumentation::StatsdSubscriber do
78
77
  flipper[:stats].disable(user)
79
78
  assert_timer 'flipper.adapter.memory.disable'
80
79
  end
80
+
81
+ context 'when client is nil' do
82
+ before do
83
+ described_class.client = nil
84
+ end
85
+
86
+ it 'does not raise error' do
87
+ expect { flipper[:stats].enable(user) }.not_to raise_error
88
+ end
89
+
90
+ it 'does not update metrics' do
91
+ flipper[:stats].enable(user)
92
+ expect(socket.buffer).to be_empty
93
+ end
94
+ end
81
95
  end
@@ -2,6 +2,8 @@ require 'rack/test'
2
2
  require 'active_support/cache'
3
3
  require 'flipper/adapters/active_support_cache_store'
4
4
  require 'flipper/adapters/operation_logger'
5
+ require 'flipper/adapters/actor_limit'
6
+ require 'flipper/adapters/sync'
5
7
 
6
8
  RSpec.describe Flipper::Middleware::Memoizer do
7
9
  include Rack::Test::Methods
@@ -80,7 +82,7 @@ RSpec.describe Flipper::Middleware::Memoizer do
80
82
  context 'with preload: true' do
81
83
  let(:app) do
82
84
  # ensure scoped for builder block, annoying...
83
- instance = flipper
85
+ flipper
84
86
  middleware = described_class
85
87
 
86
88
  Rack::Builder.new do
@@ -141,7 +143,7 @@ RSpec.describe Flipper::Middleware::Memoizer do
141
143
  context 'with preload specific' do
142
144
  let(:app) do
143
145
  # ensure scoped for builder block, annoying...
144
- instance = flipper
146
+ flipper
145
147
  middleware = described_class
146
148
 
147
149
  Rack::Builder.new do
@@ -196,10 +198,77 @@ RSpec.describe Flipper::Middleware::Memoizer do
196
198
  end
197
199
  end
198
200
 
201
+ context 'with preload block' do
202
+ let(:app) do
203
+ app = lambda do |_env|
204
+ flipper[:stats].enabled?
205
+ flipper[:stats].enabled?
206
+ flipper[:shiny].enabled?
207
+ flipper[:shiny].enabled?
208
+ [200, {}, []]
209
+ end
210
+
211
+ described_class.new(app, preload: ->(request) {
212
+ case request.path
213
+ when "/true"
214
+ true
215
+ when "/specific"
216
+ [:stats]
217
+ else
218
+ false
219
+ end
220
+ })
221
+ end
222
+
223
+ include_examples 'flipper middleware'
224
+
225
+ it 'eagerly caches known features for duration of request if block returns true' do
226
+ flipper[:stats].enable
227
+ flipper[:shiny].enable
228
+
229
+ # clear the log of operations
230
+ adapter.reset
231
+
232
+ get '/true', {}, 'flipper' => flipper
233
+
234
+ expect(adapter.operations.size).to be(1)
235
+ expect(adapter.count(:get_all)).to be(1)
236
+ expect(adapter.count(:get)).to be(0)
237
+ end
238
+
239
+ it 'does not eagerly cache known features if block returns false' do
240
+ flipper[:stats].enable
241
+ flipper[:shiny].enable
242
+
243
+ # clear the log of operations
244
+ adapter.reset
245
+
246
+ get '/false', {}, 'flipper' => flipper
247
+
248
+ expect(adapter.operations.size).to be(2)
249
+ expect(adapter.count(:get_all)).to be(0)
250
+ expect(adapter.count(:get)).to be(2)
251
+ end
252
+
253
+ it 'eagerly caches specified features for duration of request if block returns array of specified features' do
254
+ flipper[:stats].enable
255
+ flipper[:shiny].enable
256
+
257
+ # clear the log of operations
258
+ adapter.reset
259
+
260
+ get '/specific', {}, 'flipper' => flipper
261
+
262
+ expect(adapter.operations.size).to be(2)
263
+ expect(adapter.count(:get_multi)).to be(1)
264
+ expect(adapter.count(:get)).to be(1)
265
+ end
266
+ end
267
+
199
268
  context 'with multiple instances' do
200
269
  let(:app) do
201
270
  # ensure scoped for builder block, annoying...
202
- instance = flipper
271
+ flipper
203
272
  middleware = described_class
204
273
 
205
274
  Rack::Builder.new do
@@ -218,7 +287,7 @@ RSpec.describe Flipper::Middleware::Memoizer do
218
287
  end
219
288
 
220
289
  def get(uri, params = {}, env = {}, &block)
221
- silence { super(uri, params, env, &block) }
290
+ capture_output { super(uri, params, env, &block) }
222
291
  end
223
292
 
224
293
  include_examples 'flipper middleware'
@@ -249,7 +318,7 @@ RSpec.describe Flipper::Middleware::Memoizer do
249
318
  context 'with flipper setup in env' do
250
319
  let(:app) do
251
320
  # ensure scoped for builder block, annoying...
252
- instance = flipper
321
+ flipper
253
322
  middleware = described_class
254
323
 
255
324
  Rack::Builder.new do
@@ -391,9 +460,8 @@ RSpec.describe Flipper::Middleware::Memoizer do
391
460
  logged_memory = Flipper::Adapters::OperationLogger.new(memory)
392
461
  cache = ActiveSupport::Cache::MemoryStore.new
393
462
  cache.clear
394
- cached = Flipper::Adapters::ActiveSupportCacheStore.new(logged_memory, cache, expires_in: 10)
463
+ cached = Flipper::Adapters::ActiveSupportCacheStore.new(logged_memory, cache)
395
464
  logged_cached = Flipper::Adapters::OperationLogger.new(cached)
396
- memo = {}
397
465
  flipper = Flipper.new(logged_cached)
398
466
  flipper[:stats].enable
399
467
  flipper[:shiny].enable
@@ -415,4 +483,36 @@ RSpec.describe Flipper::Middleware::Memoizer do
415
483
  expect(logged_memory.count(:get_all)).to be(1)
416
484
  end
417
485
  end
486
+
487
+ context 'with preload:true and Sync adapter wrapped with ActorLimit' do
488
+ it 'preloads even when remote has more actors than local limit' do
489
+ local = Flipper::Adapters::Memory.new
490
+ remote = Flipper::Adapters::Memory.new
491
+ remote_flipper = Flipper.new(remote)
492
+
493
+ # Remote has more actors than limit allows (actor-only enables, not boolean)
494
+ 10.times { |i| remote_flipper[:stats].enable_actor Flipper::Actor.new("User;#{i}") }
495
+
496
+ # Sync adapter will sync from remote to local, then ActorLimit wraps it
497
+ # Use interval: 0 to force sync on every call
498
+ sync_adapter = Flipper::Adapters::Sync.new(local, remote, interval: 0)
499
+ limited_adapter = Flipper::Adapters::ActorLimit.new(sync_adapter, 5)
500
+ test_flipper = Flipper.new(limited_adapter)
501
+
502
+ app = lambda do |env|
503
+ f = env['flipper']
504
+ f[:stats].enabled?
505
+ [200, {}, []]
506
+ end
507
+ middleware = described_class.new(app, preload: true)
508
+
509
+ # Preload should work without raising ActorLimit::LimitExceeded
510
+ expect {
511
+ middleware.call('flipper' => test_flipper)
512
+ }.not_to raise_error
513
+
514
+ # Verify actors were synced (all 10, not just 5)
515
+ expect(test_flipper[:stats].actors_value.size).to eq(10)
516
+ end
517
+ end
418
518
  end
@@ -0,0 +1,72 @@
1
+ require 'active_record'
2
+ require 'flipper/model/active_record'
3
+
4
+ # Turn off migration logging for specs
5
+ ActiveRecord::Migration.verbose = false
6
+
7
+ RSpec.describe Flipper::Model::ActiveRecord do
8
+ before(:all) do
9
+ ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:')
10
+ end
11
+
12
+ before(:each) do
13
+ ActiveRecord::Base.connection.execute <<-SQL
14
+ CREATE TABLE users (
15
+ id integer PRIMARY KEY,
16
+ name string NOT NULL,
17
+ age integer,
18
+ is_confirmed boolean,
19
+ created_at datetime NOT NULL,
20
+ updated_at datetime NOT NULL
21
+ )
22
+ SQL
23
+ end
24
+
25
+ after(:each) do
26
+ ActiveRecord::Base.connection.execute("DROP table IF EXISTS `users`")
27
+ end
28
+
29
+ class User < ActiveRecord::Base
30
+ include Flipper::Model::ActiveRecord
31
+ end
32
+
33
+ class DelegatedUser < DelegateClass(User)
34
+ end
35
+
36
+ class Admin < User
37
+ end
38
+
39
+ it "doesn't warn for to_ary" do
40
+ # looks like we should remove this but you are wrong, we have specs that
41
+ # fail if there are warnings and if this regresses it will print a warning
42
+ # so it is in fact testing something
43
+ user = User.create!(name: "Test")
44
+ Flipper.enabled?(:something, DelegatedUser.new(user))
45
+ end
46
+
47
+ describe "flipper_id" do
48
+ it "returns class name and id" do
49
+ expect(User.new(id: 1).flipper_id).to eq("User;1")
50
+ end
51
+
52
+ it "uses base class name" do
53
+ expect(Admin.new(id: 2).flipper_id).to eq("User;2")
54
+ end
55
+ end
56
+
57
+ describe "flipper_properties" do
58
+ subject { User.create!(name: "Test", age: 22, is_confirmed: true) }
59
+
60
+ it "includes all attributes" do
61
+ expect(subject.flipper_properties).to eq({
62
+ "type" => "User",
63
+ "id" => subject.id,
64
+ "name" => "Test",
65
+ "age" => 22,
66
+ "is_confirmed" => true,
67
+ "created_at" => subject.created_at,
68
+ "updated_at" => subject.updated_at
69
+ })
70
+ end
71
+ end
72
+ end