Skip to content

Commit a1f565d

Browse files
authored
AAP-44920 Isolate test that does reload to fix cross-talk failure (#722)
## Description As seen in #715, changes that are blatantly obvious unrelated will lead to ldap test failures. This isn't test flake per se. It's pytest-mp ordering. The test suite could _only ever pass_ with >1 workers, because not all tests can be ran in the same process, because the `importlib.reload` pollutes the globals, leading to failures. ## Type of Change - [ ] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [ ] Documentation update - [x] Test update - [ ] Refactoring (no functional changes) - [ ] Development environment change - [ ] Configuration change ## Self-Review Checklist - [x] I have performed a self-review of my code - [ ] I have added relevant comments to complex code sections - [ ] I have updated documentation where needed - [x] I have considered the security impact of these changes - [x] I have considered performance implications - [x] I have thought about error handling and edge cases - [x] I have tested the changes in my local environment ## Testing Instructions ``` pytest test_app/tests/authentication/authenticator_plugins/test_ldap.py test_app/tests/authentication/authenticator_plugins/test_ldap_get_or_build_user.py ``` Before this change you can just run ``` pytest test_app/tests/authentication/authenticator_plugins/test_ldap.py ``` and that will fail. This fixes it ### Prerequisites postgres running for those tests, as is standard ### Steps to Test command above ### Expected Results no fails ### Required Actions - [ ] Requires documentation updates - [ ] Requires downstream repository changes - [ ] Requires infrastructure/deployment changes - [ ] Requires coordination with other teams - [ ] Blocked by PR/MR: #XXX Blocks the other PR! ### Screenshots/Logs failures this fixes: ``` ============================================= ERRORS ============================================= _________ ERROR at setup of test_AuthenticatorPlugin_authenticate_authenticator_disabled _________ ldap_configuration = {'BIND_DN': 'cn=ldapadmin,dc=example,dc=org', 'BIND_PASSWORD': '$encrypted$UTF8$AESCBC$Z0FBQUFBQm9FQ1pVdG5EaEpNR3FXcEJpU1dHanV1ZkswTjFGZEdmcTR4MS1GaVdQdUl4cUx6UDBSNk5YUGtFRWxnQWZCc2Y5OG1raDZDd3J1eUZMOElwTWV0ZTg0WjB3TExjc0dBLXZzaHR1cWw1S3hsMVREaDg9', 'CONNECTION_OPTIONS': {'OPT_NETWORK_TIMEOUT': 30, 'OPT_REFERRALS': 0}, 'GROUP_SEARCH': ['ou=groups,dc=example,dc=org', 'SCOPE_SUBTREE', '(objectClass=groupOfNames)'], ...} @pytest.fixture def ldap_settings(ldap_configuration): > return LDAPSettings(defaults=ldap_configuration) test_app/tests/authentication/authenticator_plugins/test_ldap.py:459: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ self = <ansible_base.authentication.authenticator_plugins.ldap.LDAPSettings object at 0x7f7970ff70e0> prefix = 'AUTH_LDAP_' defaults = {'BIND_DN': 'cn=ldapadmin,dc=example,dc=org', 'BIND_PASSWORD': '$encrypted$UTF8$AESCBC$Z0FBQUFBQm9FQ1pVdG5EaEpNR3FXcEJpU1dHanV1ZkswTjFGZEdmcTR4MS1GaVdQdUl4cUx6UDBSNk5YUGtFRWxnQWZCc2Y5OG1raDZDd3J1eUZMOElwTWV0ZTg0WjB3TExjc0dBLXZzaHR1cWw1S3hsMVREaDg9', 'CONNECTION_OPTIONS': {'OPT_NETWORK_TIMEOUT': 30, 'OPT_REFERRALS': 0}, 'GROUP_SEARCH': ['ou=groups,dc=example,dc=org', 'SCOPE_SUBTREE', '(objectClass=groupOfNames)'], ...} def __init__(self, prefix: str = 'AUTH_LDAP_', defaults: dict = {}): # This init method double checks the passed defaults while initializing a settings objects print((LDAPSettings, self)) > super(LDAPSettings, self).__init__(prefix, defaults) E TypeError: super(type, obj): obj must be an instance or subtype of type ansible_base/authentication/authenticator_plugins/ldap.py:367: TypeError ------------------------------------- Captured stdout setup -------------------------------------- (<class 'ansible_base.authentication.authenticator_plugins.ldap.LDAPSettings'>, <ansible_base.authentication.authenticator_plugins.ldap.LDAPSettings object at 0x7f7970ff70e0>) ------------------------------------- Captured stderr setup -------------------------------------- 2025-04-29 01:07:32,088 DEBUG [] ansible_base.authentication.authenticator_plugins.utils Attempting to load class ansible_base.authentication.authenticator_plugins.ldap 2025-04-29 01:07:32,088 DEBUG [] ansible_base.authentication.authenticator_plugins.utils Attempting to load class ansible_base.authentication.authenticator_plugins.ldap 2025-04-29 01:07:32,089 ERROR [] ansible_base.lib.utils.models SYSTEM_USERNAME is set to _system but no user with that username exists. 2025-04-29 01:07:32,089 ERROR [] ansible_base.lib.utils.models SYSTEM_USERNAME is set to _system but no user with that username exists. 2025-04-29 01:07:32,092 INFO [] ansible_base.lib.utils.create_system_user Created system user _system 2025-04-29 01:07:32,092 INFO [] ansible_base.lib.utils.create_system_user Created system user _system --------------------------------------- Captured log setup --------------------------------------- DEBUG ansible_base.authentication.authenticator_plugins.utils:utils.py:35 Attempting to load class ansible_base.authentication.authenticator_plugins.ldap ERROR ansible_base.lib.utils.models:models.py:117 SYSTEM_USERNAME is set to _system but no user with that username exists. INFO ansible_base.lib.utils.create_system_user:create_system_user.py:34 Created system user _system ________________________ ERROR at setup of test_ldap_validate_ldap_filter ________________________ ldap_configuration = {'BIND_DN': 'cn=ldapadmin,dc=example,dc=org', 'BIND_PASSWORD': 'securepassword', 'CONNECTION_OPTIONS': {'OPT_NETWORK_TIMEOUT': 30, 'OPT_REFERRALS': 0}, 'GROUP_SEARCH': ['ou=groups,dc=example,dc=org', 'SCOPE_SUBTREE', '(objectClass=groupOfNames)'], ...} @pytest.fixture def ldap_settings(ldap_configuration): > return LDAPSettings(defaults=ldap_configuration) test_app/tests/authentication/authenticator_plugins/test_ldap.py:459: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ self = <ansible_base.authentication.authenticator_plugins.ldap.LDAPSettings object at 0x7f7970d532c0> prefix = 'AUTH_LDAP_' defaults = {'BIND_DN': 'cn=ldapadmin,dc=example,dc=org', 'BIND_PASSWORD': 'securepassword', 'CONNECTION_OPTIONS': {'OPT_NETWORK_TIMEOUT': 30, 'OPT_REFERRALS': 0}, 'GROUP_SEARCH': ['ou=groups,dc=example,dc=org', 'SCOPE_SUBTREE', '(objectClass=groupOfNames)'], ...} def __init__(self, prefix: str = 'AUTH_LDAP_', defaults: dict = {}): # This init method double checks the passed defaults while initializing a settings objects print((LDAPSettings, self)) > super(LDAPSettings, self).__init__(prefix, defaults) E TypeError: super(type, obj): obj must be an instance or subtype of type ansible_base/authentication/authenticator_plugins/ldap.py:367: TypeError ------------------------------------- Captured stdout setup -------------------------------------- (<class 'ansible_base.authentication.authenticator_plugins.ldap.LDAPSettings'>, <ansible_base.authentication.authenticator_plugins.ldap.LDAPSettings object at 0x7f7970d532c0>) ============================================ FAILURES ============================================ ________________________ test_ldap_backend_authenticate_unbind_exception _________________________ logger = <MagicMock name='logger' id='140159561948896'> authenticate = <MagicMock name='authenticate' id='140159561605216'> unauthenticated_api_client = <rest_framework.test.APIClient object at 0x7f7970e6c260> ldap_authenticator = <Authenticator: Test LDAP Authenticator>, shut_up_logging = None user = <User: user> @suppress_type_checks @pytest.mark.django_db @mock.patch("rest_framework.views.APIView.authentication_classes", [SessionAuthentication]) @mock.patch("ansible_base.authentication.authenticator_plugins.ldap.LDAPBackend.authenticate") @mock.patch("ansible_base.authentication.authenticator_plugins.ldap.logger") def test_ldap_backend_authenticate_unbind_exception( logger, authenticate, unauthenticated_api_client, ldap_authenticator, shut_up_logging, user, ): """ Test normal login flow. Force authenticate() to return a user. But an exception is thrown during unbind. """ user.ldap_user = MagicMock() user.ldap_user.attrs = MagicMock() user.ldap_user.attrs.data = {} user.ldap_user._connection.unbind_s.side_effect = Exception("Something went wrong") authenticate.return_value = user client = unauthenticated_api_client client.login(username=user.username, password="bar") url = get_relative_url(authenticated_test_page) response = client.get(url) logger.exception.assert_any_call(f"Got unexpected LDAP exception when forcing LDAP disconnect for user {user.username}, login will still proceed") > assert response.status_code == 200 E assert 401 == 200 E + where 401 = <Response status_code=401, "application/json">.status_code test_app/tests/authentication/authenticator_plugins/test_ldap.py:431: AssertionError ------------------------------------- Captured stderr setup -------------------------------------- 2025-04-29 01:07:23,951 DEBUG [] ansible_base.authentication.authenticator_plugins.utils Attempting to load class ansible_base.authentication.authenticator_plugins.ldap 2025-04-29 01:07:23,951 DEBUG [] ansible_base.authentication.authenticator_plugins.utils Attempting to load class ansible_base.authentication.authenticator_plugins.ldap 2025-04-29 01:07:23,951 ERROR [] ansible_base.lib.utils.models SYSTEM_USERNAME is set to _system but no user with that username exists. 2025-04-29 01:07:23,951 ERROR [] ansible_base.lib.utils.models SYSTEM_USERNAME is set to _system but no user with that username exists. 2025-04-29 01:07:23,955 INFO [] ansible_base.lib.utils.create_system_user Created system user _system 2025-04-29 01:07:23,955 INFO [] ansible_base.lib.utils.create_system_user Created system user _system --------------------------------------- Captured log setup --------------------------------------- DEBUG ansible_base.authentication.authenticator_plugins.utils:utils.py:35 Attempting to load class ansible_base.authentication.authenticator_plugins.ldap ERROR ansible_base.lib.utils.models:models.py:117 SYSTEM_USERNAME is set to _system but no user with that username exists. INFO ansible_base.lib.utils.create_system_user:create_system_user.py:34 Created system user _system -------------------------------------- Captured stdout call -------------------------------------- (<class 'ansible_base.authentication.authenticator_plugins.ldap.LDAPSettings'>, <ansible_base.authentication.authenticator_plugins.ldap.LDAPSettings object at 0x7f7970ff6990>) ___________________________ test_ldap_backend_authenticate_valid_user ____________________________ logger = <MagicMock name='logger' id='140159560778528'> authenticate = <MagicMock name='authenticate' id='140159560498064'> unauthenticated_api_client = <rest_framework.test.APIClient object at 0x7f7970d51e20> ldap_authenticator = <Authenticator: Test LDAP Authenticator>, shut_up_logging = None user = <User: user> @suppress_type_checks @pytest.mark.django_db @mock.patch("rest_framework.views.APIView.authentication_classes", [SessionAuthentication]) @mock.patch("ansible_base.authentication.authenticator_plugins.ldap.LDAPBackend.authenticate") @mock.patch("ansible_base.authentication.authenticator_plugins.ldap.logger") def test_ldap_backend_authenticate_valid_user( logger, authenticate, unauthenticated_api_client, ldap_authenticator, shut_up_logging, user, ): """ Test normal login flow. Force authenticate() to return a user. """ user.ldap_user = MagicMock() user.ldap_user.attrs = MagicMock() user.ldap_user.attrs.data = {} authenticate.return_value = user client = unauthenticated_api_client client.login(username=user.username, password="bar") url = get_relative_url(authenticated_test_page) response = client.get(url) logger.debug.assert_any_call(f"Forcing LDAP connection to close for {ldap_authenticator.name}") logger.info.assert_any_call(f"User {user.username} authenticated by LDAP {ldap_authenticator.name}") assert user.ldap_user._connection.unbind_s.call_count == 1 assert user.ldap_user._connection_bound is False > assert response.status_code == 200 E assert 401 == 200 E + where 401 = <Response status_code=401, "application/json">.status_code test_app/tests/authentication/authenticator_plugins/test_ldap.py:400: AssertionError ------------------------------------- Captured stderr setup -------------------------------------- 2025-04-29 01:07:28,989 DEBUG [] ansible_base.authentication.authenticator_plugins.utils Attempting to load class ansible_base.authentication.authenticator_plugins.ldap 2025-04-29 01:07:28,989 DEBUG [] ansible_base.authentication.authenticator_plugins.utils Attempting to load class ansible_base.authentication.authenticator_plugins.ldap 2025-04-29 01:07:28,990 ERROR [] ansible_base.lib.utils.models SYSTEM_USERNAME is set to _system but no user with that username exists. 2025-04-29 01:07:28,990 ERROR [] ansible_base.lib.utils.models SYSTEM_USERNAME is set to _system but no user with that username exists. 2025-04-29 01:07:28,992 INFO [] ansible_base.lib.utils.create_system_user Created system user _system 2025-04-29 01:07:28,992 INFO [] ansible_base.lib.utils.create_system_user Created system user _system --------------------------------------- Captured log setup --------------------------------------- DEBUG ansible_base.authentication.authenticator_plugins.utils:utils.py:35 Attempting to load class ansible_base.authentication.authenticator_plugins.ldap ERROR ansible_base.lib.utils.models:models.py:117 SYSTEM_USERNAME is set to _system but no user with that username exists. INFO ansible_base.lib.utils.create_system_user:create_system_user.py:34 Created system user _system -------------------------------------- Captured stdout call -------------------------------------- (<class 'ansible_base.authentication.authenticator_plugins.ldap.LDAPSettings'>, <ansible_base.authentication.authenticator_plugins.ldap.LDAPSettings object at 0x7f7970d71400>) ____________________ test_ldap_validate_connection_options_newctx_comes_last _____________________ ldap_configuration = {'BIND_DN': 'cn=ldapadmin,dc=example,dc=org', 'BIND_PASSWORD': 'securepassword', 'CONNECTION_OPTIONS': {'OPT_NETWORK_TIMEOUT': 30, 'OPT_REFERRALS': 0, 'OPT_X_TLS_NEWCTX': 0, 'OPT_X_TLS_PACKAGE': 'GnuTLS'}, 'GROUP_SEARCH': ['ou=groups,dc=example,dc=org', 'SCOPE_SUBTREE', '(objectClass=groupOfNames)'], ...} def test_ldap_validate_connection_options_newctx_comes_last(ldap_configuration): ldap_configuration["CONNECTION_OPTIONS"]["OPT_X_TLS_NEWCTX"] = 0 ldap_configuration["CONNECTION_OPTIONS"]["OPT_X_TLS_PACKAGE"] = "GnuTLS" > settings = LDAPSettings(defaults=ldap_configuration) test_app/tests/authentication/authenticator_plugins/test_ldap.py:465: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ self = <ansible_base.authentication.authenticator_plugins.ldap.LDAPSettings object at 0x7f7970d50440> prefix = 'AUTH_LDAP_' defaults = {'BIND_DN': 'cn=ldapadmin,dc=example,dc=org', 'BIND_PASSWORD': 'securepassword', 'CONNECTION_OPTIONS': {'OPT_NETWORK_TIMEOUT': 30, 'OPT_REFERRALS': 0, 'OPT_X_TLS_NEWCTX': 0, 'OPT_X_TLS_PACKAGE': 'GnuTLS'}, 'GROUP_SEARCH': ['ou=groups,dc=example,dc=org', 'SCOPE_SUBTREE', '(objectClass=groupOfNames)'], ...} def __init__(self, prefix: str = 'AUTH_LDAP_', defaults: dict = {}): # This init method double checks the passed defaults while initializing a settings objects print((LDAPSettings, self)) > super(LDAPSettings, self).__init__(prefix, defaults) E TypeError: super(type, obj): obj must be an instance or subtype of type ansible_base/authentication/authenticator_plugins/ldap.py:367: TypeError ```
1 parent e08cc74 commit a1f565d

File tree

3 files changed

+37
-26
lines changed

3 files changed

+37
-26
lines changed

ansible_base/authentication/authenticator_plugins/ldap.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -363,7 +363,7 @@ def validate(self, attrs):
363363
class LDAPSettings(BaseLDAPSettings):
364364
def __init__(self, prefix: str = 'AUTH_LDAP_', defaults: dict = {}):
365365
# This init method double checks the passed defaults while initializing a settings objects
366-
super(LDAPSettings, self).__init__(prefix, defaults)
366+
super().__init__(prefix, defaults)
367367

368368
# SERVER_URI needs to be a string, not an array
369369
setattr(self, 'SERVER_URI', ','.join(defaults['SERVER_URI']))

test_app/tests/authentication/authenticator_plugins/test_ldap.py

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import importlib
21
from collections import OrderedDict
32
from unittest import mock
43
from unittest.mock import MagicMock
@@ -680,27 +679,3 @@ def test_ldap_user_search_validation(
680679
)
681680
def test_ldap_search_field_is_single_search(value, expected_result):
682681
assert LDAPSearchField.is_single_search(value) is expected_result
683-
684-
685-
@pytest.mark.django_db
686-
@pytest.mark.parametrize(
687-
"username",
688-
[
689-
("Timmy"),
690-
("TIMMY"),
691-
("TiMmY"),
692-
],
693-
)
694-
def test_get_or_build_user(username, ldap_authenticator):
695-
from ansible_base.authentication.authenticator_plugins import ldap
696-
697-
with mock.patch(
698-
'ansible_base.authentication.utils.authentication.get_or_create_authenticator_user', return_value=(None, None, None)
699-
) as get_or_create_authenticator_user:
700-
importlib.reload(ldap)
701-
plugin = AuthenticatorPlugin(database_instance=ldap_authenticator)
702-
ldap_object = MagicMock()
703-
plugin.get_or_build_user(username, ldap_object)
704-
assert get_or_create_authenticator_user.called
705-
assert username.lower() in get_or_create_authenticator_user.call_args[0]
706-
assert username not in get_or_create_authenticator_user.call_args[0]
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import importlib
2+
from unittest import mock
3+
from unittest.mock import MagicMock
4+
5+
import pytest
6+
7+
from ansible_base.authentication.authenticator_plugins import ldap
8+
9+
"""
10+
This module is separated from the rest of test_ldap.py because it reloads the module
11+
which will replace the auth utils module with newly loaded classes.
12+
So it gives best isolation to keep this in a module that does not have
13+
other imports from the same module, which can leave stale references.
14+
"""
15+
16+
17+
@pytest.mark.django_db
18+
@pytest.mark.parametrize(
19+
"username",
20+
[
21+
("Timmy"),
22+
("TIMMY"),
23+
("TiMmY"),
24+
],
25+
)
26+
def test_get_or_build_user(username, ldap_authenticator):
27+
with mock.patch(
28+
'ansible_base.authentication.utils.authentication.get_or_create_authenticator_user', return_value=(None, None, None)
29+
) as get_or_create_authenticator_user:
30+
importlib.reload(ldap)
31+
plugin = ldap.AuthenticatorPlugin(database_instance=ldap_authenticator)
32+
ldap_object = MagicMock()
33+
plugin.get_or_build_user(username, ldap_object)
34+
assert get_or_create_authenticator_user.called
35+
assert username.lower() in get_or_create_authenticator_user.call_args[0]
36+
assert username not in get_or_create_authenticator_user.call_args[0]

0 commit comments

Comments
 (0)