devise_token_auth_multi_email 0.9.5 → 0.9.9

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d0c66feab8adcbd8779659cfca55d37dafc2f7b597478d249f252e6a8d359fa9
4
- data.tar.gz: 9e62f19a8c193602b0c8063d2b569a07ed6864a961d9363e95aef335907f74d6
3
+ metadata.gz: 43896725154b8afab36fcca00754a5bd5743cdbe08bf44401f0be917ec3cdb59
4
+ data.tar.gz: 4ddd80d829695020b27863ebe5891d9f3037f7070cd857658302e499eed3a711
5
5
  SHA512:
6
- metadata.gz: 25b3abeb4b9b3b13efca53a386affd1d080673c2815154ed60942aa7190d8860dd9a95bfd0306d34b32fe859150dac52684d6e5c678f3557d2637dcd89418b2b
7
- data.tar.gz: c722c5166cc96ca5145174b898f5c8490d7230a6b8485ff78439c9dc1dddae87e1b5f05e4fb3164f9030fd500ec19e44765b5ffd92fb11bbb9c65b061d14c87f
6
+ metadata.gz: 7429dbc8c2df36d2df63333d5e34de27b48de8313742942578c920a19de01c5c4fa16b02a702acabfa28a3c9c8a0c45eed728f11b5e9d006886d04da7190240c
7
+ data.tar.gz: c1d928e209ff6e3341c6b33233c1161b43ccae48bc208c4de62548164ad54d4e18f369b3325d3f2d7dbeda49b275a952d6876366cccc54c0009f760e45b6ebff
@@ -8,15 +8,18 @@ module DeviseTokenAuth::Concerns::UserOmniauthCallbacks
8
8
  validates :email, :devise_token_auth_email => true, allow_nil: true, allow_blank: true, if: lambda { uid_and_provider_defined? && email_provider? }
9
9
  validates_presence_of :uid, if: lambda { uid_and_provider_defined? && !email_provider? }
10
10
 
11
- # Only skip the uniqueness validation for models that use devise-multi_email
12
- # (detected by the presence of the multi_email_association class method, which
13
- # Devise::MultiEmail::ParentModelExtensions adds via :multi_email_authenticatable).
14
- # Standard models always validate; multi_email models manage uniqueness via the
15
- # emails association table instead.
16
- unless Gem.loaded_specs['devise-multi_email'] && respond_to?(:multi_email_association)
17
- # only validate unique emails among email registration users
18
- validates :email, uniqueness: { case_sensitive: false, scope: :provider }, on: :create, if: lambda { uid_and_provider_defined? && email_provider? }
19
- end
11
+ # Only validate email uniqueness for models that do NOT use devise-multi_email.
12
+ # Multi-email models manage uniqueness via the emails association table instead.
13
+ #
14
+ # The check is done at runtime (inside the lambda) rather than at class-load
15
+ # time, so that it works correctly regardless of the order in which modules are
16
+ # included. If the class responds to :multi_email_association at the point the
17
+ # validation is about to run, we know this is a multi-email model and skip the
18
+ # check. This also avoids calling column_for_attribute(:email) on a model that
19
+ # has no email column, which would otherwise cause MySQL to call
20
+ # nil.case_sensitive? and raise a NoMethodError.
21
+ validates :email, uniqueness: { case_sensitive: false, scope: :provider }, on: :create,
22
+ if: lambda { uid_and_provider_defined? && email_provider? && !self.class.respond_to?(:multi_email_association) }
20
23
 
21
24
  # keep uid in sync with email
22
25
  before_save :sync_uid
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DeviseTokenAuth
4
- VERSION = '0.9.5'.freeze
4
+ VERSION = '0.9.9'.freeze
5
5
  end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+
