Files
computer-playbook/tests/unit/python/plugins/lookup/test_sso.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

221 lines
7.7 KiB
Python

"""Unit tests for the ``sso`` lookup plugin.
The plugin is a thin wrapper around
``utils.roles.applications.services.sso.get_sso_config`` — these tests
focus on the lookup-shape contract (term parsing, want-path selection,
error paths). Resolver semantics are covered by
``tests/unit/python/utils/roles/applications/services/test_sso.py``.
"""
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):
with mock.patch.dict(sys.modules):
for key in (
"utils.roles.applications.config",
"utils.roles.applications",
"utils",
"utils.cache.applications",
"utils.cache",
"utils.roles.applications.services.sso",
"utils.roles.applications.services",
):
sys.modules.pop(key, None)
importlib.import_module("utils.roles.applications.config")
importlib.import_module("utils.cache.applications")
importlib.import_module("utils.roles.applications.services.sso")
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(
*,
enabled=False,
shared=False,
flavor=None,
oauth2_origin_host=None,
oauth2_origin_port=None,
oauth2_acl=None,
oauth2_allowed_groups=None,
oauth2_pass_access_token=None,
include_app=True,
):
if not include_app:
return {}
sso: dict = {"enabled": enabled, "shared": shared}
if flavor is not None:
sso["flavor"] = flavor
oauth2_sub: dict = {}
if oauth2_origin_host is not None or oauth2_origin_port is not None:
oauth2_sub["origin"] = {
**({"host": oauth2_origin_host} if oauth2_origin_host is not None else {}),
**({"port": oauth2_origin_port} if oauth2_origin_port is not None else {}),
}
if oauth2_acl is not None:
oauth2_sub["acl"] = oauth2_acl
if oauth2_allowed_groups is not None:
oauth2_sub["allowed_groups"] = oauth2_allowed_groups
if oauth2_pass_access_token is not None:
oauth2_sub["pass_access_token"] = oauth2_pass_access_token
if oauth2_sub:
sso["oauth2"] = oauth2_sub
return {"web-app-x": {"services": {"sso": sso}}}
class SsoLookupTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.mod = _load_module("plugins/lookup/sso.py", "sso")
def _run(self, applications, terms):
lk = self.mod.LookupModule()
lk._loader = mock.MagicMock()
with mock.patch.object(self.mod, "lookup_loader") as loader_mock:
loader_mock.get.return_value = mock.MagicMock(
run=lambda *_a, **_k: [applications]
)
return lk.run(terms, variables={"applications": applications})
def test_zero_terms_raises(self):
with self.assertRaises(AnsibleError):
self._run(_apps(), [])
def test_three_terms_raises(self):
with self.assertRaises(AnsibleError):
self._run(_apps(), ["web-app-x", "is_enabled", "extra"])
def test_empty_application_id_raises(self):
with self.assertRaises(AnsibleError):
self._run(_apps(), [""])
def test_unknown_want_path_raises(self):
with self.assertRaises(AnsibleError) as ctx:
self._run(_apps(enabled=True), ["web-app-x", "bogus_key"])
self.assertIn("unknown want_path", str(ctx.exception))
def test_default_returns_full_dict(self):
result = self._run(_apps(enabled=True, flavor="oauth2"), ["web-app-x"])
self.assertEqual(len(result), 1)
payload = result[0]
self.assertEqual(payload["enabled"], True)
self.assertEqual(payload["flavor"], "oauth2")
self.assertTrue(payload["is_proxy_gated"])
def test_explicit_all_returns_full_dict(self):
result = self._run(_apps(enabled=True), ["web-app-x", "all"])
self.assertEqual(len(result), 1)
self.assertIsInstance(result[0], dict)
def test_is_enabled_returns_bool(self):
result = self._run(_apps(enabled=True), ["web-app-x", "is_enabled"])
self.assertEqual(result, [True])
def test_is_proxy_gated_true_for_oauth2(self):
result = self._run(
_apps(enabled=True, flavor="oauth2"), ["web-app-x", "is_proxy_gated"]
)
self.assertEqual(result, [True])
def test_pass_access_token_defaults_to_false(self):
result = self._run(
_apps(enabled=True, flavor="oauth2"),
["web-app-x", "oauth2_pass_access_token"],
)
self.assertEqual(result, [False])
def test_pass_access_token_reads_the_declared_opt_in(self):
result = self._run(
_apps(enabled=True, flavor="oauth2", oauth2_pass_access_token=True),
["web-app-x", "oauth2_pass_access_token"],
)
self.assertEqual(result, [True])
def test_pass_access_token_accepts_a_rendered_string(self):
result = self._run(
_apps(enabled=True, flavor="oauth2", oauth2_pass_access_token="True"),
["web-app-x", "oauth2_pass_access_token"],
)
self.assertEqual(result, [True])
def test_pass_access_token_rejects_a_rendered_false_string(self):
result = self._run(
_apps(enabled=True, flavor="oauth2", oauth2_pass_access_token="False"),
["web-app-x", "oauth2_pass_access_token"],
)
self.assertEqual(result, [False])
def test_is_proxy_gated_false_for_oidc(self):
result = self._run(
_apps(enabled=True, flavor="oidc"), ["web-app-x", "is_proxy_gated"]
)
self.assertEqual(result, [False])
def test_is_proxy_gated_false_when_disabled(self):
result = self._run(
_apps(enabled=False, flavor="oauth2"), ["web-app-x", "is_proxy_gated"]
)
self.assertEqual(result, [False])
def test_flavor_returns_string(self):
result = self._run(
_apps(enabled=True, flavor="oauth2"), ["web-app-x", "flavor"]
)
self.assertEqual(result, ["oauth2"])
def test_oauth2_origin_host_returns_value(self):
result = self._run(
_apps(enabled=True, flavor="oauth2", oauth2_origin_host="application"),
["web-app-x", "oauth2_origin_host"],
)
self.assertEqual(result, ["application"])
def test_oauth2_origin_port_stringified(self):
result = self._run(
_apps(enabled=True, flavor="oauth2", oauth2_origin_port=8000),
["web-app-x", "oauth2_origin_port"],
)
self.assertEqual(result, ["8000"])
def test_oauth2_acl_returns_dict(self):
acl = {"whitelist": ["/api/", "/static/"]}
result = self._run(
_apps(enabled=True, flavor="oauth2", oauth2_acl=acl),
["web-app-x", "oauth2_acl"],
)
self.assertEqual(result, [acl])
def test_oauth2_allowed_groups_returns_list(self):
groups = ["/roles/web-app-x/admin"]
result = self._run(
_apps(enabled=True, flavor="oauth2", oauth2_allowed_groups=groups),
["web-app-x", "oauth2_allowed_groups"],
)
self.assertEqual(result, [groups])
def test_missing_app_returns_defaults(self):
result = self._run({}, ["web-app-x", "is_enabled"])
self.assertEqual(result, [False])
def test_empty_want_path_treated_as_all(self):
result = self._run(_apps(enabled=True), ["web-app-x", " "])
self.assertEqual(len(result), 1)
self.assertIsInstance(result[0], dict)
if __name__ == "__main__":
unittest.main()