devise_invitable 3.0.0 → 3.0.2
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 +33 -0
- data/README.md +80 -12
- data/app/controllers/devise/invitations_controller.rb +7 -1
- data/lib/devise_invitable/invitation.rb +12 -1
- data/lib/devise_invitable/models.rb +31 -7
- data/lib/devise_invitable/version.rb +1 -1
- data/lib/devise_invitable.rb +1 -0
- data/test/integration/invitation_test.rb +21 -0
- data/test/models/invitable_test.rb +53 -12
- data/test/models/separate_invitations_test.rb +32 -0
- data/test/rails_app/app/controllers/boolean_inviter_invitations_controller.rb +11 -0
- data/test/rails_app/app/controllers/restricted_invitations_controller.rb +9 -0
- data/test/rails_app/app/models/user.rb +6 -1
- data/test/rails_app/config/routes.rb +2 -0
- data/test/routes_test.rb +6 -0
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 54bcb44af270be6f5192f01771df589f95045ee16206315e7784c16c49d93df5
|
|
4
|
+
data.tar.gz: 74fea42d1bae1ec385ca62291817679a59c3430fc6f4e609c966ee08db17c2d3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: dd0dbba52b99e4117a02072d3f48fbb3623614697a5e972a4f99081351479c9a2846696752a112ffd63d9528631ef196763146af5cc2792996246bcdaa755a97
|
|
7
|
+
data.tar.gz: 406bdf51f7114cc26354cf682658a3f44cb192db2f94cb45d0c7fd72384774d1464678275aac671a973b1660031084c21b4a290279e8e52a9ea69109a8186068
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,36 @@
|
|
|
1
|
+
## Unreleased
|
|
2
|
+
|
|
3
|
+
## 3.0.2
|
|
4
|
+
|
|
5
|
+
- Add `DeviseInvitable::Invitation#deliver` to send a skipped pending
|
|
6
|
+
invitation later, including resource-less invitations
|
|
7
|
+
- Start the `invite_for` validity period when an invitation is sent instead of
|
|
8
|
+
when it is created, so approval delays do not shorten the acceptance window
|
|
9
|
+
- Leave an invitation unsent when delivery fails so it can be retried
|
|
10
|
+
|
|
11
|
+
## 3.0.1
|
|
12
|
+
|
|
13
|
+
- Raise `DeviseInvitable::AmbiguousInviteKeyError` instead of selecting an
|
|
14
|
+
arbitrary resource when an invite key matches multiple records
|
|
15
|
+
([#878](https://github.com/scambra/devise_invitable/issues/878))
|
|
16
|
+
- Use the signed-in resource as the inviter when a custom
|
|
17
|
+
`authenticate_inviter!` method returns a boolean success value
|
|
18
|
+
([#825](https://github.com/scambra/devise_invitable/issues/825))
|
|
19
|
+
- Do not assign attributes or modify associations on an existing resource when
|
|
20
|
+
its invitation is rejected ([#747](https://github.com/scambra/devise_invitable/issues/747))
|
|
21
|
+
- Avoid uninitialized `@accepting_invitation` warnings on supported Ruby 2.7
|
|
22
|
+
([#776](https://github.com/scambra/devise_invitable/issues/776))
|
|
23
|
+
- Document personal-data storage and retention considerations for deferred and
|
|
24
|
+
placeholder invitations ([#777](https://github.com/scambra/devise_invitable/issues/777))
|
|
25
|
+
- Add regression coverage for custom `authenticate_inviter!` methods that
|
|
26
|
+
redirect unauthorized inviters ([#893](https://github.com/scambra/devise_invitable/issues/893))
|
|
27
|
+
- Add regression coverage to ensure `devise_for` preserves a custom controller
|
|
28
|
+
`module` instead of replacing its registrations controller
|
|
29
|
+
([#877](https://github.com/scambra/devise_invitable/issues/877))
|
|
30
|
+
- Add regression coverage that instance-level `invite!` runs both the before
|
|
31
|
+
and after invitation-created callbacks
|
|
32
|
+
([#640](https://github.com/scambra/devise_invitable/issues/640))
|
|
33
|
+
|
|
1
34
|
## 3.0.0
|
|
2
35
|
|
|
3
36
|
- Drop support for Ruby < 2.7, Rails < 7.0, and Devise < 5.0
|
data/README.md
CHANGED
|
@@ -246,6 +246,34 @@ above is `'users/invitations'`, so our routes.rb would have this line:
|
|
|
246
246
|
devise_for :users, controllers: { invitations: 'users/invitations' }
|
|
247
247
|
```
|
|
248
248
|
|
|
249
|
+
The invitations controller prepends its authentication, invitation-limit, and
|
|
250
|
+
token-loading callbacks. These callbacks may render or redirect and halt the
|
|
251
|
+
callback chain before an ordinary callback inherited from
|
|
252
|
+
`ApplicationController` runs. If application context must be available even
|
|
253
|
+
when one of these guards halts the request, prepend that setup in the custom
|
|
254
|
+
invitations controller. For example, an around callback can set and restore
|
|
255
|
+
the locale so flash messages created by the guards use the requested locale:
|
|
256
|
+
|
|
257
|
+
```ruby
|
|
258
|
+
class Users::InvitationsController < Devise::InvitationsController
|
|
259
|
+
prepend_around_action :use_locale
|
|
260
|
+
|
|
261
|
+
private
|
|
262
|
+
|
|
263
|
+
def use_locale(&action)
|
|
264
|
+
I18n.with_locale(locale_from_request, &action)
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def locale_from_request
|
|
268
|
+
params[:locale].presence_in(I18n.available_locales.map(&:to_s)) ||
|
|
269
|
+
I18n.default_locale
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
The same ordering rule applies to tenant selection, request tracing, and other
|
|
275
|
+
setup that must run before Devise Invitable's controller guards.
|
|
276
|
+
|
|
249
277
|
be sure that you generate the views and put them into the controller that you
|
|
250
278
|
generated, so for this example it would be:
|
|
251
279
|
|
|
@@ -394,6 +422,36 @@ rejected by both Devise's password validation and
|
|
|
394
422
|
`active_for_authentication?` checks until an invitation is accepted or the
|
|
395
423
|
resource is claimed by direct registration.
|
|
396
424
|
|
|
425
|
+
### Privacy and data retention
|
|
426
|
+
|
|
427
|
+
Invitation keys commonly contain personal data such as an email address. With
|
|
428
|
+
the default `create_resource_on_invitation: false`, no resource row is created,
|
|
429
|
+
but the invite-key values are stored in plaintext in the
|
|
430
|
+
`devise_invitations.invite_key` JSON column so the resource can be created when
|
|
431
|
+
the invitation is accepted. `invite_key_digest` is used for matching; it does
|
|
432
|
+
not replace the stored values and should not be treated as anonymization.
|
|
433
|
+
|
|
434
|
+
With `create_resource_on_invitation: true`, the resource and any additional
|
|
435
|
+
attributes or associations supplied to `invite!` are persisted immediately.
|
|
436
|
+
Choose this mode only when the application needs a placeholder and its privacy
|
|
437
|
+
policy permits storing that data before acceptance.
|
|
438
|
+
|
|
439
|
+
`invite_for` limits token validity but does not delete invitation or resource
|
|
440
|
+
records. Applications are responsible for choosing a lawful basis for
|
|
441
|
+
processing, disclosing that processing where required, limiting invited data,
|
|
442
|
+
restricting access, protecting it at rest, and deleting pending, revoked,
|
|
443
|
+
superseded, and accepted invitation data according to their retention policy.
|
|
444
|
+
Consent is one possible lawful basis under GDPR, but it is not the only one.
|
|
445
|
+
See the [European Commission GDPR principles](https://commission.europa.eu/law/law-topic/data-protection/information-business-and-organisations/principles-gdpr_en)
|
|
446
|
+
and obtain appropriate legal advice for the application.
|
|
447
|
+
|
|
448
|
+
A one-way hash alone is insufficient for the built-in deferred-invitation
|
|
449
|
+
flow, because devise_invitable must recover the invite keys to create the
|
|
450
|
+
resource at acceptance. Applications that must not retain the email address
|
|
451
|
+
need a custom flow, such as asking the invitee to enter it during acceptance
|
|
452
|
+
and verifying it against a stored digest, or using suitably protected
|
|
453
|
+
reversible storage.
|
|
454
|
+
|
|
397
455
|
If you want to create the invitation but not send it, you can set
|
|
398
456
|
`skip_invitation` to `true`.
|
|
399
457
|
|
|
@@ -412,16 +470,32 @@ user and will be decrypted when received.
|
|
|
412
470
|
accept_user_invitation_url(invitation_token: user.raw_invitation_token)
|
|
413
471
|
```
|
|
414
472
|
|
|
415
|
-
When `skip_invitation` is used,
|
|
416
|
-
`invitation_sent_at
|
|
417
|
-
|
|
418
|
-
the invite. You can set the column, or call `deliver_invitation` to send the
|
|
419
|
-
invitation and set the column:
|
|
473
|
+
When `skip_invitation` is used, the resulting invitation remains pending with
|
|
474
|
+
an unset `invitation_sent_at`. Deliver it later by calling `deliver` on the
|
|
475
|
+
invitation:
|
|
420
476
|
|
|
421
477
|
```ruby
|
|
422
|
-
user.
|
|
478
|
+
invitation = user.current_invitation
|
|
479
|
+
invitation.deliver
|
|
423
480
|
```
|
|
424
481
|
|
|
482
|
+
This works for both placeholder-backed and resource-less invitations. For a
|
|
483
|
+
resource-less invitation, `deliver` reconstructs the resource from the stored
|
|
484
|
+
invite key. It generates a fresh token, sends the email, and sets
|
|
485
|
+
`invitation_sent_at`. Calling it again after successful delivery does nothing.
|
|
486
|
+
|
|
487
|
+
For example, an approval workflow can find unsent pending invitations and
|
|
488
|
+
deliver an approved one after applying its own authorization policy:
|
|
489
|
+
|
|
490
|
+
```ruby
|
|
491
|
+
invitations = DeviseInvitable::Invitation.pending.where(invitation_sent_at: nil)
|
|
492
|
+
invitation = invitations.find(approved_invitation_id)
|
|
493
|
+
invitation.deliver
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
Invitation validity starts at `invitation_sent_at`, so time spent awaiting
|
|
497
|
+
approval does not reduce the configured `invite_for` period.
|
|
498
|
+
|
|
425
499
|
You can add `:skip_invitation` to attributes hash if `skip_invitation` is
|
|
426
500
|
added to `attr_accessible`.
|
|
427
501
|
|
|
@@ -437,12 +511,6 @@ User.invite!(email: 'new_user@example.com', skip_invitation: true)
|
|
|
437
511
|
To check if a particular user is created by invitation, irrespective to state
|
|
438
512
|
of invitation one can use `created_by_invite?`
|
|
439
513
|
|
|
440
|
-
**Warning**
|
|
441
|
-
|
|
442
|
-
When using `skip_invitation` you must send the email with the user object
|
|
443
|
-
instance that generated the tokens, as `user.raw_invitation_token` is
|
|
444
|
-
available only to the instance and is not persisted in the database.
|
|
445
|
-
|
|
446
514
|
You can also set `invited_by` when using the `invite!` class method:
|
|
447
515
|
|
|
448
516
|
```ruby
|
|
@@ -85,7 +85,13 @@ class Devise::InvitationsController < DeviseController
|
|
|
85
85
|
end
|
|
86
86
|
|
|
87
87
|
def current_inviter
|
|
88
|
-
authenticate_inviter!
|
|
88
|
+
authenticated = authenticate_inviter!
|
|
89
|
+
return authenticated if authenticated.respond_to?(:has_invitations_left?)
|
|
90
|
+
|
|
91
|
+
# Custom API/token authenticators may return true on success instead of
|
|
92
|
+
# the authenticated resource, so fall back to Devise's current scope.
|
|
93
|
+
current_resource_method = :"current_#{resource_name}"
|
|
94
|
+
send(current_resource_method) if respond_to?(current_resource_method, true)
|
|
89
95
|
end
|
|
90
96
|
|
|
91
97
|
def has_invitations_left?
|
|
@@ -67,7 +67,18 @@ module DeviseInvitable
|
|
|
67
67
|
end
|
|
68
68
|
|
|
69
69
|
def invitation_period_valid?(period)
|
|
70
|
-
|
|
70
|
+
invitation_sent_at &&
|
|
71
|
+
(period.to_i.zero? || invitation_sent_at.utc >= period.ago)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Deliver a pending invitation that has not been sent yet. Resource-less
|
|
75
|
+
# invitations are reconstructed from their stored invite key.
|
|
76
|
+
def deliver(options = {})
|
|
77
|
+
return unless pending? && invitation_sent_at.nil?
|
|
78
|
+
|
|
79
|
+
resource = invitable || invitable_type.constantize.new(invite_key)
|
|
80
|
+
resource.instance_variable_set(:@current_invitation, self)
|
|
81
|
+
resource.deliver_invitation(options)
|
|
71
82
|
end
|
|
72
83
|
|
|
73
84
|
def invite_key
|
|
@@ -231,7 +231,7 @@ module Devise
|
|
|
231
231
|
|
|
232
232
|
# Returns true if accept_invitation! was called
|
|
233
233
|
def accepting_invitation?
|
|
234
|
-
@accepting_invitation
|
|
234
|
+
instance_variable_defined?(:@accepting_invitation) && @accepting_invitation
|
|
235
235
|
end
|
|
236
236
|
|
|
237
237
|
# Verifies whether a user accepted an invitation (false when user is accepting it)
|
|
@@ -267,7 +267,6 @@ module Devise
|
|
|
267
267
|
run_callbacks :invitation_created do
|
|
268
268
|
now = Time.now.utc
|
|
269
269
|
invitation.invitation_created_at = now
|
|
270
|
-
invitation.invitation_sent_at = now unless skip_invitation
|
|
271
270
|
|
|
272
271
|
validate = options.key?(:validate) ? options[:validate] : self.class.validate_on_invite
|
|
273
272
|
save_invitation = !self.class.create_resource_on_invitation || save(validate: validate)
|
|
@@ -332,10 +331,15 @@ module Devise
|
|
|
332
331
|
# Deliver the invitation email
|
|
333
332
|
def deliver_invitation(options = {})
|
|
334
333
|
generate_invitation_token! unless @raw_invitation_token
|
|
334
|
+
marked_as_sent = false
|
|
335
335
|
unless current_invitation.invitation_sent_at
|
|
336
336
|
current_invitation.update_attribute :invitation_sent_at, Time.now.utc
|
|
337
|
+
marked_as_sent = true
|
|
337
338
|
end
|
|
338
339
|
send_devise_notification(:invitation_instructions, @raw_invitation_token, options)
|
|
340
|
+
rescue
|
|
341
|
+
current_invitation.update_attribute(:invitation_sent_at, nil) if marked_as_sent
|
|
342
|
+
raise
|
|
339
343
|
end
|
|
340
344
|
|
|
341
345
|
# provide alias to the encrypted invitation_token stored by devise
|
|
@@ -350,7 +354,7 @@ module Devise
|
|
|
350
354
|
def invitation_due_at
|
|
351
355
|
return nil if (self.class.invite_for == 0 || self.class.invite_for.nil?)
|
|
352
356
|
|
|
353
|
-
return unless (time = self.
|
|
357
|
+
return unless (time = self.invitation_sent_at)
|
|
354
358
|
|
|
355
359
|
time + self.class.invite_for
|
|
356
360
|
end
|
|
@@ -494,8 +498,18 @@ module Devise
|
|
|
494
498
|
attributes_hash[k] = attribute
|
|
495
499
|
end
|
|
496
500
|
|
|
501
|
+
ensure_unique_invite_key_match!(invite_key_array, attributes_hash)
|
|
497
502
|
invitable = find_or_initialize_with_errors(invite_key_array, attributes_hash)
|
|
498
503
|
invitable.assign_attributes(attributes_hash)
|
|
504
|
+
# Association writers may persist changes immediately. Reject an
|
|
505
|
+
# existing resource before assigning non-key attributes or yielding.
|
|
506
|
+
if invitable.persisted? && (invitable.invitation_taken? || !self.resend_invitation)
|
|
507
|
+
invite_key_array.each do |key|
|
|
508
|
+
invitable.add_taken_error(key)
|
|
509
|
+
end
|
|
510
|
+
return [invitable, nil]
|
|
511
|
+
end
|
|
512
|
+
|
|
499
513
|
invitable.assign_attributes(attributes)
|
|
500
514
|
invitable.skip_invitation = skip_invitation unless skip_invitation.nil?
|
|
501
515
|
invitable.invited_by_for_next_invitation = invited_by
|
|
@@ -508,10 +522,6 @@ module Devise
|
|
|
508
522
|
invite_key_array.each { |key| invitable.add_taken_error(key) }
|
|
509
523
|
elsif invitable.new_record?
|
|
510
524
|
invitable.clear_errors_on_valid_keys if !self.validate_on_invite
|
|
511
|
-
elsif invitable.invitation_taken? || !self.resend_invitation
|
|
512
|
-
invite_key_array.each do |key|
|
|
513
|
-
invitable.add_taken_error(key)
|
|
514
|
-
end
|
|
515
525
|
end
|
|
516
526
|
|
|
517
527
|
resource_attribute_names = attributes.keys.map(&:to_sym)
|
|
@@ -639,6 +649,20 @@ module Devise
|
|
|
639
649
|
|
|
640
650
|
private
|
|
641
651
|
|
|
652
|
+
def ensure_unique_invite_key_match!(required_attributes, attributes)
|
|
653
|
+
filtered_attributes = attributes.to_h.with_indifferent_access
|
|
654
|
+
.slice(*required_attributes)
|
|
655
|
+
.delete_if { |_key, value| value.blank? }
|
|
656
|
+
return unless filtered_attributes.size == required_attributes.size
|
|
657
|
+
|
|
658
|
+
filtered_attributes = devise_parameter_filter.filter(filtered_attributes)
|
|
659
|
+
matches = where(filtered_attributes).limit(2).to_a
|
|
660
|
+
return unless matches.many?
|
|
661
|
+
|
|
662
|
+
raise DeviseInvitable::AmbiguousInviteKeyError,
|
|
663
|
+
"Invite key matches multiple #{devise_invitable_base_class.name} records"
|
|
664
|
+
end
|
|
665
|
+
|
|
642
666
|
# The random password, as set after an invitation, must conform
|
|
643
667
|
# to any password format validation rules of the application.
|
|
644
668
|
# This default fixes the most common scenarios: Passwords must contain
|
data/lib/devise_invitable.rb
CHANGED
|
@@ -247,6 +247,27 @@ class InvitationTest < ActionDispatch::IntegrationTest
|
|
|
247
247
|
assert_redirected_to new_admin_session_path
|
|
248
248
|
end
|
|
249
249
|
|
|
250
|
+
test 'custom inviter authentication can redirect without rendering twice' do
|
|
251
|
+
sign_in_as_user
|
|
252
|
+
|
|
253
|
+
get new_restricted_invitation_path
|
|
254
|
+
|
|
255
|
+
assert_redirected_to root_path
|
|
256
|
+
assert_equal 'You are not authorized to access this page', flash[:alert]
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
test 'boolean inviter authentication uses the signed in resource as inviter' do
|
|
260
|
+
inviter = User.create!(
|
|
261
|
+
email: 'boolean-inviter@example.com',
|
|
262
|
+
password: '123456',
|
|
263
|
+
password_confirmation: '123456'
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
post boolean_inviter_invitation_path, params: { user: { email: 'boolean-invitee@example.com' } }
|
|
267
|
+
|
|
268
|
+
assert_equal inviter, User.find_by!(email: 'boolean-invitee@example.com').invited_by
|
|
269
|
+
end
|
|
270
|
+
|
|
250
271
|
test 'authenticated admin should be able to send an admin invitation' do
|
|
251
272
|
admin = Admin.create(email: 'admin@test.com', password: '123456', password_confirmation: '123456')
|
|
252
273
|
sign_in_as_user admin
|
|
@@ -15,6 +15,17 @@ class InvitableTest < ActiveSupport::TestCase
|
|
|
15
15
|
assert_nil new_user.invitation_token
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
+
test 'accepting_invitation? does not warn before invitation acceptance' do
|
|
19
|
+
original_verbose = $VERBOSE
|
|
20
|
+
$VERBOSE = true
|
|
21
|
+
|
|
22
|
+
_, warnings = capture_io { refute_predicate new_user, :accepting_invitation? }
|
|
23
|
+
|
|
24
|
+
refute_includes warnings, 'instance variable @accepting_invitation not initialized'
|
|
25
|
+
ensure
|
|
26
|
+
$VERBOSE = original_verbose
|
|
27
|
+
end
|
|
28
|
+
|
|
18
29
|
test 'should update the invitations count counter cache' do
|
|
19
30
|
if defined?(ActiveRecord)
|
|
20
31
|
current_user = new_user
|
|
@@ -102,27 +113,31 @@ class InvitableTest < ActiveSupport::TestCase
|
|
|
102
113
|
user = User.invite!(email: 'valid@email.com')
|
|
103
114
|
|
|
104
115
|
User.stubs(:invite_for).returns(nil)
|
|
105
|
-
user.
|
|
116
|
+
user.invitation_sent_at = Time.now.utc
|
|
106
117
|
assert_predicate user, :valid_invitation?
|
|
107
118
|
|
|
108
119
|
User.stubs(:invite_for).returns(nil)
|
|
109
|
-
user.
|
|
120
|
+
user.invitation_sent_at = 1.year.ago
|
|
110
121
|
assert_predicate user, :valid_invitation?
|
|
111
122
|
|
|
112
123
|
User.stubs(:invite_for).returns(0)
|
|
113
|
-
user.
|
|
124
|
+
user.invitation_sent_at = Time.now.utc
|
|
114
125
|
assert_predicate user, :valid_invitation?
|
|
115
126
|
|
|
116
127
|
User.stubs(:invite_for).returns(0)
|
|
117
|
-
user.
|
|
128
|
+
user.invitation_sent_at = 1.day.ago
|
|
118
129
|
assert_predicate user, :valid_invitation?
|
|
119
130
|
|
|
120
131
|
User.stubs(:invite_for).returns(1.day)
|
|
121
|
-
user.
|
|
132
|
+
user.invitation_sent_at = Time.now.utc
|
|
122
133
|
assert_predicate user, :valid_invitation?
|
|
123
134
|
|
|
124
135
|
User.stubs(:invite_for).returns(1.day)
|
|
125
|
-
user.
|
|
136
|
+
user.invitation_sent_at = 2.days.ago
|
|
137
|
+
refute_predicate user, :valid_invitation?
|
|
138
|
+
|
|
139
|
+
User.stubs(:invite_for).returns(0)
|
|
140
|
+
user.invitation_sent_at = nil
|
|
126
141
|
refute_predicate user, :valid_invitation?
|
|
127
142
|
end
|
|
128
143
|
|
|
@@ -130,8 +145,7 @@ class InvitableTest < ActiveSupport::TestCase
|
|
|
130
145
|
User.stubs(:invite_for).returns(1.day)
|
|
131
146
|
|
|
132
147
|
user = User.invite!(email: 'valid@email.com')
|
|
133
|
-
|
|
134
|
-
valid_until = sent_at + User.invite_for
|
|
148
|
+
valid_until = user.invitation_sent_at + User.invite_for
|
|
135
149
|
|
|
136
150
|
assert_equal user.invitation_due_at, valid_until
|
|
137
151
|
end
|
|
@@ -283,7 +297,7 @@ class InvitableTest < ActiveSupport::TestCase
|
|
|
283
297
|
User.stubs(:invite_for).returns(1.day)
|
|
284
298
|
user = User.invite!(email: 'valid@email.com')
|
|
285
299
|
assert user.invited_to_sign_up?
|
|
286
|
-
user.
|
|
300
|
+
user.invitation_sent_at = Time.now.utc - 2.days
|
|
287
301
|
token, user.reset_password_token = Devise.token_generator.generate(User, :reset_password_token)
|
|
288
302
|
user.reset_password_sent_at = Time.now.utc
|
|
289
303
|
user.save
|
|
@@ -430,6 +444,30 @@ class InvitableTest < ActiveSupport::TestCase
|
|
|
430
444
|
assert_equal [{error: :taken}], same_user.errors.details[:email]
|
|
431
445
|
end
|
|
432
446
|
|
|
447
|
+
test 'should not modify associations when an existing user cannot be invited' do
|
|
448
|
+
existing_user = User.create!(email: 'existing@email.com', password: '123456789')
|
|
449
|
+
User.invite!({ email: 'invitee@email.com' }, existing_user)
|
|
450
|
+
sent_invitation = existing_user.sent_invitations.first
|
|
451
|
+
|
|
452
|
+
invited_user = User.invite!(
|
|
453
|
+
email: existing_user.email,
|
|
454
|
+
sent_invitation_ids: []
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
assert_equal [{ error: :taken }], invited_user.errors.details[:email]
|
|
458
|
+
assert_equal existing_user, sent_invitation.reload.invited_by
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
test 'should reject an invite key that matches multiple records' do
|
|
462
|
+
2.times do
|
|
463
|
+
User.new(email: 'duplicate@email.com').save(validate: false)
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
assert_raises DeviseInvitable::AmbiguousInviteKeyError do
|
|
467
|
+
User.invite!(email: 'duplicate@email.com')
|
|
468
|
+
end
|
|
469
|
+
end
|
|
470
|
+
|
|
433
471
|
test 'should return a record with errors if user with pending invitation was found by e-mail' do
|
|
434
472
|
existing_user = User.invite!(email: 'valid@email.com')
|
|
435
473
|
user = User.invite!(email: 'valid@email.com')
|
|
@@ -447,7 +485,7 @@ class InvitableTest < ActiveSupport::TestCase
|
|
|
447
485
|
end
|
|
448
486
|
end
|
|
449
487
|
|
|
450
|
-
test 'should
|
|
488
|
+
test 'should not assign or validate other attributes if user was found by e-mail' do
|
|
451
489
|
begin
|
|
452
490
|
validate_on_invite = User.validate_on_invite
|
|
453
491
|
User.validate_on_invite = true
|
|
@@ -456,7 +494,8 @@ class InvitableTest < ActiveSupport::TestCase
|
|
|
456
494
|
user = User.invite!(email: 'valid@email.com', username: 'a' * 50)
|
|
457
495
|
assert_equal user, existing_user
|
|
458
496
|
assert_equal [{error: :taken}], user.errors.details[:email]
|
|
459
|
-
|
|
497
|
+
assert_empty user.errors[:username]
|
|
498
|
+
assert_nil user.username
|
|
460
499
|
ensure
|
|
461
500
|
User.validate_on_invite = validate_on_invite
|
|
462
501
|
end
|
|
@@ -503,7 +542,7 @@ class InvitableTest < ActiveSupport::TestCase
|
|
|
503
542
|
test 'should return record with errors if invitation_token has expired' do
|
|
504
543
|
User.stubs(:invite_for).returns(10.hours)
|
|
505
544
|
invited_user = User.invite!(email: 'valid@email.com')
|
|
506
|
-
invited_user.
|
|
545
|
+
invited_user.invitation_sent_at = 2.days.ago
|
|
507
546
|
invited_user.save(validate: false)
|
|
508
547
|
user = User.accept_invitation!(invitation_token: Thread.current[:token])
|
|
509
548
|
assert_equal user, invited_user
|
|
@@ -741,8 +780,10 @@ class InvitableTest < ActiveSupport::TestCase
|
|
|
741
780
|
|
|
742
781
|
test "user.invite! should trigger callbacks" do
|
|
743
782
|
user = User.new(email: 'valid@email.com')
|
|
783
|
+
assert_callbacks_not_fired :before_invitation_created, user
|
|
744
784
|
assert_callbacks_not_fired :after_invitation_created, user
|
|
745
785
|
user.invite!
|
|
786
|
+
assert_callbacks_fired :before_invitation_created, user
|
|
746
787
|
assert_callbacks_fired :after_invitation_created, user
|
|
747
788
|
end
|
|
748
789
|
|
|
@@ -26,6 +26,38 @@ class SeparateInvitationsTest < ActiveSupport::TestCase
|
|
|
26
26
|
assert_includes User.no_active_invitation, registered
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
+
test 'an unsent resource-less invitation can be delivered later' do
|
|
30
|
+
User.create_resource_on_invitation = false
|
|
31
|
+
User.stubs(:invite_for).returns(1.day)
|
|
32
|
+
|
|
33
|
+
invited = User.invite!(email: 'deferred@example.com', skip_invitation: true)
|
|
34
|
+
invitation = invited.current_invitation
|
|
35
|
+
invitation.update!(invitation_created_at: 2.days.ago)
|
|
36
|
+
|
|
37
|
+
assert_nil invitation.invitation_sent_at
|
|
38
|
+
assert_difference('ActionMailer::Base.deliveries.size', 1) do
|
|
39
|
+
invitation.reload.deliver
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
assert_predicate invitation.reload, :invitation_sent_at
|
|
43
|
+
assert invitation.invitation_period_valid?(1.day)
|
|
44
|
+
assert_equal ['deferred@example.com'], ActionMailer::Base.deliveries.last.to
|
|
45
|
+
|
|
46
|
+
assert_no_difference('ActionMailer::Base.deliveries.size') do
|
|
47
|
+
invitation.deliver
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
test 'a failed later delivery remains unsent and can be retried' do
|
|
52
|
+
User.create_resource_on_invitation = false
|
|
53
|
+
invited = User.invite!(email: 'retry@example.com', skip_invitation: true)
|
|
54
|
+
invitation = invited.current_invitation.reload
|
|
55
|
+
User.any_instance.stubs(:send_devise_notification).raises('delivery failed')
|
|
56
|
+
|
|
57
|
+
assert_raises(RuntimeError) { invitation.deliver }
|
|
58
|
+
assert_nil invitation.reload.invitation_sent_at
|
|
59
|
+
end
|
|
60
|
+
|
|
29
61
|
test 'accepting one of multiple invitations creates the resource and supersedes the others' do
|
|
30
62
|
User.create_resource_on_invitation = false
|
|
31
63
|
first_inviter = User.create!(email: 'first@example.com', password: '123456789')
|
|
@@ -37,7 +37,8 @@ class User < PARENT_MODEL_CLASS
|
|
|
37
37
|
devise :database_authenticatable, :registerable, :validatable, :confirmable, :invitable, :recoverable,
|
|
38
38
|
create_resource_on_invitation: true
|
|
39
39
|
|
|
40
|
-
attr_accessor :
|
|
40
|
+
attr_accessor :before_invitation_created_callback_works, :after_invitation_created_callback_works,
|
|
41
|
+
:after_invitation_accepted_callback_works, :bio, :token
|
|
41
42
|
validates :username, length: { maximum: 20 }
|
|
42
43
|
|
|
43
44
|
attr_accessor :testing_accepted_or_not_invited
|
|
@@ -53,6 +54,10 @@ class User < PARENT_MODEL_CLASS
|
|
|
53
54
|
object.after_invitation_created_callback_works = true
|
|
54
55
|
end
|
|
55
56
|
|
|
57
|
+
before_invitation_created do |object|
|
|
58
|
+
object.before_invitation_created_callback_works = true
|
|
59
|
+
end
|
|
60
|
+
|
|
56
61
|
after_invitation_accepted do |object|
|
|
57
62
|
object.after_invitation_accepted_callback_works = true
|
|
58
63
|
end
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
RailsApp::Application.routes.draw do
|
|
2
2
|
devise_for :users
|
|
3
3
|
devise_scope :user do
|
|
4
|
+
resource :boolean_inviter_invitation, only: [:new, :create]
|
|
4
5
|
resource :free_invitation, only: [:new, :create]
|
|
6
|
+
resource :restricted_invitation, only: [:new, :create]
|
|
5
7
|
resource :admin, only: [:new, :create]
|
|
6
8
|
end
|
|
7
9
|
devise_for :admins
|
data/test/routes_test.rb
CHANGED
|
@@ -2,6 +2,12 @@ require 'test_helper'
|
|
|
2
2
|
|
|
3
3
|
class RoutesTest < ActionController::TestCase
|
|
4
4
|
|
|
5
|
+
test 'preserve custom controller module for registrations' do
|
|
6
|
+
mapping = Devise::Mapping.new(:users, module: :users)
|
|
7
|
+
|
|
8
|
+
assert_equal 'users/registrations', mapping.controllers[:registrations]
|
|
9
|
+
end
|
|
10
|
+
|
|
5
11
|
test 'map new user invitation' do
|
|
6
12
|
assert_recognizes({ controller: 'devise/invitations', action: 'new' }, { path: 'users/invitation/new', method: :get })
|
|
7
13
|
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
|
+
version: 3.0.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sergio Cambra
|
|
@@ -137,8 +137,10 @@ files:
|
|
|
137
137
|
- test/rails_app/Rakefile
|
|
138
138
|
- test/rails_app/app/controllers/admins_controller.rb
|
|
139
139
|
- test/rails_app/app/controllers/application_controller.rb
|
|
140
|
+
- test/rails_app/app/controllers/boolean_inviter_invitations_controller.rb
|
|
140
141
|
- test/rails_app/app/controllers/free_invitations_controller.rb
|
|
141
142
|
- test/rails_app/app/controllers/home_controller.rb
|
|
143
|
+
- test/rails_app/app/controllers/restricted_invitations_controller.rb
|
|
142
144
|
- test/rails_app/app/controllers/users_controller.rb
|
|
143
145
|
- test/rails_app/app/helpers/application_helper.rb
|
|
144
146
|
- test/rails_app/app/mailers/application_mailer.rb
|
|
@@ -219,8 +221,10 @@ test_files:
|
|
|
219
221
|
- test/rails_app/Rakefile
|
|
220
222
|
- test/rails_app/app/controllers/admins_controller.rb
|
|
221
223
|
- test/rails_app/app/controllers/application_controller.rb
|
|
224
|
+
- test/rails_app/app/controllers/boolean_inviter_invitations_controller.rb
|
|
222
225
|
- test/rails_app/app/controllers/free_invitations_controller.rb
|
|
223
226
|
- test/rails_app/app/controllers/home_controller.rb
|
|
227
|
+
- test/rails_app/app/controllers/restricted_invitations_controller.rb
|
|
224
228
|
- test/rails_app/app/controllers/users_controller.rb
|
|
225
229
|
- test/rails_app/app/helpers/application_helper.rb
|
|
226
230
|
- test/rails_app/app/mailers/application_mailer.rb
|