pillowfort 0.1.2 → 0.2.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 (33) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +10 -0
  3. data/app/controllers/pillowfort/concerns/controller_activation.rb +27 -0
  4. data/app/controllers/pillowfort/concerns/controller_authentication.rb +2 -9
  5. data/app/models/pillowfort/concerns/model_activation.rb +84 -0
  6. data/app/models/pillowfort/concerns/model_authentication.rb +10 -25
  7. data/app/models/pillowfort/concerns/model_password_reset.rb +62 -0
  8. data/lib/pillowfort/controller_methods.rb +12 -0
  9. data/lib/pillowfort/model_finder.rb +7 -0
  10. data/lib/pillowfort/token_generator.rb +19 -0
  11. data/lib/pillowfort/version.rb +1 -1
  12. data/spec/{dummy/spec/controllers → controllers}/accounts_controller_spec.rb +19 -1
  13. data/spec/dummy/app/controllers/accounts_controller.rb +2 -0
  14. data/spec/dummy/app/models/account.rb +2 -0
  15. data/spec/dummy/config/database.yml +5 -1
  16. data/spec/dummy/config/environments/development.rb +42 -0
  17. data/spec/dummy/db/development.sqlite3 +0 -0
  18. data/spec/dummy/db/migrate/20150210215727_add_password_reset_tokens.rb +8 -0
  19. data/spec/dummy/db/migrate/20150211185152_add_activation_token_to_account.rb +9 -0
  20. data/spec/dummy/db/migrate/20150413161345_add_auth_token_ttl_to_account.rb +7 -0
  21. data/spec/dummy/db/schema.rb +9 -3
  22. data/spec/dummy/db/test.sqlite3 +0 -0
  23. data/spec/dummy/log/test.log +15233 -1641
  24. data/spec/dummy/spec/spec_helper.rb +1 -10
  25. data/spec/factories/accounts.rb +19 -0
  26. data/spec/models/account_spec.rb +531 -0
  27. data/spec/{dummy/spec/rails_helper.rb → rails_helper.rb} +1 -1
  28. data/spec/spec_helper.rb +25 -0
  29. data/spec/{dummy/spec/support → support}/helpers/authentication_helper.rb +0 -0
  30. metadata +62 -17
  31. data/spec/dummy/log/development.log +0 -0
  32. data/spec/dummy/spec/factories/accounts.rb +0 -10
  33. data/spec/dummy/spec/models/account_spec.rb +0 -276
@@ -15,26 +15,17 @@
15
15
  #
16
16
  # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
17
17
  RSpec.configure do |config|
18
+ require 'rspec/its'
18
19
  # rspec-expectations config goes here. You can use an alternate
19
20
  # assertion/expectation library such as wrong or the stdlib/minitest
20
21
  # assertions if you prefer.
21
22
  config.expect_with :rspec do |expectations|
22
- # This option will default to `true` in RSpec 4. It makes the `description`
23
- # and `failure_message` of custom matchers include text for helper methods
24
- # defined using `chain`, e.g.:
25
- # be_bigger_than(2).and_smaller_than(4).description
26
- # # => "be bigger than 2 and smaller than 4"
27
- # ...rather than:
28
- # # => "be bigger than 2"
29
23
  expectations.include_chain_clauses_in_custom_matcher_descriptions = true
30
24
  end
31
25
 
32
26
  # rspec-mocks config goes here. You can use an alternate test double
33
27
  # library (such as bogus or mocha) by changing the `mock_with` option here.
34
28
  config.mock_with :rspec do |mocks|
35
- # Prevents you from mocking or stubbing a method that does not exist on
36
- # a real object. This is generally recommended, and will default to
37
- # `true` in RSpec 4.
38
29
  mocks.verify_partial_doubles = true
39
30
  end
40
31
 
