devise_invitable 2.0.14 → 3.0.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 +41 -0
- data/README.md +108 -97
- data/app/controllers/devise/invitations_controller.rb +1 -1
- data/app/controllers/devise_invitable/registrations_controller.rb +15 -10
- data/lib/devise_invitable/invitation.rb +89 -0
- data/lib/devise_invitable/inviter.rb +23 -1
- data/lib/devise_invitable/migrator.rb +113 -0
- data/lib/devise_invitable/models.rb +328 -81
- data/lib/devise_invitable/rails.rb +10 -0
- data/lib/devise_invitable/version.rb +1 -1
- data/lib/devise_invitable.rb +39 -4
- data/lib/generators/active_record/devise_invitable_install_generator.rb +22 -0
- data/lib/generators/active_record/devise_invitable_upgrade_generator.rb +28 -0
- data/lib/generators/active_record/templates/create_devise_invitations.rb +23 -0
- data/lib/generators/active_record/templates/upgrade_devise_invitations.rb +72 -0
- data/lib/generators/devise_invitable/devise_invitable_generator.rb +2 -4
- data/lib/generators/devise_invitable/install_generator.rb +8 -10
- data/lib/generators/devise_invitable/upgrade_generator.rb +16 -0
- data/lib/generators/mongoid/devise_invitable_install_generator.rb +7 -0
- data/lib/generators/mongoid/devise_invitable_upgrade_generator.rb +11 -0
- data/lib/tasks/devise_invitable_tasks.rake +10 -0
- data/test/functional/registrations_controller_test.rb +24 -3
- data/test/generators_test.rb +16 -9
- data/test/integration/invitation_test.rb +3 -3
- data/test/mailers/invitation_mail_test.rb +1 -0
- data/test/models/separate_invitations_test.rb +181 -0
- data/test/models_test.rb +34 -0
- data/test/rails_app/app/models/user.rb +2 -1
- data/test/rails_app/config/initializers/devise.rb +0 -9
- data/test/rails_app/db/migrate/20100401102949_create_tables.rb +22 -0
- metadata +22 -13
- data/lib/generators/active_record/devise_invitable_generator.rb +0 -19
- data/lib/generators/active_record/templates/migration.rb +0 -22
- data/lib/generators/mongoid/devise_invitable_generator.rb +0 -8
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
require 'rails/generators/active_record'
|
|
2
|
+
|
|
3
|
+
module ActiveRecord
|
|
4
|
+
module Generators
|
|
5
|
+
class DeviseInvitableInstallGenerator < ActiveRecord::Generators::Base
|
|
6
|
+
source_root File.expand_path('../templates', __FILE__)
|
|
7
|
+
argument :name, type: :string, default: 'devise_invitation'
|
|
8
|
+
|
|
9
|
+
def copy_devise_invitations_migration
|
|
10
|
+
migration_template 'create_devise_invitations.rb',
|
|
11
|
+
'db/migrate/create_devise_invitations.rb',
|
|
12
|
+
migration_version: migration_version
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def migration_version
|
|
16
|
+
if Rails::VERSION::MAJOR >= 5
|
|
17
|
+
"[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]"
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
require 'rails/generators/active_record'
|
|
2
|
+
|
|
3
|
+
module ActiveRecord
|
|
4
|
+
module Generators
|
|
5
|
+
class DeviseInvitableUpgradeGenerator < ActiveRecord::Generators::Base
|
|
6
|
+
source_root File.expand_path('../templates', __FILE__)
|
|
7
|
+
|
|
8
|
+
class_option :invite_keys, type: :array, default: ['email']
|
|
9
|
+
|
|
10
|
+
def copy_upgrade_migration
|
|
11
|
+
migration_template 'upgrade_devise_invitations.rb',
|
|
12
|
+
"db/migrate/upgrade_#{table_name}_devise_invitations.rb",
|
|
13
|
+
migration_version: migration_version,
|
|
14
|
+
invite_keys: options[:invite_keys]
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def migration_version
|
|
18
|
+
if Rails::VERSION::MAJOR >= 5
|
|
19
|
+
"[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]"
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def invite_keys
|
|
24
|
+
options[:invite_keys]
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
class CreateDeviseInvitations < ActiveRecord::Migration<%= migration_version %>
|
|
2
|
+
def change
|
|
3
|
+
create_table :devise_invitations do |t|
|
|
4
|
+
t.string :invitation_token
|
|
5
|
+
t.datetime :invitation_created_at
|
|
6
|
+
t.datetime :invitation_sent_at
|
|
7
|
+
t.datetime :invitation_accepted_at
|
|
8
|
+
t.datetime :superseded_at
|
|
9
|
+
t.string :superseded_reason
|
|
10
|
+
t.datetime :revoked_at
|
|
11
|
+
t.datetime :resource_created_at
|
|
12
|
+
t.json :invite_key, null: false, default: {}
|
|
13
|
+
t.string :invite_key_digest, null: false
|
|
14
|
+
t.references :invitable, polymorphic: true
|
|
15
|
+
t.references :invited_by, polymorphic: true
|
|
16
|
+
t.timestamps
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
add_index :devise_invitations, :invitation_token, unique: true
|
|
20
|
+
add_index :devise_invitations, [:invitable_type, :invite_key_digest],
|
|
21
|
+
name: 'index_devise_invitations_on_type_and_invite_key'
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
require 'digest'
|
|
2
|
+
|
|
3
|
+
class Upgrade<%= table_name.camelize %>DeviseInvitations < ActiveRecord::Migration<%= migration_version %>
|
|
4
|
+
OLD_INVITATION_COLUMNS = %i[
|
|
5
|
+
invitation_token invitation_created_at invitation_sent_at
|
|
6
|
+
invitation_accepted_at invited_by_id invited_by_type
|
|
7
|
+
].freeze
|
|
8
|
+
|
|
9
|
+
def up
|
|
10
|
+
create_devise_invitations_table unless table_exists?(:devise_invitations)
|
|
11
|
+
|
|
12
|
+
resource_class = Class.new(ActiveRecord::Base) do
|
|
13
|
+
self.table_name = '<%= table_name %>'
|
|
14
|
+
self.inheritance_column = :_type_disabled
|
|
15
|
+
end
|
|
16
|
+
invitation_class = Class.new(ActiveRecord::Base) do
|
|
17
|
+
self.table_name = 'devise_invitations'
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
resource_class.where.not(invitation_created_at: nil).find_each do |resource|
|
|
21
|
+
invite_key = resource.attributes.slice(<%= invite_keys.map(&:to_s).map(&:inspect).join(', ') %>)
|
|
22
|
+
digest_components = invite_key.map { |key, value| [key.to_s, value.to_s] }.sort.flatten
|
|
23
|
+
invitation_class.create!(
|
|
24
|
+
invitation_token: resource.invitation_token,
|
|
25
|
+
invitation_created_at: resource.invitation_created_at,
|
|
26
|
+
invitation_sent_at: resource.invitation_sent_at,
|
|
27
|
+
invitation_accepted_at: resource.invitation_accepted_at,
|
|
28
|
+
resource_created_at: resource.invitation_created_at,
|
|
29
|
+
invite_key: invite_key,
|
|
30
|
+
invite_key_digest: Digest::SHA256.hexdigest((['<%= class_name %>'] + digest_components).join("\0")),
|
|
31
|
+
invitable_type: '<%= class_name %>',
|
|
32
|
+
invitable_id: resource.id,
|
|
33
|
+
invited_by_type: resource.invited_by_type,
|
|
34
|
+
invited_by_id: resource.invited_by_id,
|
|
35
|
+
created_at: resource.invitation_created_at,
|
|
36
|
+
updated_at: resource.invitation_accepted_at || resource.invitation_sent_at || resource.invitation_created_at
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
OLD_INVITATION_COLUMNS.each do |column|
|
|
41
|
+
remove_column :<%= table_name %>, column if column_exists?(:<%= table_name %>, column)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def down
|
|
46
|
+
raise ActiveRecord::IrreversibleMigration,
|
|
47
|
+
'Multiple invitation records cannot be losslessly moved back onto <%= table_name %>'
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def create_devise_invitations_table
|
|
53
|
+
create_table :devise_invitations do |t|
|
|
54
|
+
t.string :invitation_token
|
|
55
|
+
t.datetime :invitation_created_at
|
|
56
|
+
t.datetime :invitation_sent_at
|
|
57
|
+
t.datetime :invitation_accepted_at
|
|
58
|
+
t.datetime :superseded_at
|
|
59
|
+
t.string :superseded_reason
|
|
60
|
+
t.datetime :revoked_at
|
|
61
|
+
t.datetime :resource_created_at
|
|
62
|
+
t.json :invite_key, null: false, default: {}
|
|
63
|
+
t.string :invite_key_digest, null: false
|
|
64
|
+
t.references :invitable, polymorphic: true
|
|
65
|
+
t.references :invited_by, polymorphic: true
|
|
66
|
+
t.timestamps
|
|
67
|
+
end
|
|
68
|
+
add_index :devise_invitations, :invitation_token, unique: true
|
|
69
|
+
add_index :devise_invitations, [:invitable_type, :invite_key_digest],
|
|
70
|
+
name: 'index_devise_invitations_on_type_and_invite_key'
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -3,14 +3,12 @@ module DeviseInvitable
|
|
|
3
3
|
class DeviseInvitableGenerator < Rails::Generators::NamedBase
|
|
4
4
|
namespace 'devise_invitable'
|
|
5
5
|
|
|
6
|
-
desc 'Add :invitable directive
|
|
6
|
+
desc 'Add the :invitable directive to the given model.'
|
|
7
7
|
|
|
8
8
|
def inject_devise_invitable_content
|
|
9
9
|
path = File.join('app', 'models', "#{file_path}.rb")
|
|
10
10
|
inject_into_file(path, 'invitable, :', after: 'devise :') if File.exist?(path)
|
|
11
11
|
end
|
|
12
|
-
|
|
13
|
-
hook_for :orm
|
|
14
12
|
end
|
|
15
13
|
end
|
|
16
|
-
end
|
|
14
|
+
end
|
|
@@ -2,14 +2,16 @@ module DeviseInvitable
|
|
|
2
2
|
module Generators
|
|
3
3
|
class InstallGenerator < Rails::Generators::Base
|
|
4
4
|
source_root File.expand_path('../../templates', __FILE__)
|
|
5
|
-
desc '
|
|
5
|
+
desc 'Install the invitations store, add configuration, and copy the English locale.'
|
|
6
|
+
|
|
7
|
+
hook_for :orm, as: :devise_invitable_install
|
|
6
8
|
|
|
7
9
|
def add_config_options_to_initializer
|
|
8
10
|
devise_initializer_path = 'config/initializers/devise.rb'
|
|
9
11
|
if File.exist?(devise_initializer_path)
|
|
10
12
|
old_content = File.read(devise_initializer_path)
|
|
11
13
|
|
|
12
|
-
if old_content.match(
|
|
14
|
+
if old_content.match(/^\s*# ==> Configuration for :invitable\n/)
|
|
13
15
|
false
|
|
14
16
|
else
|
|
15
17
|
inject_into_file(devise_initializer_path, before: " # ==> Configuration for :confirmable\n") do
|
|
@@ -44,14 +46,10 @@ module DeviseInvitable
|
|
|
44
46
|
# Default: true
|
|
45
47
|
# config.resend_invitation = false
|
|
46
48
|
|
|
47
|
-
#
|
|
48
|
-
# the
|
|
49
|
-
# Default:
|
|
50
|
-
# config.
|
|
51
|
-
|
|
52
|
-
# The foreign key to the inviting model (if invited_by_class_name is set)
|
|
53
|
-
# Default: :invited_by_id
|
|
54
|
-
# config.invited_by_foreign_key = :invited_by_id
|
|
49
|
+
# Create a placeholder resource as soon as an invitation is sent. When false,
|
|
50
|
+
# the resource is created only after the invitation is accepted.
|
|
51
|
+
# Default: false
|
|
52
|
+
# config.create_resource_on_invitation = true
|
|
55
53
|
|
|
56
54
|
# The column name used for counter_cache column. If this is nil,
|
|
57
55
|
# the #invited_by association is declared without counter_cache.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module DeviseInvitable
|
|
2
|
+
module Generators
|
|
3
|
+
class UpgradeGenerator < Rails::Generators::NamedBase
|
|
4
|
+
namespace 'devise_invitable:upgrade'
|
|
5
|
+
|
|
6
|
+
class_option :invite_keys,
|
|
7
|
+
type: :array,
|
|
8
|
+
default: ['email'],
|
|
9
|
+
desc: 'Columns used as invite keys in the old resource table'
|
|
10
|
+
|
|
11
|
+
desc 'Generate a migration from resource-backed invitations to the invitations table.'
|
|
12
|
+
|
|
13
|
+
hook_for :orm, as: :devise_invitable_upgrade
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
module Mongoid
|
|
2
|
+
module Generators
|
|
3
|
+
class DeviseInvitableUpgradeGenerator < Rails::Generators::NamedBase
|
|
4
|
+
desc 'Print the Mongoid invitation data migration command.'
|
|
5
|
+
|
|
6
|
+
def show_migration_task
|
|
7
|
+
say "Run: bin/rails devise_invitable:migrate_invitations[#{class_name}]"
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
namespace :devise_invitable do
|
|
2
|
+
desc 'Move legacy invitation fields into devise_invitations (MODEL=User also supported)'
|
|
3
|
+
task :migrate_invitations, [:model] => :environment do |_task, args|
|
|
4
|
+
model_name = args[:model] || ENV['MODEL']
|
|
5
|
+
raise ArgumentError, 'Pass a model name: rake devise_invitable:migrate_invitations[User]' if model_name.blank?
|
|
6
|
+
|
|
7
|
+
count = DeviseInvitable::Migrator.call(model_name.constantize)
|
|
8
|
+
puts "Migrated #{count} invitation#{'s' unless count == 1} for #{model_name}"
|
|
9
|
+
end
|
|
10
|
+
end
|
|
@@ -25,6 +25,7 @@ class DeviseInvitable::RegistrationsControllerTest < ActionController::TestCase
|
|
|
25
25
|
sign_out @issuer
|
|
26
26
|
|
|
27
27
|
@invitee = User.where(email: invitee_email).first
|
|
28
|
+
invitation = @invitee.current_invitation
|
|
28
29
|
assert_nil @invitee.invitation_accepted_at
|
|
29
30
|
assert_not_nil @invitee.invitation_token
|
|
30
31
|
assert !@invitee.confirmed?
|
|
@@ -45,10 +46,11 @@ class DeviseInvitable::RegistrationsControllerTest < ActionController::TestCase
|
|
|
45
46
|
end
|
|
46
47
|
|
|
47
48
|
assert @invitee.encrypted_password.present?
|
|
48
|
-
|
|
49
|
+
assert_nil @invitee.invitation_accepted_at
|
|
49
50
|
assert_nil @invitee.invitation_token
|
|
50
|
-
|
|
51
|
-
|
|
51
|
+
assert_equal 'registered', invitation.reload.superseded_reason
|
|
52
|
+
assert_equal @issuer, invitation.invited_by
|
|
53
|
+
refute_predicate @invitee, :invitation_pending?
|
|
52
54
|
assert @invitee.confirmation_token.present?
|
|
53
55
|
assert !@invitee.confirmed?
|
|
54
56
|
end
|
|
@@ -79,6 +81,25 @@ class DeviseInvitable::RegistrationsControllerTest < ActionController::TestCase
|
|
|
79
81
|
assert !@user.confirmed?
|
|
80
82
|
end
|
|
81
83
|
|
|
84
|
+
test "registration supersedes invitations when placeholders are disabled" do
|
|
85
|
+
User.create_resource_on_invitation = false
|
|
86
|
+
invitee_email = 'deferred@example.org'
|
|
87
|
+
User.invite!(email: invitee_email)
|
|
88
|
+
invitation = DeviseInvitable::Invitation.first
|
|
89
|
+
|
|
90
|
+
assert_nil User.where(email: invitee_email).first
|
|
91
|
+
|
|
92
|
+
post :create, params: { user: { email: invitee_email, password: '1password', bio: '.' } }
|
|
93
|
+
|
|
94
|
+
invitee = User.find_by!(email: invitee_email)
|
|
95
|
+
assert_equal invitee, invitation.reload.invitable
|
|
96
|
+
assert_equal 'registered', invitation.superseded_reason
|
|
97
|
+
assert_nil invitation.invitation_accepted_at
|
|
98
|
+
refute_predicate invitee, :invitation_pending?
|
|
99
|
+
ensure
|
|
100
|
+
User.create_resource_on_invitation = true
|
|
101
|
+
end
|
|
102
|
+
|
|
82
103
|
test "not invitable resources can register" do
|
|
83
104
|
@request.env['devise.mapping'] = Devise.mappings[:admin]
|
|
84
105
|
invitee_email = 'invitee@example.org'
|
data/test/generators_test.rb
CHANGED
|
@@ -6,30 +6,37 @@ require 'generators/devise_invitable/devise_invitable_generator'
|
|
|
6
6
|
class GeneratorsTest < ActiveSupport::TestCase
|
|
7
7
|
RAILS_APP_PATH = File.expand_path("../rails_app", __FILE__)
|
|
8
8
|
|
|
9
|
-
test "rails g should include the
|
|
9
|
+
test "rails g should include the generators" do
|
|
10
10
|
@output = `cd #{RAILS_APP_PATH} && rails g`
|
|
11
|
-
generators = %w/devise_invitable devise_invitable:form_for devise_invitable:install devise_invitable:invitation_views devise_invitable:simple_form_for devise_invitable:views/
|
|
11
|
+
generators = %w/devise_invitable devise_invitable:form_for devise_invitable:install devise_invitable:invitation_views devise_invitable:simple_form_for devise_invitable:upgrade devise_invitable:views/
|
|
12
12
|
|
|
13
13
|
generators.each do |generator|
|
|
14
|
-
@output
|
|
14
|
+
assert_includes @output, generator
|
|
15
15
|
end
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
test "rails g devise_invitable:install" do
|
|
19
19
|
@output = `cd #{RAILS_APP_PATH} && rails g devise_invitable:install -p`
|
|
20
|
-
puts @output
|
|
21
|
-
assert @output.match(%r{(inject|insert|File unchanged! The supplied flag value not found!).* config/initializers/devise\.rb\n})
|
|
22
20
|
assert @output.match(%r|create.* config/locales/devise_invitable\.en\.yml\n|)
|
|
21
|
+
if DEVISE_ORM == :active_record
|
|
22
|
+
assert_match(%r|create.*db/migrate/\d{14}_create_devise_invitations\.rb|, @output)
|
|
23
|
+
end
|
|
23
24
|
end
|
|
24
25
|
|
|
25
26
|
test "rails g devise_invitable Octopussy" do
|
|
26
27
|
@output = `cd #{RAILS_APP_PATH} && rails g devise_invitable Octopussy -p`
|
|
27
28
|
assert @output.match(%r{(inject|insert|File unchanged! The supplied flag value not found!).* app/models/octopussy\.rb\n})
|
|
28
|
-
|
|
29
|
+
assert_no_match(%r|invoke.* #{DEVISE_ORM}\n|, @output)
|
|
30
|
+
assert_no_match(%r|db/migrate/\d{14}_devise_invitable_add_to_octopussies\.rb|, @output)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
test "rails g devise_invitable:upgrade User" do
|
|
35
|
+
@output = `cd #{RAILS_APP_PATH} && rails g devise_invitable:upgrade User -p`
|
|
29
36
|
if DEVISE_ORM == :active_record
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
37
|
+
assert_match(%r|create.*db/migrate/\d{14}_upgrade_users_devise_invitations\.rb|, @output)
|
|
38
|
+
else
|
|
39
|
+
assert_match(/devise_invitable:migrate_invitations\[User\]/, @output)
|
|
33
40
|
end
|
|
34
41
|
end
|
|
35
42
|
end
|
|
@@ -152,7 +152,7 @@ class InvitationTest < ActionDispatch::IntegrationTest
|
|
|
152
152
|
|
|
153
153
|
visit new_user_password_path
|
|
154
154
|
fill_in 'user_email', with: 'valid@email.com'
|
|
155
|
-
|
|
155
|
+
find('input[type="submit"]').click
|
|
156
156
|
|
|
157
157
|
visit edit_user_password_path(reset_password_token: Thread.current[:token])
|
|
158
158
|
set_password visit: false, button: 'Change my password'
|
|
@@ -209,7 +209,7 @@ class InvitationTest < ActionDispatch::IntegrationTest
|
|
|
209
209
|
assert_equal 2, user.invitation_limit
|
|
210
210
|
end
|
|
211
211
|
|
|
212
|
-
test 'should
|
|
212
|
+
test 'should decrement invitation limit for each invitation record' do
|
|
213
213
|
User.stubs(:invitation_limit).returns(3)
|
|
214
214
|
|
|
215
215
|
user = create_full_user
|
|
@@ -227,7 +227,7 @@ class InvitationTest < ActionDispatch::IntegrationTest
|
|
|
227
227
|
assert_equal root_path, current_path
|
|
228
228
|
assert page.has_css?('p#notice', text: 'An invitation email has been sent to user@test.com.')
|
|
229
229
|
user = User.find(user.id)
|
|
230
|
-
assert_equal
|
|
230
|
+
assert_equal 1, user.invitation_limit
|
|
231
231
|
end
|
|
232
232
|
|
|
233
233
|
test 'invited_by should be set when user invites someone' do
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
require 'test_helper'
|
|
2
|
+
require 'model_tests_helper'
|
|
3
|
+
|
|
4
|
+
class SeparateInvitationsTest < ActiveSupport::TestCase
|
|
5
|
+
setup do
|
|
6
|
+
@create_resource_on_invitation = User.create_resource_on_invitation
|
|
7
|
+
DeviseInvitable::Invitation.delete_all
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
teardown do
|
|
11
|
+
User.create_resource_on_invitation = @create_resource_on_invitation
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
test 'an invitation can be persisted without creating a resource' do
|
|
15
|
+
User.create_resource_on_invitation = false
|
|
16
|
+
registered = User.create!(email: 'registered@example.com', password: '123456789')
|
|
17
|
+
|
|
18
|
+
invited = User.invite!(email: 'deferred@example.com')
|
|
19
|
+
invitation = DeviseInvitable::Invitation.first
|
|
20
|
+
|
|
21
|
+
assert_predicate invited, :new_record?
|
|
22
|
+
assert_equal 0, User.where(email: 'deferred@example.com').count
|
|
23
|
+
assert_equal({ 'email' => 'deferred@example.com' }, invitation.invite_key.to_h.stringify_keys)
|
|
24
|
+
assert_nil invitation.invitable_id
|
|
25
|
+
assert_nil invitation.resource_created_at
|
|
26
|
+
assert_includes User.no_active_invitation, registered
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
test 'accepting one of multiple invitations creates the resource and supersedes the others' do
|
|
30
|
+
User.create_resource_on_invitation = false
|
|
31
|
+
first_inviter = User.create!(email: 'first@example.com', password: '123456789')
|
|
32
|
+
second_inviter = User.create!(email: 'second@example.com', password: '123456789')
|
|
33
|
+
|
|
34
|
+
User.invite!({ email: 'multiple@example.com' }, first_inviter)
|
|
35
|
+
first = DeviseInvitable::Invitation.order(id: :asc).first
|
|
36
|
+
User.invite!({ email: 'multiple@example.com' }, second_inviter)
|
|
37
|
+
accepted_token = Thread.current[:token]
|
|
38
|
+
|
|
39
|
+
user = User.accept_invitation!(
|
|
40
|
+
invitation_token: accepted_token,
|
|
41
|
+
password: '123456789',
|
|
42
|
+
password_confirmation: '123456789'
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
assert_predicate user, :persisted?
|
|
46
|
+
refute_predicate user, :invitation_pending?
|
|
47
|
+
assert DeviseInvitable::Invitation.accepted.where(invitable: user).exists?
|
|
48
|
+
assert_equal 'accepted_other_invitation', first.reload.superseded_reason
|
|
49
|
+
assert_equal first_inviter, first.invited_by
|
|
50
|
+
assert_equal second_inviter, DeviseInvitable::Invitation.accepted.find_by!(invitable: user).invited_by
|
|
51
|
+
assert_equal user, first.invitable
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
test 'resending stages the new inviter without changing the previous invitation' do
|
|
55
|
+
User.create_resource_on_invitation = true
|
|
56
|
+
first_inviter = User.create!(email: 'first@example.com', password: '123456789')
|
|
57
|
+
second_inviter = User.create!(email: 'second@example.com', password: '123456789')
|
|
58
|
+
|
|
59
|
+
User.invite!({ email: 'multiple@example.com' }, first_inviter)
|
|
60
|
+
first_invitation = DeviseInvitable::Invitation.order(id: :asc).first
|
|
61
|
+
|
|
62
|
+
User.invite!({ email: 'multiple@example.com' }, second_inviter) do |invitable|
|
|
63
|
+
assert_equal second_inviter, invitable.invited_by
|
|
64
|
+
assert_equal second_inviter, invitable.invited_by_for_next_invitation
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
assert_equal first_inviter, first_invitation.reload.invited_by
|
|
68
|
+
assert_equal second_inviter, DeviseInvitable::Invitation.order(id: :asc).last.invited_by
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
test 'an inviter exposes its sent invitations' do
|
|
72
|
+
inviter = Admin.create!(email: 'admin@example.com', password: '123456789')
|
|
73
|
+
|
|
74
|
+
User.invite!({ email: 'invited@example.com' }, inviter)
|
|
75
|
+
|
|
76
|
+
assert_equal [DeviseInvitable::Invitation.last], inviter.sent_invitations.to_a
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
test 'deleting an inviter preserves sent invitations' do
|
|
80
|
+
inviter = Admin.create!(email: 'admin@example.com', password: '123456789')
|
|
81
|
+
User.invite!({ email: 'invited@example.com' }, inviter)
|
|
82
|
+
invitation = DeviseInvitable::Invitation.last
|
|
83
|
+
|
|
84
|
+
inviter.destroy!
|
|
85
|
+
|
|
86
|
+
assert_predicate invitation.reload, :persisted?
|
|
87
|
+
assert_nil invitation.invited_by
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
test 'a placeholder is inactive until an invitation is accepted' do
|
|
91
|
+
User.create_resource_on_invitation = true
|
|
92
|
+
|
|
93
|
+
user = User.invite!(email: 'placeholder@example.com')
|
|
94
|
+
user.update!(confirmed_at: Time.now.utc)
|
|
95
|
+
|
|
96
|
+
assert_predicate user, :invitation_pending?
|
|
97
|
+
refute_predicate user, :active_for_authentication?
|
|
98
|
+
assert_nil user.valid_password?('unknown')
|
|
99
|
+
|
|
100
|
+
user.password = user.password_confirmation = '123456789'
|
|
101
|
+
assert user.accept_invitation!
|
|
102
|
+
refute_predicate user.reload, :invitation_pending?
|
|
103
|
+
assert_predicate user, :active_for_authentication?
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
test 'resource attributes raise when placeholder creation is disabled' do
|
|
107
|
+
User.create_resource_on_invitation = false
|
|
108
|
+
|
|
109
|
+
error = assert_raises DeviseInvitable::ResourceAttributesRequirePlaceholderError do
|
|
110
|
+
User.invite!(email: 'attributes@example.com', username: 'invited')
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
assert_includes error.message, 'username'
|
|
114
|
+
assert_includes error.message, 'create_resource_on_invitation is disabled'
|
|
115
|
+
assert_equal 0, DeviseInvitable::Invitation.count
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
test 'resource attributes assigned by a block raise when placeholder creation is disabled' do
|
|
119
|
+
User.create_resource_on_invitation = false
|
|
120
|
+
|
|
121
|
+
error = assert_raises DeviseInvitable::ResourceAttributesRequirePlaceholderError do
|
|
122
|
+
User.invite!(email: 'attributes@example.com') do |invitable|
|
|
123
|
+
invitable.username = 'invited'
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
assert_includes error.message, 'username'
|
|
128
|
+
assert_equal 0, DeviseInvitable::Invitation.count
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
test 'deferred invitations validate their invite keys' do
|
|
132
|
+
User.create_resource_on_invitation = false
|
|
133
|
+
|
|
134
|
+
invited = User.invite!(email: 'not-an-email')
|
|
135
|
+
|
|
136
|
+
assert_includes invited.errors.details[:email], error: :invalid
|
|
137
|
+
assert_equal 0, DeviseInvitable::Invitation.count
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
test 'legacy invitation data migration is idempotent' do
|
|
141
|
+
skip unless DEVISE_ORM == :mongoid
|
|
142
|
+
|
|
143
|
+
user = User.create!(email: 'legacy@example.com', password: '123456789')
|
|
144
|
+
created_at = 1.day.ago
|
|
145
|
+
User.collection.find(_id: user.id).update_one(
|
|
146
|
+
'$set' => {
|
|
147
|
+
invitation_token: 'legacy-digest',
|
|
148
|
+
invitation_created_at: created_at,
|
|
149
|
+
invitation_sent_at: created_at,
|
|
150
|
+
invited_by_type: 'User'
|
|
151
|
+
}
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
assert_equal 1, DeviseInvitable::Migrator.call(User)
|
|
155
|
+
assert_equal 0, DeviseInvitable::Migrator.call(User)
|
|
156
|
+
|
|
157
|
+
invitation = DeviseInvitable::Invitation.find_by!(invitation_token: 'legacy-digest')
|
|
158
|
+
assert_equal user, invitation.invitable
|
|
159
|
+
assert_equal({ 'email' => 'legacy@example.com' }, invitation.invite_key.to_h.stringify_keys)
|
|
160
|
+
assert_equal created_at.to_i, invitation.resource_created_at.to_i
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
test 'legacy invitation migration processes multiple resources in a batch' do
|
|
164
|
+
skip unless DEVISE_ORM == :mongoid
|
|
165
|
+
|
|
166
|
+
first = User.create!(email: 'first-legacy@example.com', password: '123456789')
|
|
167
|
+
second = User.create!(email: 'second-legacy@example.com', password: '123456789')
|
|
168
|
+
created_at = 1.day.ago
|
|
169
|
+
User.collection.find(_id: first.id).update_one(
|
|
170
|
+
'$set' => { invitation_token: 'first-legacy-digest', invitation_created_at: created_at }
|
|
171
|
+
)
|
|
172
|
+
User.collection.find(_id: second.id).update_one(
|
|
173
|
+
'$set' => { invitation_token: 'second-legacy-digest', invitation_created_at: created_at }
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
assert_equal 2, DeviseInvitable::Migrator.call(User)
|
|
177
|
+
assert_equal 2, DeviseInvitable::Invitation.where(:invitable_id.in => [first.id, second.id]).count
|
|
178
|
+
refute first.reload.attributes.key?('invitation_created_at')
|
|
179
|
+
refute second.reload.attributes.key?('invitation_created_at')
|
|
180
|
+
end
|
|
181
|
+
end
|
data/test/models_test.rb
CHANGED
|
@@ -32,10 +32,19 @@ class ModelsTest < ActiveSupport::TestCase
|
|
|
32
32
|
assert_nil User.invitation_limit
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
+
test 'does not create placeholder resources by default' do
|
|
36
|
+
assert_equal false, Devise.create_resource_on_invitation
|
|
37
|
+
end
|
|
38
|
+
|
|
35
39
|
test 'should have a default value for invite_key' do
|
|
36
40
|
assert !User.invite_key.nil?
|
|
37
41
|
end
|
|
38
42
|
|
|
43
|
+
test 'uses the root model class for invitations' do
|
|
44
|
+
assert_equal User, User.devise_invitable_base_class
|
|
45
|
+
assert_equal User, Invitable.devise_invitable_base_class
|
|
46
|
+
end
|
|
47
|
+
|
|
39
48
|
test 'set a custom value for invite_for' do
|
|
40
49
|
old_invite_for = User.invite_for
|
|
41
50
|
User.invite_for = 5.days
|
|
@@ -63,6 +72,31 @@ class ModelsTest < ActiveSupport::TestCase
|
|
|
63
72
|
User.invitation_limit = old_invitation_limit
|
|
64
73
|
end
|
|
65
74
|
|
|
75
|
+
test 'warns when deprecated invited_by settings are configured globally' do
|
|
76
|
+
DeviseInvitable.deprecator.expects(:warn).with(includes('invited_by_class_name')).once
|
|
77
|
+
DeviseInvitable.deprecator.expects(:warn).with(includes('invited_by_foreign_key')).once
|
|
78
|
+
|
|
79
|
+
Devise.invited_by_class_name = 'Admin'
|
|
80
|
+
Devise.invited_by_foreign_key = :inviter_id
|
|
81
|
+
ensure
|
|
82
|
+
Devise.class_variable_set(:@@invited_by_class_name, nil)
|
|
83
|
+
Devise.class_variable_set(:@@invited_by_foreign_key, nil)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
test 'warns when deprecated invited_by settings are configured on a model' do
|
|
87
|
+
DeviseInvitable.deprecator.expects(:warn).with(includes('invited_by_class_name')).once
|
|
88
|
+
DeviseInvitable.deprecator.expects(:warn).with(includes('invited_by_foreign_key')).once
|
|
89
|
+
|
|
90
|
+
model = Class.new(User) do
|
|
91
|
+
devise :invitable,
|
|
92
|
+
invited_by_class_name: 'Admin',
|
|
93
|
+
invited_by_foreign_key: :inviter_id
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
assert_equal 'Admin', model.invited_by_class_name
|
|
97
|
+
assert_equal :inviter_id, model.invited_by_foreign_key
|
|
98
|
+
end
|
|
99
|
+
|
|
66
100
|
test 'set a default value for validate_on_invite' do
|
|
67
101
|
assert_equal true, Invitable.validate_on_invite
|
|
68
102
|
end
|
|
@@ -34,7 +34,8 @@ class User < PARENT_MODEL_CLASS
|
|
|
34
34
|
validates_presence_of :encrypted_password, if: :password_required?
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
-
devise :database_authenticatable, :registerable, :validatable, :confirmable, :invitable, :recoverable
|
|
37
|
+
devise :database_authenticatable, :registerable, :validatable, :confirmable, :invitable, :recoverable,
|
|
38
|
+
create_resource_on_invitation: true
|
|
38
39
|
|
|
39
40
|
attr_accessor :after_invitation_created_callback_works, :after_invitation_accepted_callback_works, :bio, :token
|
|
40
41
|
validates :username, length: { maximum: 20 }
|
|
@@ -120,15 +120,6 @@ Devise.setup do |config|
|
|
|
120
120
|
# Default: true
|
|
121
121
|
# config.resend_invitation = false
|
|
122
122
|
|
|
123
|
-
# The class name of the inviting model. If this is nil,
|
|
124
|
-
# the #invited_by association is declared to be polymorphic.
|
|
125
|
-
# Default: nil
|
|
126
|
-
# config.invited_by_class_name = 'User'
|
|
127
|
-
|
|
128
|
-
# The foreign key to the inviting model (if invited_by_class_name is set)
|
|
129
|
-
# Default: :invited_by_id
|
|
130
|
-
# config.invited_by_foreign_key = :invited_by_id
|
|
131
|
-
|
|
132
123
|
# The column name used for counter_cache column. If this is nil,
|
|
133
124
|
# the #invited_by association is declared without counter_cache.
|
|
134
125
|
# Default: nil
|