5
+ # Tests targeting DeviseTokenAuth::Concerns::ResourceFinder in isolation.
6
+ # The concern is exercised via DeviseTokenAuth::SessionsController, which
7
+ # includes it transitively through SetUserByToken.
8
+ class DeviseTokenAuth::Concerns::ResourceFinderTest < ActionController::TestCase
9
+ tests DeviseTokenAuth::SessionsController
10
+
11
+ describe DeviseTokenAuth::Concerns::ResourceFinder do
12
+ describe '#provider' do
13
+ it 'returns "email"' do
14
+ assert_equal 'email', @controller.provider
15
+ end
16
+ end
17
+
18
+ describe '#resource_class' do
19
+ it 'returns User when called without arguments (default devise mapping)' do
20
+ assert_equal User, @controller.resource_class
21
+ end
22
+
23
+ it 'returns the correct class for an explicit mapping argument' do
24
+ assert_equal Mang, @controller.resource_class(:mang)
25
+ end
26
+ end
27
+
28
+ if DEVISE_TOKEN_AUTH_ORM == :active_record
29
+ describe '#database_adapter' do
30
+ it 'returns a String identifying the configured database adapter' do
31
+ assert_kind_of String, @controller.database_adapter
32
+ refute_empty @controller.database_adapter
33
+ end
34
+ end
35
+
36
+ describe '#find_resource' do
37
+ describe 'standard user' do
38
+ before do
39
+ @existing_user = create(:user, :confirmed)
40
+ post :create, params: { email: @existing_user.email,
41
+ password: @existing_user.password }
42
+ end
43
+
44
+ it 'assigns the matching resource' do
45
+ assert_equal @existing_user, assigns(:resource)
46
+ end
47
+ end
48
+
49
+ describe 'non-existent email' do
50
+ before do
51
+ post :create, params: { email: 'nobody@example.com',
52
+ password: 'wrongpassword' }
53
+ end
54
+
55
+ it 'returns 401' do
56
+ assert_equal 401, response.status
57
+ end
58
+
59
+ it 'does not assign a resource' do
60
+ assert_nil assigns(:resource)
61
+ end
62
+ end
63
+ end
64
+ else
65
+ # Mongoid: connection_db_config is not available; database_adapter returns nil.
66
+ describe '#database_adapter' do
67
+ it 'returns nil for Mongoid (no SQL connection)' do
68
+ assert_nil @controller.database_adapter
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+
75
+ # Tests for the multi_email branch of find_resource require ActiveRecord and
76
+ # the /multi_email_auth routes. They are placed in a separate integration test
77
+ # class to avoid forcing a route/mapping override on the unit tests above.
78
+ if DEVISE_TOKEN_AUTH_ORM == :active_record
79
+ class DeviseTokenAuth::Concerns::ResourceFinderMultiEmailTest < ActionDispatch::IntegrationTest
80
+ SIGN_IN_PASSWORD = 'secret123'
81
+
82
+ describe 'ResourceFinder#find_resource with a multi_email user' do
83
+ before do
84
+ # Register and immediately confirm a MultiEmailUser.
85
+ @email = Faker::Internet.unique.email
86
+ post '/multi_email_auth',
87
+ params: { email: @email, password: SIGN_IN_PASSWORD,
88
+ password_confirmation: SIGN_IN_PASSWORD,
89
+ confirm_success_url: Faker::Internet.url }
90
+ assert_equal 200, response.status, "Setup failed: #{response.body}"
91
+ @user = assigns(:resource)
92
+ @user.confirm
93
+ end
94
+
95
+ it 'finds the multi_email user via the emails association' do
96
+ post '/multi_email_auth/sign_in',
97
+ params: { email: @email, password: SIGN_IN_PASSWORD }
98
+ assert_equal 200, response.status
99
+ assert_equal @user, assigns(:resource)
100
+ end
101
+
102
+ it 'returns 401 when email is unknown' do
103
+ post '/multi_email_auth/sign_in',
104
+ params: { email: 'nobody@example.com', password: SIGN_IN_PASSWORD }
105
+ assert_equal 401, response.status
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,199 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+
5
+ # Tests targeting DeviseTokenAuth::Concerns::SetUserByToken behaviors that are
6
+ # not already covered by demo_user_controller_test.rb or
7
+ # token_validations_controller_test.rb.
8
+ #
9
+ # The DemoUserController at /demo/members_only is used as the test target
10
+ # because it runs set_user_by_token via the before_action inherited from
11
+ # ApplicationController (which includes SetUserByToken).
12
+ class DeviseTokenAuth::Concerns::SetUserByTokenTest < ActionDispatch::IntegrationTest
13
+ include Warden::Test::Helpers
14
+
15
+ describe DeviseTokenAuth::Concerns::SetUserByToken do
16
+ before do
17
+ @resource = create(:user, :confirmed)
18
+ @auth_headers = @resource.create_new_auth_token
19
+ @token = @auth_headers['access-token']
20
+ @client_id = @auth_headers['client']
21
+ @uid = @auth_headers['uid']
22
+ end
23
+
24
+ # -------------------------------------------------------------------------
25
+ # #set_request_start
26
+ # -------------------------------------------------------------------------
27
+ describe '#set_request_start' do
28
+ before do
29
+ age_token(@resource, @client_id)
30
+ get '/demo/members_only', params: {}, headers: @auth_headers
31
+ end
32
+
33
+ it 'sets @request_started_at to the current time' do
34
+ assert_kind_of Time, assigns(:request_started_at)
35
+ end
36
+
37
+ it 'initializes @resource' do
38
+ refute_nil assigns(:resource)
39
+ end
40
+
41
+ it 'initializes @token' do
42
+ refute_nil assigns(:token)
43
+ end
44
+ end
45
+
46
+ # -------------------------------------------------------------------------
47
+ # #set_user_by_token – authentication via query parameters
48
+ # -------------------------------------------------------------------------
49
+ describe 'token auth via query params' do
50
+ before do
51
+ age_token(@resource, @client_id)
52
+ # Pass the three auth values as query-string parameters, not headers.
53
+ get '/demo/members_only',
54
+ params: {
55
+ 'access-token' => @token,
56
+ 'client' => @client_id,
57
+ 'uid' => @uid
58
+ }
59
+ end
60
+
61
+ it 'returns 200' do
62
+ assert_equal 200, response.status
63
+ end
64
+
65
+ it 'authenticates the correct user' do
66
+ assert_equal @resource, assigns(:resource)
67
+ end
68
+
69
+ it 'returns a new access token in the response headers' do
70
+ assert response.headers['access-token'].present?
71
+ end
72
+ end
73
+
74
+ # -------------------------------------------------------------------------
75
+ # #set_user_by_token – authentication via auth cookie
76
+ # -------------------------------------------------------------------------
77
+ describe 'token auth via auth cookie' do
78
+ before do
79
+ DeviseTokenAuth.cookie_enabled = true
80
+
81
+ # Sign in to receive the auth cookie from the server.
82
+ post '/auth/sign_in',
83
+ params: { email: @resource.email, password: @resource.password }
84
+ assert_equal 200, response.status, "Sign-in failed: #{response.body}"
85
+
86
+ # The integration session retains the Set-Cookie from sign-in.
87
+ # Make a protected request without explicit auth headers; the cookie
88
+ # should carry the credentials for set_user_by_token to use.
89
+ get '/demo/members_only', params: {}, headers: {}
90
+ end
91
+
92
+ after do
93
+ DeviseTokenAuth.cookie_enabled = false
94
+ end
95
+
96
+ it 'returns 200' do
97
+ assert_equal 200, response.status
98
+ end
99
+
100
+ it 'authenticates the correct user' do
101
+ assert_equal @resource, assigns(:resource)
102
+ end
103
+ end
104
+
105
+ # -------------------------------------------------------------------------
106
+ # #decode_bearer_token – tested indirectly via /auth/validate_token
107
+ # -------------------------------------------------------------------------
108
+ describe '#decode_bearer_token' do
109
+ before do
110
+ age_token(@resource, @client_id)
111
+ end
112
+
113
+ describe 'with a blank Authorization header' do
114
+ before do
115
+ get '/auth/validate_token', params: {}, headers: {}
116
+ end
117
+
118
+ it 'returns 401 (no credentials provided)' do
119
+ assert_equal 401, response.status
120
+ end
121
+ end
122
+
123
+ describe 'with an invalid base64 Bearer token' do
124
+ before do
125
+ get '/auth/validate_token', params: {},
126
+ headers: { 'Authorization' => 'Bearer not-valid-base64!!!' }
127
+ end
128
+
129
+ it 'returns 401 (decoded token treated as empty hash)' do
130
+ assert_equal 401, response.status
131
+ end
132
+ end
133
+
134
+ describe 'with valid base64 but non-JSON payload' do
135
+ before do
136
+ non_json_token = Base64.strict_encode64('this is not json')
137
+ get '/auth/validate_token', params: {},
138
+ headers: { 'Authorization' => "Bearer #{non_json_token}" }
139
+ end
140
+
141
+ it 'returns 401 (JSON parse error rescued to empty hash)' do
142
+ assert_equal 401, response.status
143
+ end
144
+ end
145
+
146
+ describe 'with a valid Bearer token (correct uid + access-token + client)' do
147
+ before do
148
+ encoded = Base64.strict_encode64(@auth_headers.to_json)
149
+ get '/auth/validate_token', params: {},
150
+ headers: { 'Authorization' => "Bearer #{encoded}" }
151
+ end
152
+
153
+ it 'returns 200' do
154
+ assert_equal 200, response.status
155
+ end
156
+
157
+ it 'authenticates the correct user' do
158
+ assert_equal @resource, assigns(:resource)
159
+ end
160
+ end
161
+ end
162
+
163
+ # -------------------------------------------------------------------------
164
+ # #set_user_by_token – no credentials at all → resource is nil
165
+ # -------------------------------------------------------------------------
166
+ describe 'when no auth credentials are provided' do
167
+ before do
168
+ get '/demo/members_only', params: {}, headers: {}
169
+ end
170
+
171
+ it 'returns 401' do
172
+ assert_equal 401, response.status
173
+ end
174
+
175
+ it 'does not authenticate a resource' do
176
+ assert_nil assigns(:resource)
177
+ end
178
+ end
179
+
180
+ # -------------------------------------------------------------------------
181
+ # #update_auth_header – cookie updated on each request when cookie_enabled
182
+ # -------------------------------------------------------------------------
183
+ describe 'update_auth_header with cookie_enabled' do
184
+ before do
185
+ DeviseTokenAuth.cookie_enabled = true
186
+ age_token(@resource, @client_id)
187
+ get '/demo/members_only', params: {}, headers: @auth_headers
188
+ end
189
+
190
+ after do
191
+ DeviseTokenAuth.cookie_enabled = false
192
+ end
193
+
194
+ it 'sets the auth cookie on the response' do
195
+ assert response.cookies[DeviseTokenAuth.cookie_name].present?
196
+ end
197
+ end
198
+ end
199
+ end
@@ -46,11 +46,26 @@ class MultiEmailRegistrationsControllerTest < ActionDispatch::IntegrationTest
46
46
  assert MultiEmailUser.respond_to?(:find_by_email)
