diff --git a/scripts/nextcloud-mail-sync.sh b/scripts/nextcloud-mail-sync.sh index 6b0adb1..7feeec6 100755 --- a/scripts/nextcloud-mail-sync.sh +++ b/scripts/nextcloud-mail-sync.sh @@ -10,6 +10,12 @@ if ! command -v jq >/dev/null 2>&1; then apt-get update && apt-get install -y jq curl >/dev/null fi +account_exists() { + # Skip if the account email is already present in the mail app. + runuser -u www-data -- php occ mail:account:list 2>/dev/null | grep -Fq " ${1}" || \ + runuser -u www-data -- php occ mail:account:list 2>/dev/null | grep -Fq "${1} " +} + token=$( curl -s -d "grant_type=password" \ -d "client_id=admin-cli" \ @@ -31,6 +37,10 @@ echo "${users}" | jq -c '.[]' | while read -r user; do email=$(echo "${user}" | jq -r '.email // empty') app_pw=$(echo "${user}" | jq -r '.attributes.mailu_app_password[0] // empty') [[ -z "${email}" || -z "${app_pw}" ]] && continue + if account_exists "${email}"; then + echo "Skipping ${email}, already exists" + continue + fi echo "Syncing ${email}" runuser -u www-data -- php occ mail:account:create \ "${username}" "${username}" "${email}" \ diff --git a/scripts/tests/test_dashboards_render_atlas.py b/scripts/tests/test_dashboards_render_atlas.py new file mode 100644 index 0000000..865aa68 --- /dev/null +++ b/scripts/tests/test_dashboards_render_atlas.py @@ -0,0 +1,58 @@ +import importlib.util +import pathlib + + +def load_module(): + path = pathlib.Path(__file__).resolve().parents[1] / "dashboards_render_atlas.py" + spec = importlib.util.spec_from_file_location("dashboards_render_atlas", path) + module = importlib.util.module_from_spec(spec) + assert spec.loader is not None + spec.loader.exec_module(module) + return module + + +def test_table_panel_options_and_filterable(): + mod = load_module() + panel = mod.table_panel( + 1, + "test", + "metric", + {"h": 1, "w": 1, "x": 0, "y": 0}, + unit="percent", + transformations=[{"id": "labelsToFields", "options": {}}], + instant=True, + options={"showColumnFilters": False}, + filterable=False, + footer={"show": False, "fields": "", "calcs": []}, + format="table", + ) + assert panel["fieldConfig"]["defaults"]["unit"] == "percent" + assert panel["fieldConfig"]["defaults"]["custom"]["filterable"] is False + assert panel["options"]["showHeader"] is True + assert panel["targets"][0]["format"] == "table" + + +def test_node_filter_and_expr_helpers(): + mod = load_module() + expr = mod.node_filter("titan-.*") + assert "label_replace" in expr + cpu_expr = mod.node_cpu_expr("titan-.*") + mem_expr = mod.node_mem_expr("titan-.*") + assert "node_cpu_seconds_total" in cpu_expr + assert "node_memory_MemAvailable_bytes" in mem_expr + + +def test_render_configmap_writes(tmp_path): + mod = load_module() + mod.DASHBOARD_DIR = tmp_path / "dash" + mod.ROOT = tmp_path + uid = "atlas-test" + info = {"configmap": tmp_path / "cm.yaml"} + data = {"title": "Atlas Test"} + mod.write_json(uid, data) + mod.render_configmap(uid, info) + json_path = mod.DASHBOARD_DIR / f"{uid}.json" + assert json_path.exists() + content = (tmp_path / "cm.yaml").read_text() + assert "kind: ConfigMap" in content + assert f"{uid}.json" in content diff --git a/scripts/tests/test_mailu_sync.py b/scripts/tests/test_mailu_sync.py index f495c87..41616b2 100644 --- a/scripts/tests/test_mailu_sync.py +++ b/scripts/tests/test_mailu_sync.py @@ -81,3 +81,101 @@ def test_kc_update_attributes_raises_without_attribute(monkeypatch): sync.SESSION = _FakeSession(_FakeResponse({}), missing_attr_resp) with pytest.raises(Exception): sync.kc_update_attributes("token", {"id": "u1", "username": "u1"}, {"mailu_app_password": "abc"}) + + +def test_kc_get_users_paginates(monkeypatch): + sync = load_sync_module(monkeypatch) + + class _PagedSession: + def __init__(self): + self.calls = 0 + + def post(self, *_, **__): + return _FakeResponse({"access_token": "tok"}) + + def get(self, *_, **__): + self.calls += 1 + if self.calls == 1: + return _FakeResponse([{"id": "u1"}, {"id": "u2"}]) + return _FakeResponse([]) # stop pagination + + sync.SESSION = _PagedSession() + users = sync.kc_get_users("tok") + assert [u["id"] for u in users] == ["u1", "u2"] + assert sync.SESSION.calls == 2 + + +def test_ensure_mailu_user_skips_foreign_domain(monkeypatch): + sync = load_sync_module(monkeypatch) + executed = [] + + class _Cursor: + def execute(self, sql, params): + executed.append((sql, params)) + + sync.ensure_mailu_user(_Cursor(), "user@other.com", "pw", "User") + assert not executed + + +def test_ensure_mailu_user_upserts(monkeypatch): + sync = load_sync_module(monkeypatch) + captured = {} + + class _Cursor: + def execute(self, sql, params): + captured.update(params) + + sync.ensure_mailu_user(_Cursor(), "user@example.com", "pw", "User Example") + assert captured["email"] == "user@example.com" + assert captured["localpart"] == "user" + # password should be hashed, not the raw string + assert captured["password"] != "pw" + + +def test_main_generates_password_and_upserts(monkeypatch): + sync = load_sync_module(monkeypatch) + users = [ + {"id": "u1", "username": "user1", "email": "user1@example.com", "attributes": {}}, + {"id": "u2", "username": "user2", "email": "user2@example.com", "attributes": {"mailu_app_password": ["keepme"]}}, + {"id": "u3", "username": "user3", "email": "user3@other.com", "attributes": {}}, + ] + updated = [] + + class _Cursor: + def __init__(self): + self.executions = [] + + def execute(self, sql, params): + self.executions.append(params) + + def close(self): + return None + + class _Conn: + def __init__(self): + self.autocommit = False + self._cursor = _Cursor() + + def cursor(self, cursor_factory=None): + return self._cursor + + def close(self): + return None + + monkeypatch.setattr(sync, "get_kc_token", lambda: "tok") + monkeypatch.setattr(sync, "kc_get_users", lambda token: users) + monkeypatch.setattr(sync, "kc_update_attributes", lambda token, user, attrs: updated.append((user["id"], attrs["mailu_app_password"]))) + conns = [] + + def _connect(**kwargs): + conn = _Conn() + conns.append(conn) + return conn + + monkeypatch.setattr(sync.psycopg2, "connect", _connect) + + sync.main() + + # Should attempt two inserts (third user skipped due to domain mismatch) + assert len(updated) == 1 # only one missing attr was backfilled + assert conns and len(conns[0]._cursor.executions) == 2