devise_invitable 0.1.0

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 (49) hide show
  1. data/.document +5 -0
  2. data/.gitignore +22 -0
  3. data/LICENSE +20 -0
  4. data/README.rdoc +18 -0
  5. data/Rakefile +54 -0
  6. data/VERSION +1 -0
  7. data/app/controllers/invitations_controller.rb +48 -0
  8. data/app/views/devise_mailer/invitation.html.erb +8 -0
  9. data/app/views/invitations/edit.html.erb +14 -0
  10. data/app/views/invitations/new.html.erb +10 -0
  11. data/devise_invitable.gemspec +121 -0
  12. data/init.rb +1 -0
  13. data/lib/devise/controllers/url_helpers.rb +20 -0
  14. data/lib/devise/models/invitable.rb +143 -0
  15. data/lib/devise_invitable.rb +14 -0
  16. data/lib/devise_invitable/locales/en.yml +5 -0
  17. data/lib/devise_invitable/mailer.rb +9 -0
  18. data/lib/devise_invitable/rails.rb +3 -0
  19. data/lib/devise_invitable/routes.rb +28 -0
  20. data/lib/devise_invitable/schema.rb +11 -0
  21. data/rails/init.rb +1 -0
  22. data/test/integration/invitable_test.rb +122 -0
  23. data/test/integration_tests_helper.rb +38 -0
  24. data/test/mailers/invitation_test.rb +62 -0
  25. data/test/model_tests_helper.rb +59 -0
  26. data/test/models/invitable_test.rb +164 -0
  27. data/test/models_test.rb +35 -0
  28. data/test/rails_app/app/controllers/admins_controller.rb +6 -0
  29. data/test/rails_app/app/controllers/application_controller.rb +10 -0
  30. data/test/rails_app/app/controllers/home_controller.rb +4 -0
  31. data/test/rails_app/app/controllers/users_controller.rb +12 -0
  32. data/test/rails_app/app/helpers/application_helper.rb +3 -0
  33. data/test/rails_app/app/models/user.rb +4 -0
  34. data/test/rails_app/app/views/home/index.html.erb +0 -0
  35. data/test/rails_app/config/boot.rb +110 -0
  36. data/test/rails_app/config/database.yml +22 -0
  37. data/test/rails_app/config/environment.rb +44 -0
  38. data/test/rails_app/config/environments/development.rb +17 -0
  39. data/test/rails_app/config/environments/production.rb +28 -0
  40. data/test/rails_app/config/environments/test.rb +28 -0
  41. data/test/rails_app/config/initializers/backtrace_silencers.rb +7 -0
  42. data/test/rails_app/config/initializers/inflections.rb +2 -0
  43. data/test/rails_app/config/initializers/new_rails_defaults.rb +21 -0
  44. data/test/rails_app/config/initializers/session_store.rb +15 -0
  45. data/test/rails_app/config/routes.rb +3 -0
  46. data/test/rails_app/vendor/plugins/devise_invitable/init.rb +1 -0
  47. data/test/routes_test.rb +20 -0
  48. data/test/test_helper.rb +58 -0
  49. metadata +156 -0
