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.
- checksums.yaml +7 -0
- data/.rspec +5 -0
- data/Gemfile +16 -0
- data/Gemfile.lock +92 -0
- data/LICENSE.txt +9 -0
- data/README.md +168 -0
- data/example/rails_app/.gitignore +17 -0
- data/example/rails_app/Gemfile +36 -0
- data/example/rails_app/Gemfile.lock +178 -0
- data/example/rails_app/README.rdoc +28 -0
- data/example/rails_app/Rakefile +6 -0
- data/example/rails_app/app/assets/images/.keep +0 -0
- data/example/rails_app/app/assets/images/justin_beaver.jpg +0 -0
- data/example/rails_app/app/assets/images/regular_beaver.jpg +0 -0
- data/example/rails_app/app/assets/javascripts/application.js +16 -0
- data/example/rails_app/app/assets/javascripts/beavers.js.coffee +3 -0
- data/example/rails_app/app/assets/stylesheets/application.css +15 -0
- data/example/rails_app/app/assets/stylesheets/beavers.css.scss +3 -0
- data/example/rails_app/app/assets/stylesheets/scaffolds.css.scss +69 -0
- data/example/rails_app/app/controllers/application_controller.rb +5 -0
- data/example/rails_app/app/controllers/beavers_controller.rb +74 -0
- data/example/rails_app/app/controllers/concerns/.keep +0 -0
- data/example/rails_app/app/helpers/application_helper.rb +2 -0
- data/example/rails_app/app/helpers/beavers_helper.rb +2 -0
- data/example/rails_app/app/mailers/.keep +0 -0
- data/example/rails_app/app/models/.keep +0 -0
- data/example/rails_app/app/models/beaver.rb +2 -0
- data/example/rails_app/app/models/concerns/.keep +0 -0
- data/example/rails_app/app/views/beavers/_form.html.erb +21 -0
- data/example/rails_app/app/views/beavers/edit.html.erb +6 -0
- data/example/rails_app/app/views/beavers/index.html.erb +25 -0
- data/example/rails_app/app/views/beavers/index.json.jbuilder +4 -0
- data/example/rails_app/app/views/beavers/new.html.erb +5 -0
- data/example/rails_app/app/views/beavers/show.html.erb +9 -0
- data/example/rails_app/app/views/beavers/show.json.jbuilder +1 -0
- data/example/rails_app/app/views/layouts/application.html.erb +18 -0
- data/example/rails_app/bin/bundle +3 -0
- data/example/rails_app/bin/rails +4 -0
- data/example/rails_app/bin/rake +4 -0
- data/example/rails_app/config/application.rb +25 -0
- data/example/rails_app/config/boot.rb +4 -0
- data/example/rails_app/config/database.yml +22 -0
- data/example/rails_app/config/environment.rb +5 -0
- data/example/rails_app/config/environments/development.rb +83 -0
- data/example/rails_app/config/environments/test.rb +41 -0
- data/example/rails_app/config/initializers/backtrace_silencers.rb +7 -0
- data/example/rails_app/config/initializers/cookies_serializer.rb +3 -0
- data/example/rails_app/config/initializers/filter_parameter_logging.rb +4 -0
- data/example/rails_app/config/initializers/flip_fab.rb +1 -0
- data/example/rails_app/config/initializers/inflections.rb +16 -0
- data/example/rails_app/config/initializers/mime_types.rb +4 -0
- data/example/rails_app/config/initializers/session_store.rb +3 -0
- data/example/rails_app/config/initializers/wrap_parameters.rb +14 -0
- data/example/rails_app/config/locales/en.yml +23 -0
- data/example/rails_app/config/rabbit_feed.yml +8 -0
- data/example/rails_app/config/routes.rb +58 -0
- data/example/rails_app/config/secrets.yml +18 -0
- data/example/rails_app/config/unicorn.rb +4 -0
- data/example/rails_app/config.ru +4 -0
- data/example/rails_app/db/migrate/20140424102400_create_beavers.rb +9 -0
- data/example/rails_app/db/schema.rb +22 -0
- data/example/rails_app/db/seeds.rb +7 -0
- data/example/rails_app/lib/assets/.keep +0 -0
- data/example/rails_app/lib/tasks/.keep +0 -0
- data/example/rails_app/log/.keep +0 -0
- data/example/rails_app/public/404.html +67 -0
- data/example/rails_app/public/422.html +67 -0
- data/example/rails_app/public/500.html +66 -0
- data/example/rails_app/public/favicon.ico +0 -0
- data/example/rails_app/public/robots.txt +5 -0
- data/example/rails_app/spec/rails_helper.rb +50 -0
- data/example/rails_app/spec/spec_helper.rb +8 -0
- data/example/rails_app/test/controllers/.keep +0 -0
- data/example/rails_app/test/controllers/beavers_controller_test.rb +49 -0
- data/example/rails_app/test/fixtures/.keep +0 -0
- data/example/rails_app/test/fixtures/beavers.yml +7 -0
- data/example/rails_app/test/helpers/.keep +0 -0
- data/example/rails_app/test/helpers/beavers_helper_test.rb +4 -0
- data/example/rails_app/test/integration/.keep +0 -0
- data/example/rails_app/test/mailers/.keep +0 -0
- data/example/rails_app/test/models/.keep +0 -0
- data/example/rails_app/test/models/beaver_test.rb +7 -0
- data/example/rails_app/test/test_helper.rb +13 -0
- data/flip_fab.gemspec +20 -0
- data/lib/flip_fab/contextual_feature.rb +83 -0
- data/lib/flip_fab/cookie_persistence.rb +52 -0
- data/lib/flip_fab/feature.rb +24 -0
- data/lib/flip_fab/features_by_name.rb +22 -0
- data/lib/flip_fab/helper.rb +8 -0
- data/lib/flip_fab/persistence.rb +19 -0
- data/lib/flip_fab/version.rb +3 -0
- data/lib/flip_fab.rb +24 -0
- data/spec/lib/flip_fab/contextual_feature_spec.rb +352 -0
- data/spec/lib/flip_fab/cookie_persistence.feature +50 -0
- data/spec/lib/flip_fab/cookie_persistence_spec.rb +90 -0
- data/spec/lib/flip_fab/feature_spec.rb +86 -0
- data/spec/lib/flip_fab/features_by_name_spec.rb +34 -0
- data/spec/lib/flip_fab/helper.feature +31 -0
- data/spec/lib/flip_fab/helper_spec.rb +90 -0
- data/spec/lib/flip_fab/persistence_spec.rb +32 -0
- data/spec/lib/flip_fab.feature +10 -0
- data/spec/lib/flip_fab_spec.rb +47 -0
- data/spec/spec_helper.rb +93 -0
- data/spec/support/test_app.rb +16 -0
- data/spec/support/test_context.rb +10 -0
- data/spec/support/test_multiple_persistence.rb +14 -0
- data/spec/support/test_persistence.rb +14 -0
- data/spec/support/test_rack_context.rb +15 -0
- 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 |
|