from __future__ import annotations """Tests for per-user Kubernetes sync Job adapters.""" import pytest from atlas_portal import firefly_user_sync, nextcloud_mail_sync, wger_user_sync def _cronjob_template() -> dict: """Build a CronJob payload shaped like the templates used in the cluster.""" return { "spec": { "jobTemplate": { "spec": { "template": { "spec": { "containers": [ { "name": "worker", "env": [ {"name": "ONLY_USERNAME", "value": "old"}, {"name": "FIREFLY_USER_EMAIL", "value": "old"}, {"name": "WGER_USERNAME", "value": "old"}, ], } ] } } } } } } @pytest.mark.parametrize( ("module", "namespace_attr", "cronjob_attr", "timeout_attr", "args", "expected_env"), [ ( nextcloud_mail_sync, "NEXTCLOUD_NAMESPACE", "NEXTCLOUD_MAIL_SYNC_CRONJOB", "NEXTCLOUD_MAIL_SYNC_WAIT_TIMEOUT_SEC", ("alice",), {"ONLY_USERNAME": "alice"}, ), ( firefly_user_sync, "FIREFLY_NAMESPACE", "FIREFLY_USER_SYNC_CRONJOB", "FIREFLY_USER_SYNC_WAIT_TIMEOUT_SEC", ("alice", "alice@example.dev", "pw"), {"FIREFLY_USER_EMAIL": "alice@example.dev", "FIREFLY_USER_PASSWORD": "pw"}, ), ( wger_user_sync, "WGER_NAMESPACE", "WGER_USER_SYNC_CRONJOB", "WGER_USER_SYNC_WAIT_TIMEOUT_SEC", ("alice", "alice@example.dev", "pw"), {"WGER_USERNAME": "alice", "WGER_EMAIL": "alice@example.dev", "WGER_PASSWORD": "pw"}, ), ], ) def test_user_sync_modules_render_jobs_and_trigger(monkeypatch, module, namespace_attr, cronjob_attr, timeout_attr, args, expected_env) -> None: monkeypatch.setattr(module.settings, namespace_attr, "apps") monkeypatch.setattr(module.settings, cronjob_attr, "sync-cron") monkeypatch.setattr(module.settings, timeout_attr, 0) monkeypatch.setattr(module.time, "time", lambda: 1000) posted: list[dict] = [] def fake_get_json(path: str) -> dict: if "cronjobs" in path: return _cronjob_template() return {"status": {"conditions": [{"type": "Complete", "status": "True"}]}} def fake_post_json(path: str, payload: dict) -> dict: posted.append(payload) return {"metadata": {"name": payload["metadata"]["name"]}} monkeypatch.setattr(module, "get_json", fake_get_json) monkeypatch.setattr(module, "post_json", fake_post_json) result = module.trigger(*args, wait=True) assert result["status"] in {"ok", "running"} env = posted[0]["spec"]["template"]["spec"]["containers"][0]["env"] env_map = {item["name"]: item["value"] for item in env} for key, value in expected_env.items(): assert env_map[key] == value assert module._job_succeeded({"status": {"succeeded": 1}}) assert module._job_failed({"status": {"failed": 1}}) @pytest.mark.parametrize( ("module", "namespace_attr", "cronjob_attr", "timeout_attr", "args"), [ ( nextcloud_mail_sync, "NEXTCLOUD_NAMESPACE", "NEXTCLOUD_MAIL_SYNC_CRONJOB", "NEXTCLOUD_MAIL_SYNC_WAIT_TIMEOUT_SEC", ("alice",), ), ( firefly_user_sync, "FIREFLY_NAMESPACE", "FIREFLY_USER_SYNC_CRONJOB", "FIREFLY_USER_SYNC_WAIT_TIMEOUT_SEC", ("alice", "alice@example.dev", "pw"), ), ( wger_user_sync, "WGER_NAMESPACE", "WGER_USER_SYNC_CRONJOB", "WGER_USER_SYNC_WAIT_TIMEOUT_SEC", ("alice", "alice@example.dev", "pw"), ), ], ) def test_user_sync_modules_cover_edge_paths(monkeypatch, module, namespace_attr, cronjob_attr, timeout_attr, args) -> None: assert module._safe_name_fragment("!!!") == "user" assert module._job_succeeded({"status": {"conditions": [None, {"type": "Complete", "status": "True"}]}}) assert not module._job_succeeded({"status": {"conditions": [{"type": "Complete", "status": "False"}]}}) assert module._job_failed({"status": {"conditions": [None, {"type": "Failed", "status": "True"}]}}) assert not module._job_failed({"status": {"conditions": [{"type": "Failed", "status": "False"}]}}) cronjob = _cronjob_template() container = cronjob["spec"]["jobTemplate"]["spec"]["template"]["spec"]["containers"][0] container["env"] = "not-a-list" job = module._job_from_cronjob(cronjob, *args) assert job["spec"]["template"]["spec"]["containers"][0]["env"] monkeypatch.setattr(module.settings, namespace_attr, "apps") monkeypatch.setattr(module.settings, cronjob_attr, "sync-cron") monkeypatch.setattr(module.settings, timeout_attr, 5) monkeypatch.setattr(module.time, "sleep", lambda *_: None) with pytest.raises(RuntimeError, match="missing username"): module.trigger("", *args[1:]) if module in {firefly_user_sync, wger_user_sync}: with pytest.raises(RuntimeError, match="missing password"): module.trigger(args[0], args[1], "") monkeypatch.setattr(module.settings, namespace_attr, "") with pytest.raises(RuntimeError, match="not configured"): module.trigger(*args) monkeypatch.setattr(module.settings, namespace_attr, "apps") def cron_then_complete(path: str) -> dict: if "cronjobs" in path: return _cronjob_template() return {"status": {"conditions": [{"type": "Complete", "status": "True"}]}} monkeypatch.setattr(module, "get_json", cron_then_complete) monkeypatch.setattr(module, "post_json", lambda path, payload: {}) assert module.trigger(*args, wait=False)["status"] == "queued" monkeypatch.setattr(module, "post_json", lambda path, payload: {"metadata": {"name": ""}}) with pytest.raises(RuntimeError, match="job name missing"): module.trigger(*args, wait=True) monkeypatch.setattr(module, "post_json", lambda path, payload: {"metadata": {"name": payload["metadata"]["name"]}}) clock = iter([0, 1, 2]) monkeypatch.setattr(module.time, "time", lambda: next(clock)) assert module.trigger(*args, wait=True)["status"] == "ok" def cron_then_failed(path: str) -> dict: if "cronjobs" in path: return _cronjob_template() return {"status": {"conditions": [{"type": "Failed", "status": "True"}]}} clock = iter([0, 1, 2]) monkeypatch.setattr(module.time, "time", lambda: next(clock)) monkeypatch.setattr(module, "get_json", cron_then_failed) assert module.trigger(*args, wait=True)["status"] == "error"