@@ -172,3 +172,168 @@ def authenticate(*args, **kwars):
172172 # Expect the log we emit
173173 with expected_log ('ansible_base.authentication.backend.logger' , "exception" , "Exception raised while trying to authenticate with" ):
174174 backend .AnsibleBaseAuth ().authenticate (None )
175+
176+
177+ @pytest .mark .django_db
178+ def test_last_login_from_with_attribute (local_authenticator , random_user , expected_log ):
179+ """Test that last_login_from is set when user has the attribute."""
180+ # Add last_login_from attribute to user
181+ random_user .last_login_from = None
182+
183+ # Create a mock authenticator plugin object with proper structure
184+ mock_authenticator_plugin = mock .MagicMock ()
185+ mock_authenticator_plugin .authenticate .return_value = random_user
186+ mock_authenticator_plugin .database_instance = local_authenticator
187+
188+ # Mock the save method to track calls
189+ with mock .patch .object (random_user , 'save' ) as mock_save :
190+ with mock .patch (
191+ "ansible_base.authentication.backend.get_authentication_backends" ,
192+ return_value = {local_authenticator .id : mock_authenticator_plugin },
193+ ):
194+ # Expected log message when user logs in
195+ with expected_log (
196+ 'ansible_base.authentication.backend.logger' ,
197+ "info" ,
198+ f'User { random_user .username } logged in from authenticator with ID "{ local_authenticator .id } "' ,
199+ ):
200+ auth_return = backend .AnsibleBaseAuth ().authenticate (None )
201+
202+ # Verify the user is returned
203+ assert auth_return == random_user
204+
205+ # Verify last_login_from was set to the authenticator database instance
206+ assert random_user .last_login_from == local_authenticator
207+
208+ # Verify save was called with the correct update_fields
209+ mock_save .assert_called_once_with (update_fields = ['last_login_from' ])
210+
211+
212+ @pytest .mark .django_db
213+ def test_last_login_from_without_attribute (local_authenticator , random_user , expected_log ):
214+ """Test that authentication works normally when user doesn't have last_login_from attribute."""
215+ # Ensure user doesn't have last_login_from attribute
216+ if hasattr (random_user , 'last_login_from' ):
217+ delattr (random_user , 'last_login_from' )
218+
219+ # Create a mock authenticator plugin object with proper structure
220+ mock_authenticator_plugin = mock .MagicMock ()
221+ mock_authenticator_plugin .authenticate .return_value = random_user
222+ mock_authenticator_plugin .database_instance = local_authenticator
223+
224+ # Mock the save method to track calls
225+ with mock .patch .object (random_user , 'save' ) as mock_save :
226+ with mock .patch (
227+ "ansible_base.authentication.backend.get_authentication_backends" ,
228+ return_value = {local_authenticator .id : mock_authenticator_plugin },
229+ ):
230+ with expected_log (
231+ 'ansible_base.authentication.backend.logger' ,
232+ "info" ,
233+ f'User { random_user .username } logged in from authenticator with ID "{ local_authenticator .id } "' ,
234+ ):
235+ auth_return = backend .AnsibleBaseAuth ().authenticate (None )
236+
237+ # Verify the user is returned
238+ assert auth_return == random_user
239+
240+ # Verify save was never called since user doesn't have last_login_from
241+ mock_save .assert_not_called ()
242+
243+
244+ @pytest .mark .django_db
245+ def test_last_login_from_multiple_authenticators (local_authenticator , github_enterprise_authenticator , random_user , expected_log ):
246+ """Test that last_login_from is set correctly when multiple authenticators are present."""
247+ # Add last_login_from attribute to user
248+ random_user .last_login_from = None
249+
250+ # Create mock authenticator plugin objects with proper structure
251+ mock_github_plugin = mock .MagicMock ()
252+ mock_github_plugin .authenticate .return_value = None
253+ mock_github_plugin .database_instance = github_enterprise_authenticator
254+
255+ mock_local_plugin = mock .MagicMock ()
256+ mock_local_plugin .authenticate .return_value = random_user
257+ mock_local_plugin .database_instance = local_authenticator
258+
259+ # Mock the save method to track calls
260+ with mock .patch .object (random_user , 'save' ) as mock_save :
261+ with mock .patch (
262+ "ansible_base.authentication.backend.get_authentication_backends" ,
263+ return_value = {github_enterprise_authenticator .id : mock_github_plugin , local_authenticator .id : mock_local_plugin },
264+ ):
265+ # Expected log message when user logs in
266+ with expected_log (
267+ 'ansible_base.authentication.backend.logger' ,
268+ "info" ,
269+ f'User { random_user .username } logged in from authenticator with ID "{ local_authenticator .id } "' ,
270+ ):
271+ auth_return = backend .AnsibleBaseAuth ().authenticate (None )
272+
273+ # Verify the user is returned
274+ assert auth_return == random_user
275+
276+ # Verify last_login_from was set to the local authenticator (the one that succeeded)
277+ assert random_user .last_login_from == local_authenticator
278+
279+ # Verify save was called with the correct update_fields
280+ mock_save .assert_called_once_with (update_fields = ['last_login_from' ])
281+
282+
283+ @pytest .mark .django_db
284+ def test_last_login_from_inactive_user (local_authenticator , random_user ):
285+ """Test that last_login_from is not set when user is inactive."""
286+ # Add last_login_from attribute to user but make user inactive
287+ random_user .last_login_from = None
288+ random_user .is_active = False
289+
290+ # Create a mock authenticator plugin object with proper structure
291+ mock_authenticator_plugin = mock .MagicMock ()
292+ mock_authenticator_plugin .authenticate .return_value = random_user
293+ mock_authenticator_plugin .database_instance = local_authenticator
294+
295+ # Mock the save method to track calls
296+ with mock .patch .object (random_user , 'save' ) as mock_save :
297+ with mock .patch (
298+ "ansible_base.authentication.backend.get_authentication_backends" ,
299+ return_value = {local_authenticator .id : mock_authenticator_plugin },
300+ ):
301+ auth_return = backend .AnsibleBaseAuth ().authenticate (None )
302+
303+ # Verify authentication failed (returns None for inactive user)
304+ assert auth_return is None
305+
306+ # Verify save was never called since authentication failed
307+ mock_save .assert_not_called ()
308+
309+ # Verify last_login_from was not changed
310+ assert random_user .last_login_from is None
311+
312+
313+ @pytest .mark .django_db
314+ def test_last_login_from_social_auth_failed (local_authenticator , random_user ):
315+ """Test that last_login_from is not set when social auth pipeline fails."""
316+ # Add last_login_from attribute to user
317+ random_user .last_login_from = None
318+
319+ # Create a mock authenticator plugin object with proper structure
320+ mock_authenticator_plugin = mock .MagicMock ()
321+ mock_authenticator_plugin .authenticate .return_value = SOCIAL_AUTH_PIPELINE_FAILED_STATUS
322+ mock_authenticator_plugin .database_instance = local_authenticator
323+
324+ # Mock the save method to track calls
325+ with mock .patch .object (random_user , 'save' ) as mock_save :
326+ with mock .patch (
327+ "ansible_base.authentication.backend.get_authentication_backends" ,
328+ return_value = {local_authenticator .id : mock_authenticator_plugin },
329+ ):
330+ auth_return = backend .AnsibleBaseAuth ().authenticate (None )
331+
332+ # Verify authentication failed (returns None)
333+ assert auth_return is None
334+
335+ # Verify save was never called since authentication failed
336+ mock_save .assert_not_called ()
337+
338+ # Verify last_login_from was not changed
339+ assert random_user .last_login_from is None
0 commit comments