diff --git a/ariadne/services/firefly.py b/ariadne/services/firefly.py index 577341a..0a57709 100644 --- a/ariadne/services/firefly.py +++ b/ariadne/services/firefly.py @@ -19,6 +19,7 @@ from .mailu import mailu HTTP_OK = 200 FIREFLY_PASSWORD_ATTR = "firefly_password" FIREFLY_PASSWORD_UPDATED_ATTR = "firefly_password_updated_at" +FIREFLY_PASSWORD_ROTATED_ATTR = "firefly_password_rotated_at" logger = get_logger(__name__) @@ -141,11 +142,109 @@ _FIREFLY_SYNC_SCRIPT = textwrap.dedent( """ ).strip() +_FIREFLY_PASSWORD_CHECK_SCRIPT = textwrap.dedent( + """ + make(ConsoleKernel::class); + $kernel->bootstrap(); + + try { + FireflyConfig::set('single_user_mode', true); + } catch (Throwable $exc) { + error_line('failed to enforce single_user_mode: ' . $exc->getMessage()); + } + + $existing_user = User::where('email', $email)->first(); + if (!$existing_user) { + error_line('firefly user missing'); + exit(3); + } + + if (Hash::check($password, $existing_user->password)) { + log_line('password match'); + exit(0); + } + + log_line('password mismatch'); + exit(1); + """ +).strip() + def _firefly_exec_command() -> str: return f"php <<'PHP'\n{_FIREFLY_SYNC_SCRIPT}\nPHP" +def _firefly_check_command() -> str: + return f"php <<'PHP'\n{_FIREFLY_PASSWORD_CHECK_SCRIPT}\nPHP" + + @dataclass(frozen=True) class FireflySyncSummary: processed: int @@ -194,6 +293,7 @@ class FireflySyncInput: password: str password_generated: bool updated_at: str + rotated_at: str def _extract_attr(attrs: Any, key: str) -> str: @@ -264,6 +364,15 @@ def _set_firefly_updated_at(username: str) -> bool: return True +def _set_firefly_rotated_at(username: str) -> bool: + now_iso = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") + try: + keycloak_admin.set_user_attribute(username, FIREFLY_PASSWORD_ROTATED_ATTR, now_iso) + except Exception: + return False + return True + + def _normalize_user(user: dict[str, Any]) -> tuple[UserSyncOutcome | None, UserIdentity | None]: username = user.get("username") if isinstance(user.get("username"), str) else "" if _should_skip_user(user, username): @@ -339,12 +448,14 @@ def _build_sync_input(user: dict[str, Any]) -> FireflySyncInput | UserSyncOutcom return UserSyncOutcome("failed", "missing firefly password") updated_at = _extract_attr(attrs, FIREFLY_PASSWORD_UPDATED_ATTR) + rotated_at = _extract_attr(attrs, FIREFLY_PASSWORD_ROTATED_ATTR) return FireflySyncInput( username=identity.username, mailu_email=mailu_email, password=password, password_generated=password_generated, updated_at=updated_at, + rotated_at=rotated_at, ) @@ -383,6 +494,39 @@ class FireflyService: output = (result.stdout or result.stderr).strip() return {"status": "ok", "detail": output} + def check_password(self, email: str, password: str) -> dict[str, Any]: + email = (email or "").strip() + if not email: + raise RuntimeError("missing email") + if not password: + raise RuntimeError("missing password") + if not settings.firefly_namespace: + raise RuntimeError("firefly sync not configured") + + env = { + "FIREFLY_USER_EMAIL": email, + "FIREFLY_USER_PASSWORD": password, + } + + try: + result = self._executor.exec( + _firefly_check_command(), + env=env, + timeout_sec=settings.firefly_user_sync_wait_timeout_sec, + check=False, + ) + except (ExecError, PodSelectionError, TimeoutError) as exc: + return {"status": "error", "detail": str(exc)} + + detail = (result.stdout or result.stderr).strip() + if result.exit_code == 0: + return {"status": "match", "detail": detail} + if result.exit_code == 1: + return {"status": "mismatch", "detail": detail} + if result.exit_code == 3: + return {"status": "missing", "detail": detail or "user missing"} + return {"status": "error", "detail": detail or f"exit_code={result.exit_code}"} + def run_cron(self) -> dict[str, Any]: if not settings.firefly_cron_token: raise RuntimeError("firefly cron token missing") @@ -401,7 +545,19 @@ class FireflyService: if isinstance(prepared, UserSyncOutcome): return prepared if _should_skip_sync(prepared.password_generated, prepared.updated_at): - return UserSyncOutcome("skipped") + if prepared.rotated_at: + return UserSyncOutcome("skipped") + check = self.check_password(prepared.mailu_email, prepared.password) + status = check.get("status") if isinstance(check, dict) else "error" + if status == "match": + return UserSyncOutcome("skipped") + if status == "mismatch": + if not _set_firefly_rotated_at(prepared.username): + return UserSyncOutcome("failed", "failed to set rotated_at") + return UserSyncOutcome("synced") + detail = check.get("detail") if isinstance(check, dict) else "" + detail = detail or "password check failed" + return UserSyncOutcome("failed", detail) result = self.sync_user(prepared.mailu_email, prepared.password, wait=True) result_status = result.get("status") if isinstance(result, dict) else "error" diff --git a/ariadne/services/vaultwarden.py b/ariadne/services/vaultwarden.py index 8c7e01c..8f34222 100644 --- a/ariadne/services/vaultwarden.py +++ b/ariadne/services/vaultwarden.py @@ -54,21 +54,24 @@ class VaultwardenService: return [url for url in urls if url] @staticmethod - def _already_present(resp: httpx.Response) -> bool: + def _invite_conflict_status(resp: httpx.Response) -> str | None: try: body = resp.text or "" except Exception: body = "" lowered = body.lower() - return any( + if "already invited" in lowered: + return "invited" + if any( marker in lowered for marker in ( - "already invited", "already exists", "already registered", "user already exists", ) - ) + ): + return "already_present" + return None def _invite_via(self, base_url: str, email: str) -> VaultwardenInvite | None: if not base_url: @@ -81,8 +84,14 @@ class VaultwardenService: result = self._rate_limited() elif resp.status_code in {HTTP_OK, HTTP_CREATED, HTTP_NO_CONTENT}: result = VaultwardenInvite(ok=True, status="invited", detail="invite created") - elif resp.status_code in {HTTP_BAD_REQUEST, HTTP_CONFLICT} and self._already_present(resp): - result = VaultwardenInvite(ok=True, status="already_present", detail="user already present") + elif resp.status_code in {HTTP_BAD_REQUEST, HTTP_CONFLICT}: + status = self._invite_conflict_status(resp) + if status == "invited": + result = VaultwardenInvite(ok=True, status="invited", detail="user already invited") + elif status == "already_present": + result = VaultwardenInvite(ok=True, status="already_present", detail="user already present") + else: + result = VaultwardenInvite(ok=False, status="error", detail=f"status {resp.status_code}") else: result = VaultwardenInvite(ok=False, status="error", detail=f"status {resp.status_code}") except Exception as exc: diff --git a/ariadne/services/vaultwarden_sync.py b/ariadne/services/vaultwarden_sync.py index 2044e03..2cacc8f 100644 --- a/ariadne/services/vaultwarden_sync.py +++ b/ariadne/services/vaultwarden_sync.py @@ -15,6 +15,7 @@ from .vaultwarden import vaultwarden VAULTWARDEN_EMAIL_ATTR = "vaultwarden_email" VAULTWARDEN_STATUS_ATTR = "vaultwarden_status" VAULTWARDEN_SYNCED_AT_ATTR = "vaultwarden_synced_at" +VAULTWARDEN_MASTER_ATTR = "vaultwarden_master_password_set_at" logger = get_logger(__name__) @@ -161,11 +162,16 @@ def _has_pending_failures(users: list[dict[str, Any]]) -> bool: attrs = user.get("attributes") if isinstance(user.get("attributes"), dict) else {} status = _extract_attr(attrs, VAULTWARDEN_STATUS_ATTR) synced_at = _extract_attr(attrs, VAULTWARDEN_SYNCED_AT_ATTR) + master_set_at = _extract_attr(attrs, VAULTWARDEN_MASTER_ATTR) synced_ts = _parse_synced_at(synced_at) if not status: return True if status in {"invited", "already_present"} and not synced_at: return True + if status == "already_present" and not master_set_at: + return True + if status == "invited" and _should_refresh_invite(synced_ts): + return True if status in {"error", "rate_limited"} and not _cooldown_active(status, synced_ts): return True return False @@ -183,6 +189,19 @@ def _set_sync_status(username: str, status: str) -> None: return +def _set_master_password_set(username: str, full_user: dict[str, Any]) -> None: + if _extract_attr(full_user.get("attributes"), VAULTWARDEN_MASTER_ATTR): + return + try: + _set_user_attribute( + username, + VAULTWARDEN_MASTER_ATTR, + time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), + ) + except Exception: + return + + def _ensure_email_attrs(username: str, full_user: dict[str, Any], email: str) -> None: try: _set_user_attribute_if_missing(username, full_user, "mailu_email", email) @@ -191,18 +210,34 @@ def _ensure_email_attrs(username: str, full_user: dict[str, Any], email: str) -> return +def _should_refresh_invite(synced_ts: float | None) -> bool: + if synced_ts is None: + return True + return (time.time() - synced_ts) >= settings.vaultwarden_invite_refresh_sec + + def _handle_existing_invite( username: str, current_status: str, current_synced_at: str, + current_synced_ts: float | None, + full_user: dict[str, Any], counters: VaultwardenSyncCounters, ) -> bool: if current_status not in {"invited", "already_present"}: return False - if not current_synced_at: - _set_sync_status(username, current_status) - counters.skipped += 1 - return True + if current_status == "already_present": + if not current_synced_at: + _set_sync_status(username, current_status) + _set_master_password_set(username, full_user) + counters.skipped += 1 + return True + if not _should_refresh_invite(current_synced_ts): + if not current_synced_at: + _set_sync_status(username, current_status) + counters.skipped += 1 + return True + return False def _sync_user( @@ -227,7 +262,14 @@ def _sync_user( counters.skipped += 1 else: _ensure_email_attrs(username, full_user, email) - if _handle_existing_invite(username, current_status, current_synced_at, counters): + if _handle_existing_invite( + username, + current_status, + current_synced_at, + current_synced_ts, + full_user, + counters, + ): status = None else: counters.processed += 1 @@ -239,6 +281,8 @@ def _sync_user( else: counters.failures += 1 _set_sync_status(username, result.status) + if result.status == "already_present": + _set_master_password_set(username, full_user) return status, ok diff --git a/ariadne/services/wger.py b/ariadne/services/wger.py index 50717e4..fc79778 100644 --- a/ariadne/services/wger.py +++ b/ariadne/services/wger.py @@ -16,6 +16,7 @@ from .mailu import mailu WGER_PASSWORD_ATTR = "wger_password" WGER_PASSWORD_UPDATED_ATTR = "wger_password_updated_at" +WGER_PASSWORD_ROTATED_ATTR = "wger_password_rotated_at" logger = get_logger(__name__) @@ -142,12 +143,67 @@ _WGER_SYNC_SCRIPT = textwrap.dedent( """ ).strip() +_WGER_PASSWORD_CHECK_SCRIPT = textwrap.dedent( + """ + from __future__ import annotations + + import os + import sys + + import django + + + def _env(name: str, default: str = "") -> str: + value = os.getenv(name, default) + return value.strip() if isinstance(value, str) else "" + + + def _setup_django() -> None: + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings.main") + django.setup() + + + def main() -> int: + username = _env("WGER_USERNAME") + password = _env("WGER_PASSWORD") + + if not username or not password: + print("missing username or password") + return 2 + + _setup_django() + + from django.contrib.auth.models import User + + user = User.objects.filter(username=username).first() + if not user: + print(f"user {username} missing") + return 3 + + if user.check_password(password): + print("password match") + return 0 + + print("password mismatch") + return 1 + + + if __name__ == "__main__": + sys.exit(main()) + """ +).strip() + def _wger_exec_command() -> str: - bootstrap = "if [ -f /vault/secrets/wger-env ]; then . /vault/secrets/wger-env; fi" + bootstrap = ". /vault/secrets/wger-env >/dev/null 2>&1 || true" return f"{bootstrap}\npython3 - <<'PY'\n{_WGER_SYNC_SCRIPT}\nPY" +def _wger_check_command() -> str: + bootstrap = ". /vault/secrets/wger-env >/dev/null 2>&1 || true" + return f"{bootstrap}\npython3 - <<'PY'\n{_WGER_PASSWORD_CHECK_SCRIPT}\nPY" + + @dataclass(frozen=True) class WgerSyncSummary: processed: int @@ -196,6 +252,7 @@ class WgerSyncInput: password: str password_generated: bool updated_at: str + rotated_at: str def _extract_attr(attrs: Any, key: str) -> str: @@ -266,6 +323,15 @@ def _set_wger_updated_at(username: str) -> bool: return True +def _set_wger_rotated_at(username: str) -> bool: + now_iso = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") + try: + keycloak_admin.set_user_attribute(username, WGER_PASSWORD_ROTATED_ATTR, now_iso) + except Exception: + return False + return True + + def _normalize_user(user: dict[str, Any]) -> tuple[UserSyncOutcome | None, UserIdentity | None]: username = user.get("username") if isinstance(user.get("username"), str) else "" if _should_skip_user(user, username): @@ -341,12 +407,14 @@ def _build_sync_input(user: dict[str, Any]) -> WgerSyncInput | UserSyncOutcome: return UserSyncOutcome("failed", "missing wger password") updated_at = _extract_attr(attrs, WGER_PASSWORD_UPDATED_ATTR) + rotated_at = _extract_attr(attrs, WGER_PASSWORD_ROTATED_ATTR) return WgerSyncInput( username=identity.username, mailu_email=mailu_email, password=password, password_generated=password_generated, updated_at=updated_at, + rotated_at=rotated_at, ) @@ -386,6 +454,39 @@ class WgerService: output = (result.stdout or result.stderr).strip() return {"status": "ok", "detail": output} + def check_password(self, username: str, password: str) -> dict[str, Any]: + username = (username or "").strip() + if not username: + raise RuntimeError("missing username") + if not password: + raise RuntimeError("missing password") + if not settings.wger_namespace: + raise RuntimeError("wger sync not configured") + + env = { + "WGER_USERNAME": username, + "WGER_PASSWORD": password, + } + + try: + result = self._executor.exec( + _wger_check_command(), + env=env, + timeout_sec=settings.wger_user_sync_wait_timeout_sec, + check=False, + ) + except (ExecError, PodSelectionError, TimeoutError) as exc: + return {"status": "error", "detail": str(exc)} + + detail = (result.stdout or result.stderr).strip() + if result.exit_code == 0: + return {"status": "match", "detail": detail} + if result.exit_code == 1: + return {"status": "mismatch", "detail": detail} + if result.exit_code == 3: + return {"status": "missing", "detail": detail or "user missing"} + return {"status": "error", "detail": detail or f"exit_code={result.exit_code}"} + def ensure_admin(self, wait: bool = False) -> dict[str, Any]: if not settings.wger_namespace: raise RuntimeError("wger admin sync not configured") @@ -415,7 +516,19 @@ class WgerService: if isinstance(prepared, UserSyncOutcome): return prepared if _should_skip_sync(prepared.password_generated, prepared.updated_at): - return UserSyncOutcome("skipped") + if prepared.rotated_at: + return UserSyncOutcome("skipped") + check = self.check_password(prepared.username, prepared.password) + status = check.get("status") if isinstance(check, dict) else "error" + if status == "match": + return UserSyncOutcome("skipped") + if status == "mismatch": + if not _set_wger_rotated_at(prepared.username): + return UserSyncOutcome("failed", "failed to set rotated_at") + return UserSyncOutcome("synced") + detail = check.get("detail") if isinstance(check, dict) else "" + detail = detail or "password check failed" + return UserSyncOutcome("failed", detail) result = self.sync_user(prepared.username, prepared.mailu_email, prepared.password, wait=True) result_status = result.get("status") if isinstance(result, dict) else "error" diff --git a/ariadne/settings.py b/ariadne/settings.py index f0ee0ae..f65f676 100644 --- a/ariadne/settings.py +++ b/ariadne/settings.py @@ -163,6 +163,7 @@ class Settings: vaultwarden_admin_rate_limit_backoff_sec: float vaultwarden_retry_cooldown_sec: float vaultwarden_failure_bailout: int + vaultwarden_invite_refresh_sec: float smtp_host: str smtp_port: int @@ -420,6 +421,7 @@ class Settings: "vaultwarden_admin_rate_limit_backoff_sec": _env_float("VAULTWARDEN_ADMIN_RATE_LIMIT_BACKOFF_SEC", 600.0), "vaultwarden_retry_cooldown_sec": _env_float("VAULTWARDEN_RETRY_COOLDOWN_SEC", 1800.0), "vaultwarden_failure_bailout": _env_int("VAULTWARDEN_FAILURE_BAILOUT", 2), + "vaultwarden_invite_refresh_sec": _env_float("VAULTWARDEN_INVITE_REFRESH_SEC", 86400.0), } @classmethod @@ -429,7 +431,7 @@ class Settings: "nextcloud_sync_cron": _env("ARIADNE_SCHEDULE_NEXTCLOUD_SYNC", "0 5 * * *"), "nextcloud_cron": _env("ARIADNE_SCHEDULE_NEXTCLOUD_CRON", "*/5 * * * *"), "nextcloud_maintenance_cron": _env("ARIADNE_SCHEDULE_NEXTCLOUD_MAINTENANCE", "30 4 * * *"), - "vaultwarden_sync_cron": _env("ARIADNE_SCHEDULE_VAULTWARDEN_SYNC", "*/15 * * * *"), + "vaultwarden_sync_cron": _env("ARIADNE_SCHEDULE_VAULTWARDEN_SYNC", "0 * * * *"), "wger_user_sync_cron": _env("ARIADNE_SCHEDULE_WGER_USER_SYNC", "0 5 * * *"), "wger_admin_cron": _env("ARIADNE_SCHEDULE_WGER_ADMIN", "15 3 * * *"), "firefly_user_sync_cron": _env("ARIADNE_SCHEDULE_FIREFLY_USER_SYNC", "0 6 * * *"), @@ -437,9 +439,9 @@ class Settings: "pod_cleaner_cron": _env("ARIADNE_SCHEDULE_POD_CLEANER", "0 * * * *"), "opensearch_prune_cron": _env("ARIADNE_SCHEDULE_OPENSEARCH_PRUNE", "23 3 * * *"), "image_sweeper_cron": _env("ARIADNE_SCHEDULE_IMAGE_SWEEPER", "30 4 * * 0"), - "vault_k8s_auth_cron": _env("ARIADNE_SCHEDULE_VAULT_K8S_AUTH", "*/15 * * * *"), - "vault_oidc_cron": _env("ARIADNE_SCHEDULE_VAULT_OIDC", "*/15 * * * *"), - "comms_guest_name_cron": _env("ARIADNE_SCHEDULE_COMMS_GUEST_NAME", "*/1 * * * *"), + "vault_k8s_auth_cron": _env("ARIADNE_SCHEDULE_VAULT_K8S_AUTH", "0 * * * *"), + "vault_oidc_cron": _env("ARIADNE_SCHEDULE_VAULT_OIDC", "0 * * * *"), + "comms_guest_name_cron": _env("ARIADNE_SCHEDULE_COMMS_GUEST_NAME", "*/5 * * * *"), "comms_pin_invite_cron": _env("ARIADNE_SCHEDULE_COMMS_PIN_INVITE", "*/30 * * * *"), "comms_reset_room_cron": _env("ARIADNE_SCHEDULE_COMMS_RESET_ROOM", "0 0 1 1 *"), "comms_seed_room_cron": _env("ARIADNE_SCHEDULE_COMMS_SEED_ROOM", "*/10 * * * *"), diff --git a/tests/test_services.py b/tests/test_services.py index 3ce2574..bd3a68c 100644 --- a/tests/test_services.py +++ b/tests/test_services.py @@ -186,6 +186,70 @@ def test_wger_sync_users(monkeypatch) -> None: 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", @@ -330,6 +394,70 @@ def test_firefly_sync_users(monkeypatch) -> None: 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", diff --git a/tests/test_vaultwarden_sync.py b/tests/test_vaultwarden_sync.py index 37da8d8..6ef3dea 100644 --- a/tests/test_vaultwarden_sync.py +++ b/tests/test_vaultwarden_sync.py @@ -97,6 +97,7 @@ def test_vaultwarden_sync_respects_retry_cooldown(monkeypatch) -> None: mailu_domain="bstein.dev", vaultwarden_retry_cooldown_sec=9999, vaultwarden_failure_bailout=2, + vaultwarden_invite_refresh_sec=9999, ) monkeypatch.setattr(vaultwarden_sync, "settings", dummy_settings) now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") @@ -137,6 +138,7 @@ def test_vaultwarden_sync_bails_after_failures(monkeypatch) -> None: mailu_domain="bstein.dev", vaultwarden_retry_cooldown_sec=0, vaultwarden_failure_bailout=1, + vaultwarden_invite_refresh_sec=0, ) monkeypatch.setattr(vaultwarden_sync, "settings", dummy_settings) dummy = DummyAdmin( @@ -162,6 +164,7 @@ def test_vaultwarden_sync_uses_keycloak_email(monkeypatch) -> None: mailu_domain="bstein.dev", vaultwarden_retry_cooldown_sec=0, vaultwarden_failure_bailout=2, + vaultwarden_invite_refresh_sec=0, ) monkeypatch.setattr(vaultwarden_sync, "settings", dummy_settings) dummy = DummyAdmin( @@ -265,6 +268,7 @@ def test_vaultwarden_sync_sets_synced_at_for_invited(monkeypatch) -> None: mailu_domain="bstein.dev", vaultwarden_retry_cooldown_sec=0, vaultwarden_failure_bailout=2, + vaultwarden_invite_refresh_sec=0, ) monkeypatch.setattr(vaultwarden_sync, "settings", dummy_settings) dummy = DummyAdmin( @@ -284,6 +288,7 @@ def test_vaultwarden_sync_skips_disabled_and_service_accounts(monkeypatch) -> No mailu_domain="bstein.dev", vaultwarden_retry_cooldown_sec=0, vaultwarden_failure_bailout=2, + vaultwarden_invite_refresh_sec=0, ) monkeypatch.setattr(vaultwarden_sync, "settings", dummy_settings) dummy = DummyAdmin( @@ -305,6 +310,7 @@ def test_vaultwarden_sync_get_user_failure(monkeypatch) -> None: mailu_domain="bstein.dev", vaultwarden_retry_cooldown_sec=0, vaultwarden_failure_bailout=2, + vaultwarden_invite_refresh_sec=0, ) monkeypatch.setattr(vaultwarden_sync, "settings", dummy_settings) @@ -328,6 +334,7 @@ def test_vaultwarden_sync_invited_attribute_failure(monkeypatch) -> None: mailu_domain="bstein.dev", vaultwarden_retry_cooldown_sec=0, vaultwarden_failure_bailout=2, + vaultwarden_invite_refresh_sec=0, ) monkeypatch.setattr(vaultwarden_sync, "settings", dummy_settings)