flip_fab 0.0.1

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 (109) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +5 -0
  3. data/Gemfile +16 -0
  4. data/Gemfile.lock +92 -0
  5. data/LICENSE.txt +9 -0
  6. data/README.md +168 -0
  7. data/example/rails_app/.gitignore +17 -0
  8. data/example/rails_app/Gemfile +36 -0
  9. data/example/rails_app/Gemfile.lock +178 -0
  10. data/example/rails_app/README.rdoc +28 -0
  11. data/example/rails_app/Rakefile +6 -0
  12. data/example/rails_app/app/assets/images/.keep +0 -0
  13. data/example/rails_app/app/assets/images/justin_beaver.jpg +0 -0
  14. data/example/rails_app/app/assets/images/regular_beaver.jpg +0 -0
  15. data/example/rails_app/app/assets/javascripts/application.js +16 -0
  16. data/example/rails_app/app/assets/javascripts/beavers.js.coffee +3 -0
  17. data/example/rails_app/app/assets/stylesheets/application.css +15 -0
  18. data/example/rails_app/app/assets/stylesheets/beavers.css.scss +3 -0
  19. data/example/rails_app/app/assets/stylesheets/scaffolds.css.scss +69 -0
  20. data/example/rails_app/app/controllers/application_controller.rb +5 -0
  21. data/example/rails_app/app/controllers/beavers_controller.rb +74 -0
  22. data/example/rails_app/app/controllers/concerns/.keep +0 -0
  23. data/example/rails_app/app/helpers/application_helper.rb +2 -0
  24. data/example/rails_app/app/helpers/beavers_helper.rb +2 -0
  25. data/example/rails_app/app/mailers/.keep +0 -0
  26. data/example/rails_app/app/models/.keep +0 -0
  27. data/example/rails_app/app/models/beaver.rb +2 -0
  28. data/example/rails_app/app/models/concerns/.keep +0 -0
  29. data/example/rails_app/app/views/beavers/_form.html.erb +21 -0
  30. data/example/rails_app/app/views/beavers/edit.html.erb +6 -0
  31. data/example/rails_app/app/views/beavers/index.html.erb +25 -0
  32. data/example/rails_app/app/views/beavers/index.json.jbuilder +4 -0
  33. data/example/rails_app/app/views/beavers/new.html.erb +5 -0
  34. data/example/rails_app/app/views/beavers/show.html.erb +9 -0
  35. data/example/rails_app/app/views/beavers/show.json.jbuilder +1 -0
  36. data/example/rails_app/app/views/layouts/application.html.erb +18 -0
  37. data/example/rails_app/bin/bundle +3 -0
  38. data/example/rails_app/bin/rails +4 -0
  39. data/example/rails_app/bin/rake +4 -0
  40. data/example/rails_app/config/application.rb +25 -0
  41. data/example/rails_app/config/boot.rb +4 -0
  42. data/example/rails_app/config/database.yml +22 -0
  43. data/example/rails_app/config/environment.rb +5 -0
  44. data/example/rails_app/config/environments/development.rb +83 -0
  45. data/example/rails_app/config/environments/test.rb +41 -0
  46. data/example/rails_app/config/initializers/backtrace_silencers.rb +7 -0
  47. data/example/rails_app/config/initializers/cookies_serializer.rb +3 -0
  48. data/example/rails_app/config/initializers/filter_parameter_logging.rb +4 -0
  49. data/example/rails_app/config/initializers/flip_fab.rb +1 -0
  50. data/example/rails_app/config/initializers/inflections.rb +16 -0
  51. data/example/rails_app/config/initializers/mime_types.rb +4 -0
  52. data/example/rails_app/config/initializers/session_store.rb +3 -0
  53. data/example/rails_app/config/initializers/wrap_parameters.rb +14 -0
  54. data/example/rails_app/config/locales/en.yml +23 -0
  55. data/example/rails_app/config/rabbit_feed.yml +8 -0
  56. data/example/rails_app/config/routes.rb +58 -0
  57. data/example/rails_app/config/secrets.yml +18 -0
  58. data/example/rails_app/config/unicorn.rb +4 -0
  59. data/example/rails_app/config.ru +4 -0
  60. data/example/rails_app/db/migrate/20140424102400_create_beavers.rb +9 -0
  61. data/example/rails_app/db/schema.rb +22 -0
  62. data/example/rails_app/db/seeds.rb +7 -0
  63. data/example/rails_app/lib/assets/.keep +0 -0
  64. data/example/rails_app/lib/tasks/.keep +0 -0
  65. data/example/rails_app/log/.keep +0 -0
  66. data/example/rails_app/public/404.html +67 -0
  67. data/example/rails_app/public/422.html +67 -0
  68. data/example/rails_app/public/500.html +66 -0
  69. data/example/rails_app/public/favicon.ico +0 -0
  70. data/example/rails_app/public/robots.txt +5 -0
  71. data/example/rails_app/spec/rails_helper.rb +50 -0
  72. data/example/rails_app/spec/spec_helper.rb +8 -0
  73. data/example/rails_app/test/controllers/.keep +0 -0
  74. data/example/rails_app/test/controllers/beavers_controller_test.rb +49 -0
  75. data/example/rails_app/test/fixtures/.keep +0 -0
  76. data/example/rails_app/test/fixtures/beavers.yml +7 -0
  77. data/example/rails_app/test/helpers/.keep +0 -0
  78. data/example/rails_app/test/helpers/beavers_helper_test.rb +4 -0
  79. data/example/rails_app/test/integration/.keep +0 -0
  80. data/example/rails_app/test/mailers/.keep +0 -0
  81. data/example/rails_app/test/models/.keep +0 -0
  82. data/example/rails_app/test/models/beaver_test.rb +7 -0
  83. data/example/rails_app/test/test_helper.rb +13 -0
  84. data/flip_fab.gemspec +20 -0
  85. data/lib/flip_fab/contextual_feature.rb +83 -0
  86. data/lib/flip_fab/cookie_persistence.rb +52 -0
  87. data/lib/flip_fab/feature.rb +24 -0
  88. data/lib/flip_fab/features_by_name.rb +22 -0
  89. data/lib/flip_fab/helper.rb +8 -0
  90. data/lib/flip_fab/persistence.rb +19 -0
  91. data/lib/flip_fab/version.rb +3 -0
  92. data/lib/flip_fab.rb +24 -0
  93. data/spec/lib/flip_fab/contextual_feature_spec.rb +352 -0
  94. data/spec/lib/flip_fab/cookie_persistence.feature +50 -0
  95. data/spec/lib/flip_fab/cookie_persistence_spec.rb +90 -0
  96. data/spec/lib/flip_fab/feature_spec.rb +86 -0
  97. data/spec/lib/flip_fab/features_by_name_spec.rb +34 -0
  98. data/spec/lib/flip_fab/helper.feature +31 -0
  99. data/spec/lib/flip_fab/helper_spec.rb +90 -0
  100. data/spec/lib/flip_fab/persistence_spec.rb +32 -0
  101. data/spec/lib/flip_fab.feature +10 -0
  102. data/spec/lib/flip_fab_spec.rb +47 -0
  103. data/spec/spec_helper.rb +93 -0
  104. data/spec/support/test_app.rb +16 -0
  105. data/spec/support/test_context.rb +10 -0
  106. data/spec/support/test_multiple_persistence.rb +14 -0
  107. data/spec/support/test_persistence.rb +14 -0
  108. data/spec/support/test_rack_context.rb +15 -0
  109. metadata +168 -0
