Files
computer-playbook/tests/unit/python/plugins/lookup/test_sso_oidc_plugin.py
T
kevinveenbirkenbachandClaude Opus 5.5 9aa56b71e9 test(lookup): restore sys.modules after loading the sso plugins fresh
test_sso.py, test_sso_oidc_plugin.py and test_sso_proxy_consumers.py
evict utils, utils.cache, utils.cache.applications and the roles
packages from sys.modules to import their plugin against fresh modules,
and never put the originals back. Ansible's plugin loader keeps the
applications lookup it loaded earlier in sys.modules, so that lookup
stayed bound to the evicted utils.cache.applications, whose
_MERGED_APPLICATIONS_CACHE utils.cache._reset_cache_for_tests() no
longer reaches.

On main this turned test_webserver_worker_limits red whenever one xdist
worker ran a test that loads the applications lookup through Ansible,
then one of these sso tests, then the worker-limits test: the stale
cache served the cpus 0.5 payload of test_a_sub_cpu_proxy_still_runs_one_worker,
so the later methods rendered worker_processes 1 instead of 4 (main run
41139196554, Workspace centos and fedora, second make test on gw1).

_load_module now runs inside mock.patch.dict(sys.modules): the plugin
keeps the fresh modules it was built against, and sys.modules returns to
its prior state afterwards. This is the change store-i18n carries since
ec0fde6561, ported unchanged so both lines stay identical.

Evidence:
- Before: pytest -n 0 test_tor_ports.py test_sso_oidc_plugin.py
  test_webserver_worker_limits.py -> "3 failed, 33 passed", the CI
  failure set ('1' != '4' twice, worker_processes 1 vs 4).
- After: test_tor_ports.py, the four sso test files and
  test_webserver_worker_limits.py -> "68 passed".

(cherry picked from commit ec0fde6561)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 02:17:21 +01:00

186 lines
5.8 KiB
Python

import importlib
import importlib.util
import sys
import unittest
import unittest.mock as mock
from ansible.errors import AnsibleError
from . import PROJECT_ROOT
def _load_module(rel_path: str, name: str):
"""Import ``rel_path`` fresh, evicting sibling-test stubs of
``utils.roles.applications.config`` from ``sys.modules`` first.
"""
with mock.patch.dict(sys.modules):
for key in (
"utils.roles.applications.config",
"utils.roles.applications",
"utils",
"utils.cache.applications",
"utils.cache",
):
sys.modules.pop(key, None)
importlib.import_module("utils.roles.applications.config")
importlib.import_module("utils.cache.applications")
path = PROJECT_ROOT / rel_path
spec = importlib.util.spec_from_file_location(name, str(path))
mod = importlib.util.module_from_spec(spec)
assert spec and spec.loader
spec.loader.exec_module(mod)
return mod
def _apps(*, oidc_enabled=True, ldap_enabled=None, flavor=None, include_app=True):
"""Build a minimal merged `applications` dict for the Nextcloud role.
`oidc_enabled` defaults to True so existing flavor-selection tests
stay focused on the ldap/flavor axis. The OIDC-off fast-path has
its own dedicated tests.
"""
if not include_app:
return {}
sso_block: dict = {"enabled": oidc_enabled}
if flavor is not None:
sso_block["oidc"] = {"plugin": flavor}
services_block: dict = {"sso": sso_block}
if ldap_enabled is not None:
services_block["ldap"] = {"enabled": ldap_enabled}
return {
"web-app-nextcloud": {
"services": services_block,
},
}
class OidcFlavorLookupTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.mod = _load_module(
"plugins/lookup/sso_oidc_plugin.py",
"sso_oidc_plugin",
)
def setUp(self):
self._patcher = mock.patch.object(self.mod, "lookup_loader")
self._loader_mock = self._patcher.start()
self._stub_payload = None
self._loader_mock.get.side_effect = self._stub_lookup
def _stub_lookup(self, name, *_args, **_kwargs):
"""Stand in for the lookups the plugin consumes.
``config`` resolves a dotted path against the payload the way
``lookup('config', app, path, default)`` does with a default supplied;
anything else answers with the payload itself.
"""
if name == "config":
return mock.MagicMock(
run=lambda terms, **_kw: [self._stub_config(*terms)],
)
return mock.MagicMock(run=lambda *_a, **_kw: [self._stub_payload])
def _stub_config(self, application_id, config_path, default=None):
node = (self._stub_payload or {}).get(application_id)
for part in str(config_path).split("."):
if not isinstance(node, dict) or part not in node:
return default
node = node[part]
return node
def tearDown(self):
self._patcher.stop()
def _run(self, applications, *, terms=None):
self._stub_payload = applications
lk = self.mod.LookupModule()
lk._loader = mock.MagicMock()
return lk.run(terms or [], variables={"applications": applications})
def test_ldap_enabled_returns_oidc_login(self):
self.assertEqual(
self._run(_apps(ldap_enabled=True)),
["oidc_login"],
)
def test_ldap_disabled_returns_sociallogin(self):
self.assertEqual(
self._run(_apps(ldap_enabled=False)),
["sociallogin"],
)
def test_ldap_missing_defaults_to_sociallogin(self):
self.assertEqual(
self._run(_apps()),
["sociallogin"],
)
def test_missing_application_returns_empty(self):
self.assertEqual(
self._run({"some-other-app": {}}),
[""],
)
def test_oidc_disabled_returns_empty(self):
self.assertEqual(
self._run(_apps(oidc_enabled=False)),
[""],
)
self.assertEqual(
self._run(_apps(oidc_enabled=False, ldap_enabled=True)),
[""],
)
self.assertEqual(
self._run(_apps(oidc_enabled=False, flavor="oidc_login")),
[""],
)
def test_explicit_flavor_overrides_ldap_fallback(self):
self.assertEqual(
self._run(_apps(ldap_enabled=True, flavor="sociallogin")),
["sociallogin"],
)
self.assertEqual(
self._run(_apps(ldap_enabled=False, flavor="user_oidc")),
["user_oidc"],
)
def test_explicit_flavor_is_stripped(self):
self.assertEqual(
self._run(_apps(ldap_enabled=False, flavor=" oidc_login ")),
["oidc_login"],
)
def test_blank_explicit_flavor_falls_back_to_ternary(self):
self.assertEqual(
self._run(_apps(ldap_enabled=True, flavor=" ")),
["oidc_login"],
)
self.assertEqual(
self._run(_apps(ldap_enabled=False, flavor="")),
["sociallogin"],
)
def test_null_explicit_flavor_falls_back_to_ternary(self):
self.assertEqual(
self._run(_apps(ldap_enabled=True, flavor=None)),
["oidc_login"],
)
def test_non_string_explicit_flavor_is_ignored(self):
self.assertEqual(
self._run(_apps(ldap_enabled=True, flavor=42)),
["oidc_login"],
)
def test_rejects_positional_terms(self):
lk = self.mod.LookupModule()
with self.assertRaises(AnsibleError):
lk.run(["unexpected"], variables={"applications": _apps()})
if __name__ == "__main__":
unittest.main()