devise_invitable 2.0.14 → 3.0.1

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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +66 -0
  3. data/README.md +166 -97
  4. data/app/controllers/devise/invitations_controller.rb +8 -2
  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 +353 -86
  10. data/lib/devise_invitable/rails.rb +10 -0
  11. data/lib/devise_invitable/version.rb +1 -1
  12. data/lib/devise_invitable.rb +40 -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 +24 -3
  26. data/test/mailers/invitation_mail_test.rb +1 -0
  27. data/test/models/invitable_test.rb +40 -2
  28. data/test/models/separate_invitations_test.rb +181 -0
  29. data/test/models_test.rb +34 -0
  30. data/test/rails_app/app/controllers/boolean_inviter_invitations_controller.rb +11 -0
  31. data/test/rails_app/app/controllers/restricted_invitations_controller.rb +9 -0
  32. data/test/rails_app/app/models/user.rb +8 -2
  33. data/test/rails_app/config/initializers/devise.rb +0 -9
  34. data/test/rails_app/config/routes.rb +2 -0
  35. data/test/rails_app/db/migrate/20100401102949_create_tables.rb +22 -0
  36. data/test/routes_test.rb +6 -0
  37. metadata +26 -13
  38. data/lib/generators/active_record/devise_invitable_generator.rb +0 -19
  39. data/lib/generators/active_record/templates/migration.rb +0 -22
  40. data/lib/generators/mongoid/devise_invitable_generator.rb +0 -8
@@ -1,6 +1,11 @@
1
1
  module DeviseInvitable
2
+ class AmbiguousInviteKeyError < StandardError; end
3
+ class ResourceAttributesRequirePlaceholderError < ArgumentError; end
4
+
2
5
  autoload :Inviter, 'devise_invitable/inviter'
6
+ autoload :Invitation, 'devise_invitable/invitation'
3
7
  autoload :Mailer, 'devise_invitable/mailer'
8
+ autoload :Migrator, 'devise_invitable/migrator'
4
9
  autoload :Mapping, 'devise_invitable/mapping'
5
10
  autoload :ParameterSanitizer, 'devise_invitable/parameter_sanitizer'
6
11
  module Controllers
@@ -10,6 +15,7 @@ module DeviseInvitable
10
15
  end
11
16
 
12
17
  require 'devise'
18
+ require 'active_support/deprecation'
13
19
  require 'devise_invitable/routes'
14
20
  require 'devise_invitable/rails'
15
21
 
@@ -58,16 +64,26 @@ module Devise
58
64
  mattr_accessor :resend_invitation
59
65
  @@resend_invitation = true
60
66
 
61
- # Public: The class name of the inviting model. If this is nil,
62
- # the #invited_by association is declared to be polymorphic. (default: nil)
67
+ # Deprecated compatibility setting. Invitations now always use a polymorphic
68
+ # invited_by association.
63
69
  mattr_accessor :invited_by_class_name
64
70
  @@invited_by_class_name = nil
65
71
 
66
- # Public: The foreign key to the inviting model (if invited_by_class_name is set)
67
- # (default: :invited_by_id)
72
+ def self.invited_by_class_name=(value)
73
+ DeviseInvitable.warn_deprecated_invited_by_setting(:invited_by_class_name)
74
+ @@invited_by_class_name = value
75
+ end
76
+
77
+ # Deprecated compatibility setting. The shared invitations table always uses
78
+ # invited_by_id and invited_by_type.
68
79
  mattr_accessor :invited_by_foreign_key
69
80
  @@invited_by_foreign_key = nil
70
81
 
82
+ def self.invited_by_foreign_key=(value)
83
+ DeviseInvitable.warn_deprecated_invited_by_setting(:invited_by_foreign_key)
84
+ @@invited_by_foreign_key = value
85
+ end
86
+
71
87
  # Public: The column name used for counter_cache column. If this is nil,
72
88
  # the #invited_by association is declared without counter_cache. (default: nil)
73
89
  mattr_accessor :invited_by_counter_cache
@@ -83,6 +99,26 @@ module Devise
83
99
  # set when user is invited or it will be set later (default: true).