47
47
  end
48
48
 
49
- test 'MultiEmailUser does NOT carry the concern uniqueness validator' do
50
- # email uniqueness is handled by the emails table, not the user model
51
- refute MultiEmailUser.validators_on(:email).any? { |v|
52
- v.is_a?(ActiveRecord::Validations::UniquenessValidator)
53
- }
49
+ test 'concern uniqueness validator never fires for MultiEmailUser' do
50
+ # The concern adds a uniqueness validator with a runtime :if guard that
51
+ # skips it whenever the model responds to :multi_email_association.
52
+ # This ensures the validator is never exercised on multi-email models
53
+ # (which have no email column and would otherwise crash on MySQL via
54
+ # nil.case_sensitive?).
55
+ #
56
+ # The :if lambda is called via instance_exec on the record (arity 0),
57
+ # so self inside the lambda is the user instance.
58
+ user = MultiEmailUser.new(provider: 'email', uid: 'test@example.com')
59
+ MultiEmailUser.validators_on(:email).select { |v|
60
+ v.is_a?(ActiveRecord::Validations::UniquenessValidator) &&
61
+ Array(v.options[:scope]).include?(:provider)
62
+ }.each do |validator|
63
+ if_procs = Array(validator.options[:if]).select { |c| c.respond_to?(:call) }
64
+ refute if_procs.empty?,
65
+ 'DTA concern uniqueness validator must have a runtime :if guard'
66
+ assert if_procs.none? { |c| user.instance_exec(&c) },
67
+ 'DTA concern uniqueness validator should not fire for multi-email models'
68
+ end
54
69
  end
55
70
 
56
71
  test 'MultiEmailUserEmail has email uniqueness validator from EmailValidatable' do
@@ -133,5 +148,147 @@ class MultiEmailRegistrationsControllerTest < ActionDispatch::IntegrationTest
133
148
  assert_not_empty @data['errors']
134
149
  end
135
150
  end
