bstein-dev-home/backend/tests/test_sync_job_helpers.py

179 lines
7.0 KiB
Python

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"