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.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +41 -0
  3. data/README.md +108 -97
  4. data/app/controllers/devise/invitations_controller.rb +1 -1
  5. data/app/controllers/devise_invitable/registrations_controller.rb +15 -10
  6. data/lib/devise_invitable/invitation.rb +89 -0
  7. data/lib/devise_invitable/inviter.rb +23 -1
  8. data/lib/devise_invitable/migrator.rb +113 -0
  9. data/lib/devise_invitable/models.rb +328 -81
  10. data/lib/devise_invitable/rails.rb +10 -0
  11. data/lib/devise_invitable/version.rb +1 -1
  12. data/lib/devise_invitable.rb +39 -4
  13. data/lib/generators/active_record/devise_invitable_install_generator.rb +22 -0
  14. data/lib/generators/active_record/devise_invitable_upgrade_generator.rb +28 -0
  15. data/lib/generators/active_record/templates/create_devise_invitations.rb +23 -0
  16. data/lib/generators/active_record/templates/upgrade_devise_invitations.rb +72 -0
  17. data/lib/generators/devise_invitable/devise_invitable_generator.rb +2 -4
  18. data/lib/generators/devise_invitable/install_generator.rb +8 -10
  19. data/lib/generators/devise_invitable/upgrade_generator.rb +16 -0
  20. data/lib/generators/mongoid/devise_invitable_install_generator.rb +7 -0
  21. data/lib/generators/mongoid/devise_invitable_upgrade_generator.rb +11 -0
  22. data/lib/tasks/devise_invitable_tasks.rake +10 -0
  23. data/test/functional/registrations_controller_test.rb +24 -3
  24. data/test/generators_test.rb +16 -9
  25. data/test/integration/invitation_test.rb +3 -3
  26. data/test/mailers/invitation_mail_test.rb +1 -0
  27. data/test/models/separate_invitations_test.rb +181 -0
  28. data/test/models_test.rb +34 -0
  29. data/test/rails_app/app/models/user.rb +2 -1
  30. data/test/rails_app/config/initializers/devise.rb +0 -9
  31. data/test/rails_app/db/migrate/20100401102949_create_tables.rb +22 -0
  32. metadata +22 -13
  33. data/lib/generators/active_record/devise_invitable_generator.rb +0 -19
  34. data/lib/generators/active_record/templates/migration.rb +0 -22
  35. 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 in the given model. Also generate migration for ActiveRecord'
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 'Add DeviseInvitable config variables to the Devise initializer and copy DeviseInvitable locale files to your application.'
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(Regexp.new(/^\s# ==> Configuration for :invitable\n/))
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
- # The class name of the inviting model. If this is nil,
48
- # the #invited_by association is declared to be polymorphic.
49
- # Default: nil
50
- # config.invited_by_class_name = 'User'
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,7 @@
1
+ module Mongoid
2
+ module Generators
3
+ class DeviseInvitableInstallGenerator < Rails::Generators::Base
4
+ desc 'DeviseInvitable stores its Mongoid field definitions in the gem model.'
5
+ end
6
+ end
7
+ 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
- assert_not_nil @invitee.invitation_accepted_at
49
+ assert_nil @invitee.invitation_accepted_at
49
50
  assert_nil @invitee.invitation_token
50
- assert @invitee.invited_by_id.present?
51
- assert @invitee.invited_by_type.present?
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'
@@ -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 6 generators" do
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.include? generator
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
- assert @output.match(%r|invoke.* #{DEVISE_ORM}\n|)
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
- assert @output.match(%r|create.* db/migrate/\d{14}_devise_invitable_add_to_octopussies\.rb\n|)
31
- elsif DEVISE_ORM == :mongoid
32
- assert !@output.match(%r|create.* db/migrate/\d{14}_devise_invitable_add_to_octopussies\.rb\n|)
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
- click_button 'Send me reset password instructions'
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 not decrement invitation limit when trying to invite again a user which is invited' do
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 2, user.invitation_limit
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
@@ -101,6 +101,7 @@ class InvitationMailTest < ActionMailer::TestCase
101
101
 
102
102
  def initialize(*args); end
103
103
  def deliver; end
104
+ def deliver_now; end
104
105
  end
105
106
  Devise.mailer = 'InvitationMailTest::CustomMailer'
106
107
 
@@ -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