from __future__ import annotations import time import types import pytest from ariadne.services import mailu as mailu_module 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_wger_sync_marks_rotated(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": { "mailu_email": ["alice@bstein.dev"], "wger_password": ["pw"], "wger_password_updated_at": ["2025-01-01T00:00:00Z"], }, } ] def get_user(self, user_id: str): return { "id": user_id, "username": "alice", "attributes": { "mailu_email": ["alice@bstein.dev"], "wger_password": ["pw"], "wger_password_updated_at": ["2025-01-01T00:00:00Z"], }, } 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", ) def fake_check(self, *_args, **_kwargs): return {"status": "mismatch", "detail": "mismatch"} monkeypatch.setattr(WgerService, "check_password", fake_check) monkeypatch.setattr(WgerService, "sync_user", lambda *_args, **_kwargs: {"status": "ok"}) svc = WgerService() result = svc.sync_users() assert result["status"] == "ok" assert any(key == "wger_password_rotated_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_firefly_sync_marks_rotated(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": { "mailu_email": ["alice@bstein.dev"], "firefly_password": ["pw"], "firefly_password_updated_at": ["2025-01-01T00:00:00Z"], }, } ] def get_user(self, user_id: str): return { "id": user_id, "username": "alice", "attributes": { "mailu_email": ["alice@bstein.dev"], "firefly_password": ["pw"], "firefly_password_updated_at": ["2025-01-01T00:00:00Z"], }, } 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", ) def fake_check(self, *_args, **_kwargs): return {"status": "mismatch", "detail": "mismatch"} monkeypatch.setattr(FireflyService, "check_password", fake_check) monkeypatch.setattr(FireflyService, "sync_user", lambda *_args, **_kwargs: {"status": "ok"}) svc = FireflyService() result = svc.sync_users() assert result["status"] == "ok" assert any(key == "firefly_password_rotated_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_rotates_long_password(monkeypatch) -> None: long_password = "x" * 100 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": {"mailu_app_password": [long_password]}, "firstName": "Alice", "lastName": "Example", } ], ) monkeypatch.setattr("ariadne.services.mailu.random_password", lambda *_args, **_kwargs: "short-pass-123") 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 updates attrs = updates[0][1]["attributes"] assert attrs["mailu_app_password"] == ["short-pass-123"] assert mailbox_calls assert mailbox_calls[0][1] == "short-pass-123" def test_mailu_sync_retries_on_password_limit(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(mailu_module.keycloak_admin, "ready", lambda: True) update_calls: list[tuple[str, dict[str, object]]] = [] monkeypatch.setattr( mailu_module.keycloak_admin, "update_user_safe", lambda user_id, payload: update_calls.append((user_id, payload)), ) monkeypatch.setattr( mailu_module.keycloak_admin, "iter_users", lambda *args, **kwargs: [ { "id": "1", "username": "alice", "enabled": True, "email": "alice@example.com", "attributes": {"mailu_app_password": ["short-pass"]}, "firstName": "Alice", "lastName": "Example", } ], ) monkeypatch.setattr("ariadne.services.mailu.random_password", lambda *_args, **_kwargs: "retry-pass-1") set_calls: list[tuple[str, str, str]] = [] monkeypatch.setattr( mailu_module.keycloak_admin, "set_user_attribute", lambda username, key, value: set_calls.append((username, key, value)), ) call_count = {"count": 0} def fake_ensure(self, _conn, _email, password, _display): call_count["count"] += 1 if call_count["count"] == 1: raise mailu_module.PasswordTooLongError("password cannot be longer than 72 bytes") assert password == "retry-pass-1" return True monkeypatch.setattr("ariadne.services.mailu.MailuService._ensure_mailbox", fake_ensure) 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 update_calls assert call_count["count"] == 2 assert set_calls 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"