devise_invitable 3.0.3 → 3.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +23 -1
- data/README.md +259 -23
- data/app/controllers/devise/invitations_controller.rb +2 -1
- data/app/controllers/devise_invitable/registrations_controller.rb +1 -1
- data/lib/devise_invitable/acceptance_token.rb +58 -0
- data/lib/devise_invitable/invitation.rb +33 -6
- data/lib/devise_invitable/invitation_delivery.rb +114 -0
- data/lib/devise_invitable/invitation_delivery_job.rb +15 -0
- data/lib/devise_invitable/models.rb +217 -47
- data/lib/devise_invitable/version.rb +1 -1
- data/lib/devise_invitable.rb +42 -2
- data/lib/generators/devise_invitable/install_generator.rb +21 -0
- data/test/integration/invitation_test.rb +26 -0
- data/test/jobs/invitation_delivery_job_test.rb +138 -0
- data/test/lazy_loading_test.rb +17 -0
- data/test/mailers/invitation_mail_test.rb +9 -5
- data/test/models/invitable_test.rb +72 -8
- data/test/models/separate_invitations_test.rb +181 -3
- data/test/models_test.rb +17 -0
- data/test/rails_app/app/models/user.rb +4 -1
- data/test/rails_app/db/migrate/20261103170000_add_options_to_users.rb +5 -0
- metadata +10 -1
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
require 'test_helper'
|
|
2
|
+
require 'model_tests_helper'
|
|
3
|
+
|
|
4
|
+
class InvitationDeliveryJobTest < ActiveSupport::TestCase
|
|
5
|
+
setup do
|
|
6
|
+
@invitation_delivery_job = Devise.invitation_delivery_job
|
|
7
|
+
@invitation_delivery_queue_name = Devise.invitation_delivery_queue_name
|
|
8
|
+
@create_resource_on_invitation = User.create_resource_on_invitation
|
|
9
|
+
@store_invite_keys_until_delivery = User.store_invite_keys_until_delivery
|
|
10
|
+
@deliver_invitation_later = User.deliver_invitation_later
|
|
11
|
+
User.create_resource_on_invitation = false
|
|
12
|
+
User.deliver_invitation_later = false
|
|
13
|
+
DeviseInvitable::Invitation.delete_all
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
teardown do
|
|
17
|
+
Devise.invitation_delivery_job = @invitation_delivery_job
|
|
18
|
+
Devise.invitation_delivery_queue_name = @invitation_delivery_queue_name
|
|
19
|
+
User.create_resource_on_invitation = @create_resource_on_invitation
|
|
20
|
+
User.store_invite_keys_until_delivery = @store_invite_keys_until_delivery
|
|
21
|
+
User.deliver_invitation_later = @deliver_invitation_later
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
test 'deliver_later passes a persisted invitation and acceptance token to the configured job' do
|
|
25
|
+
job_class = Class.new do
|
|
26
|
+
class << self
|
|
27
|
+
attr_reader :arguments
|
|
28
|
+
|
|
29
|
+
def perform_later(*arguments)
|
|
30
|
+
@arguments = arguments
|
|
31
|
+
:enqueued
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
Devise.invitation_delivery_job = job_class
|
|
36
|
+
|
|
37
|
+
user = User.invite!(email: 'queued@example.com', skip_invitation: true)
|
|
38
|
+
invitation = user.current_invitation
|
|
39
|
+
|
|
40
|
+
assert_equal :enqueued, invitation.deliver_later
|
|
41
|
+
enqueued_invitation, acceptance_token, options = job_class.arguments
|
|
42
|
+
assert_equal invitation, enqueued_invitation
|
|
43
|
+
assert_equal({}, options)
|
|
44
|
+
assert_equal 'queued@example.com',
|
|
45
|
+
DeviseInvitable::AcceptanceToken.decode(acceptance_token).dig(:invite_key, :email)
|
|
46
|
+
|
|
47
|
+
assert_difference('ActionMailer::Base.deliveries.size', 1) do
|
|
48
|
+
DeviseInvitable::InvitationDeliveryJob.perform_now(
|
|
49
|
+
DeviseInvitable::Invitation.find(invitation.id),
|
|
50
|
+
acceptance_token,
|
|
51
|
+
options
|
|
52
|
+
)
|
|
53
|
+
end
|
|
54
|
+
assert_predicate invitation.reload, :invitation_sent_at
|
|
55
|
+
assert_equal ['queued@example.com'], ActionMailer::Base.deliveries.last.to
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
test 'the default job delivers a reloaded resource-less invitation from stored keys' do
|
|
59
|
+
User.store_invite_keys_until_delivery = true
|
|
60
|
+
user = User.invite!(email: 'stored@example.com', skip_invitation: true)
|
|
61
|
+
invitation = DeviseInvitable::Invitation.find(user.current_invitation.id)
|
|
62
|
+
|
|
63
|
+
assert_difference('ActionMailer::Base.deliveries.size', 1) do
|
|
64
|
+
DeviseInvitable::InvitationDeliveryJob.perform_now(invitation)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
assert_predicate invitation.reload, :invitation_sent_at
|
|
68
|
+
assert_empty invitation.invite_key
|
|
69
|
+
assert_equal ['stored@example.com'], ActionMailer::Base.deliveries.last.to
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
test 'the default job does not enqueue through send_devise_notification' do
|
|
73
|
+
user = User.invite!(email: 'direct@example.com', skip_invitation: true)
|
|
74
|
+
invitation = user.current_invitation
|
|
75
|
+
acceptance_token = user.acceptance_token
|
|
76
|
+
User.any_instance.stubs(:send_devise_notification).raises('must not be called')
|
|
77
|
+
|
|
78
|
+
assert_difference('ActionMailer::Base.deliveries.size', 1) do
|
|
79
|
+
DeviseInvitable::InvitationDeliveryJob.perform_now(
|
|
80
|
+
invitation,
|
|
81
|
+
acceptance_token
|
|
82
|
+
)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
assert_predicate invitation.reload, :invitation_sent_at
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
test 'the setting enqueues the normal invitation flow with the configured job' do
|
|
89
|
+
job_class = Class.new do
|
|
90
|
+
class << self
|
|
91
|
+
attr_reader :arguments
|
|
92
|
+
|
|
93
|
+
def perform_later(*arguments)
|
|
94
|
+
@arguments = arguments
|
|
95
|
+
:enqueued
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
Devise.invitation_delivery_job = job_class
|
|
100
|
+
User.deliver_invitation_later = true
|
|
101
|
+
User.any_instance.stubs(:send_devise_notification).raises('must not be called')
|
|
102
|
+
|
|
103
|
+
assert_no_difference('ActionMailer::Base.deliveries.size') do
|
|
104
|
+
User.invite!(email: 'configured@example.com')
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
invitation, acceptance_token, options = job_class.arguments
|
|
108
|
+
assert_predicate invitation, :persisted?
|
|
109
|
+
assert_nil invitation.invitation_sent_at
|
|
110
|
+
assert_equal({ validate: false }, options)
|
|
111
|
+
assert_equal 'configured@example.com',
|
|
112
|
+
DeviseInvitable::AcceptanceToken.decode(acceptance_token).dig(:invite_key, :email)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
test 'the normal flow keeps using send_devise_notification when the setting is disabled' do
|
|
116
|
+
User.any_instance.expects(:send_devise_notification)
|
|
117
|
+
.with(:invitation_instructions, kind_of(String), { validate: false })
|
|
118
|
+
.returns(:application_delivery)
|
|
119
|
+
|
|
120
|
+
assert_equal :application_delivery,
|
|
121
|
+
User.invite_mail!(email: 'application-delivery@example.com')
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
test 'the default job uses the configured queue' do
|
|
125
|
+
Devise.invitation_delivery_queue_name = :critical_mailers
|
|
126
|
+
|
|
127
|
+
assert_equal 'critical_mailers', DeviseInvitable::InvitationDeliveryJob.new.queue_name
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
test 'a reloaded resource-less invitation needs stored keys or an acceptance token' do
|
|
131
|
+
user = User.invite!(email: 'missing@example.com', skip_invitation: true)
|
|
132
|
+
invitation = DeviseInvitable::Invitation.find(user.current_invitation.id)
|
|
133
|
+
|
|
134
|
+
assert_raises(DeviseInvitable::MissingInviteKeyError) do
|
|
135
|
+
invitation.deliver_later
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
require 'test_helper'
|
|
2
|
+
require 'open3'
|
|
3
|
+
|
|
4
|
+
class LazyLoadingTest < ActiveSupport::TestCase
|
|
5
|
+
LIBRARY = File.expand_path('../lib', __dir__)
|
|
6
|
+
|
|
7
|
+
test 'loading devise invitable does not load Active Job' do
|
|
8
|
+
script = <<~RUBY
|
|
9
|
+
$LOAD_PATH.unshift #{LIBRARY.inspect}
|
|
10
|
+
require 'devise_invitable'
|
|
11
|
+
abort 'Active Job was loaded' if defined?(ActiveJob::Base)
|
|
12
|
+
RUBY
|
|
13
|
+
_stdout, stderr, status = Open3.capture3(RbConfig.ruby, '-e', script)
|
|
14
|
+
|
|
15
|
+
assert status.success?, stderr
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -55,11 +55,12 @@ class InvitationMailTest < ActionMailer::TestCase
|
|
|
55
55
|
test 'body should have link to confirm the account' do
|
|
56
56
|
host = ActionMailer::Base.default_url_options[:host]
|
|
57
57
|
body = mail.html_part.body.decoded
|
|
58
|
-
|
|
58
|
+
token = Regexp.escape(ERB::Util.url_encode(Thread.current[:token]))
|
|
59
|
+
invitation_url_regexp = %r{<a href=\"http://#{host}/users/invitation/accept\?invitation_token=#{token}">}
|
|
59
60
|
assert_match invitation_url_regexp, body
|
|
60
61
|
|
|
61
62
|
body = mail.text_part.body.decoded
|
|
62
|
-
invitation_url_regexp = %r{http://#{host}/users/invitation/accept\?invitation_token=#{
|
|
63
|
+
invitation_url_regexp = %r{http://#{host}/users/invitation/accept\?invitation_token=#{token}}
|
|
63
64
|
assert_match invitation_url_regexp, body
|
|
64
65
|
end
|
|
65
66
|
|
|
@@ -68,11 +69,12 @@ class InvitationMailTest < ActionMailer::TestCase
|
|
|
68
69
|
user
|
|
69
70
|
@user = User.find(user.id).invite!
|
|
70
71
|
body = mail.html_part.body.decoded
|
|
71
|
-
|
|
72
|
+
token = Regexp.escape(ERB::Util.url_encode(Thread.current[:token]))
|
|
73
|
+
invitation_url_regexp = %r{<a href=\"http://#{host}/users/invitation/accept\?invitation_token=#{token}">}
|
|
72
74
|
assert_match invitation_url_regexp, body
|
|
73
75
|
|
|
74
76
|
body = mail.text_part.body.decoded
|
|
75
|
-
invitation_url_regexp = %r{http://#{host}/users/invitation/accept\?invitation_token=#{
|
|
77
|
+
invitation_url_regexp = %r{http://#{host}/users/invitation/accept\?invitation_token=#{token}}
|
|
76
78
|
assert_match invitation_url_regexp, body
|
|
77
79
|
end
|
|
78
80
|
|
|
@@ -105,6 +107,8 @@ class InvitationMailTest < ActionMailer::TestCase
|
|
|
105
107
|
end
|
|
106
108
|
Devise.mailer = 'InvitationMailTest::CustomMailer'
|
|
107
109
|
|
|
108
|
-
|
|
110
|
+
assert_nothing_raised do
|
|
111
|
+
User.invite!({ email: 'valid@email.com' }, nil, { invited_at: Time.now })
|
|
112
|
+
end
|
|
109
113
|
end
|
|
110
114
|
end
|
|
@@ -27,13 +27,13 @@ class InvitableTest < ActiveSupport::TestCase
|
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
test 'should update the invitations count counter cache' do
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
assert_equal current_user.reload.invitations_count, 2
|
|
30
|
+
skip unless defined?(ActiveRecord)
|
|
31
|
+
|
|
32
|
+
current_user = new_user
|
|
33
|
+
2.times do |index|
|
|
34
|
+
User.invite!({ email: "valid#{index}@email.com" }, current_user)
|
|
36
35
|
end
|
|
36
|
+
assert_equal current_user.reload.invitations_count, 2
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
test 'should not generate the raw invitation token after creating a record' do
|
|
@@ -88,11 +88,65 @@ class InvitableTest < ActiveSupport::TestCase
|
|
|
88
88
|
|
|
89
89
|
test 'should return the correct raw_invitation_token ' do
|
|
90
90
|
user = new_user
|
|
91
|
-
raw, enc = Devise.token_generator.generate(
|
|
91
|
+
raw, enc = Devise.token_generator.generate(DeviseInvitable::Invitation, :invitation_token)
|
|
92
92
|
# stub the generator so the tokens are the same
|
|
93
93
|
Devise.token_generator.stubs(:generate).returns([raw, enc])
|
|
94
94
|
user.invite!
|
|
95
|
-
|
|
95
|
+
payload = DeviseInvitable::AcceptanceToken.decode(user.raw_invitation_token)
|
|
96
|
+
|
|
97
|
+
refute_includes user.raw_invitation_token, user.email
|
|
98
|
+
assert_equal raw, payload[:raw_token]
|
|
99
|
+
assert_equal({ 'email' => user.email }, payload[:invite_key].to_h.stringify_keys)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
test 'should reject a tampered encrypted invitation token' do
|
|
103
|
+
user = User.invite!(email: 'valid@email.com')
|
|
104
|
+
token = user.raw_invitation_token.dup
|
|
105
|
+
token[-1] = token[-1] == 'a' ? 'b' : 'a'
|
|
106
|
+
|
|
107
|
+
invited_user = User.accept_invitation!(invitation_token: token)
|
|
108
|
+
|
|
109
|
+
assert_equal [{ error: :invalid }], invited_user.errors.details[:invitation_token]
|
|
110
|
+
assert_predicate user.reload.current_invitation, :pending?
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
test 'should accept a legacy raw token with plaintext invite keys' do
|
|
114
|
+
raw, encrypted = Devise.token_generator.generate(DeviseInvitable::Invitation, :invitation_token)
|
|
115
|
+
invitation = DeviseInvitable::Invitation.create!(
|
|
116
|
+
invitable_type: 'User',
|
|
117
|
+
invitation_token: encrypted,
|
|
118
|
+
invitation_created_at: Time.now.utc,
|
|
119
|
+
invitation_sent_at: Time.now.utc,
|
|
120
|
+
invite_key: { email: 'legacy@email.com' }
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
invited_user = User.accept_invitation!(
|
|
124
|
+
invitation_token: raw,
|
|
125
|
+
password: '123456789',
|
|
126
|
+
password_confirmation: '123456789'
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
assert_predicate invited_user, :persisted?
|
|
130
|
+
assert_equal 'legacy@email.com', invited_user.email
|
|
131
|
+
assert_equal invited_user, invitation.reload.invitable
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
test 'should restore placeholder invite keys from the encrypted token' do
|
|
135
|
+
user = User.invite!(email: 'private@email.com', skip_invitation: true)
|
|
136
|
+
acceptance_token = user.raw_invitation_token
|
|
137
|
+
invitation = user.current_invitation
|
|
138
|
+
invite_key_digest = invitation.invite_key_digest
|
|
139
|
+
user.update_attribute(:email, nil)
|
|
140
|
+
|
|
141
|
+
invited_user = User.accept_invitation!(
|
|
142
|
+
invitation_token: acceptance_token,
|
|
143
|
+
password: '123456789',
|
|
144
|
+
password_confirmation: '123456789'
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
assert_equal 'private@email.com', invited_user.email
|
|
148
|
+
assert_empty invitation.reload.invite_key
|
|
149
|
+
assert_equal invite_key_digest, invitation.invite_key_digest
|
|
96
150
|
end
|
|
97
151
|
|
|
98
152
|
test 'should set invitation created and sent at each time' do
|
|
@@ -839,6 +893,16 @@ class InvitableTest < ActiveSupport::TestCase
|
|
|
839
893
|
assert_equal 3, User.created_by_invite.count
|
|
840
894
|
end
|
|
841
895
|
|
|
896
|
+
test 'invitation scopes do not use distinct with json columns' do
|
|
897
|
+
skip unless defined?(ActiveRecord)
|
|
898
|
+
|
|
899
|
+
assert_equal :json, User.type_for_attribute('options').type
|
|
900
|
+
|
|
901
|
+
[:created_by_invite, :invitation_not_accepted, :invitation_accepted].each do |scope|
|
|
902
|
+
refute_match(/\bSELECT\s+DISTINCT\b/i, User.public_send(scope).to_sql)
|
|
903
|
+
end
|
|
904
|
+
end
|
|
905
|
+
|
|
842
906
|
test "should preserve return values of Devise::Recoverable#reset_password" do
|
|
843
907
|
user = new_user
|
|
844
908
|
retval = user.reset_password('anewpassword', 'anewpassword')
|
|
@@ -4,11 +4,15 @@ require 'model_tests_helper'
|
|
|
4
4
|
class SeparateInvitationsTest < ActiveSupport::TestCase
|
|
5
5
|
setup do
|
|
6
6
|
@create_resource_on_invitation = User.create_resource_on_invitation
|
|
7
|
+
@store_invite_keys_until_delivery = User.store_invite_keys_until_delivery
|
|
8
|
+
@supersede_other_invitations_on_accept = User.supersede_other_invitations_on_accept
|
|
7
9
|
DeviseInvitable::Invitation.delete_all
|
|
8
10
|
end
|
|
9
11
|
|
|
10
12
|
teardown do
|
|
11
13
|
User.create_resource_on_invitation = @create_resource_on_invitation
|
|
14
|
+
User.store_invite_keys_until_delivery = @store_invite_keys_until_delivery
|
|
15
|
+
User.supersede_other_invitations_on_accept = @supersede_other_invitations_on_accept
|
|
12
16
|
end
|
|
13
17
|
|
|
14
18
|
test 'an invitation can be persisted without creating a resource' do
|
|
@@ -20,7 +24,9 @@ class SeparateInvitationsTest < ActiveSupport::TestCase
|
|
|
20
24
|
|
|
21
25
|
assert_predicate invited, :new_record?
|
|
22
26
|
assert_equal 0, User.where(email: 'deferred@example.com').count
|
|
23
|
-
|
|
27
|
+
assert_empty invitation.invite_key
|
|
28
|
+
payload = DeviseInvitable::AcceptanceToken.decode(invited.raw_invitation_token)
|
|
29
|
+
assert_equal({ 'email' => 'deferred@example.com' }, payload[:invite_key].to_h.stringify_keys)
|
|
24
30
|
assert_nil invitation.invitable_id
|
|
25
31
|
assert_nil invitation.resource_created_at
|
|
26
32
|
assert_includes User.no_active_invitation, registered
|
|
@@ -28,18 +34,21 @@ class SeparateInvitationsTest < ActiveSupport::TestCase
|
|
|
28
34
|
|
|
29
35
|
test 'an unsent resource-less invitation can be delivered later' do
|
|
30
36
|
User.create_resource_on_invitation = false
|
|
37
|
+
User.store_invite_keys_until_delivery = true
|
|
31
38
|
User.stubs(:invite_for).returns(1.day)
|
|
32
39
|
|
|
33
40
|
invited = User.invite!(email: 'deferred@example.com', skip_invitation: true)
|
|
34
|
-
invitation = invited.current_invitation
|
|
41
|
+
invitation = DeviseInvitable::Invitation.find(invited.current_invitation.id)
|
|
35
42
|
invitation.update!(invitation_created_at: 2.days.ago)
|
|
36
43
|
|
|
37
44
|
assert_nil invitation.invitation_sent_at
|
|
45
|
+
assert_equal({ 'email' => 'deferred@example.com' }, invitation.invite_key.to_h.stringify_keys)
|
|
38
46
|
assert_difference('ActionMailer::Base.deliveries.size', 1) do
|
|
39
47
|
invitation.reload.deliver
|
|
40
48
|
end
|
|
41
49
|
|
|
42
50
|
assert_predicate invitation.reload, :invitation_sent_at
|
|
51
|
+
assert_empty invitation.invite_key
|
|
43
52
|
assert invitation.invitation_period_valid?(1.day)
|
|
44
53
|
assert_equal ['deferred@example.com'], ActionMailer::Base.deliveries.last.to
|
|
45
54
|
|
|
@@ -64,12 +73,30 @@ class SeparateInvitationsTest < ActiveSupport::TestCase
|
|
|
64
73
|
|
|
65
74
|
test 'a failed later delivery remains unsent and can be retried' do
|
|
66
75
|
User.create_resource_on_invitation = false
|
|
76
|
+
User.store_invite_keys_until_delivery = true
|
|
67
77
|
invited = User.invite!(email: 'retry@example.com', skip_invitation: true)
|
|
68
|
-
invitation = invited.current_invitation.
|
|
78
|
+
invitation = DeviseInvitable::Invitation.find(invited.current_invitation.id)
|
|
69
79
|
User.any_instance.stubs(:send_devise_notification).raises('delivery failed')
|
|
70
80
|
|
|
71
81
|
assert_raises(RuntimeError) { invitation.deliver }
|
|
72
82
|
assert_nil invitation.reload.invitation_sent_at
|
|
83
|
+
assert_equal({ 'email' => 'retry@example.com' }, invitation.invite_key.to_h.stringify_keys)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
test 'the sent timestamp is available while rendering but saved after delivery' do
|
|
87
|
+
User.create_resource_on_invitation = false
|
|
88
|
+
User.store_invite_keys_until_delivery = true
|
|
89
|
+
invited = User.invite!(email: 'timestamp@example.com', skip_invitation: true)
|
|
90
|
+
invitation = DeviseInvitable::Invitation.find(invited.current_invitation.id)
|
|
91
|
+
|
|
92
|
+
User.any_instance.stubs(:send_devise_notification).with do
|
|
93
|
+
assert_predicate invitation, :invitation_sent_at
|
|
94
|
+
assert_nil DeviseInvitable::Invitation.find(invitation.id).invitation_sent_at
|
|
95
|
+
true
|
|
96
|
+
end.returns(:delivery)
|
|
97
|
+
|
|
98
|
+
assert_equal :delivery, invitation.deliver
|
|
99
|
+
assert_predicate invitation.reload, :invitation_sent_at
|
|
73
100
|
end
|
|
74
101
|
|
|
75
102
|
test 'accepting one of multiple invitations creates the resource and supersedes the others' do
|
|
@@ -97,6 +124,120 @@ class SeparateInvitationsTest < ActiveSupport::TestCase
|
|
|
97
124
|
assert_equal user, first.invitable
|
|
98
125
|
end
|
|
99
126
|
|
|
127
|
+
test 'multiple invitations can be accepted independently without changing the password' do
|
|
128
|
+
User.create_resource_on_invitation = false
|
|
129
|
+
User.supersede_other_invitations_on_accept = false
|
|
130
|
+
|
|
131
|
+
first_invited = User.invite!(
|
|
132
|
+
{ email: 'multiple@example.com' },
|
|
133
|
+
nil,
|
|
134
|
+
invitation_data: { organization_id: 1 }
|
|
135
|
+
)
|
|
136
|
+
first_token = first_invited.raw_invitation_token
|
|
137
|
+
first = DeviseInvitable::Invitation.last
|
|
138
|
+
second_invited = User.invite!(
|
|
139
|
+
{ email: 'multiple@example.com' },
|
|
140
|
+
nil,
|
|
141
|
+
invitation_data: { organization_id: 2 }
|
|
142
|
+
)
|
|
143
|
+
second_token = second_invited.raw_invitation_token
|
|
144
|
+
second = DeviseInvitable::Invitation.last
|
|
145
|
+
|
|
146
|
+
user = User.accept_invitation!(
|
|
147
|
+
invitation_token: first_token,
|
|
148
|
+
password: '123456789',
|
|
149
|
+
password_confirmation: '123456789'
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
assert_predicate first.reload, :provisioning?
|
|
153
|
+
refute_predicate first, :pending?
|
|
154
|
+
assert_predicate second.reload, :pending?
|
|
155
|
+
assert_equal user, second.invitable
|
|
156
|
+
assert_nil second.superseded_at
|
|
157
|
+
refute_nil second.invitation_token
|
|
158
|
+
|
|
159
|
+
found_user = User.find_by_invitation_token(second_token, true)
|
|
160
|
+
assert_equal({ 'organization_id' => 2 }, found_user.current_invitation.invitation_data.to_h.stringify_keys)
|
|
161
|
+
encrypted_password = user.encrypted_password
|
|
162
|
+
|
|
163
|
+
accepted_again = User.accept_invitation!(
|
|
164
|
+
invitation_token: second_token,
|
|
165
|
+
password: 'different-password',
|
|
166
|
+
password_confirmation: 'different-password'
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
assert_equal user, accepted_again
|
|
170
|
+
assert_equal encrypted_password, accepted_again.reload.encrypted_password
|
|
171
|
+
assert accepted_again.valid_password?('123456789')
|
|
172
|
+
refute accepted_again.valid_password?('different-password')
|
|
173
|
+
refute_predicate second.reload, :pending?
|
|
174
|
+
assert_nil second.resource_created_at
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
test 'accepting a later placeholder invitation transfers provisioning to it' do
|
|
178
|
+
User.create_resource_on_invitation = true
|
|
179
|
+
User.supersede_other_invitations_on_accept = false
|
|
180
|
+
|
|
181
|
+
user = User.invite!(email: 'placeholder@example.com')
|
|
182
|
+
first = user.current_invitation
|
|
183
|
+
User.invite!(email: 'placeholder@example.com')
|
|
184
|
+
second = DeviseInvitable::Invitation.last
|
|
185
|
+
second_token = Thread.current[:token]
|
|
186
|
+
|
|
187
|
+
accepted_user = User.accept_invitation!(
|
|
188
|
+
invitation_token: second_token,
|
|
189
|
+
password: '123456789',
|
|
190
|
+
password_confirmation: '123456789'
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
refute_predicate accepted_user, :invitation_pending?
|
|
194
|
+
assert_predicate second.reload, :provisioning?
|
|
195
|
+
refute_predicate second, :pending?
|
|
196
|
+
assert_predicate first.reload, :pending?
|
|
197
|
+
refute_predicate first, :provisioning?
|
|
198
|
+
assert_equal accepted_user, first.invitable
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
test 'invitation data is encrypted into the acceptance token but not persisted' do
|
|
202
|
+
User.create_resource_on_invitation = false
|
|
203
|
+
|
|
204
|
+
invited = User.invite!(
|
|
205
|
+
{ email: 'context@example.com' },
|
|
206
|
+
nil,
|
|
207
|
+
invitation_data: { organization_id: 42, role: 'member' }
|
|
208
|
+
)
|
|
209
|
+
invitation = invited.current_invitation
|
|
210
|
+
expected_data = { 'organization_id' => 42, 'role' => 'member' }
|
|
211
|
+
|
|
212
|
+
assert_equal expected_data, invited.invitation_data_before_creation.to_h.stringify_keys
|
|
213
|
+
assert_nil DeviseInvitable::Invitation.find(invitation.id).invitation_data
|
|
214
|
+
|
|
215
|
+
found_user = User.find_by_invitation_token(invited.raw_invitation_token, true)
|
|
216
|
+
|
|
217
|
+
assert_equal expected_data, found_user.current_invitation.invitation_data.to_h.stringify_keys
|
|
218
|
+
|
|
219
|
+
accepted_user = User.accept_invitation!(
|
|
220
|
+
invitation_token: invited.raw_invitation_token,
|
|
221
|
+
password: '123456789',
|
|
222
|
+
password_confirmation: '123456789'
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
assert_equal expected_data, accepted_user.invitation_data_after_acceptance.to_h.stringify_keys
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
test 'invitation data has a bounded serialized size' do
|
|
229
|
+
error = assert_raises DeviseInvitable::InvitationDataTooLargeError do
|
|
230
|
+
User.invite!(
|
|
231
|
+
{ email: 'context@example.com' },
|
|
232
|
+
nil,
|
|
233
|
+
invitation_data: { value: 'x' * 1_024 }
|
|
234
|
+
)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
assert_includes error.message, '1024 serialized bytes'
|
|
238
|
+
assert_equal 0, DeviseInvitable::Invitation.count
|
|
239
|
+
end
|
|
240
|
+
|
|
100
241
|
test 'resending stages the new inviter without changing the previous invitation' do
|
|
101
242
|
User.create_resource_on_invitation = true
|
|
102
243
|
first_inviter = User.create!(email: 'first@example.com', password: '123456789')
|
|
@@ -138,6 +279,7 @@ class SeparateInvitationsTest < ActiveSupport::TestCase
|
|
|
138
279
|
|
|
139
280
|
user = User.invite!(email: 'placeholder@example.com')
|
|
140
281
|
user.update!(confirmed_at: Time.now.utc)
|
|
282
|
+
user.pending_invitations.to_a
|
|
141
283
|
|
|
142
284
|
assert_predicate user, :invitation_pending?
|
|
143
285
|
refute_predicate user, :active_for_authentication?
|
|
@@ -149,6 +291,34 @@ class SeparateInvitationsTest < ActiveSupport::TestCase
|
|
|
149
291
|
assert_predicate user, :active_for_authentication?
|
|
150
292
|
end
|
|
151
293
|
|
|
294
|
+
test 'invitation pending uses preloaded invitations' do
|
|
295
|
+
pending_user = User.invite!(email: 'pending@example.com')
|
|
296
|
+
active_user = User.create!(email: 'active@example.com', password: '123456789')
|
|
297
|
+
DeviseInvitable::Invitation.create!(
|
|
298
|
+
invitable: active_user,
|
|
299
|
+
invitation_accepted_at: Time.now.utc,
|
|
300
|
+
resource_created_at: Time.now.utc,
|
|
301
|
+
invite_key: { email: active_user.email }
|
|
302
|
+
)
|
|
303
|
+
users = User.includes(:pending_invitations).index_by(&:email)
|
|
304
|
+
|
|
305
|
+
users.each_value do |user|
|
|
306
|
+
assert association_loaded?(user.pending_invitations)
|
|
307
|
+
end
|
|
308
|
+
check_statuses = proc do
|
|
309
|
+
assert_equal 1, users[pending_user.email].pending_invitations.size
|
|
310
|
+
assert_empty users[active_user.email].pending_invitations
|
|
311
|
+
assert_predicate users[pending_user.email], :invitation_pending?
|
|
312
|
+
refute_predicate users[active_user.email], :invitation_pending?
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
if respond_to?(:assert_no_queries)
|
|
316
|
+
assert_no_queries(&check_statuses)
|
|
317
|
+
else
|
|
318
|
+
check_statuses.call
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
|
|
152
322
|
test 'resource attributes raise when placeholder creation is disabled' do
|
|
153
323
|
User.create_resource_on_invitation = false
|
|
154
324
|
|
|
@@ -224,4 +394,12 @@ class SeparateInvitationsTest < ActiveSupport::TestCase
|
|
|
224
394
|
refute first.reload.attributes.key?('invitation_created_at')
|
|
225
395
|
refute second.reload.attributes.key?('invitation_created_at')
|
|
226
396
|
end
|
|
397
|
+
|
|
398
|
+
private
|
|
399
|
+
|
|
400
|
+
def association_loaded?(association)
|
|
401
|
+
return association.loaded? if association.respond_to?(:loaded?)
|
|
402
|
+
|
|
403
|
+
association._loaded?
|
|
404
|
+
end
|
|
227
405
|
end
|
data/test/models_test.rb
CHANGED
|
@@ -36,6 +36,23 @@ class ModelsTest < ActiveSupport::TestCase
|
|
|
36
36
|
assert_equal false, Devise.create_resource_on_invitation
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
+
test 'does not store invite keys until delivery by default' do
|
|
40
|
+
assert_equal false, Devise.store_invite_keys_until_delivery
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
test 'does not deliver invitations later by default' do
|
|
44
|
+
assert_equal false, Devise.deliver_invitation_later
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
test 'uses the default invitation delivery job and queue' do
|
|
48
|
+
assert_equal 'DeviseInvitable::InvitationDeliveryJob', Devise.invitation_delivery_job
|
|
49
|
+
assert_equal :mailers, Devise.invitation_delivery_queue_name
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
test 'supersedes other invitations on acceptance by default' do
|
|
53
|
+
assert_equal true, Devise.supersede_other_invitations_on_accept
|
|
54
|
+
end
|
|
55
|
+
|
|
39
56
|
test 'should have a default value for invite_key' do
|
|
40
57
|
assert !User.invite_key.nil?
|
|
41
58
|
end
|
|
@@ -32,7 +32,8 @@ class User < PARENT_MODEL_CLASS
|
|
|
32
32
|
create_resource_on_invitation: true
|
|
33
33
|
|
|
34
34
|
attr_accessor :before_invitation_created_callback_works, :after_invitation_created_callback_works,
|
|
35
|
-
:after_invitation_accepted_callback_works, :bio, :
|
|
35
|
+
:after_invitation_accepted_callback_works, :bio, :invitation_data_after_acceptance,
|
|
36
|
+
:invitation_data_before_creation, :token
|
|
36
37
|
validates :username, length: { maximum: 20 }
|
|
37
38
|
|
|
38
39
|
attr_accessor :testing_accepted_or_not_invited
|
|
@@ -50,10 +51,12 @@ class User < PARENT_MODEL_CLASS
|
|
|
50
51
|
|
|
51
52
|
before_invitation_created do |object|
|
|
52
53
|
object.before_invitation_created_callback_works = true
|
|
54
|
+
object.invitation_data_before_creation = object.current_invitation.invitation_data
|
|
53
55
|
end
|
|
54
56
|
|
|
55
57
|
after_invitation_accepted do |object|
|
|
56
58
|
object.after_invitation_accepted_callback_works = true
|
|
59
|
+
object.invitation_data_after_acceptance = object.current_invitation.invitation_data
|
|
57
60
|
end
|
|
58
61
|
|
|
59
62
|
def send_devise_notification(method, raw = nil, *args)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: devise_invitable
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.0
|
|
4
|
+
version: 3.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sergio Cambra
|
|
@@ -94,8 +94,11 @@ files:
|
|
|
94
94
|
- config/locales/zh-HK.yml
|
|
95
95
|
- config/locales/zh-TW.yml
|
|
96
96
|
- lib/devise_invitable.rb
|
|
97
|
+
- lib/devise_invitable/acceptance_token.rb
|
|
97
98
|
- lib/devise_invitable/controllers/helpers.rb
|
|
98
99
|
- lib/devise_invitable/invitation.rb
|
|
100
|
+
- lib/devise_invitable/invitation_delivery.rb
|
|
101
|
+
- lib/devise_invitable/invitation_delivery_job.rb
|
|
99
102
|
- lib/devise_invitable/inviter.rb
|
|
100
103
|
- lib/devise_invitable/mailer.rb
|
|
101
104
|
- lib/devise_invitable/mapping.rb
|
|
@@ -126,6 +129,8 @@ files:
|
|
|
126
129
|
- test/integration/invitation_remove_test.rb
|
|
127
130
|
- test/integration/invitation_test.rb
|
|
128
131
|
- test/integration_tests_helper.rb
|
|
132
|
+
- test/jobs/invitation_delivery_job_test.rb
|
|
133
|
+
- test/lazy_loading_test.rb
|
|
129
134
|
- test/mailers/invitation_mail_test.rb
|
|
130
135
|
- test/mailers/parent_mailer_test.rb
|
|
131
136
|
- test/model_tests_helper.rb
|
|
@@ -174,6 +179,7 @@ files:
|
|
|
174
179
|
- test/rails_app/config/master.key
|
|
175
180
|
- test/rails_app/config/routes.rb
|
|
176
181
|
- test/rails_app/db/migrate/20100401102949_create_tables.rb
|
|
182
|
+
- test/rails_app/db/migrate/20261103170000_add_options_to_users.rb
|
|
177
183
|
- test/rails_app/mongoid.yml
|
|
178
184
|
- test/rails_app/script/rails
|
|
179
185
|
- test/routes_test.rb
|
|
@@ -210,6 +216,8 @@ test_files:
|
|
|
210
216
|
- test/integration/invitation_remove_test.rb
|
|
211
217
|
- test/integration/invitation_test.rb
|
|
212
218
|
- test/integration_tests_helper.rb
|
|
219
|
+
- test/jobs/invitation_delivery_job_test.rb
|
|
220
|
+
- test/lazy_loading_test.rb
|
|
213
221
|
- test/mailers/invitation_mail_test.rb
|
|
214
222
|
- test/mailers/parent_mailer_test.rb
|
|
215
223
|
- test/model_tests_helper.rb
|
|
@@ -258,6 +266,7 @@ test_files:
|
|
|
258
266
|
- test/rails_app/config/master.key
|
|
259
267
|
- test/rails_app/config/routes.rb
|
|
260
268
|
- test/rails_app/db/migrate/20100401102949_create_tables.rb
|
|
269
|
+
- test/rails_app/db/migrate/20261103170000_add_options_to_users.rb
|
|
261
270
|
- test/rails_app/mongoid.yml
|
|
262
271
|
- test/rails_app/script/rails
|
|
263
272
|
- test/routes_test.rb
|