151
+
152
+ # -----------------------------------------------------------------------
153
+ # Shared helper: register, confirm, and sign in a MultiEmailUser.
154
+ # Returns [user, auth_headers].
155
+ # -----------------------------------------------------------------------
156
+ def sign_in_confirmed_user(email: nil)
157
+ email ||= Faker::Internet.unique.email
158
+ post '/multi_email_auth', params: registration_params(email: email)
159
+ assert_equal 200, response.status, "Setup registration failed: #{response.body}"
160
+ user = assigns(:resource)
161
+ user.confirm
162
+
163
+ post '/multi_email_auth/sign_in',
164
+ params: { email: email, password: 'secret123' }
165
+ assert_equal 200, response.status, "Sign-in failed: #{response.body}"
166
+
167
+ auth_headers = {
168
+ 'access-token' => response.headers['access-token'],
169
+ 'client' => response.headers['client'],
170
+ 'uid' => response.headers['uid'],
171
+ 'token-type' => response.headers['token-type']
172
+ }
173
+ [user, auth_headers]
174
+ end
175
+
176
+ # -----------------------------------------------------------------------
177
+ # Update account (PUT /multi_email_auth)
178
+ # -----------------------------------------------------------------------
179
+ describe 'account update' do
180
+ describe 'successful account update (name field)' do
181
+ before do
182
+ @user, @auth_headers = sign_in_confirmed_user
183
+ age_token(@user, @auth_headers['client'])
184
+
185
+ put '/multi_email_auth',
186
+ params: { name: 'Updated Name' },
187
+ headers: @auth_headers
188
+ @data = JSON.parse(response.body)
189
+ end
190
+
191
+ test 'request is successful' do
192
+ assert_equal 200, response.status
193
+ end
194
+
195
+ test 'response status is success' do
196
+ assert_equal 'success', @data['status']
197
+ end
198
+
199
+ test 'updated name is reflected in the response' do
200
+ assert_equal 'Updated Name', @data['data']['name']
201
+ end
202
+
203
+ test 'name is persisted to the database' do
204
+ @user.reload
205
+ assert_equal 'Updated Name', @user.name
206
+ end
207
+ end
208
+
209
+ describe 'account update without authentication' do
210
+ before do
211
+ put '/multi_email_auth',
212
+ params: { name: 'Unauthenticated Update' }
213
+ @data = JSON.parse(response.body)
214
+ end
215
+
216
+ test 'request fails with 404' do
217
+ assert_equal 404, response.status
218
+ end
219
+
220
+ test 'user not found error is returned' do
221
+ assert @data['errors']
222
+ end
223
+ end
224
+
225
+ describe 'account update with empty body' do
226
+ before do
227
+ @user, @auth_headers = sign_in_confirmed_user
228
+ age_token(@user, @auth_headers['client'])
229
+
230
+ put '/multi_email_auth',
231
+ params: {},
232
+ headers: @auth_headers
233
+ @data = JSON.parse(response.body)
234
+ end
235
+
236
+ test 'request fails with 422' do
237
+ assert_equal 422, response.status
238
+ end
239
+ end
240
+ end
241
+
242
+ # -----------------------------------------------------------------------
243
+ # Destroy account (DELETE /multi_email_auth)
244
+ # -----------------------------------------------------------------------
245
+ describe 'account destroy' do
246
+ describe 'successful account deletion' do
247
+ before do
248
+ @user, @auth_headers = sign_in_confirmed_user
249
+ @user_id = @user.id
250
+ age_token(@user, @auth_headers['client'])
251
+
252
+ delete '/multi_email_auth', headers: @auth_headers
253
+ @data = JSON.parse(response.body)
254
+ end
255
+
256
+ test 'request is successful' do
257
+ assert_equal 200, response.status
258
+ end
259
+
260
+ test 'success status is returned' do
261
+ assert_equal 'success', @data['status']
262
+ end
263
+
264
+ test 'user is removed from the database' do
265
+ refute MultiEmailUser.where(id: @user_id).exists?,
266
+ 'MultiEmailUser should be deleted after destroy'
267
+ end
268
+
269
+ test 'associated email records are also deleted (dependent: :destroy)' do
270
+ refute MultiEmailUserEmail.where(multi_email_user_id: @user_id).exists?,
271
+ 'Email records should be destroyed with the user'
272
+ end
273
+ end
274
+
275
+ describe 'account deletion without authentication' do
276
+ before do
277
+ delete '/multi_email_auth'
278
+ @data = JSON.parse(response.body)
279
+ end
280
+
281
+ test 'request fails with 404' do
282
+ assert_equal 404, response.status
283
+ end
284
+
285
+ test 'error message is returned' do
286
+ assert @data['errors']
287
+ assert @data['errors'].include?(
288
+ I18n.t('devise_token_auth.registrations.account_to_destroy_not_found')
289
+ )
290
+ end
291
+ end
292
+ end
136
293
  end
137
294
  end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+