84
100
  mattr_accessor :require_password_on_accepting
85
101
  @@require_password_on_accepting = true
102
+
103
+ # Public: Create the invited resource as soon as an invitation is sent.
104
+ # When false, only the invitation and its invite keys are persisted and the
105
+ # resource is created when the invitation is accepted.
106
+ mattr_accessor :create_resource_on_invitation
107
+ @@create_resource_on_invitation = false
108
+ end
109
+
110
+ module DeviseInvitable
111
+ def self.deprecator
112
+ @deprecator ||= ActiveSupport::Deprecation.new('3.0', 'DeviseInvitable')
113
+ end
114
+
115
+ def self.warn_deprecated_invited_by_setting(setting)
116
+ deprecator.warn(
117
+ "The #{setting} setting is deprecated and has no effect. " \
118
+ "Invitations now always use a polymorphic invited_by association " \
119
+ "with invited_by_id and invited_by_type."
120
+ )
121
+ end
86
122
  end
87
123
 
88
124
  Devise.add_module :invitable, controller: :invitations, model: 'devise_invitable/models', route: { invitation: [nil, :new, :accept] }
@@ -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
@@ -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
@@ -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
 
@@ -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
@@ -430,6 +441,30 @@ class InvitableTest < ActiveSupport::TestCase
430
441
  assert_equal [{error: :taken}], same_user.errors.details[:email]
431
442
  end
432
443
 
444
+ test 'should not modify associations when an existing user cannot be invited' do
445
+ existing_user = User.create!(email: 'existing@email.com', password: '123456789')
446
+ User.invite!({ email: 'invitee@email.com' }, existing_user)
447
+ sent_invitation = existing_user.sent_invitations.first
448
+
449
+ invited_user = User.invite!(
450
+ email: existing_user.email,
451
+ sent_invitation_ids: []
452
+ )
453
+
454
+ assert_equal [{ error: :taken }], invited_user.errors.details[:email]
455
+ assert_equal existing_user, sent_invitation.reload.invited_by
456
+ end
457
+
458
+ test 'should reject an invite key that matches multiple records' do
459
+ 2.times do
460
+ User.new(email: 'duplicate@email.com').save(validate: false)
461
+ end
462
+
463
+ assert_raises DeviseInvitable::AmbiguousInviteKeyError do
464
+ User.invite!(email: 'duplicate@email.com')
465
+ end
466
+ end
467
+
433
468
  test 'should return a record with errors if user with pending invitation was found by e-mail' do
434
469
  existing_user = User.invite!(email: 'valid@email.com')
435
470
  user = User.invite!(email: 'valid@email.com')
@@ -447,7 +482,7 @@ class InvitableTest < ActiveSupport::TestCase
447
482
  end
448
483
  end
449
484
 
450
- test 'should return a record with errors if user was found by e-mail with validate_on_invite' do
485
+ test 'should not assign or validate other attributes if user was found by e-mail' do
451
486
  begin
452
487
  validate_on_invite = User.validate_on_invite
453
488
  User.validate_on_invite = true
@@ -456,7 +491,8 @@ class InvitableTest < ActiveSupport::TestCase
456
491
  user = User.invite!(email: 'valid@email.com', username: 'a' * 50)
457
492
  assert_equal user, existing_user
458
493
  assert_equal [{error: :taken}], user.errors.details[:email]
459
- refute_empty user.errors[:username]
494
+ assert_empty user.errors[:username]
495
+ assert_nil user.username
460
496
  ensure
461
497
  User.validate_on_invite = validate_on_invite
462
498
  end
@@ -741,8 +777,10 @@ class InvitableTest < ActiveSupport::TestCase
741
777
 
742
778
  test "user.invite! should trigger callbacks" do
743
779
  user = User.new(email: 'valid@email.com')
780
+ assert_callbacks_not_fired :before_invitation_created, user
744
781
  assert_callbacks_not_fired :after_invitation_created, user
745
782
  user.invite!
783
+ assert_callbacks_fired :before_invitation_created, user
746
784
  assert_callbacks_fired :after_invitation_created, user
747
785
  end
748
786