@@ -0,0 +1,14 @@
1
+ Devise.module_eval do
2
+ # Time interval where the invitation token is valid.
3
+ mattr_accessor :invite_for
4
+ @@invite_for = 0
5
+ end
6
+ Devise::ALL << :invitable
7
+ Devise::CONTROLLERS = Devise::CONTROLLERS.merge(:invitations => [:invitable])
8
+
9
+ module DeviseInvitable; end
10
+
11
+ require 'devise_invitable/mailer'
12
+ require 'devise_invitable/routes'
13
+ require 'devise_invitable/schema'
14
+ require 'devise_invitable/rails'
@@ -0,0 +1,5 @@
1
+ en:
2
+ devise:
3
+ invitations:
4
+ send_invitation: 'An email with instructions about how to set the password has been sent.'
5
+ updated: 'Your password was set successfully. You are now signed in.'
@@ -0,0 +1,9 @@
1
+ module DeviseInvitable
2
+ module Mailer
3
+ # Deliver an invitation when is requested
4
+ def invitation(record)
5
+ setup_mail(record, :invitation)
6
+ end
7
+ end
8
+ end
9
+ DeviseMailer.send :include, DeviseInvitable::Mailer
@@ -0,0 +1,3 @@
1
+ Rails.configuration.after_initialize do
2
+ I18n.load_path.unshift File.expand_path(File.join(File.dirname(__FILE__), 'locales', 'en.yml'))
3
+ end
@@ -0,0 +1,28 @@
1
+ module DeviseInvitable
2
+ module Routes #:doc:
3
+ # Adds route generation for invitable. This method is responsible to
4
+ # generate all needed routes for devise, based on what modules you have
5
+ # defined in your model.
6
+ # Examples: Let's say you have an User model configured to use
7
+ # invitable module. After creating this inside your routes:
8
+ #
9
+ # map.devise_for :users
10
+ #
11
+ # this method is going to look inside your User model and create the
12
+ # needed routes:
13
+ #
14
+ # # Invitation routes for Invitable, if User model has :invitable configured
15
+ # new_user_invitation GET /users/invitation/new(.:format) {:controller=>"invitations", :action=>"new"}
16
+ # edit_user_invitation GET /users/invitation/edit(.:format) {:controller=>"invitations", :action=>"edit"}
17
+ # user_invitation PUT /users/invitation(.:format) {:controller=>"invitations", :action=>"update"}
18
+ # POST /users/invitation(.:format) {:controller=>"invitations", :action=>"create"}
19
+ #
20
+
21
+ protected
22
+ def invitable(routes, mapping)
23
+ routes.resource :invitation, :only => [:new, :create, :edit, :update], :as => mapping.path_names[:invitation]
24
+ end
25
+ end
26
+ end
27
+
28
+ ActionController::Routing::RouteSet::Mapper.send :include, DeviseInvitable::Routes
@@ -0,0 +1,11 @@
1
+ module DeviseInvitable
2
+ module Schema
3
+ # Creates invitation_token and invitation_sent_at.
4
+ def invitable
5
+ apply_schema :invitation_token, String, :limit => 20
6
+ apply_schema :invitation_sent_at, DateTime
7
+ end
8
+ end
9
+ end
10
+
11
+ Devise::Schema.send :include, DeviseInvitable::Schema
@@ -0,0 +1 @@
1
+ require 'devise_invitable'
@@ -0,0 +1,122 @@
1
+ require 'test/test_helper'
2
+ require 'test/integration_tests_helper'
3
+
4
+ class InvitationTest < ActionController::IntegrationTest
5
+
6
+ def send_invitation(&block)
7
+ visit new_user_invitation_path
8
+
9
+ assert_response :success
10
+ assert_template 'invitations/new'
11
+ assert warden.authenticated?(:user)
12
+
13
+ fill_in 'email', :with => 'user@test.com'
14
+ yield if block_given?
15
+ click_button 'Send an invitation'
16
+ end
17
+
18
+ def set_password(options={}, &block)
19
+ unless options[:visit] == false
20
+ visit edit_user_invitation_path(:invitation_token => options[:invitation_token])
21
+ end
22
+ assert_response :success
23
+ assert_template 'invitations/edit'
24
+
25
+ fill_in 'Password', :with => '987654321'
26
+ fill_in 'Password confirmation', :with => '987654321'
27
+ yield if block_given?
28
+ click_button 'Set my password'
29
+ end
30
+
31
+ test 'not authenticated user should not be able to send an invitation' do
32
+ get new_user_invitation_path
33
+ assert_not warden.authenticated?(:user)
34
+
35
+ assert_redirected_to new_user_session_path(:unauthenticated => true)
36
+ end
37
+
38
+ test 'authenticated user should be able to send an invitation' do
39
+ sign_in_as_user
40
+
41
+ send_invitation
42
+ assert_template 'home/index'
43
+ assert_equal 'An email with instructions about how to set the password has been sent.', flash[:success]
44
+ end
45
+
46
+ test 'authenticated user with invalid email should receive an error message' do
47
+ user = create_user
48
+ sign_in_as_user
49
+ send_invitation do
50
+ fill_in 'email', :with => user.email
51
+ end
52
+
53
+ assert_response :success
54
+ assert_template 'invitations/new'
55
+ assert_have_selector "input[type=text][value='#{user.email}']"
56
+ assert_contain 'Email already exists'
57
+ end
58
+
59
+ test 'authenticated user should not be able to visit edit invitation page' do
60
+ sign_in_as_user
61
+
62
+ get edit_user_invitation_path
63
+
64
+ assert_response :redirect
65
+ assert_redirected_to root_path
66
+ assert warden.authenticated?(:user)
67
+ end
68
+
69
+ test 'not authenticated user with invalid invitation token should not be able to set his password' do
70
+ user = create_user
71
+ set_password :invitation_token => 'invalid_token'
72
+
73
+ assert_response :success
74
+ assert_template 'invitations/edit'
75
+ assert_have_selector '#errorExplanation'
76
+ assert_contain 'Invitation token is invalid'
77
+ assert_not user.reload.valid_password?('987654321')
78
+ end
79
+
80
+ test 'not authenticated user with valid invitation token but invalid password should not be able to set his password' do
81
+ user = create_user(false)
82
+ set_password :invitation_token => user.invitation_token do
83
+ fill_in 'Password confirmation', :with => 'other_password'
84
+ end
85
+
86
+ assert_response :success
87
+ assert_template 'invitations/edit'
88
+ assert_have_selector '#errorExplanation'
89
+ assert_contain 'Password doesn\'t match confirmation'
90
+ assert_not user.reload.valid_password?('987654321')
91
+ end
92
+
93
+ test 'not authenticated user with valid data should be able to change his password' do
94
+ user = create_user(false)
95
+ set_password :invitation_token => user.invitation_token
96
+
97
+ assert_template 'home/index'
98
+ assert_equal 'Your password was set successfully. You are now signed in.', flash[:success]
99
+ assert user.reload.valid_password?('987654321')
100
+ end
101
+
102
+ test 'after entering invalid data user should still be able to set his password' do
103
+ user = create_user(false)
104
+ set_password :invitation_token => user.invitation_token do
105
+ fill_in 'Password confirmation', :with => 'other_password'
106
+ end
107
+ assert_response :success
108
+ assert_have_selector '#errorExplanation'
109
+ assert_not user.reload.valid_password?('987654321')
110
+
111
+ set_password :invitation_token => user.invitation_token
112
+ assert_equal 'Your password was set successfully. You are now signed in.', flash[:success]
113
+ assert user.reload.valid_password?('987654321')
114
+ end
115
+
116
+ test 'sign in user automatically after setting it\'s password' do
117
+ user = create_user(false)
118
+ set_password :invitation_token => user.invitation_token
119
+
120
+ assert warden.authenticated?(:user)
121
+ end
122
+ end
@@ -0,0 +1,38 @@
1
+ class ActionController::IntegrationTest
2
+
3
+ def warden
4
+ request.env['warden']
5
+ end
6
+
7
+ def sign_in_as_user
8
+ Warden::Proxy.any_instance.stubs(:user).at_least_once.returns(User.new)
9
+ end
10
+
11
+ def create_user(accept_invitation = true)
12
+ user = User.new :email => 'newuser@test.com'
13
+ user.invitation_token = 'token'
14
+ user.invitation_sent_at = Time.now.utc
15
+ user.save(false)
16
+ user.accept_invitation! if accept_invitation
17
+ user
18
+ end
19
+
20
+ # Fix assert_redirect_to in integration sessions because they don't take into
21
+ # account Middleware redirects.
22
+ #
23
+ def assert_redirected_to(url)
24
+ assert [301, 302].include?(@integration_session.status),
25
+ "Expected status to be 301 or 302, got #{@integration_session.status}"
26
+
27
+ url = prepend_host(url)
28
+ location = prepend_host(@integration_session.headers["Location"])
29
+ assert_equal url, location
30
+ end
31
+
32
+ protected
33
+
34
+ def prepend_host(url)
35
+ url = "http://#{request.host}#{url}" if url[0] == ?/
36
+ url
37
+ end
38
+ end
@@ -0,0 +1,62 @@
1
+ require 'test/test_helper'
2
+
3
+ class InviationTest < ActionMailer::TestCase
4
+
5
+ def setup
6
+ setup_mailer
7
+ DeviseMailer.sender = 'test@example.com'
8
+ end
9
+
10
+ def user
11
+ @user ||= begin
12
+ user = create_user_with_invitation('token')
13
+ user.send_invitation
14
+ user
15
+ end
16
+ end
17
+
18
+ def mail
19
+ @mail ||= begin
20
+ user
21
+ ActionMailer::Base.deliveries.last
22
+ end
23
+ end
24
+
25
+ test 'email sent after reseting the user password' do
26
+ assert_not_nil mail
27
+ end
28
+
29
+ test 'content type should be set to html' do
30
+ assert_equal 'text/html', mail.content_type
31
+ end
32
+
33
+ test 'send invitation to the user email' do
34
+ assert_equal [user.email], mail.to
35
+ end
36
+
37
+ test 'setup sender from configuration' do
38
+ assert_equal ['test@example.com'], mail.from
39
+ end
40
+
41
+ test 'setup subject from I18n' do
42
+ store_translations :en, :devise => { :mailer => { :invitation => 'Invitation' } } do
43
+ assert_equal 'Invitation', mail.subject
44
+ end
45
+ end
46
+
47
+ test 'subject namespaced by model' do
48
+ store_translations :en, :devise => { :mailer => { :user => { :invitation => 'User Invitation' } } } do
49
+ assert_equal 'User Invitation', mail.subject
50
+ end
51
+ end
52
+
53
+ test 'body should have user info' do
54
+ assert_match /#{user.email}/, mail.body
55
+ end
56
+
57
+ test 'body should have link to confirm the account' do
58
+ host = ActionMailer::Base.default_url_options[:host]
59
+ invitation_url_regexp = %r{<a href=\"http://#{host}/users/invitation/edit\?invitation_token=#{user.invitation_token}">}
60
+ assert_match invitation_url_regexp, mail.body
61
+ end
62
+ end
@@ -0,0 +1,59 @@
1
+ class ActiveSupport::TestCase
2
+ def setup_mailer
3
+ ActionMailer::Base.deliveries = []
4
+ end
5
+
6
+ def store_translations(locale, translations, &block)
7
+ begin
8
+ I18n.backend.store_translations locale, translations
9
+ yield
10
+ ensure
11
+ I18n.reload!
12
+ end
13
+ end
14
+
15
+ # Helpers for creating new users
16
+ #
17
+ def generate_unique_email
18
+ @@email_count ||= 0
19
+ @@email_count += 1
20
+ "test#{@@email_count}@email.com"
21
+ end
22
+
23
+ def valid_attributes(attributes={})
24
+ { :email => generate_unique_email,
25
+ :password => '123456',
26
+ :password_confirmation => '123456' }.update(attributes)
27
+ end
28
+
29
+ def new_user(attributes={})
30
+ User.new(valid_attributes(attributes))
31
+ end
32
+
33
+ def create_user(attributes={})
34
+ User.create!(valid_attributes(attributes))
35
+ end
36
+
37
+ def create_user_with_invitation(invitation_token, attributes={})
38
+ user = new_user({:password => nil, :password_confirmation => nil}.update(attributes))
39
+ user.invitation_token = invitation_token
40
+ user.invitation_sent_at = Time.now.utc
41
+ user.save(false)
42
+ user
43
+ end
44
+
45
+ # Execute the block setting the given values and restoring old values after
46
+ # the block is executed.
47
+ def swap(object, new_values)
48
+ old_values = {}
49
+ new_values.each do |key, value|
50
+ old_values[key] = object.send key
51
+ object.send :"#{key}=", value
52
+ end
53
+ yield
54
+ ensure
55
+ old_values.each do |key, value|
56
+ object.send :"#{key}=", value
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,164 @@
1
+ require 'test/test_helper'
2
+ require 'test/model_tests_helper'
3
+
4
+ class InvitableTest < ActiveSupport::TestCase
5
+
6
+ def setup
7
+ setup_mailer
8
+ end
9
+
10
+ test 'should not generate invitation token after creating a record' do
11
+ assert_nil new_user.invitation_token
12
+ end
13
+
14
+ test 'should regenerate invitation token each time' do
15
+ user = new_user
16
+ 3.times do
17
+ token = user.invitation_token
18
+ user.reset_invitation!
19
+ assert_not_equal token, user.invitation_token
20
+ end
21
+ end
22
+
23
+ test 'should test invitation sent at with invite_for configuration value' do
24
+ user = create_user_with_invitation('')
25
+
26
+ User.stubs(:invite_for).returns(nil)
27
+ user.invitation_sent_at = Time.now.utc
28
+ assert user.valid_invitation?
29
+
30
+ User.stubs(:invite_for).returns(nil)
31
+ user.invitation_sent_at = 1.year.ago
32
+ assert user.valid_invitation?
33
+
34
+ User.stubs(:invite_for).returns(0)
35
+ user.invitation_sent_at = Time.now.utc
36
+ assert user.valid_invitation?
37
+
38
+ User.stubs(:invite_for).returns(0)
39
+ user.invitation_sent_at = 1.day.ago
40
+ assert user.valid_invitation?
41
+
42
+ User.stubs(:invite_for).returns(1.day)
43
+ user.invitation_sent_at = Time.now.utc
44
+ assert user.valid_invitation?
45
+
46
+ User.stubs(:invite_for).returns(1.day)
47
+ user.invitation_sent_at = 1.day.ago
48
+ assert_not user.valid_invitation?
49
+ end
50
+
51
+ test 'should never generate the same invitation token for different users' do
52
+ invitation_tokens = []
53
+ 3.times do
54
+ user = new_user
55
+ user.reset_invitation!
56
+ token = user.invitation_token
57
+ assert !invitation_tokens.include?(token)
58
+ invitation_tokens << token
59
+ end
60
+ end
61
+
62
+ test 'should set password and password confirmation from params' do
63
+ create_user_with_invitation('valid_token', :password => nil, :password_confirmation => nil)
64
+ user = User.accept_invitation!(:invitation_token => 'valid_token', :password => '123456789', :password_confirmation => '123456789')
65
+ assert user.valid_password?('123456789')
66
+ end
67
+
68
+ test 'should set password and save the record' do
69
+ user = create_user_with_invitation('valid_token', :password => nil, :password_confirmation => nil)
70
+ old_encrypted_password = user.encrypted_password
71
+ user = User.accept_invitation!(:invitation_token => 'valid_token', :password => '123456789', :password_confirmation => '123456789')
72
+ assert_not_equal old_encrypted_password, user.encrypted_password
73
+ end
74
+
75
+ test 'should clear invitation token while setting the password' do
76
+ user = new_user
77
+ user.update_attribute(:invitation_token, 'valid_token')
78
+ assert_present user.invitation_token
79
+ assert user.accept_invitation!
80
+ assert_nil user.invitation_token
81
+ end
82
+
83
+ test 'should not clear invitation token if record is invalid' do
84
+ user = new_user
85
+ user.update_attribute(:invitation_token, 'valid_token')
86
+ assert_present user.invitation_token
87
+ User.any_instance.stubs(:valid?).returns(false)
88
+ User.accept_invitation!(:invitation_token => 'valid_token', :password => '123456789', :password_confirmation => '987654321')
89
+ user.reload
90
+ assert_present user.invitation_token
91
+ end
92
+
93
+ test 'should reset reset password token and send invitation by email' do
94
+ user = new_user
95
+ assert_difference('ActionMailer::Base.deliveries.size') do
96
+ token = user.invitation_token
97
+ user.reset_invitation!
98
+ assert_not_equal token, user.invitation_token
99
+ end
100
+ end
101
+
102
+ test 'should return a record with invitation token and no errors to send invitation by email' do
103
+ invited_user = User.send_invitation(:email => "invalid@email.com")
104
+ assert invited_user.errors.blank?
105
+ assert_present invited_user.invitation_token
106
+ end
107
+
108
+ test 'should return a record with errors if user was found by e-mail' do
109
+ user = create_user_with_invitation('')
110
+ user.update_attribute(:invitation_token, nil)
111
+ invited_user = User.send_invitation(:email => user.email)
112
+ assert_equal invited_user, user
113
+ assert_equal 'already exists', invited_user.errors[:email]
114
+ end
115
+
116
+ test 'should return a new record with errors if e-mail is blank' do
117
+ invited_user = User.send_invitation(:email => '')
118
+ assert invited_user.new_record?
119
+ assert_equal "can't be blank", invited_user.errors[:email]
120
+ end
121
+
122
+ test 'should find a user to set his password based on invitation_token' do
123
+ user = new_user
124
+ user.reset_invitation!
125
+
126
+ invited_user = User.accept_invitation!(:invitation_token => user.invitation_token)
127
+ assert_equal invited_user, user
128
+ end
129
+
130
+ test 'should return a new record with errors if no invitation_token is found' do
131
+ invited_user = User.accept_invitation!(:invitation_token => 'invalid_token')
132
+ assert invited_user.new_record?
133
+ assert_equal 'is invalid', invited_user.errors[:invitation_token]
134
+ end
135
+
136
+ test 'should return a new record with errors if invitation_token is blank' do
137
+ invited_user = User.accept_invitation!(:invitation_token => '')
138
+ assert invited_user.new_record?
139
+ assert_equal "can't be blank", invited_user.errors[:invitation_token]
140
+ end
141
+
142
+ test 'should return record with errors if invitation_token has expired' do
143
+ user = create_user_with_invitation('valid_token')
144
+ user.update_attribute(:invitation_sent_at, 1.day.ago)
145
+ User.stubs(:invite_for).returns(10.hours)
146
+ invited_user = User.accept_invitation!(:invitation_token => 'valid_token')
147
+ assert_equal user, invited_user
148
+ assert_equal "is invalid", invited_user.errors[:invitation_token]
149
+ end
150
+
151
+ test 'should set successfully user password given the new password and confirmation' do
152
+ user = new_user(:password => nil, :password_confirmation => nil)
153
+ user.reset_invitation!
154
+
155
+ invited_user = User.accept_invitation!(
156
+ :invitation_token => user.invitation_token,
157
+ :password => 'new_password',
158
+ :password_confirmation => 'new_password'
159
+ )
160
+ user.reload
161
+
162
+ assert user.valid_password?('new_password')
163
+ end
164
+ end