@@ -0,0 +1,352 @@
1
+ module FlipFab
2
+ describe ContextualFeature do
3
+ let(:override) { }
4
+ let(:default) { :disabled }
5
+ let(:persistence_adapters) { [TestPersistence] }
6
+ let(:feature) { Feature.new :example_feature, { default: default, persistence_adapters: persistence_adapters } }
7
+ let(:feature_states) {{ example_feature: :enabled }}
8
+ let(:context) { TestContext.new feature_states, { 'example_feature' => override } }
9
+ subject{ described_class.new feature, context }
10
+
11
+ describe '.new' do
12
+
13
+ it 'assigns the feature' do
14
+ expect(subject.feature).to eq(feature)
15
+ end
16
+
17
+ it 'assigns the context' do
18
+ expect(subject.context).to eq(context)
19
+ end
20
+
21
+ context 'when the feature has been overridden' do
22
+ let(:override) { 'disabled' }
23
+
24
+ it 'persists the override' do
25
+ expect{ subject }.to change{ feature_states }.from({ example_feature: :enabled }).to({ example_feature: :disabled })
26
+ end
27
+
28
+ context 'when the override provided is not one of enabled or disabled, it does not persist the override' do
29
+ let(:override) { '' }
30
+
31
+ it 'does not persist the override' do
32
+ expect{ subject }.not_to change{ feature_states }.from({ example_feature: :enabled })
33
+ end
34
+ end
35
+ end
36
+ end
37
+
38
+ describe '#enabled?' do
39
+
40
+ context 'when the feature is enabled in the adapter' do
41
+ let(:feature_states) {{ example_feature: :enabled }}
42
+
43
+ it 'returns true' do
44
+ expect(subject.enabled?).to be_truthy
45
+ end
46
+
47
+ context 'when the feature has been overridden' do
48
+ let(:override) { 'disabled' }
49
+
50
+ it 'returns false' do
51
+ expect(subject.enabled?).to be_falsey
52
+ end
53
+ end
54
+ end
55
+
56
+ context 'when the feature is disabled in the adapter' do
57
+ let(:feature_states) {{ example_feature: :disabled }}
58
+
59
+ it 'returns false' do
60
+ expect(subject.enabled?).to be_falsey
61
+ end
62
+ end
63
+
64
+ context 'when the feature is not specified in the adapter' do
65
+ let(:feature_states) {{ }}
66
+
67
+ context 'when the default is :enabled' do
68
+ let(:default) { :enabled }
69
+
70
+ it 'returns true' do
71
+ expect(subject.enabled?).to be_truthy
72
+ end
73
+ end
74
+
75
+ context 'when the default is :disabled' do
76
+ let(:default) { :disabled }
77
+
78
+ it 'returns false' do
79
+ expect(subject.enabled?).to be_falsey
80
+ end
81
+ end
82
+ end
83
+
84
+ context 'when there are multiple adapters' do
85
+ let(:persistence_adapters) { [TestPersistence, TestMultiplePersistence] }
86
+
87
+ context 'when the first adapter has enabled and the second adapter has nil' do
88
+ let(:feature_states) {{ example_feature: :enabled, different_example_feature: nil }}
89
+
90
+ it 'returns true' do
91
+ expect(subject.enabled?).to be_truthy
92
+ end
93
+ end
94
+
95
+ context 'when the first adapter has nil and the second adapter has enabled' do
96
+ let(:feature_states) {{ example_feature: nil, different_example_feature: :enabled }}
97
+
98
+ it 'returns true' do
99
+ expect(subject.enabled?).to be_truthy
100
+ end
101
+ end
102
+
103
+ context 'when the first adapter has disabled and the second adapter has enabled' do
104
+ let(:feature_states) {{ example_feature: :disabled, different_example_feature: :enabled }}
105
+
106
+ it 'returns false' do
107
+ expect(subject.enabled?).to be_falsey
108
+ end
109
+ end
110
+ end
111
+ end
112
+
113
+ describe '#disabled?' do
114
+
115
+ context 'when #enabled? returns true' do
116
+ let(:feature_states) {{ example_feature: :enabled }}
117
+
118
+ it 'returns false' do
119
+ expect(subject.disabled?).to be_falsey
120
+ end
121
+ end
122
+
123
+ context 'when #enabled? returns false' do
124
+ let(:feature_states) {{ example_feature: :disabled }}
125
+
126
+ it 'returns true' do
127
+ expect(subject.disabled?).to be_truthy
128
+ end
129
+ end
130
+ end
131
+
132
+ describe '#state=' do
133
+
134
+ context 'when the provided value is not :enabled or :disabled' do
135
+
136
+ it 'raises' do
137
+ expect{ subject.state = '' }.to raise_error 'Invalid state provided: ``, possible states are :enabled, :disabled'
138
+ expect{ subject.state = 'enabled' }.to raise_error 'Invalid state provided: `enabled`, possible states are :enabled, :disabled'
139
+ end
140
+ end
141
+
142
+ context 'when the provided value is :enabled or :disabled' do
143
+
144
+ it 'changes the state of the feature' do
145
+ expect{ subject.state = :disabled }.to change{subject.enabled?}.from(true).to(false)
146
+ expect{ subject.state = :enabled }.to change{subject.enabled?}.from(false).to(true)
147
+ end
148
+ end
149
+ end
150
+
151
+ describe '#enable' do
152
+
153
+ context 'when the state has been overridden' do
154
+ let(:override) { 'disabled' }
155
+
156
+ context 'and the persistence adapter has the opposite state' do
157
+ let(:feature_states) {{ example_feature: :disabled }}
158
+
159
+ it 'does not change the state of the feature' do
160
+ expect{subject.enable}.not_to change{subject.enabled?}.from(false)
161
+ end
162
+
163
+ it 'does not persist the state in the adapter' do
164
+ expect_any_instance_of(TestPersistence).not_to receive(:write).with(:enabled)
165
+ subject.enable
166
+ end
167
+ end
168
+ end
169
+
170
+ context 'when there are multiple persistence adapters' do
171
+ let(:persistence_adapters) { [TestPersistence, TestMultiplePersistence] }
172
+ let(:feature_states) {{ example_feature: :disabled, different_example_feature: :disabled }}
173
+
174
+ it 'changes the state of the feature' do
175
+ expect{subject.enable}.to change{subject.enabled?}.from(false).to(true)
176
+ end
177
+
178
+ it 'persists the state in the adapters' do
179
+ expect{ subject.enable }.to change{ feature_states }.from({ example_feature: :disabled, different_example_feature: :disabled }).to({ example_feature: :enabled, different_example_feature: :enabled })
180
+ end
181
+ end
182
+
183
+ context 'when there is a persistence adapter' do
184
+ let(:persistence_adapters) { [TestPersistence] }
185
+
186
+ context 'and the persistence adapter has the same state' do
187
+ let(:feature_states) {{ example_feature: :enabled }}
188
+
189
+ it 'does not change the state of the feature' do
190
+ expect{subject.enable}.not_to change{subject.enabled?}.from(true)
191
+ end
192
+ end
193
+
194
+ context 'and the persistence adapter has the opposite state' do
195
+ let(:feature_states) {{ example_feature: :disabled }}
196
+
197
+ it 'changes the state of the feature' do
198
+ expect{subject.enable}.to change{subject.enabled?}.from(false).to(true)
199
+ end
200
+
201
+ it 'persists the state in the adapter' do
202
+ expect{ subject.enable }.to change{ feature_states }.from({ example_feature: :disabled }).to({ example_feature: :enabled })
203
+ end
204
+ end
205
+
206
+ context 'and the persistence adapter has no state' do
207
+ let(:feature_states) {{ }}
208
+
209
+ context 'and the feature is disabled' do
210
+ let(:default) { :disabled }
211
+
212
+ it 'changes the state of the feature' do
213
+ expect{subject.enable}.to change{subject.enabled?}.from(false).to(true)
214
+ end
215
+
216
+ it 'persists the state in the adapter' do
217
+ expect{ subject.enable }.to change{ feature_states }.from({ }).to({ example_feature: :enabled })
218
+ end
219
+ end
220
+
221
+ context 'and the feature is enabled' do
222
+ let(:default) { :enabled }
223
+
224
+ it 'does not change the state of the feature' do
225
+ expect{subject.enable}.not_to change{subject.enabled?}.from(true)
226
+ end
227
+
228
+ it 'persists the state in the adapter' do
229
+ expect{ subject.enable }.to change{ feature_states }.from({ }).to({ example_feature: :enabled })
230
+ end
231
+ end
232
+ end
233
+
234
+ context 'when there is not a persistence adapter' do
235
+ let(:persistence_adapters) { [] }
236
+
237
+ context 'and the feature is enabled' do
238
+ let(:default) { :enabled }
239
+
240
+ it 'does not change the state of the feature' do
241
+ expect{subject.enable}.not_to change{subject.enabled?}.from(true)
242
+ end
243
+ end
244
+
245
+ context 'and the feature is disabled' do
246
+ let(:default) { :disabled }
247
+
248
+ it 'changes the state of the feature' do
249
+ expect{subject.enable}.to change{subject.enabled?}.from(false).to(true)
250
+ end
251
+ end
252
+ end
253
+ end
254
+ end
255
+
256
+
257
+ describe '#disable' do
258
+
259
+ context 'when the state has been overridden' do
260
+ let(:override) { 'enabled' }
261
+
262
+ context 'and the persistence adapter has the opposite state' do
263
+ let(:feature_states) {{ example_feature: :enabled }}
264
+
265
+ it 'does not change the state of the feature' do
266
+ expect{subject.disable}.not_to change{subject.disabled?}.from(false)
267
+ end
268
+
269
+ it 'does not persist the state in the adapter' do
270
+ expect_any_instance_of(TestPersistence).not_to receive(:write).with(:disabled)
271
+ subject.disable
272
+ end
273
+ end
274
+ end
275
+
276
+ context 'when there is a persistence adapter' do
277
+ let(:persistence_adapters) { [TestPersistence] }
278
+
279
+ context 'and the persistence adapter has the same state' do
280
+ let(:feature_states) {{ example_feature: :disabled }}
281
+
282
+ it 'does not change the state of the feature' do
283
+ expect{subject.disable}.not_to change{subject.disabled?}.from(true)
284
+ end
285
+ end
286
+
287
+ context 'and the persistence adapter has the opposite state' do
288
+ let(:feature_states) {{ example_feature: :enabled }}
289
+
290
+ it 'changes the state of the feature' do
291
+ expect{subject.disable}.to change{subject.disabled?}.from(false).to(true)
292
+ end
293
+
294
+ it 'persists the state in the adapter' do
295
+ expect_any_instance_of(TestPersistence).to receive(:write).with(:disabled)
296
+ subject.disable
297
+ end
298
+ end
299
+
300
+ context 'and the persistence adapter has no state' do
301
+ let(:feature_states) {{ }}
302
+
303
+ context 'and the feature is enabled' do
304
+ let(:default) { :enabled }
305
+
306
+ it 'changes the state of the feature' do
307
+ expect{subject.disable}.to change{subject.disabled?}.from(false).to(true)
308
+ end
309
+
310
+ it 'persists the state in the adapter' do
311
+ expect_any_instance_of(TestPersistence).to receive(:write).with(:disabled)
312
+ subject.disable
313
+ end
314
+ end
315
+
316
+ context 'and the feature is disabled' do
317
+ let(:default) { :disabled }
318
+
319
+ it 'does not change the state of the feature' do
320
+ expect{subject.disable}.not_to change{subject.disabled?}.from(true)
321
+ end
322
+
323
+ it 'persists the state in the adapter' do
324
+ expect_any_instance_of(TestPersistence).to receive(:write).with(:disabled)
325
+ subject.disable
326
+ end
327
+ end
328
+ end
329
+
330
+ context 'when there is not a persistence adapter' do
331
+ let(:persistence_adapters) { [] }
332
+
333
+ context 'and the feature is disabled' do
334
+ let(:default) { :disabled }
335
+
336
+ it 'does not change the state of the feature' do
337
+ expect{subject.disable}.not_to change{subject.disabled?}.from(true)
338
+ end
339
+ end
340
+
341
+ context 'and the feature is enabled' do
342
+ let(:default) { :enabled }
343
+
344
+ it 'changes the state of the feature' do
345
+ expect{subject.disable}.to change{subject.disabled?}.from(false).to(true)
346
+ end
347
+ end
348
+ end
349
+ end
350
+ end
351
+ end
352
+ end
@@ -0,0 +1,50 @@
1
+ Feature: Persisting the feature state in a cookie
2
+
3
+ Background:
4
+ Given the host is 'www.simplybusiness.co.uk'
5
+ And the feature name is 'example_feature'
6
+ And the time is '2015-01-22 15:26:31 +0000'
7
+ And the state of the feature is 'enabled'
8
+
9
+ Scenario: The cookie should apply to the root path (/)
10
+ When I persist the feature state in a cookie
11
+ Then the cookie has the path '/'
12
+
13
+ Scenario Outline: The cookie should apply for all domains under the top-level domain (no domain for localhost or IP addresses, however)
14
+ Given the host is '<host>'
15
+ When I persist the feature state in a cookie
16
+ Then the cookie has the domain '<cookie domain>'
17
+
18
+ Examples:
19
+ | host | cookie domain |
20
+ | localhost | |
21
+ | 127.0.0.1 | |
22
+ | 192.168.2.40 | |
23
+ | www.simplybusiness.co.uk | .simplybusiness.co.uk |
24
+ | www.quote.simplybusiness.co.uk | .simplybusiness.co.uk |
25
+ | simplybusiness.co.uk | .simplybusiness.co.uk |
26
+ | www.simplybusiness.com | .simplybusiness.com |
27
+
28
+ Scenario Outline: The cookie should be named using the name of the gem and name of feature, concatenated with a dot
29
+ Given the feature name is '<feature name>'
30
+ When I persist the feature state in a cookie
31
+ Then the cookie has the name '<cookie name>'
32
+
33
+ Examples:
34
+ | feature name | cookie name |
35
+ | cool_new_feature | flip_fab.cool_new_feature |
36
+ | other_cool_new_feature | flip_fab.other_cool_new_feature |
37
+
38
+ Scenario: The cookie should expire after 1 year
39
+ When I persist the feature state in a cookie
40
+ Then the cookie expires at 'Fri, 22 Jan 2016 15:26:31 -0000'
41
+
42
+ Scenario Outline: The cookie's value should be the state of the feature
43
+ Given the state of the feature is '<feature state>'
44
+ When I persist the feature state in a cookie
45
+ Then the cookie value is '<feature state>'
46
+
47
+ Examples:
48
+ | feature state |
49
+ | enabled |
50
+ | disabled |
@@ -0,0 +1,90 @@
1
+ require 'rack'
2
+ require 'timecop'
3
+
4
+ module FlipFab
5
+ describe CookiePersistence do
6
+ let(:cookies) { }
7
+ let(:context) { TestRackContext.new cookies, 'simplybusiness.co.uk' }
8
+ before { FlipFab.define_feature :example_feature }
9
+ after { FlipFab.features.clear }
10
+ subject{ described_class.new :example_feature, context }
11
+
12
+ it 'runs the feature' do
13
+ feature
14
+ end
15
+
16
+ step 'the host is :host' do |host|
17
+ @host = host
18
+ end
19
+
20
+ step 'the feature name is :feature_name' do |feature_name|
21
+ @feature_name = feature_name
22
+ end
23
+
24
+ step 'the time is :current_time' do |current_time|
25
+ Timecop.freeze(Time.parse current_time)
26
+ end
27
+
28
+ step 'the state of the feature is :feature_state' do |feature_state|
29
+ @feature_state = feature_state
30
+ end
31
+
32
+ step 'I persist the feature state in a cookie' do
33
+ context = TestRackContext.new '', @host
34
+ (described_class.new @feature_name, context).write @feature_state
35
+ @cookie = context.response_cookies
36
+ end
37
+
38
+ step 'the cookie has the path :path' do |path|
39
+ expect(@cookie).to match(/path=#{path};/)
40
+ end
41
+
42
+ step 'the cookie has the domain :domain' do |domain|
43
+ if domain == ''
44
+ expect(@cookie).not_to match(/domain/)
45
+ else
46
+ expect(@cookie).to match(/domain=#{domain};/)
47
+ end
48
+ end
49
+
50
+ step 'the cookie has the name :name' do |name|
51
+ expect(@cookie).to match(/\A#{name}.*/)
52
+ end
53
+
54
+ step 'the cookie expires at :expiration' do |expiration|
55
+ expect(@cookie).to match(/expires=#{expiration}\Z/)
56
+ end
57
+
58
+ step 'the cookie value is :value' do |value|
59
+ expect(@cookie).to match(/\=#{value};/)
60
+ end
61
+
62
+ describe '#read' do
63
+
64
+ context 'when there is no existing cookie' do
65
+ let(:cookies) { }
66
+
67
+ it 'returns nil' do
68
+ expect(subject.read).to be_nil
69
+ end
70
+ end
71
+
72
+ context 'when the feature state is defined in the cookie' do
73
+ let(:cookies) { 'flip_fab.example_feature=enabled' }
74
+
75
+ it 'returns the feature state' do
76
+ expect(subject.read).to eq(:enabled)
77
+ end
78
+ end
79
+ end
80
+
81
+ describe '#write' do
82
+ before { Timecop.freeze(Time.local(1990)) }
83
+ after { Timecop.return }
84
+
85
+ it 'saves the feature state' do
86
+ expect{ subject.write :enabled }.to change{ context.response_cookies }.from(nil).to('flip_fab.example_feature=enabled; domain=.simplybusiness.co.uk; path=/; expires=Tue, 01 Jan 1991 00:00:00 -0000')
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,86 @@
1
+ module FlipFab
2
+ describe Feature do
3
+ let(:name) { :example_test }
4
+ let(:options) {{ default: :enabled, persistence_adapters: [] }}
5
+ subject { described_class.new name, options }
6
+
7
+ describe '.new' do
8
+
9
+ it 'assigns the name' do
10
+ expect(subject.name).to eq(:example_test)
11
+ end
12
+
13
+ it 'assigns the default' do
14
+ expect(subject.default).to eq(:enabled)
15
+ end
16
+
17
+ it 'assigns the persistence adapters' do
18
+ expect(subject.persistence_adapters).to eq([])
19
+ end
20
+
21
+ context 'when the default is not provided' do
22
+ let(:options) { {} }
23
+
24
+ it 'assigns the default to :disabled' do
25
+ expect(subject.default).to eq(:disabled)
26
+ end
27
+ end
28
+
29
+ context 'when the persistence adapters are not provided' do
30
+ let(:options) { {} }
31
+
32
+ it 'uses a cookie adapter' do
33
+ expect(subject.persistence_adapters).to eq([CookiePersistence])
34
+ end
35
+ end
36
+ end
37
+
38
+ describe '#enabled?' do
39
+
40
+ context 'when the feature is enabled' do
41
+ let(:options) {{ default: :enabled, persistence_adapters: [] }}
42
+
43
+ it 'returns true' do
44
+ expect(subject.enabled?).to be_truthy
45
+ end
46
+ end
47
+
48
+ context 'when the feature is disabled' do
49
+ let(:options) {{ default: :disabled, persistence_adapters: [] }}
50
+
51
+ it 'returns false' do
52
+ expect(subject.enabled?).to be_falsey
53
+ end
54
+ end
55
+ end
56
+
57
+ describe '#disabled?' do
58
+
59
+ context 'when the feature is disabled' do
60
+ let(:options) {{ default: :disabled, persistence_adapters: [] }}
61
+
62
+ it 'returns true' do
63
+ expect(subject.disabled?).to be_truthy
64
+ end
65
+ end
66
+
67
+ context 'when the feature is enabled' do
68
+ let(:options) {{ default: :enabled, persistence_adapters: [] }}
69
+
70
+ it 'returns false' do
71
+ expect(subject.disabled?).to be_falsey
72
+ end
73
+ end
74
+ end
75
+
76
+ describe '#with_context' do
77
+ let(:context) { double(:context) }
78
+
79
+ it 'returns a contextual feature' do
80
+ expect(subject.with_context context).to be_a ContextualFeature
81
+ expect((subject.with_context context).feature).to eq(subject)
82
+ expect((subject.with_context context).context).to eq(context)
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,34 @@
1
+ module FlipFab
2
+ describe FeaturesByName do
3
+ let(:feature) { Feature.new :example_feature }
4
+ let(:features) { { example_feature: feature } }
5
+ subject{ described_class.new features }
6
+
7
+ describe '#[]' do
8
+
9
+ context 'when the feature exists' do
10
+
11
+ it 'returns the feature' do
12
+ expect(subject[:example_feature]).to eq(feature)
13
+ end
14
+ end
15
+
16
+ context 'when the feature does not exist' do
17
+
18
+ it 'raises' do
19
+ expect{ subject[:no_feature] }.to raise_error 'no feature has been defined with the name: no_feature'
20
+ end
21
+ end
22
+ end
23
+
24
+ describe '#with_context' do
25
+ let(:context) { double(:context) }
26
+
27
+ it 'returns contextual features by name' do
28
+ expect(subject.with_context context).to be_a described_class
29
+ expect((subject.with_context context)[:example_feature]).to be_a ContextualFeature
30
+ expect((subject.with_context context)[:example_feature].feature).to eq(feature)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,31 @@
1
+ Feature: Feature flipping in the context of a request, session, etc.
2
+
3
+ Scenario Outline: A feature can be flipped and remains flipped in the provided context
4
+ Given there is a feature with a default state of '<default_state>'
5
+ And there are two contexts
6
+ Then the feature is '<default_state>' in the first context, '<default_state>' in the second context
7
+ When I 'enable' the feature in the first context
8
+ Then the feature is 'enabled' in the first context, '<default_state>' in the second context
9
+ When I 'disable' the feature in the first context
10
+ Then the feature is 'disabled' in the first context, '<default_state>' in the second context
11
+
12
+ Examples:
13
+ | default_state |
14
+ | enabled |
15
+ | disabled |
16
+
17
+ Scenario Outline: A feature's state can be set in the URL parameters and remains in that state for that user
18
+ Given there is a feature with a default state of '<default_state>' with cookie persistence
19
+ When I override the state in the URL parameters with '<overridden_state>'
20
+ Then the feature is '<overridden_state>' for the user
21
+ When I 'enable' the feature for the user
22
+ Then the feature is '<overridden_state>' for the user
23
+ When I 'disable' the feature for the user
24
+ Then the feature is '<overridden_state>' for the user
25
+
26
+ Examples:
27
+ | default_state | overridden_state |
28
+ | enabled | enabled |
29
+ | disabled | disabled |
30
+ | enabled | disabled |
31
+ | disabled | enabled |