pillowfort 0.1.2 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
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