5
+ # Unit tests for DeviseTokenAuth::Concerns::ConfirmableSupport.
6
+ #
7
+ # This concern is included into models that have DeviseTokenAuth's
8
+ # send_confirmation_email setting enabled along with Devise's :confirmable
9
+ # module. It overrides postpone_email_change? to avoid relying on
10
+ # devise_will_save_change_to_email?, and exposes email_value_in_database
11
+ # as a Rails-version-safe helper.
12
+ #
13
+ # These tests use ConfirmableUser, which is the only model in the test app
14
+ # that has :confirmable and triggers ConfirmableSupport inclusion.
15
+ return unless DEVISE_TOKEN_AUTH_ORM == :active_record
16
+
17
+ class ConfirmableSupportConcernTest < ActiveSupport::TestCase
18
+ describe DeviseTokenAuth::Concerns::ConfirmableSupport do
19
+ # ConfirmableUser includes ConfirmableSupport via the
20
+ # DeviseTokenAuth.send_confirmation_email + :confirmable check in user.rb.
21
+ test 'ConfirmableUser includes ConfirmableSupport' do
22
+ assert ConfirmableUser.ancestors.include?(DeviseTokenAuth::Concerns::ConfirmableSupport),
23
+ 'Expected ConfirmableUser to include ConfirmableSupport'
24
+ end
25
+
26
+ # -------------------------------------------------------------------------
27
+ # email_value_in_database (protected)
28
+ # -------------------------------------------------------------------------
29
+ describe '#email_value_in_database' do
30
+ test 'returns the persisted email (not an in-memory change)' do
31
+ original_email = 'original@example.com'
32
+ resource = create(:confirmable_user, email: original_email)
33
+
34
+ # Change email in memory only — do not save
35
+ resource.email = 'inmemory@example.com'
36
+
37
+ persisted = resource.send(:email_value_in_database)
38
+ assert_equal original_email, persisted,
39
+ 'email_value_in_database should return the DB value, not the in-memory change'
40
+ end
41
+
42
+ test 'returns nil for a brand-new (unsaved) record' do
43
+ resource = ConfirmableUser.new(email: 'new@example.com')
44
+ result = resource.send(:email_value_in_database)
45
+ # A new record has no persisted value — expect nil or blank string
46
+ assert result.blank?,
47
+ "Expected blank value for unsaved record, got: #{result.inspect}"
48
+ end
49
+ end
50
+
51
+ # -------------------------------------------------------------------------
52
+ # postpone_email_change? (public via override)
53
+ # -------------------------------------------------------------------------
54
+ describe '#postpone_email_change?' do
55
+ test 'returns true when reconfirmable is enabled and email has changed' do
56
+ resource = create(:confirmable_user, email: 'before@example.com')
57
+
58
+ # Change email but do not save — postpone_email_change? inspects pending
59
+ # changes before the save happens.
60
+ resource.email = 'after@example.com'
61
+
62
+ assert resource.postpone_email_change?,
63
+ 'Expected postpone_email_change? to return true when email changed and reconfirmable is on'
64
+ end
65
+
66
+ test 'returns false when reconfirmable is disabled' do
67
+ swap ConfirmableUser, reconfirmable: false do
68
+ resource = create(:confirmable_user, email: 'reconf@example.com')
69
+ resource.email = 'reconf_new@example.com'
70
+ refute resource.postpone_email_change?,
71
+ 'Expected postpone_email_change? to return false when reconfirmable is off'
72
+ end
73
+ end
74
+
75
+ test 'returns false when email has not changed' do
76
+ resource = create(:confirmable_user, email: 'same@example.com')
77
+ # No change to email
78
+ refute resource.postpone_email_change?,
79
+ 'Expected postpone_email_change? to return false when email is unchanged'
80
+ end
81
+
82
+ test 'resets @bypass_confirmation_postpone after the check' do
83
+ resource = create(:confirmable_user, email: 'reset_test@example.com')
84
+ resource.instance_variable_set(:@bypass_confirmation_postpone, true)
85
+ resource.email = 'reset_new@example.com'
86
+
87
+ # The flag bypasses the postpone on the first call
88
+ refute resource.postpone_email_change?,
89
+ 'Expected postpone_email_change? to return false when bypass flag is set'
90
+
91
+ # After the first call the flag should be cleared
92
+ refute resource.instance_variable_get(:@bypass_confirmation_postpone),
93
+ '@bypass_confirmation_postpone should be reset to false after the call'
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,326 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+
5
+ # Unit tests for methods defined in DeviseTokenAuth::Concerns::User that are
6
+ # not already exercised by user_test.rb or the controller-level tests.
7
+ #
8
+ # Covered here:
9
+ # - build_auth_headers / build_bearer_token
10
+ # - confirmed?
11
+ # - token_validation_response
12
+ # - extend_batch_buffer
13
+ # - remove_tokens_after_password_reset / should_remove_tokens_after_password_reset?
14
+ # - does_token_match?
15
+ # - tokens_match? (class-level cache)
16
+ class UserConcernMethodsTest < ActiveSupport::TestCase
17
+ describe DeviseTokenAuth::Concerns::User do
18
+ # -------------------------------------------------------------------------
19
+ # Setup helpers
20
+ # -------------------------------------------------------------------------
21
+ def build_confirmed_user
22
+ create(:user, :confirmed)
23
+ end
24
+
25
+ # -------------------------------------------------------------------------
26
+ # build_auth_headers / build_bearer_token
27
+ # -------------------------------------------------------------------------
28
+ describe '#build_auth_headers' do
29
+ before do
30
+ @resource = build_confirmed_user
31
+ @auth = @resource.create_new_auth_token
32
+ @client_id = @auth['client']
33
+ @token = @auth['access-token']
34
+ @headers = @resource.build_auth_headers(@token, @client_id)
35
+ end
36
+
37
+ test 'includes access-token key' do
38
+ assert @headers.key?(DeviseTokenAuth.headers_names[:'access-token'])
39
+ end
40
+
41
+ test 'includes token-type key' do
42
+ assert @headers.key?(DeviseTokenAuth.headers_names[:'token-type'])
43
+ end
44
+
45
+ test 'includes client key' do
46
+ assert @headers.key?(DeviseTokenAuth.headers_names[:'client'])
47
+ end
48
+
49
+ test 'includes expiry key' do
50
+ assert @headers.key?(DeviseTokenAuth.headers_names[:'expiry'])
51
+ end
52
+
53
+ test 'includes uid key' do
54
+ assert @headers.key?(DeviseTokenAuth.headers_names[:'uid'])
55
+ end
56
+
57
+ test 'uid value matches resource uid' do
58
+ assert_equal @resource.uid, @headers[DeviseTokenAuth.headers_names[:'uid']]
59
+ end
60
+
61
+ test 'access-token value matches token' do
62
+ assert_equal @token, @headers[DeviseTokenAuth.headers_names[:'access-token']]
63
+ end
64
+
65
+ test 'token-type is Bearer' do
66
+ assert_equal 'Bearer', @headers[DeviseTokenAuth.headers_names[:'token-type']]
67
+ end
68
+ end
69
+
70
+ describe '#build_bearer_token' do
71
+ before do
72
+ @resource = build_confirmed_user
73
+ @auth = @resource.create_new_auth_token
74
+ @client_id = @auth['client']
75
+ @token = @auth['access-token']
76
+ end
77
+
78
+ test 'returns a hash with an Authorization key when cookies are disabled' do
79
+ DeviseTokenAuth.cookie_enabled = false
80
+ auth_payload = { 'uid' => @resource.uid }
81
+ result = @resource.build_bearer_token(auth_payload)
82
+ assert result.key?(DeviseTokenAuth.headers_names[:'authorization'])
83
+ end
84
+
85
+ test 'Authorization value starts with "Bearer "' do
86
+ DeviseTokenAuth.cookie_enabled = false
87
+ auth_payload = { 'uid' => @resource.uid }
88
+ result = @resource.build_bearer_token(auth_payload)
89
+ assert result[DeviseTokenAuth.headers_names[:'authorization']].start_with?('Bearer ')
90
+ end
91
+
92
+ test 'returns an empty hash when cookie_enabled is true' do
93
+ DeviseTokenAuth.cookie_enabled = true
94
+ auth_payload = { 'uid' => @resource.uid }
95
+ result = @resource.build_bearer_token(auth_payload)
96
+ assert_empty result
97
+ ensure
98
+ DeviseTokenAuth.cookie_enabled = false
99
+ end
100
+ end
101
+
102
+ # -------------------------------------------------------------------------
103
+ # confirmed?
104
+ # -------------------------------------------------------------------------
105
+ describe '#confirmed?' do
106
+ test 'returns true for a user without :confirmable devise module' do
107
+ # User does not include :confirmable, so confirmed? must return true.
108
+ resource = build_confirmed_user
109
+ refute resource.devise_modules.include?(:confirmable)
110
+ assert resource.confirmed?
111
+ end
112
+
113
+ if DEVISE_TOKEN_AUTH_ORM == :active_record
114
+ test 'returns true for a ConfirmableUser that has been confirmed' do
115
+ resource = create(:confirmable_user)
116
+ resource.confirm
117
+ assert resource.confirmed?
118
+ end
119
+
120
+ test 'returns false for a ConfirmableUser that has not been confirmed' do
121
+ resource = create(:confirmable_user)
122
+ # Reload to drop any in-memory state set during creation
123
+ resource.reload
124
+ refute resource.confirmed?
125
+ end
126
+ end
127
+ end
128
+
129
+ # -------------------------------------------------------------------------
130
+ # token_validation_response
131
+ # -------------------------------------------------------------------------
132
+ describe '#token_validation_response' do
133
+ before do
134
+ @resource = build_confirmed_user
135
+ @response = @resource.token_validation_response
136
+ end
137
+
138
+ test 'returns a hash' do
139
+ assert_kind_of Hash, @response
140
+ end
141
+
142
+ test 'does not include :tokens key' do
143
+ refute @response.key?('tokens'), 'tokens should be excluded from token_validation_response'
144
+ end
145
+
146
+ test 'does not include :created_at key' do
147
+ refute @response.key?('created_at'), 'created_at should be excluded'
148
+ end
149
+
150
+ test 'does not include :updated_at key' do
151
+ refute @response.key?('updated_at'), 'updated_at should be excluded'
152
+ end
153
+
154
+ test 'includes the email key' do
155
+ assert @response.key?('email')
156
+ end
157
+
158
+ test 'includes the uid key' do
159
+ assert @response.key?('uid')
160
+ end
161
+ end
162
+
163
+ # -------------------------------------------------------------------------
164
+ # extend_batch_buffer
165
+ # -------------------------------------------------------------------------
166
+ describe '#extend_batch_buffer' do
167
+ before do
168
+ @resource = build_confirmed_user
169
+ @auth = @resource.create_new_auth_token
170
+ @client_id = @auth['client']
171
+ @token = @auth['access-token']
172
+ age_token(@resource, @client_id)
173
+ end
174
+
175
+ test 'returns a hash with auth header keys' do
176
+ result = @resource.extend_batch_buffer(@token, @client_id)
177
+ assert result.key?(DeviseTokenAuth.headers_names[:'access-token'])
178
+ end
179
+
180
+ test 'updates the updated_at timestamp for the client token' do
181
+ # age_token set updated_at to the past; extend_batch_buffer should
182
+ # refresh it to approximately now.
183
+ @resource.extend_batch_buffer(@token, @client_id)
184
+ updated_at = @resource.tokens[@client_id]['updated_at']
185
+ # updated_at should now be within the last 5 seconds
186
+ assert updated_at.to_time >= Time.zone.now - 5.seconds,
187
+ 'updated_at should be refreshed to approximately now by extend_batch_buffer'
188
+ end
189
+
190
+ test 'persists the token record to the database' do
191
+ @resource.extend_batch_buffer(@token, @client_id)
192
+ reloaded = @resource.class.find(@resource.id)
193
+ assert reloaded.tokens[@client_id]
194
+ end
195
+ end
196
+
197
+ # -------------------------------------------------------------------------
198
+ # does_token_match?
199
+ # -------------------------------------------------------------------------
200
+ describe '#does_token_match?' do
201
+ before do
202
+ @resource = build_confirmed_user
203
+ @auth = @resource.create_new_auth_token
204
+ @client_id = @auth['client']
205
+ @token = @auth['access-token']
206
+ end
207
+
208
+ test 'returns false when token_hash is nil' do
209
+ refute @resource.does_token_match?(nil, @token)
210
+ end
211
+
212
+ test 'returns false when token does not match' do
213
+ token_hash = @resource.tokens[@client_id]['token']
214
+ refute @resource.does_token_match?(token_hash, 'wrong_token')
215
+ end
216
+
217
+ test 'returns true when token matches its hash' do
218
+ # Obtain the raw token before it gets rotated; use the original
219
+ # auth headers directly since create_new_auth_token returns the
220
+ # plaintext token in the headers hash.
221
+ token_hash = @resource.tokens[@client_id]['token']
222
+ assert @resource.does_token_match?(token_hash, @token)
223
+ end
224
+ end
225
+
226
+ # -------------------------------------------------------------------------
227
+ # DeviseTokenAuth::Concerns::User.tokens_match? (class-level)
228
+ # -------------------------------------------------------------------------
229
+ describe '.tokens_match?' do
230
+ test 'returns truthy when hash matches token' do
231
+ raw = DeviseTokenAuth::TokenFactory.create
232
+ assert DeviseTokenAuth::Concerns::User.tokens_match?(raw.token_hash, raw.token)
233
+ end
234
+
235
+ test 'returns falsy when hash does not match token' do
236
+ raw = DeviseTokenAuth::TokenFactory.create
237
+ refute DeviseTokenAuth::Concerns::User.tokens_match?(raw.token_hash, 'wrong')
238
+ end
239
+
240
+ test 'populates and reuses the equality cache' do
241
+ raw = DeviseTokenAuth::TokenFactory.create
242
+ # Reset the cache to isolate this test
243
+ DeviseTokenAuth::Concerns::User.instance_variable_set(:@token_equality_cache, nil)
244
+
245
+ # First call — cache is empty, result computed from BCrypt
246
+ first = DeviseTokenAuth::Concerns::User.tokens_match?(raw.token_hash, raw.token)
247
+ cache_after_first = DeviseTokenAuth::Concerns::User.instance_variable_get(:@token_equality_cache)
248
+
249
+ assert first, 'Expected tokens_match? to return truthy for a matching pair'
250
+ assert_equal 1, cache_after_first.size, 'Cache should contain one entry after first call'
251
+
252
+ # Second call — same inputs, result served from cache (no BCrypt re-computation)
253
+ second = DeviseTokenAuth::Concerns::User.tokens_match?(raw.token_hash, raw.token)
254
+ cache_after_second = DeviseTokenAuth::Concerns::User.instance_variable_get(:@token_equality_cache)
255
+
256
+ assert second, 'Cached result should also be truthy'
257
+ assert_equal 1, cache_after_second.size, 'Cache size should remain 1 (no duplicate entry)'
258
+ end
259
+ end
260
+
261
+ # -------------------------------------------------------------------------
262
+ # should_remove_tokens_after_password_reset? / remove_tokens_after_password_reset
263
+ # -------------------------------------------------------------------------
264
+ if DEVISE_TOKEN_AUTH_ORM == :active_record
265
+ describe '#should_remove_tokens_after_password_reset?' do
266
+ before do
267
+ @resource = build_confirmed_user
268
+ end
269
+
270
+ test 'returns false when remove_tokens_after_password_reset is false' do
271
+ DeviseTokenAuth.remove_tokens_after_password_reset = false
272
+ refute @resource.send(:should_remove_tokens_after_password_reset?)
273
+ end
274
+
275
+ test 'returns false when remove_tokens_after_password_reset is true but password has not changed' do
276
+ DeviseTokenAuth.remove_tokens_after_password_reset = true
277
+ # Reload so there is no pending change
278
+ @resource.reload
279
+ refute @resource.send(:should_remove_tokens_after_password_reset?)
280
+ ensure
281
+ DeviseTokenAuth.remove_tokens_after_password_reset = false
282
+ end
283
+
284
+ test 'returns true when remove_tokens_after_password_reset is true and password changed' do
285
+ DeviseTokenAuth.remove_tokens_after_password_reset = true
286
+ @resource.password = @resource.password_confirmation = 'NewSecret999!'
287
+ assert @resource.send(:should_remove_tokens_after_password_reset?)
288
+ ensure
289
+ DeviseTokenAuth.remove_tokens_after_password_reset = false
290
+ end
291
+ end
292
+
293
+ describe '#remove_tokens_after_password_reset' do
294
+ before do
295
+ @resource = build_confirmed_user
296
+ # Create multiple tokens on multiple clients
297
+ 3.times { @resource.create_new_auth_token }
298
+ @resource.save!
299
+ end
300
+
301
+ test 'keeps only the most recent token when password changes and setting is enabled' do
302
+ DeviseTokenAuth.remove_tokens_after_password_reset = true
303
+
304
+ # Simulate a password change
305
+ @resource.password = @resource.password_confirmation = 'NewSecret999!'
306
+ @resource.save!
307
+
308
+ assert_equal 1, @resource.tokens.count,
309
+ 'All but the most-recent token should be removed after password reset'
310
+ ensure
311
+ DeviseTokenAuth.remove_tokens_after_password_reset = false
312
+ end
313
+
314
+ test 'does not alter tokens when setting is disabled' do
315
+ DeviseTokenAuth.remove_tokens_after_password_reset = false
316
+ count_before = @resource.tokens.count
317
+
318
+ @resource.password = @resource.password_confirmation = 'NewSecret999!'
319
+ @resource.save!
320
+
321
+ assert_equal count_before, @resource.tokens.count
322
+ end
323
+ end
324
+ end
325
+ end
326
+ end
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+
5
+ # Unit tests for DeviseTokenAuth::Concerns::UserOmniauthCallbacks.
6
+ #
7
+ # The concern is included in every model that includes
8
+ # DeviseTokenAuth::Concerns::User. It is responsible for:
9
+ # - Keeping uid in sync with email for email-provider users (sync_uid).
10
+ # - Validating uid presence for OAuth providers.
11
+ # - Providing the email_provider? and uid_and_provider_defined? helpers.
12
+ class UserOmniauthCallbacksConcernTest < ActiveSupport::TestCase
13
+ describe DeviseTokenAuth::Concerns::UserOmniauthCallbacks do
14
+ # -------------------------------------------------------------------------
15
+ # #uid_and_provider_defined?
16
+ # -------------------------------------------------------------------------
17
+ describe '#uid_and_provider_defined?' do
18
+ test 'returns true for User (which has provider and uid columns)' do
19
+ resource = User.new
20
+ assert resource.send(:uid_and_provider_defined?)
21
+ end
22
+
23
+ if DEVISE_TOKEN_AUTH_ORM == :active_record
24
+ test 'returns true for MultiEmailUser' do
25
+ resource = MultiEmailUser.new
26
+ assert resource.send(:uid_and_provider_defined?)
27
+ end
28
+ end
29
+ end
30
+
31
+ # -------------------------------------------------------------------------
32
+ # #email_provider?
33
+ # -------------------------------------------------------------------------
34
+ describe '#email_provider?' do
35
+ test 'returns true when provider is "email"' do
36
+ resource = User.new(provider: 'email')
37
+ assert resource.send(:email_provider?)
38
+ end
39
+
40
+ test 'returns false when provider is not "email"' do
41
+ resource = User.new(provider: 'facebook')
42
+ refute resource.send(:email_provider?)
43
+ end
44
+
45
+ test 'returns false when provider is nil' do
46
+ resource = User.new(provider: nil)
47
+ refute resource.send(:email_provider?)
48
+ end
49
+ end
50
+
51
+ # -------------------------------------------------------------------------
52
+ # sync_uid (via before_save / before_create callbacks)
53
+ # -------------------------------------------------------------------------
54
+ describe 'sync_uid' do
55
+ if DEVISE_TOKEN_AUTH_ORM == :active_record
56
+ describe 'email-provider user' do
57
+ test 'uid is set to email when creating a new email-provider user' do
58
+ email = Faker::Internet.unique.email
59
+ resource = create(:user, email: email, provider: 'email')
60
+ assert_equal email, resource.uid,
61
+ 'uid should be synced from email on creation'
62
+ end
63
+
64
+ test 'uid is updated when email changes on an existing user' do
65
+ resource = create(:user, :confirmed)
66
+ new_email = Faker::Internet.unique.email
67
+ resource.email = new_email
68
+ resource.save(validate: false)
69
+ assert_equal new_email, resource.uid,
70
+ 'uid should stay in sync with email after update'
71
+ end
72
+ end
73
+
74
+ describe 'OAuth provider user' do
75
+ test 'uid is NOT overwritten with email for non-email providers' do
76
+ uid = '12345'
77
+ resource = build(:user, :facebook, uid: uid)
78
+ resource.save(validate: false)
79
+ assert_equal uid, resource.uid,
80
+ 'uid should remain unchanged for OAuth providers'
81
+ end
82
+ end
83
+ end
84
+ end
85
+
86
+ # -------------------------------------------------------------------------
87
+ # Validation: email presence (email provider)
88
+ # -------------------------------------------------------------------------
89
+ describe 'email presence validation' do
90
+ if DEVISE_TOKEN_AUTH_ORM == :active_record
91
+ test 'fails to save email-provider user without email' do
92
+ resource = User.new(provider: 'email', uid: '', password: 'secret123',
93
+ password_confirmation: 'secret123')
94
+ refute resource.save
95
+ assert resource.errors[:email].any?
96
+ end
97
+
98
+ test 'saves OAuth user without email' do
99
+ resource = User.new(provider: 'facebook', uid: '999', email: nil)
100
+ # skip uniqueness / other validations to test only email presence
101
+ resource.password = resource.password_confirmation = 'secret123'
102
+ result = resource.save(validate: false)
103
+ assert result
104
+ end
105
+ end
106
+ end
107
+
108
+ # -------------------------------------------------------------------------
109
+ # Validation: uid presence (OAuth provider)
110
+ # -------------------------------------------------------------------------
111
+ describe 'uid presence validation for OAuth users' do
112
+ if DEVISE_TOKEN_AUTH_ORM == :active_record
113
+ test 'fails to save OAuth user without uid' do
114
+ resource = User.new(provider: 'facebook', uid: nil)
115
+ resource.password = resource.password_confirmation = 'secret123'
116
+ resource.save
117
+ assert resource.errors[:uid].any?,
118
+ 'Expected a validation error on :uid for OAuth users with nil uid'
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: devise_token_auth_multi_email
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.5
4
+ version: 0.9.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lynn Hurley
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2026-04-08 00:00:00.000000000 Z
12
+ date: 2026-04-09 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rails
@@ -275,6 +275,8 @@ files:
275
275
  - test/controllers/demo_group_controller_test.rb