@@ -0,0 +1,19 @@
1
+ FactoryGirl.define do
2
+ sequence :email do |n|
3
+ "foo.bar.#{n}@baz.org"
4
+ end
5
+
6
+ factory :account do
7
+ email
8
+ password { "SuperSafe123" }
9
+ activation_token { "thisismytoken" }
10
+ activation_token_expires_at { 1.hour.from_now }
11
+ activated_at nil
12
+
13
+ trait :activated do
14
+ activation_token nil
15
+ activation_token_expires_at nil
16
+ activated_at { Time.now }
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,531 @@
1
+ require 'rails_helper'
2
+
3
+ # ------------------------------------------------------------------------------
4
+ # Shared Examples
5
+ # ------------------------------------------------------------------------------
6
+
7
+ RSpec.shared_examples 'an auth token resetter' do
8
+ describe 'its affect on the auth_token' do
9
+ subject { account.auth_token }
10
+
11
+ describe 'before the call' do
12
+ it { should eq(auth_token) }
13
+ end
14
+
15
+ describe 'after the call' do
16
+ before { call_the_method }
17
+ it { should_not eq(auth_token) }
18
+ end
19
+ end
20
+
21
+ describe 'its affect on the auth_token_expires_at' do
22
+ subject { account.auth_token_expires_at }
23
+
24
+ describe 'before the call' do
25
+ it { should eq(auth_token_expires_at) }
26
+ end
27
+
28
+ describe 'after the call' do
29
+ before { call_the_method }
30
+ it { should be > auth_token_expires_at }
31
+ end
32
+ end
33
+ end
34
+
35
+ # ------------------------------------------------------------------------------
36
+ # The Spec!
37
+ # ------------------------------------------------------------------------------
38
+
39
+ RSpec.describe Account, :type => :model do
40
+
41
+ describe 'its validations' do
42
+ before { account.save }
43
+ subject { account.errors.messages }
44
+
45
+ describe 'email validations' do
46
+ let(:account) { FactoryGirl.build(:account, email: email) }
47
+
48
+ context 'presence_of' do
49
+ let(:email) { nil }
50
+
51
+ it { should include(email: ["can't be blank"]) }
52
+ end
53
+
54
+ context 'uniqueness' do
55
+ let(:email) { 'foobar@baz.com' }
56
+ let(:dup_account) { FactoryGirl.build(:account, email: email) }
57
+ before { dup_account.save }
58
+ subject { dup_account.errors.messages}
59
+
60
+ it { should include(email: ["has already been taken"]) }
61
+ end
62
+ end
63
+
64
+ describe 'password validations' do
65
+ let(:account) { FactoryGirl.build(:account, password: password) }
66
+
67
+ context 'presence_of' do
68
+ let(:password) { nil }
69
+
70
+ it { should include(password: [/can't be blank/]) }
71
+ end
72
+
73
+ context 'length of' do
74
+ context "when it's too short" do
75
+ let(:password) { "x"*3 }
76
+
77
+ it { should include(password: [/is too short/])}
78
+ end
79
+
80
+ context "when it's too long" do
81
+ let(:password) { "x"*80 }
82
+
83
+ it { should include(password: [/is too long/])}
84
+ end
85
+
86
+ context "when the record is persisted" do
87
+ let(:password) { 'foobarbaz' }
88
+ before { account.save }
89
+
90
+ context "when the password is updated with a nil value" do
91
+ before {account.update_attribute :password, nil }
92
+ it { should be_empty }
93
+ end
94
+
95
+ context "when the password is updated with a short value" do
96
+ before {account.update_attributes password: '3' }
97
+ it { should include(password: [/is too short/]) }
98
+ end
99
+
100
+ context "when the password is updated with a short value" do
101
+ before {account.update_attributes password: "x"*80 }
102
+ it { should include(password: [/is too long/]) }
103
+ end
104
+ end
105
+ end
106
+
107
+ describe 'activation validations' do
108
+ let(:activation_token) { "my_token" }
109
+ let(:activation_token_expires_at) { 1.hour.from_now }
110
+ let(:activated_at) { nil }
111
+
112
+ let(:account) {
113
+ FactoryGirl.build(:account,
114
+ activation_token: activation_token,
115
+ activation_token_expires_at: activation_token_expires_at,
116
+ activated_at: activated_at
117
+ )
118
+ }
119
+
120
+ before { account.save }
121
+ subject { account.errors.messages }
122
+
123
+ it { should be_empty }
124
+
125
+ context 'all fields set' do
126
+ let(:activated_at) { 1.day.ago }
127
+
128
+ it { should include( activation_token_expires_at: [/must be blank/] ) }
129
+ it { should include( activated_at: [/must be blank/] ) }
130
+ end
131
+
132
+ context 'no fields set' do
133
+ let(:activation_token) { nil }
134
+ let(:activation_token_expires_at) { nil }
135
+
136
+ it { should include( activation_token: [/can't be blank/] ) }
137
+ it { should include( activated_at: [/can't be blank/] ) }
138
+ end
139
+
140
+ context 'duplicate activation token' do
141
+ let(:dup_account) {
142
+ FactoryGirl.build(:account, activation_token: activation_token)
143
+ }
144
+ before { dup_account.save }
145
+ subject { dup_account.errors.messages }
146
+
147
+ it { should include activation_token: [/has already been taken/] }
148
+ end
149
+
150
+ context 'account activated under the old rules...' do
151
+ let(:activation_token) { nil }
152
+ let(:activated_at) { 1.day.ago }
153
+
154
+ it { should_not have_key :activation_token }
155
+ end
156
+ end
157
+ end
158
+ end
159
+
160
+ describe 'the instance methods' do
161
+ let(:account) {
162
+ FactoryGirl.create :account,
163
+ auth_token: auth_token,
164
+ auth_token_expires_at: auth_token_expires_at,
165
+ auth_token_ttl: auth_token_ttl,
166
+ password_reset_token: password_reset_token,
167
+ password_reset_token_expires_at: password_reset_token_expires_at,
168
+ activation_token: activation_token,
169
+ activation_token_expires_at: activation_token_expires_at
170
+ }
171
+
172
+ let(:auth_token) { 'abc123def456' }
173
+ let(:auth_token_expires_at) { 1.day.from_now }
174
+ let(:auth_token_ttl) { 1.hour }
175
+ let(:password_reset_token) { '123abc456def' }
176
+ let(:password_reset_token_expires_at) { 1.hour.from_now }
177
+ let(:activation_token) { 'activateme' }
178
+ let(:activation_token_expires_at) { 1.hour.from_now }
179
+
180
+ describe '#ensure_auth_token' do
181
+ subject { account.auth_token }
182
+ before { account.ensure_auth_token }
183
+
184
+ context 'when the token is nil' do
185
+ let(:auth_token) { nil }
186
+ it { should_not be_nil }
187
+ end
188
+
189
+ context 'when the token is not nil' do
190
+ let(:auth_token) { 'deadbeef' }
191
+ it { should eq('deadbeef') }
192
+ end
193
+ end
194
+
195
+ describe '#reset_auth_token' do
196
+ let(:call_the_method) { account.reset_auth_token }
197
+ it_behaves_like 'an auth token resetter'
198
+
199
+ describe 'its persistence' do
200
+ subject { account }
201
+ after { call_the_method }
202
+ it { should_not receive(:save) }
203
+ end
204
+ end
205
+
206
+ describe '#reset_auth_token!' do
207
+ let(:call_the_method) { account.reset_auth_token! }
208
+ it_behaves_like 'an auth token resetter'
209
+
210
+ describe 'its persistence' do
211
+ subject { account }
212
+ after { call_the_method }
213
+ it { should receive(:save) }
214
+ end
215
+ end
216
+
217
+ describe '#auth_token_expired?' do
218
+ subject { account.auth_token_expired? }
219
+
220
+ context 'when the token expiration is in the future' do
221
+ let(:auth_token_expires_at) { 1.minute.from_now }
222
+ it { should be_falsey }
223
+ end
224
+
225
+ context 'when the token expiration is in the past' do
226
+ let(:auth_token_expires_at) { 1.minute.ago }
227
+ it { should be_truthy }
228
+ end
229
+ end
230
+
231
+ describe '#password=' do
232
+ let!(:current_password) { account.password.to_s }
233
+ subject { account.password.to_s }
234
+
235
+ describe 'before the call' do
236
+ it { should == (current_password) }
237
+ end
238
+
239
+ describe 'after the call' do
240
+ before { account.password = 'fudge_knuckles_45' }
241
+ it { should_not eq(current_password) }
242
+ end
243
+ end
244
+
245
+ # ------------------------------------------------------------------------
246
+ # Password reset tokens
247
+ # ------------------------------------------------------------------------
248
+ describe '#password_token_expired?' do
249
+ subject { account.password_token_expired? }
250
+ describe 'an unexpired token' do
251
+ it { should be_falsey }
252
+ end
253
+
254
+ describe 'an expired token' do
255
+ let(:password_reset_token_expires_at) { 1.hour.ago }
256
+ it { should be_truthy }
257
+ end
258
+ end
259
+
260
+ shared_examples_for 'password token creator' do
261
+ describe '#password_reset_token' do
262
+ subject { account.password_reset_token }
263
+ it { should_not eq(password_reset_token) }
264
+ end
265
+
266
+ describe '#password_reset_token_expires_at' do
267
+ subject { account.password_reset_token_expires_at }
268
+ it { should_not eq(password_reset_token_expires_at) }
269
+ end
270
+ end
271
+
272
+ describe '#create_password_reset_token with default expiration time' do
273
+ before { account.create_password_reset_token }
274
+ it_behaves_like 'password token creator'
275
+
276
+ describe '#password_reset_token_expires_at' do
277
+ subject { account.password_reset_token_expires_at }
278
+ it { should be_within(5.seconds).of 1.hour.from_now }
279
+ end
280
+ end
281
+
282
+ describe '#create_password_reset_token with specific expiration time' do
283
+ before { account.create_password_reset_token(expiry: 10.minutes.from_now) }
284
+ it_behaves_like 'password token creator'
285
+ describe '#password_reset_token_expires_at' do
286
+ subject { account.password_reset_token_expires_at }
287
+ it {should be_within(5.seconds).of 10.minutes.from_now }
288
+ end
289
+ end
290
+
291
+ describe '#clear_password_reset_token' do
292
+ before { account.clear_password_reset_token }
293
+ subject { account }
294
+ its(:password_reset_token) { should be_blank }
295
+ its(:password_reset_token_expires_at) { should be_blank }
296
+ its(:password_token_expired?) { should be_truthy }
297
+ end
298
+
299
+ # ------------------------------------------------------------------------
300
+ # Activation
301
+ # ------------------------------------------------------------------------
302
+
303
+ describe '#actived?' do
304
+ subject { account }
305
+ it { should_not be_activated }
306
+ its(:activated_at) { should be_blank }
307
+ its(:activation_token) { should eq(activation_token) }
308
+ its(:activation_token_expires_at) { should eq(activation_token_expires_at) }
309
+ its(:activation_token_expired?) { should be_falsey }
310
+
311
+ context 'already activated' do
312
+ before do
313
+ subject.activate!
314
+ end
315
+
316
+ it { should be_activated }
317
+ its(:activated_at) { should be_within(5.seconds).of Time.now }
318
+ its(:activation_token) { should_not be_blank }
319
+ its(:activation_token_expires_at) { should be_blank }
320
+ its(:activation_token_expired?) { should be_truthy }
321
+ end
322
+ end
323
+
324
+ describe '#create_activation_token' do
325
+ subject { account }
326
+ before { subject.create_activation_token }
327
+
328
+ its(:activation_token) { should_not be_blank }
329
+ its(:activation_token_expires_at) { should be_within(5.seconds).of 1.hour.from_now }
330
+ its(:activation_token_expired?) { should be_falsey }
331
+
332
+ context 'with specific expiration' do
333
+ before { subject.create_activation_token(expiry: 5.minutes.from_now) }
334
+ its(:activation_token_expires_at) { should be_within(5.seconds).of 5.minutes.from_now }
335
+ its(:activation_token_expired?) { should be_falsey }
336
+ end
337
+
338
+ context 'with expiration in the past' do
339
+ before { subject.create_activation_token(expiry: 10.minutes.ago) }
340
+ its(:activation_token_expired?) { should be_truthy }
341
+ end
342
+ end
343
+ end
344
+
345
+ describe 'the class methods' do
346
+ let(:email) { 'foobar@baz.com' }
347
+ let(:token) { 'deadbeef' }
348
+ let(:password) { 'admin4lolz' }
349
+ let(:auth_token_expires_at) { 1.day.from_now }
350
+ let(:auth_token_ttl) { 1.day }
351
+ let(:activation_token) { 'activateme' }
352
+ let(:activation_token_expires_at) { 1.hour.from_now }
353
+ let(:password_reset_token) { 'resetme' }
354
+ let(:password_reset_token_expires_at) { 1.hour.from_now }
355
+
356
+ let!(:account) {
357
+ FactoryGirl.create :account,
358
+ email: email,
359
+ auth_token: token,
360
+ password: password,
361
+ auth_token_expires_at: auth_token_expires_at,
362
+ auth_token_ttl: auth_token_ttl,
363
+ password_reset_token: password_reset_token,
364
+ password_reset_token_expires_at: password_reset_token_expires_at,
365
+ activation_token: activation_token,
366
+ activation_token_expires_at: activation_token_expires_at
367
+ }
368
+
369
+ describe '.authenticate_securely' do
370
+ let(:email_param) { email }
371
+ let(:token_param) { token }
372
+ let(:block) { ->(resource) {} }
373
+
374
+ subject { Account.authenticate_securely(email_param, token_param, &block) }
375
+
376
+ context 'when email is nil' do
377
+ let(:email_param) { nil }
378
+ it { should be_falsey }
379
+ end
380
+
381
+ context 'when token is nil' do
382
+ let(:token_param) { nil }
383
+ it { should be_falsey }
384
+ end
385
+
386
+ context 'when email and token are provided' do
387
+
388
+ context 'email case-sensitivity' do
389
+ describe 'when an uppercased email address is provided' do
390
+ let(:email_param) { email.upcase }
391
+
392
+ it 'should yield the matched account' do
393
+ expect { |b| Account.authenticate_securely(email_param, token_param, &b) }.to yield_with_args(account)
394
+ end
395
+ end
396
+
397
+ describe 'when a downcased email address is provided' do
398
+ let(:email_param) { email.downcase }
399
+
400
+ it 'should yield the matched account' do
401
+ expect { |b| Account.authenticate_securely(email_param, token_param, &b) }.to yield_with_args(account)
402
+ end
403
+ end
404
+
405
+ context 'when the resource has a longer auth token ttl date than usual...' do
406
+ let(:now) { Time.now }
407
+ let(:auth_token_ttl) { 1.week }
408
+ before { Account.authenticate_securely(email_param, token_param, &block) }
409
+ subject { account.reload }
410
+
411
+ its(:auth_token_expires_at) { should be_within(2.seconds).of(now + auth_token_ttl)}
412
+ end
413
+ end
414
+
415
+ context 'when the resource is located' do
416
+
417
+ context 'when the auth_token is expired' do
418
+ let(:auth_token_expires_at) { 1.week.ago }
419
+
420
+ it 'should reset the account auth_token' do
421
+ allow(Account).to receive(:find_by_email_case_insensitive) { account }
422
+ expect(account).to receive(:reset_auth_token!)
423
+ subject
424
+ end
425
+
426
+ it { should be_falsey }
427
+ end
428
+
429
+ context 'when the auth_token is current' do
430
+
431
+ context 'when the auth_token matches' do
432
+ it 'should yield the matched account' do
433
+ expect { |b| Account.authenticate_securely(email_param, token_param, &b) }.to yield_with_args(account)
434
+ end
435
+ end
436
+
437
+ context 'when the auth_token does not match' do
438
+ it { should be_falsey }
439
+ end
440
+ end
441
+ end
442
+
443
+ context 'when the resource is not located' do
444
+ it { should be_falsey }
445
+ end
446
+
447
+ end
448
+ end
449
+
450
+ describe '.find_and_activate' do
451
+ let(:email_param) { email }
452
+ let(:token_param) { activation_token }
453
+ let(:block) { ->(resource) {} }
454
+
455
+ subject { Account.find_and_activate(email_param, token_param) }
456
+
457
+ context 'when the resource is located' do
458
+ context 'when the token matches' do
459
+ it 'should activate the matched account' do
460
+ expect_any_instance_of(Account).to receive(:activate!)
461
+ subject
462
+ end
463
+
464
+ it 'should yield the matched account' do
465
+ expect { |b| Account.find_and_activate(email_param, token_param, &b) }.to yield_with_args(account)
466
+ end
467
+
468
+ context "when the activation_token is expired" do
469
+ let(:activation_token_expires_at) { 1.day.ago }
470
+ it { should be_falsey }
471
+ end
472
+ end
473
+
474
+ context "when the activation_token doesn't match" do
475
+ let(:token_param) { 'notmytoken' }
476
+ it { should be_falsey }
477
+ end
478
+ end
479
+
480
+ context "when the email doesn't match" do
481
+ let(:email_param) { 'notmyemail@gmail.com' }
482
+ it { should be_falsey }
483
+ end
484
+ end
485
+
486
+ describe '.find_and_validate_password_reset_token' do
487
+ let(:email_param) { email }
488
+ let(:token_param) { password_reset_token }
489
+ subject { Account.find_and_validate_password_reset_token(email_param, token_param) }
490
+
491
+ context "when the email doesn't match" do
492
+ let(:email_param) { "bad_actor@gmail.com" }
493
+ it { should be_falsey }
494
+ end
495
+
496
+ context "when the token doesn't match" do
497
+ let(:token_param) { 'notmytoken' }
498
+ it { should be_falsey }
499
+ end
500
+
501
+ it 'should yield the matched account' do
502
+ expect { |b| Account.find_and_validate_password_reset_token(email_param, token_param, &b) }.to yield_with_args(account)
503
+ end
504
+ end
505
+
506
+ describe '.find_and_authenticate' do
507
+ let(:email_param) { email }
508
+ let(:password_param) { password }
509
+
510
+ subject { Account.find_and_authenticate(email_param, password_param) }
511
+
512
+
513
+ context 'when the resource is located' do
514
+
515
+ context 'when the password matches' do
516
+ it { should eq(account) }
517
+ end
518
+
519
+ context 'when the password does not match' do
520
+ let(:password_param) { "#{password}_bad" }
521
+ it { should be_falsey }
522
+ end
523
+ end
524
+
525
+ context 'when the resource is not located' do
526
+ let(:email_param) { "#{email}_evil" }
527
+ it { should be_falsey }
528
+ end
529
+ end
530
+ end
531
+ end