mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 02:54:26 +00:00
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 sinceec0fde6561, 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 commitec0fde6561) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
221 lines
7.7 KiB
Python
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()
|