276
276
  - test/controllers/demo_mang_controller_test.rb
277
277
  - test/controllers/demo_user_controller_test.rb
278
+ - test/controllers/devise_token_auth/concerns/resource_finder_test.rb
279
+ - test/controllers/devise_token_auth/concerns/set_user_by_token_test.rb
278
280
  - test/controllers/devise_token_auth/confirmations_controller_test.rb
279
281
  - test/controllers/devise_token_auth/multi_email_coexistence_test.rb
280
282
  - test/controllers/devise_token_auth/multi_email_confirmations_controller_test.rb
@@ -389,10 +391,13 @@ files:
389
391
  - test/lib/generators/devise_token_auth/install_views_generator_test.rb
390
392
  - test/models/concerns/mongoid_support_test.rb
391
393
  - test/models/concerns/tokens_serialization_test.rb
394
+ - test/models/confirmable_support_concern_test.rb
392
395
  - test/models/confirmable_user_test.rb
393
396
  - test/models/multi_email_user_email_test.rb
394
397
  - test/models/multi_email_user_test.rb
395
398
  - test/models/only_email_user_test.rb
399
+ - test/models/user_concern_methods_test.rb
400
+ - test/models/user_omniauth_callbacks_concern_test.rb
396
401
  - test/models/user_test.rb
