devise_invitable 3.0.4 → 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.
@@ -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
@@ -92,7 +92,61 @@ class InvitableTest < ActiveSupport::TestCase
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
@@ -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')
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.4
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