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.
@@ -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
- invitation_url_regexp = %r{<a href=\"http://#{host}/users/invitation/accept\?invitation_token=#{Thread.current[:token]}">}
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=#{Thread.current[: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
- invitation_url_regexp = %r{<a href=\"http://#{host}/users/invitation/accept\?invitation_token=#{Thread.current[:token]}">}
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=#{Thread.current[: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
- User.invite!({ email: 'valid@email.com' }, nil, { invited_at: Time.now })
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
- if defined?(ActiveRecord)
31
- current_user = new_user
32
- 2.times do |index|
33
- User.invite!({ email: "valid#{index}@email.com" }, current_user)
34
- end
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(user.class, :invitation_token)
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
- assert_equal user.raw_invitation_token, raw
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
- assert_equal({ 'email' => 'deferred@example.com' }, invitation.invite_key.to_h.stringify_keys)
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.reload
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, :token
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)
@@ -0,0 +1,5 @@
1
+ class AddOptionsToUsers < ActiveRecord::Migration[7.0]
2
+ def change
3
+ add_column :users, :options, :json
4
+ end
5
+ end
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.3
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