397
402
  - test/support/controllers/routes.rb
398
403
  - test/test_helper.rb
@@ -430,6 +435,8 @@ test_files:
430
435
  - test/controllers/demo_group_controller_test.rb
431
436
  - test/controllers/demo_mang_controller_test.rb
432
437
  - test/controllers/demo_user_controller_test.rb
438
+ - test/controllers/devise_token_auth/concerns/resource_finder_test.rb
439
+ - test/controllers/devise_token_auth/concerns/set_user_by_token_test.rb
433
440
  - test/controllers/devise_token_auth/confirmations_controller_test.rb
434
441
  - test/controllers/devise_token_auth/multi_email_coexistence_test.rb
435
442
  - test/controllers/devise_token_auth/multi_email_confirmations_controller_test.rb
@@ -544,10 +551,13 @@ test_files:
544
551
  - test/lib/generators/devise_token_auth/install_views_generator_test.rb
545
552
  - test/models/concerns/mongoid_support_test.rb
546
553
  - test/models/concerns/tokens_serialization_test.rb
554
+ - test/models/confirmable_support_concern_test.rb
547
555
  - test/models/confirmable_user_test.rb
548
556
  - test/models/multi_email_user_email_test.rb
549
557
  - test/models/multi_email_user_test.rb
550
558
  - test/models/only_email_user_test.rb
559
+ - test/models/user_concern_methods_test.rb
560
+ - test/models/user_omniauth_callbacks_concern_test.rb
551
561
  - test/models/user_test.rb
552
562
  - test/support/controllers/routes.rb
553
563
  - test/test_helper.rb