from __future__ import annotations import time import types import pytest from ariadne.services.firefly import FireflyService from ariadne.services.mailu import MailuService from ariadne.services.nextcloud import NextcloudService from ariadne.services.wger import WgerService from ariadne.services.vaultwarden import VaultwardenService class DummyExecutor: def __init__(self, stdout: str = "ok", stderr: str = "", exit_code: int = 0): self.calls = [] self._stdout = stdout self._stderr = stderr self._exit_code = exit_code def exec(self, command, env=None, timeout_sec=None, check=True): self.calls.append((command, env, timeout_sec, check)) return types.SimpleNamespace( stdout=self._stdout, stderr=self._stderr, exit_code=self._exit_code, ok=self._exit_code == 0, ) class DummyResponse: def __init__(self, status_code=200, text=""): self.status_code = status_code self.text = text def raise_for_status(self): return None class DummyVaultwardenClient: def __init__(self): self.calls = [] self.responses = {} def post(self, path, json=None, data=None): self.calls.append((path, json, data)) resp = self.responses.get(path) if resp is None: resp = DummyResponse(200, "") return resp def close(self): return None def test_nextcloud_sync_mail_no_user(monkeypatch) -> None: dummy = types.SimpleNamespace( nextcloud_namespace="nextcloud", nextcloud_mail_sync_cronjob="nextcloud-mail-sync", nextcloud_mail_sync_wait_timeout_sec=90.0, nextcloud_mail_sync_job_ttl_sec=3600, nextcloud_pod_label="app=nextcloud", nextcloud_container="nextcloud", nextcloud_exec_timeout_sec=30.0, nextcloud_db_host="", nextcloud_db_port=5432, nextcloud_db_name="nextcloud", nextcloud_db_user="nextcloud", nextcloud_db_password="", mailu_domain="bstein.dev", mailu_host="mail.bstein.dev", ) monkeypatch.setattr("ariadne.services.nextcloud.settings", dummy) monkeypatch.setattr("ariadne.services.nextcloud.keycloak_admin.ready", lambda: True) monkeypatch.setattr("ariadne.services.nextcloud.keycloak_admin.find_user", lambda *_args, **_kwargs: None) svc = NextcloudService() result = svc.sync_mail("alice", wait=True) assert result["status"] == "ok" def test_wger_sync_user_exec(monkeypatch) -> None: dummy = types.SimpleNamespace( wger_namespace="health", wger_user_sync_cronjob="wger-user-sync", wger_admin_cronjob="wger-admin-ensure", wger_user_sync_wait_timeout_sec=60.0, wger_pod_label="app=wger", wger_container="wger", wger_admin_username="admin", wger_admin_password="pw", wger_admin_email="admin@bstein.dev", ) monkeypatch.setattr("ariadne.services.wger.settings", dummy) calls: list[dict[str, str]] = [] class DummyExecutor: def exec(self, _cmd, env=None, timeout_sec=None, check=True): calls.append(env or {}) return types.SimpleNamespace(stdout="ok", stderr="", exit_code=0, ok=True) monkeypatch.setattr("ariadne.services.wger.PodExecutor", lambda *_args, **_kwargs: DummyExecutor()) svc = WgerService() result = svc.sync_user("alice", "alice@bstein.dev", "pw", wait=True) assert result["status"] == "ok" assert calls[0]["WGER_USERNAME"] == "alice" def test_wger_ensure_admin_exec(monkeypatch) -> None: dummy = types.SimpleNamespace( wger_namespace="health", wger_user_sync_cronjob="wger-user-sync", wger_admin_cronjob="wger-admin-ensure", wger_user_sync_wait_timeout_sec=60.0, wger_pod_label="app=wger", wger_container="wger", wger_admin_username="admin", wger_admin_password="pw", wger_admin_email="admin@bstein.dev", ) monkeypatch.setattr("ariadne.services.wger.settings", dummy) calls: list[dict[str, str]] = [] class DummyExecutor: def exec(self, _cmd, env=None, timeout_sec=None, check=True): calls.append(env or {}) return types.SimpleNamespace(stdout="ok", stderr="", exit_code=0, ok=True) monkeypatch.setattr("ariadne.services.wger.PodExecutor", lambda *_args, **_kwargs: DummyExecutor()) svc = WgerService() result = svc.ensure_admin(wait=False) assert result["status"] == "ok" assert calls[0]["WGER_ADMIN_USERNAME"] == "admin" def test_wger_sync_users(monkeypatch) -> None: dummy = types.SimpleNamespace( wger_namespace="health", wger_user_sync_wait_timeout_sec=60.0, wger_pod_label="app=wger", wger_container="wger", wger_admin_username="admin", wger_admin_password="pw", wger_admin_email="admin@bstein.dev", mailu_domain="bstein.dev", ) monkeypatch.setattr("ariadne.services.wger.settings", dummy) calls: list[tuple[str, str, str]] = [] class DummyAdmin: def ready(self) -> bool: return True def iter_users(self, page_size=200, brief=False): return [{"id": "1", "username": "alice", "attributes": {}}] def get_user(self, user_id: str): return {"id": user_id, "username": "alice", "attributes": {}} def set_user_attribute(self, username: str, key: str, value: str) -> None: calls.append((username, key, value)) monkeypatch.setattr("ariadne.services.wger.keycloak_admin", DummyAdmin()) monkeypatch.setattr( "ariadne.services.wger.mailu.resolve_mailu_email", lambda *_args, **_kwargs: "alice@bstein.dev", ) monkeypatch.setattr("ariadne.services.wger.random_password", lambda *_args: "pw") def fake_sync_user(self, *_args, **_kwargs): return {"status": "ok", "detail": "ok"} monkeypatch.setattr(WgerService, "sync_user", fake_sync_user) svc = WgerService() result = svc.sync_users() assert result["status"] == "ok" assert any(key == "wger_password" for _user, key, _value in calls) assert any(key == "wger_password_updated_at" for _user, key, _value in calls) def test_firefly_sync_user_exec(monkeypatch) -> None: dummy = types.SimpleNamespace( firefly_namespace="finance", firefly_user_sync_cronjob="firefly-user-sync", firefly_user_sync_wait_timeout_sec=60.0, firefly_pod_label="app=firefly", firefly_container="firefly", firefly_cron_base_url="http://firefly/cron", firefly_cron_token="token", firefly_cron_timeout_sec=10.0, ) monkeypatch.setattr("ariadne.services.firefly.settings", dummy) class DummyExecutor: def exec(self, _cmd, env=None, timeout_sec=None, check=True): return types.SimpleNamespace(stdout="ok", stderr="", exit_code=0, ok=True) monkeypatch.setattr("ariadne.services.firefly.PodExecutor", lambda *_args, **_kwargs: DummyExecutor()) svc = FireflyService() result = svc.sync_user("alice@bstein.dev", "pw", wait=True) assert result["status"] == "ok" def test_firefly_sync_missing_inputs(monkeypatch) -> None: dummy = types.SimpleNamespace( firefly_namespace="finance", firefly_user_sync_cronjob="firefly-user-sync", firefly_user_sync_wait_timeout_sec=60.0, firefly_pod_label="app=firefly", firefly_container="firefly", firefly_cron_base_url="http://firefly/cron", firefly_cron_token="token", firefly_cron_timeout_sec=10.0, ) monkeypatch.setattr("ariadne.services.firefly.settings", dummy) monkeypatch.setattr("ariadne.services.firefly.PodExecutor", lambda *_args, **_kwargs: None) svc = FireflyService() with pytest.raises(RuntimeError): svc.sync_user("", "pw", wait=True) with pytest.raises(RuntimeError): svc.sync_user("alice@bstein.dev", "", wait=True) def test_firefly_sync_missing_config(monkeypatch) -> None: dummy = types.SimpleNamespace( firefly_namespace="", firefly_user_sync_cronjob="", firefly_user_sync_wait_timeout_sec=60.0, firefly_pod_label="app=firefly", firefly_container="firefly", firefly_cron_base_url="http://firefly/cron", firefly_cron_token="token", firefly_cron_timeout_sec=10.0, ) monkeypatch.setattr("ariadne.services.firefly.settings", dummy) svc = FireflyService() with pytest.raises(RuntimeError): svc.sync_user("alice@bstein.dev", "pw", wait=True) def test_firefly_run_cron(monkeypatch) -> None: dummy = types.SimpleNamespace( firefly_namespace="finance", firefly_user_sync_cronjob="firefly-user-sync", firefly_user_sync_wait_timeout_sec=60.0, firefly_pod_label="app=firefly", firefly_container="firefly", firefly_cron_base_url="http://firefly/cron", firefly_cron_token="token", firefly_cron_timeout_sec=10.0, ) monkeypatch.setattr("ariadne.services.firefly.settings", dummy) monkeypatch.setattr("ariadne.services.firefly.PodExecutor", lambda *_args, **_kwargs: DummyExecutor()) class DummyHTTP: def __init__(self): self.calls = [] def __enter__(self): return self def __exit__(self, exc_type, exc, tb): return False def get(self, url): self.calls.append(url) return types.SimpleNamespace(status_code=200) monkeypatch.setattr("ariadne.services.firefly.httpx.Client", lambda *args, **kwargs: DummyHTTP()) svc = FireflyService() result = svc.run_cron() assert result["status"] == "ok" def test_firefly_sync_users(monkeypatch) -> None: dummy = types.SimpleNamespace( firefly_namespace="finance", firefly_user_sync_wait_timeout_sec=60.0, firefly_pod_label="app=firefly", firefly_container="firefly", firefly_cron_base_url="http://firefly/cron", firefly_cron_token="token", firefly_cron_timeout_sec=10.0, mailu_domain="bstein.dev", ) monkeypatch.setattr("ariadne.services.firefly.settings", dummy) calls: list[tuple[str, str, str]] = [] class DummyAdmin: def ready(self) -> bool: return True def iter_users(self, page_size=200, brief=False): return [{"id": "1", "username": "alice", "attributes": {}}] def get_user(self, user_id: str): return {"id": user_id, "username": "alice", "attributes": {}} def set_user_attribute(self, username: str, key: str, value: str) -> None: calls.append((username, key, value)) monkeypatch.setattr("ariadne.services.firefly.keycloak_admin", DummyAdmin()) monkeypatch.setattr( "ariadne.services.firefly.mailu.resolve_mailu_email", lambda *_args, **_kwargs: "alice@bstein.dev", ) monkeypatch.setattr("ariadne.services.firefly.random_password", lambda *_args: "pw") def fake_sync_user(self, *_args, **_kwargs): return {"status": "ok", "detail": "ok"} monkeypatch.setattr(FireflyService, "sync_user", fake_sync_user) svc = FireflyService() result = svc.sync_users() assert result["status"] == "ok" assert any(key == "firefly_password" for _user, key, _value in calls) assert any(key == "firefly_password_updated_at" for _user, key, _value in calls) def test_mailu_sync_updates_attrs(monkeypatch) -> None: dummy_settings = types.SimpleNamespace( mailu_domain="bstein.dev", mailu_db_host="localhost", mailu_db_port=5432, mailu_db_name="mailu", mailu_db_user="mailu", mailu_db_password="secret", mailu_default_quota=20000000000, mailu_system_users=[], mailu_system_password="", ) monkeypatch.setattr("ariadne.services.mailu.settings", dummy_settings) monkeypatch.setattr("ariadne.services.mailu.keycloak_admin.ready", lambda: True) monkeypatch.setattr( "ariadne.services.mailu.keycloak_admin.iter_users", lambda *args, **kwargs: [ { "id": "1", "username": "alice", "enabled": True, "email": "alice@example.com", "attributes": {}, "firstName": "Alice", "lastName": "Example", } ], ) updates: list[tuple[str, dict[str, object]]] = [] monkeypatch.setattr( "ariadne.services.mailu.keycloak_admin.update_user_safe", lambda user_id, payload: updates.append((user_id, payload)), ) mailbox_calls: list[tuple[str, str, str]] = [] monkeypatch.setattr( "ariadne.services.mailu.MailuService._ensure_mailbox", lambda self, _conn, email, password, display: mailbox_calls.append((email, password, display)) or True, ) class DummyConn: def __enter__(self): return self def __exit__(self, exc_type, exc, tb): return False monkeypatch.setattr("ariadne.services.mailu.psycopg.connect", lambda *args, **kwargs: DummyConn()) svc = MailuService() summary = svc.sync("provision", force=True) assert summary.processed == 1 assert summary.updated == 1 assert mailbox_calls assert updates assert "mailu_email" in updates[0][1]["attributes"] def test_mailu_sync_skips_disabled(monkeypatch) -> None: dummy_settings = types.SimpleNamespace( mailu_domain="bstein.dev", mailu_db_host="localhost", mailu_db_port=5432, mailu_db_name="mailu", mailu_db_user="mailu", mailu_db_password="secret", mailu_default_quota=20000000000, mailu_system_users=[], mailu_system_password="", ) monkeypatch.setattr("ariadne.services.mailu.settings", dummy_settings) monkeypatch.setattr("ariadne.services.mailu.keycloak_admin.ready", lambda: True) monkeypatch.setattr( "ariadne.services.mailu.keycloak_admin.iter_users", lambda *args, **kwargs: [ { "id": "1", "username": "alice", "enabled": True, "attributes": {"mailu_enabled": ["false"]}, } ], ) mailbox_calls: list[tuple[str, str, str]] = [] monkeypatch.setattr( "ariadne.services.mailu.MailuService._ensure_mailbox", lambda self, _conn, email, password, display: mailbox_calls.append((email, password, display)) or True, ) class DummyConn: def __enter__(self): return self def __exit__(self, exc_type, exc, tb): return False monkeypatch.setattr("ariadne.services.mailu.psycopg.connect", lambda *args, **kwargs: DummyConn()) svc = MailuService() summary = svc.sync("provision") assert summary.skipped == 1 assert mailbox_calls == [] def test_mailu_sync_system_mailboxes(monkeypatch) -> None: dummy_settings = types.SimpleNamespace( mailu_domain="bstein.dev", mailu_db_host="localhost", mailu_db_port=5432, mailu_db_name="mailu", mailu_db_user="mailu", mailu_db_password="secret", mailu_default_quota=20000000000, mailu_system_users=["no-reply-portal@bstein.dev"], mailu_system_password="systempw", ) monkeypatch.setattr("ariadne.services.mailu.settings", dummy_settings) monkeypatch.setattr("ariadne.services.mailu.keycloak_admin.ready", lambda: True) monkeypatch.setattr("ariadne.services.mailu.keycloak_admin.iter_users", lambda *args, **kwargs: []) mailbox_calls: list[tuple[str, str, str]] = [] monkeypatch.setattr( "ariadne.services.mailu.MailuService._ensure_mailbox", lambda self, _conn, email, password, display: mailbox_calls.append((email, password, display)) or True, ) class DummyConn: def __enter__(self): return self def __exit__(self, exc_type, exc, tb): return False monkeypatch.setattr("ariadne.services.mailu.psycopg.connect", lambda *args, **kwargs: DummyConn()) svc = MailuService() summary = svc.sync("schedule") assert summary.system_mailboxes == 1 assert mailbox_calls[0][0] == "no-reply-portal@bstein.dev" def test_vaultwarden_invite_uses_admin_session(monkeypatch) -> None: dummy_settings = types.SimpleNamespace( vaultwarden_namespace="vaultwarden", vaultwarden_admin_secret_name="vaultwarden-admin", vaultwarden_admin_secret_key="ADMIN_TOKEN", vaultwarden_admin_rate_limit_backoff_sec=600, vaultwarden_admin_session_ttl_sec=900, vaultwarden_service_host="vaultwarden-service.vaultwarden.svc.cluster.local", vaultwarden_pod_label="app=vaultwarden", vaultwarden_pod_port=80, ) client = DummyVaultwardenClient() monkeypatch.setattr("ariadne.services.vaultwarden.settings", dummy_settings) monkeypatch.setattr("ariadne.services.vaultwarden.get_secret_value", lambda *args, **kwargs: "token") monkeypatch.setattr("ariadne.services.vaultwarden.httpx.Client", lambda *args, **kwargs: client) monkeypatch.setattr( "ariadne.services.vaultwarden.VaultwardenService._find_pod_ip", staticmethod(lambda *args, **kwargs: "127.0.0.1"), ) svc = VaultwardenService() result = svc.invite_user("alice@bstein.dev") assert result.ok is True assert any(call[0] == "/admin" for call in client.calls) assert any(call[0] == "/admin/invite" for call in client.calls) def test_vaultwarden_invite_handles_rate_limit(monkeypatch) -> None: dummy_settings = types.SimpleNamespace( vaultwarden_namespace="vaultwarden", vaultwarden_admin_secret_name="vaultwarden-admin", vaultwarden_admin_secret_key="ADMIN_TOKEN", vaultwarden_admin_rate_limit_backoff_sec=600, vaultwarden_admin_session_ttl_sec=900, vaultwarden_service_host="vaultwarden-service.vaultwarden.svc.cluster.local", vaultwarden_pod_label="app=vaultwarden", vaultwarden_pod_port=80, ) client = DummyVaultwardenClient() client.responses["/admin/invite"] = DummyResponse(429, "rate limited") monkeypatch.setattr("ariadne.services.vaultwarden.settings", dummy_settings) monkeypatch.setattr("ariadne.services.vaultwarden.get_secret_value", lambda *args, **kwargs: "token") monkeypatch.setattr("ariadne.services.vaultwarden.httpx.Client", lambda *args, **kwargs: client) monkeypatch.setattr( "ariadne.services.vaultwarden.VaultwardenService._find_pod_ip", staticmethod(lambda *args, **kwargs: "127.0.0.1"), ) svc = VaultwardenService() result = svc.invite_user("alice@bstein.dev") assert result.status == "rate_limited" def test_vaultwarden_invite_existing_user(monkeypatch) -> None: dummy_settings = types.SimpleNamespace( vaultwarden_namespace="vaultwarden", vaultwarden_admin_secret_name="vaultwarden-admin", vaultwarden_admin_secret_key="ADMIN_TOKEN", vaultwarden_admin_rate_limit_backoff_sec=600, vaultwarden_admin_session_ttl_sec=900, vaultwarden_service_host="vaultwarden-service.vaultwarden.svc.cluster.local", vaultwarden_pod_label="app=vaultwarden", vaultwarden_pod_port=80, ) client = DummyVaultwardenClient() client.responses["/admin/invite"] = DummyResponse(409, "user already exists") monkeypatch.setattr("ariadne.services.vaultwarden.settings", dummy_settings) monkeypatch.setattr("ariadne.services.vaultwarden.get_secret_value", lambda *args, **kwargs: "token") monkeypatch.setattr("ariadne.services.vaultwarden.httpx.Client", lambda *args, **kwargs: client) monkeypatch.setattr( "ariadne.services.vaultwarden.VaultwardenService._find_pod_ip", staticmethod(lambda *args, **kwargs: "127.0.0.1"), ) svc = VaultwardenService() result = svc.invite_user("alice@bstein.dev") assert result.status == "already_present" def test_vaultwarden_invite_rejects_invalid_email() -> None: svc = VaultwardenService() result = svc.invite_user("bad-email") assert result.status == "invalid_email" def test_vaultwarden_invite_rate_limited_short_circuit() -> None: svc = VaultwardenService() svc._rate_limited_until = time.time() + 60 result = svc.invite_user("alice@bstein.dev") assert result.status == "rate_limited" def test_vaultwarden_invite_handles_admin_exception(monkeypatch) -> None: dummy_settings = types.SimpleNamespace( vaultwarden_namespace="vaultwarden", vaultwarden_admin_secret_name="vaultwarden-admin", vaultwarden_admin_secret_key="ADMIN_TOKEN", vaultwarden_admin_rate_limit_backoff_sec=600, vaultwarden_admin_session_ttl_sec=900, vaultwarden_service_host="vaultwarden-service.vaultwarden.svc.cluster.local", vaultwarden_pod_label="app=vaultwarden", vaultwarden_pod_port=80, ) monkeypatch.setattr("ariadne.services.vaultwarden.settings", dummy_settings) monkeypatch.setattr( "ariadne.services.vaultwarden.VaultwardenService._find_pod_ip", staticmethod(lambda *args, **kwargs: (_ for _ in ()).throw(RuntimeError("boom"))), ) svc = VaultwardenService() monkeypatch.setattr( svc, "_admin_session", lambda *_args, **_kwargs: (_ for _ in ()).throw(RuntimeError("rate limited")), ) result = svc.invite_user("alice@bstein.dev") assert result.status == "rate_limited" def test_vaultwarden_invite_handles_bad_body(monkeypatch) -> None: dummy_settings = types.SimpleNamespace( vaultwarden_namespace="vaultwarden", vaultwarden_admin_secret_name="vaultwarden-admin", vaultwarden_admin_secret_key="ADMIN_TOKEN", vaultwarden_admin_rate_limit_backoff_sec=600, vaultwarden_admin_session_ttl_sec=900, vaultwarden_service_host="vaultwarden-service.vaultwarden.svc.cluster.local", vaultwarden_pod_label="app=vaultwarden", vaultwarden_pod_port=80, ) class BadTextResponse: def __init__(self, status_code=500): self.status_code = status_code def raise_for_status(self): return None @property def text(self): raise RuntimeError("boom") class BadTextClient(DummyVaultwardenClient): def post(self, path, json=None, data=None): self.calls.append((path, json, data)) return BadTextResponse(500) monkeypatch.setattr("ariadne.services.vaultwarden.settings", dummy_settings) monkeypatch.setattr("ariadne.services.vaultwarden.get_secret_value", lambda *args, **kwargs: "token") monkeypatch.setattr("ariadne.services.vaultwarden.httpx.Client", lambda *args, **kwargs: BadTextClient()) monkeypatch.setattr( "ariadne.services.vaultwarden.VaultwardenService._find_pod_ip", staticmethod(lambda *args, **kwargs: "127.0.0.1"), ) svc = VaultwardenService() result = svc.invite_user("alice@bstein.dev") assert result.status == "error" def test_vaultwarden_invite_handles_fallback_skip(monkeypatch) -> None: dummy_settings = types.SimpleNamespace( vaultwarden_namespace="vaultwarden", vaultwarden_admin_secret_name="vaultwarden-admin", vaultwarden_admin_secret_key="ADMIN_TOKEN", vaultwarden_admin_rate_limit_backoff_sec=600, vaultwarden_admin_session_ttl_sec=900, vaultwarden_service_host="vaultwarden-service.vaultwarden.svc.cluster.local", vaultwarden_pod_label="app=vaultwarden", vaultwarden_pod_port=80, ) monkeypatch.setattr("ariadne.services.vaultwarden.settings", dummy_settings) monkeypatch.setattr( "ariadne.services.vaultwarden.VaultwardenService._find_pod_ip", staticmethod(lambda *args, **kwargs: (_ for _ in ()).throw(RuntimeError("boom"))), ) svc = VaultwardenService() monkeypatch.setattr(svc, "_admin_session", lambda *_args, **_kwargs: (_ for _ in ()).throw(RuntimeError("nope"))) result = svc.invite_user("alice@bstein.dev") assert result.status == "error" def test_vaultwarden_find_pod_ip(monkeypatch) -> None: monkeypatch.setattr( "ariadne.services.vaultwarden.get_json", lambda *args, **kwargs: { "items": [ { "status": { "phase": "Running", "podIP": "10.0.0.1", "conditions": [{"type": "Ready", "status": "True"}], } } ] }, ) assert VaultwardenService._find_pod_ip("ns", "app=vaultwarden") == "10.0.0.1" def test_vaultwarden_find_pod_ip_skips_missing_ip(monkeypatch) -> None: monkeypatch.setattr( "ariadne.services.vaultwarden.get_json", lambda *args, **kwargs: { "items": [ {"status": {"phase": "Running", "podIP": ""}}, {"status": {"phase": "Running", "podIP": "10.0.0.2", "conditions": []}}, ] }, ) assert VaultwardenService._find_pod_ip("ns", "app=vaultwarden") == "10.0.0.2" def test_vaultwarden_find_pod_ip_conditions_default_ready(monkeypatch) -> None: monkeypatch.setattr( "ariadne.services.vaultwarden.get_json", lambda *args, **kwargs: { "items": [ {"status": {"phase": "Running", "podIP": "10.0.0.3", "conditions": ["bad"]}}, ] }, ) assert VaultwardenService._find_pod_ip("ns", "app=vaultwarden") == "10.0.0.3" def test_vaultwarden_find_pod_ip_no_pods(monkeypatch) -> None: monkeypatch.setattr("ariadne.services.vaultwarden.get_json", lambda *args, **kwargs: {"items": []}) with pytest.raises(RuntimeError): VaultwardenService._find_pod_ip("ns", "app=vaultwarden") def test_vaultwarden_find_pod_ip_missing_ip(monkeypatch) -> None: monkeypatch.setattr( "ariadne.services.vaultwarden.get_json", lambda *args, **kwargs: { "items": [ {"status": {"phase": "Pending", "conditions": ["bad"]}}, ] }, ) with pytest.raises(RuntimeError): VaultwardenService._find_pod_ip("ns", "app=vaultwarden") def test_vaultwarden_admin_session_rate_limit(monkeypatch) -> None: dummy_settings = types.SimpleNamespace( vaultwarden_namespace="vaultwarden", vaultwarden_admin_secret_name="vaultwarden-admin", vaultwarden_admin_secret_key="ADMIN_TOKEN", vaultwarden_admin_rate_limit_backoff_sec=1, vaultwarden_admin_session_ttl_sec=900, vaultwarden_service_host="vaultwarden-service.vaultwarden.svc.cluster.local", vaultwarden_pod_label="app=vaultwarden", vaultwarden_pod_port=80, ) client = DummyVaultwardenClient() client.responses["/admin"] = DummyResponse(429, "") monkeypatch.setattr("ariadne.services.vaultwarden.settings", dummy_settings) monkeypatch.setattr("ariadne.services.vaultwarden.get_secret_value", lambda *args, **kwargs: "token") monkeypatch.setattr("ariadne.services.vaultwarden.httpx.Client", lambda *args, **kwargs: client) svc = VaultwardenService() with pytest.raises(RuntimeError): svc._admin_session("http://vaultwarden") def test_vaultwarden_admin_session_reuses_client() -> None: svc = VaultwardenService() svc._admin_client = DummyVaultwardenClient() svc._admin_session_expires_at = time.time() + 60 svc._admin_session_base_url = "http://vaultwarden" client = svc._admin_session("http://vaultwarden") assert client is svc._admin_client def test_vaultwarden_admin_session_rate_limited_until() -> None: svc = VaultwardenService() svc._rate_limited_until = time.time() + 60 with pytest.raises(RuntimeError): svc._admin_session("http://vaultwarden") def test_vaultwarden_admin_session_closes_existing(monkeypatch) -> None: dummy_settings = types.SimpleNamespace( vaultwarden_namespace="vaultwarden", vaultwarden_admin_secret_name="vaultwarden-admin", vaultwarden_admin_secret_key="ADMIN_TOKEN", vaultwarden_admin_rate_limit_backoff_sec=600, vaultwarden_admin_session_ttl_sec=900, vaultwarden_service_host="vaultwarden-service.vaultwarden.svc.cluster.local", vaultwarden_pod_label="app=vaultwarden", vaultwarden_pod_port=80, ) class CloseFail: def close(self): raise RuntimeError("boom") monkeypatch.setattr("ariadne.services.vaultwarden.settings", dummy_settings) monkeypatch.setattr("ariadne.services.vaultwarden.get_secret_value", lambda *args, **kwargs: "token") monkeypatch.setattr("ariadne.services.vaultwarden.httpx.Client", lambda *args, **kwargs: DummyVaultwardenClient()) svc = VaultwardenService() svc._admin_client = CloseFail() svc._admin_session_expires_at = time.time() - 10 svc._admin_session_base_url = "http://old" assert svc._admin_session("http://vaultwarden") is not None def test_nextcloud_missing_config(monkeypatch) -> None: dummy = types.SimpleNamespace( nextcloud_namespace="", nextcloud_mail_sync_cronjob="", nextcloud_mail_sync_wait_timeout_sec=90.0, nextcloud_mail_sync_job_ttl_sec=3600, nextcloud_pod_label="app=nextcloud", nextcloud_container="nextcloud", nextcloud_exec_timeout_sec=30.0, nextcloud_db_host="", nextcloud_db_port=5432, nextcloud_db_name="nextcloud", nextcloud_db_user="nextcloud", nextcloud_db_password="", mailu_domain="bstein.dev", mailu_host="mail.bstein.dev", ) monkeypatch.setattr("ariadne.services.nextcloud.settings", dummy) svc = NextcloudService() with pytest.raises(RuntimeError): svc.sync_mail("alice") def test_wger_sync_missing_inputs(monkeypatch) -> None: dummy = types.SimpleNamespace( wger_namespace="health", wger_user_sync_cronjob="wger-user-sync", wger_admin_cronjob="wger-admin-ensure", wger_user_sync_wait_timeout_sec=60.0, wger_pod_label="app=wger", wger_container="wger", wger_admin_username="admin", wger_admin_password="pw", wger_admin_email="admin@bstein.dev", ) monkeypatch.setattr("ariadne.services.wger.settings", dummy) monkeypatch.setattr("ariadne.services.wger.PodExecutor", lambda *_args, **_kwargs: DummyExecutor()) svc = WgerService() with pytest.raises(RuntimeError): svc.sync_user("", "email", "pw", wait=True) with pytest.raises(RuntimeError): svc.sync_user("alice", "email", "", wait=True) def test_wger_sync_missing_config(monkeypatch) -> None: dummy = types.SimpleNamespace( wger_namespace="", wger_user_sync_cronjob="", wger_admin_cronjob="wger-admin-ensure", wger_user_sync_wait_timeout_sec=60.0, wger_pod_label="app=wger", wger_container="wger", wger_admin_username="admin", wger_admin_password="pw", wger_admin_email="admin@bstein.dev", ) monkeypatch.setattr("ariadne.services.wger.settings", dummy) monkeypatch.setattr("ariadne.services.wger.PodExecutor", lambda *_args, **_kwargs: DummyExecutor()) svc = WgerService() with pytest.raises(RuntimeError): svc.sync_user("alice", "email", "pw", wait=True) def test_wger_ensure_admin(monkeypatch) -> None: dummy = types.SimpleNamespace( wger_namespace="health", wger_user_sync_cronjob="wger-user-sync", wger_admin_cronjob="wger-admin-ensure", wger_user_sync_wait_timeout_sec=60.0, wger_pod_label="app=wger", wger_container="wger", wger_admin_username="admin", wger_admin_password="pw", wger_admin_email="admin@bstein.dev", ) monkeypatch.setattr("ariadne.services.wger.settings", dummy) monkeypatch.setattr("ariadne.services.wger.PodExecutor", lambda *_args, **_kwargs: DummyExecutor()) svc = WgerService() result = svc.ensure_admin(wait=True) assert result["status"] == "ok" def test_wger_ensure_admin_missing_creds(monkeypatch) -> None: dummy = types.SimpleNamespace( wger_namespace="health", wger_user_sync_cronjob="wger-user-sync", wger_admin_cronjob="wger-admin-ensure", wger_user_sync_wait_timeout_sec=60.0, wger_pod_label="app=wger", wger_container="wger", wger_admin_username="", wger_admin_password="", wger_admin_email="", ) monkeypatch.setattr("ariadne.services.wger.settings", dummy) monkeypatch.setattr("ariadne.services.wger.PodExecutor", lambda *_args, **_kwargs: DummyExecutor()) svc = WgerService() result = svc.ensure_admin(wait=True) assert result["status"] == "error" def test_wger_ensure_admin_missing_config(monkeypatch) -> None: dummy = types.SimpleNamespace( wger_namespace="", wger_user_sync_cronjob="wger-user-sync", wger_admin_cronjob="", wger_user_sync_wait_timeout_sec=60.0, wger_pod_label="app=wger", wger_container="wger", wger_admin_username="admin", wger_admin_password="pw", wger_admin_email="admin@bstein.dev", ) monkeypatch.setattr("ariadne.services.wger.settings", dummy) monkeypatch.setattr("ariadne.services.wger.PodExecutor", lambda *_args, **_kwargs: DummyExecutor()) svc = WgerService() with pytest.raises(RuntimeError): svc.ensure_admin(wait=True) def test_mailu_mailbox_exists_handles_error(monkeypatch) -> None: dummy_settings = types.SimpleNamespace( mailu_sync_url="", mailu_sync_wait_timeout_sec=10.0, mailu_db_host="localhost", mailu_db_port=5432, mailu_db_name="mailu", mailu_db_user="mailu", mailu_db_password="secret", mailu_domain="bstein.dev", ) monkeypatch.setattr("ariadne.services.mailu.settings", dummy_settings) monkeypatch.setattr("ariadne.services.mailu.psycopg.connect", lambda *args, **kwargs: (_ for _ in ()).throw(RuntimeError("boom"))) svc = MailuService() assert svc.mailbox_exists("alice@bstein.dev") is False def test_mailu_mailbox_exists_success(monkeypatch) -> None: dummy_settings = types.SimpleNamespace( mailu_sync_url="", mailu_sync_wait_timeout_sec=10.0, mailu_db_host="localhost", mailu_db_port=5432, mailu_db_name="mailu", mailu_db_user="mailu", mailu_db_password="secret", mailu_domain="bstein.dev", ) monkeypatch.setattr("ariadne.services.mailu.settings", dummy_settings) class DummyCursor: def execute(self, *_args, **_kwargs): return None def fetchone(self): return {"id": 1} def __enter__(self): return self def __exit__(self, *_args): return False class DummyConn: def cursor(self): return DummyCursor() def __enter__(self): return self def __exit__(self, *_args): return False monkeypatch.setattr("ariadne.services.mailu.psycopg.connect", lambda *args, **kwargs: DummyConn()) svc = MailuService() assert svc.mailbox_exists("alice@bstein.dev") is True def test_mailu_wait_for_mailbox(monkeypatch) -> None: dummy_settings = types.SimpleNamespace( mailu_sync_url="", mailu_sync_wait_timeout_sec=10.0, mailu_db_host="localhost", mailu_db_port=5432, mailu_db_name="mailu", mailu_db_user="mailu", mailu_db_password="secret", mailu_domain="bstein.dev", ) monkeypatch.setattr("ariadne.services.mailu.settings", dummy_settings) monkeypatch.setattr(MailuService, "mailbox_exists", lambda self, email: True) svc = MailuService() assert svc.wait_for_mailbox("alice@bstein.dev", timeout_sec=1.0) is True def test_mailu_mailbox_exists_empty_email(monkeypatch) -> None: dummy_settings = types.SimpleNamespace( mailu_sync_url="", mailu_sync_wait_timeout_sec=10.0, mailu_db_host="localhost", mailu_db_port=5432, mailu_db_name="mailu", mailu_db_user="mailu", mailu_db_password="secret", mailu_domain="bstein.dev", ) monkeypatch.setattr("ariadne.services.mailu.settings", dummy_settings) svc = MailuService() assert svc.mailbox_exists("") is False def test_nextcloud_sync_missing_username(monkeypatch) -> None: dummy = types.SimpleNamespace( nextcloud_namespace="nextcloud", nextcloud_mail_sync_cronjob="nextcloud-mail-sync", nextcloud_mail_sync_wait_timeout_sec=90.0, nextcloud_mail_sync_job_ttl_sec=3600, nextcloud_pod_label="app=nextcloud", nextcloud_container="nextcloud", nextcloud_exec_timeout_sec=30.0, nextcloud_db_host="", nextcloud_db_port=5432, nextcloud_db_name="nextcloud", nextcloud_db_user="nextcloud", nextcloud_db_password="", mailu_domain="bstein.dev", mailu_host="mail.bstein.dev", ) monkeypatch.setattr("ariadne.services.nextcloud.settings", dummy) monkeypatch.setattr("ariadne.services.nextcloud.PodExecutor", lambda *_args, **_kwargs: DummyExecutor()) svc = NextcloudService() with pytest.raises(RuntimeError): svc.sync_mail(" ", wait=True) def test_nextcloud_sync_no_match(monkeypatch) -> None: dummy = types.SimpleNamespace( nextcloud_namespace="nextcloud", nextcloud_mail_sync_cronjob="nextcloud-mail-sync", nextcloud_mail_sync_wait_timeout_sec=90.0, nextcloud_mail_sync_job_ttl_sec=3600, nextcloud_pod_label="app=nextcloud", nextcloud_container="nextcloud", nextcloud_exec_timeout_sec=30.0, nextcloud_db_host="", nextcloud_db_port=5432, nextcloud_db_name="nextcloud", nextcloud_db_user="nextcloud", nextcloud_db_password="", mailu_domain="bstein.dev", mailu_host="mail.bstein.dev", ) monkeypatch.setattr("ariadne.services.nextcloud.settings", dummy) monkeypatch.setattr("ariadne.services.nextcloud.keycloak_admin.ready", lambda: True) monkeypatch.setattr("ariadne.services.nextcloud.keycloak_admin.find_user", lambda *_args, **_kwargs: None) monkeypatch.setattr("ariadne.services.nextcloud.PodExecutor", lambda *_args, **_kwargs: DummyExecutor()) svc = NextcloudService() result = svc.sync_mail("alice", wait=False) assert result["status"] == "ok"