from __future__ import annotations import dataclasses from datetime import datetime, timezone import os from fastapi import HTTPException from fastapi.testclient import TestClient os.environ.setdefault("PORTAL_DATABASE_URL", "postgresql://user:pass@localhost/db") from ariadne.auth.keycloak import AuthContext import ariadne.app as app_module def _client(monkeypatch, ctx: AuthContext) -> TestClient: monkeypatch.setattr(app_module.authenticator, "authenticate", lambda token: ctx) monkeypatch.setattr(app_module.portal_db, "ensure_schema", lambda *args, **kwargs: None) monkeypatch.setattr(app_module.ariadne_db, "ensure_schema", lambda *args, **kwargs: None) monkeypatch.setattr(app_module.provisioning, "start", lambda: None) monkeypatch.setattr(app_module.scheduler, "start", lambda: None) monkeypatch.setattr(app_module.provisioning, "stop", lambda: None) monkeypatch.setattr(app_module.scheduler, "stop", lambda: None) monkeypatch.setattr(app_module.portal_db, "close", lambda: None) monkeypatch.setattr(app_module.ariadne_db, "close", lambda: None) monkeypatch.setattr(app_module.storage, "record_event", lambda *args, **kwargs: None) monkeypatch.setattr(app_module.storage, "record_task_run", lambda *args, **kwargs: None) return TestClient(app_module.app) def test_health_ok(monkeypatch) -> None: ctx = AuthContext(username="", email="", groups=[], claims={}) client = _client(monkeypatch, ctx) resp = client.get("/health") assert resp.status_code == 200 assert resp.json() == {"ok": True} def test_startup_and_shutdown(monkeypatch) -> None: monkeypatch.setattr(app_module.portal_db, "ensure_schema", lambda *args, **kwargs: None) monkeypatch.setattr(app_module.ariadne_db, "ensure_schema", lambda *args, **kwargs: None) monkeypatch.setattr(app_module.provisioning, "start", lambda: None) monkeypatch.setattr(app_module.scheduler, "add_task", lambda *args, **kwargs: None) monkeypatch.setattr(app_module.scheduler, "start", lambda: None) monkeypatch.setattr(app_module.scheduler, "stop", lambda: None) monkeypatch.setattr(app_module.provisioning, "stop", lambda: None) monkeypatch.setattr(app_module.portal_db, "close", lambda: None) monkeypatch.setattr(app_module.ariadne_db, "close", lambda: None) app_module._startup() app_module._shutdown() def test_record_event_handles_exception(monkeypatch) -> None: monkeypatch.setattr(app_module.storage, "record_event", lambda *args, **kwargs: (_ for _ in ()).throw(RuntimeError("fail"))) app_module._record_event("event", {"ok": True}) def test_parse_event_detail_variants() -> None: assert app_module._parse_event_detail(None) == "" assert app_module._parse_event_detail("not-json") == "not-json" def test_missing_auth_header(monkeypatch) -> None: ctx = AuthContext(username="", email="", groups=[], claims={}) client = _client(monkeypatch, ctx) resp = client.get("/api/admin/access/requests") assert resp.status_code == 401 def test_invalid_token(monkeypatch) -> None: ctx = AuthContext(username="", email="", groups=[], claims={}) client = _client(monkeypatch, ctx) monkeypatch.setattr(app_module.authenticator, "authenticate", lambda token: (_ for _ in ()).throw(ValueError("bad"))) resp = client.get( "/api/admin/access/requests", headers={"Authorization": "Bearer token"}, ) assert resp.status_code == 401 def test_forbidden_admin(monkeypatch) -> None: ctx = AuthContext(username="alice", email="", groups=["dev"], claims={}) client = _client(monkeypatch, ctx) resp = client.get( "/api/admin/access/requests", headers={"Authorization": "Bearer token"}, ) assert resp.status_code == 403 def test_account_access_denied(monkeypatch) -> None: ctx = AuthContext(username="alice", email="", groups=[], claims={}) client = _client(monkeypatch, ctx) resp = client.post( "/api/account/firefly/reset", headers={"Authorization": "Bearer token"}, ) assert resp.status_code == 403 def test_metrics_endpoint(monkeypatch) -> None: ctx = AuthContext(username="", email="", groups=[], claims={}) client = _client(monkeypatch, ctx) resp = client.get("/metrics") assert resp.status_code == 200 def test_mailu_event_endpoint(monkeypatch) -> None: ctx = AuthContext(username="", email="", groups=[], claims={}) client = _client(monkeypatch, ctx) class DummyEvents: def handle_event(self, payload): assert payload == {"wait": False} return 202, {"status": "accepted"} monkeypatch.setattr(app_module, "mailu_events", DummyEvents()) resp = client.post("/events", json={"wait": False}) assert resp.status_code == 202 assert resp.json()["status"] == "accepted" def test_list_access_requests(monkeypatch) -> None: ctx = AuthContext(username="bstein", email="", groups=["admin"], claims={}) client = _client(monkeypatch, ctx) now = datetime.now(timezone.utc) monkeypatch.setattr( app_module.storage, "list_pending_requests", lambda: [ { "request_code": "REQ1", "username": "alice", "contact_email": "alice@example.com", "note": "hello", "status": "pending", "created_at": now, } ], ) resp = client.get( "/api/admin/access/requests", headers={"Authorization": "Bearer token"}, ) assert resp.status_code == 200 payload = resp.json() assert payload["requests"][0]["username"] == "alice" def test_list_access_requests_error(monkeypatch) -> None: ctx = AuthContext(username="bstein", email="", groups=["admin"], claims={}) client = _client(monkeypatch, ctx) monkeypatch.setattr(app_module.storage, "list_pending_requests", lambda: (_ for _ in ()).throw(RuntimeError("fail"))) resp = client.get( "/api/admin/access/requests", headers={"Authorization": "Bearer token"}, ) assert resp.status_code == 502 def test_list_audit_events(monkeypatch) -> None: ctx = AuthContext(username="bstein", email="", groups=["admin"], claims={}) client = _client(monkeypatch, ctx) now = datetime.now(timezone.utc) monkeypatch.setattr( app_module.storage, "list_events", lambda **kwargs: [ { "id": 1, "event_type": "mailu_rotate", "detail": '{"status":"ok"}', "created_at": now, } ], ) resp = client.get( "/api/admin/audit/events", headers={"Authorization": "Bearer token"}, ) assert resp.status_code == 200 payload = resp.json() assert payload["events"][0]["detail"]["status"] == "ok" def test_list_audit_events_error(monkeypatch) -> None: ctx = AuthContext(username="bstein", email="", groups=["admin"], claims={}) client = _client(monkeypatch, ctx) monkeypatch.setattr(app_module.storage, "list_events", lambda **kwargs: (_ for _ in ()).throw(RuntimeError("fail"))) resp = client.get( "/api/admin/audit/events", headers={"Authorization": "Bearer token"}, ) assert resp.status_code == 502 def test_list_audit_task_runs(monkeypatch) -> None: ctx = AuthContext(username="bstein", email="", groups=["admin"], claims={}) client = _client(monkeypatch, ctx) now = datetime.now(timezone.utc) monkeypatch.setattr( app_module.storage, "list_task_runs", lambda **kwargs: [ { "id": 1, "request_code": "REQ1", "task": "mailu_sync", "status": "ok", "detail": "done", "started_at": now, "finished_at": now, "duration_ms": 120, } ], ) resp = client.get( "/api/admin/audit/task-runs", headers={"Authorization": "Bearer token"}, ) assert resp.status_code == 200 payload = resp.json() assert payload["task_runs"][0]["task"] == "mailu_sync" def test_list_audit_task_runs_error(monkeypatch) -> None: ctx = AuthContext(username="bstein", email="", groups=["admin"], claims={}) client = _client(monkeypatch, ctx) monkeypatch.setattr(app_module.storage, "list_task_runs", lambda **kwargs: (_ for _ in ()).throw(RuntimeError("fail"))) resp = client.get( "/api/admin/audit/task-runs", headers={"Authorization": "Bearer token"}, ) assert resp.status_code == 502 def test_access_flags_from_keycloak(monkeypatch) -> None: ctx = AuthContext(username="bstein", email="", groups=["admin"], claims={}) client = _client(monkeypatch, ctx) monkeypatch.setattr(app_module.keycloak_admin, "ready", lambda: True) monkeypatch.setattr(app_module.keycloak_admin, "list_group_names", lambda **kwargs: ["demo", "test"]) resp = client.get( "/api/admin/access/flags", headers={"Authorization": "Bearer token"}, ) assert resp.status_code == 200 assert resp.json()["flags"] == ["demo", "test"] def test_access_flags_fallback(monkeypatch) -> None: ctx = AuthContext(username="bstein", email="", groups=["admin"], claims={}) client = _client(monkeypatch, ctx) monkeypatch.setattr(app_module.keycloak_admin, "ready", lambda: False) monkeypatch.setattr( app_module, "settings", dataclasses.replace(app_module.settings, allowed_flag_groups=["demo"]), ) resp = client.get( "/api/admin/access/flags", headers={"Authorization": "Bearer token"}, ) assert resp.status_code == 200 assert resp.json()["flags"] == ["demo"] def test_access_request_approve(monkeypatch) -> None: ctx = AuthContext(username="bstein", email="", groups=["admin"], claims={}) client = _client(monkeypatch, ctx) captured = {} def fake_fetchone(_query, params): captured["flags"] = params[1] return {"request_code": "REQ1"} monkeypatch.setattr(app_module.portal_db, "fetchone", fake_fetchone) monkeypatch.setattr(app_module.provisioning, "provision_access_request", lambda code: None) monkeypatch.setattr(app_module.keycloak_admin, "ready", lambda: True) monkeypatch.setattr(app_module.keycloak_admin, "list_group_names", lambda **kwargs: ["demo"]) resp = client.post( "/api/admin/access/requests/alice/approve", headers={"Authorization": "Bearer token"}, json={"flags": ["demo", "test", "admin"], "note": "ok"}, ) assert resp.status_code == 200 assert resp.json()["request_code"] == "REQ1" assert captured["flags"] == ["demo"] def test_access_request_approve_bad_json(monkeypatch) -> None: ctx = AuthContext(username="bstein", email="", groups=["admin"], claims={}) client = _client(monkeypatch, ctx) monkeypatch.setattr(app_module.portal_db, "fetchone", lambda *args, **kwargs: {"request_code": "REQ1"}) resp = client.post( "/api/admin/access/requests/alice/approve", headers={"Authorization": "Bearer token", "Content-Type": "application/json"}, data="{bad}", ) assert resp.status_code == 200 def test_access_request_approve_db_error(monkeypatch) -> None: ctx = AuthContext(username="bstein", email="", groups=["admin"], claims={}) client = _client(monkeypatch, ctx) monkeypatch.setattr( app_module.portal_db, "fetchone", lambda *args, **kwargs: (_ for _ in ()).throw(RuntimeError("fail")), ) resp = client.post( "/api/admin/access/requests/alice/approve", headers={"Authorization": "Bearer token"}, json={}, ) assert resp.status_code == 502 def test_access_request_approve_skipped(monkeypatch) -> None: ctx = AuthContext(username="bstein", email="", groups=["admin"], claims={}) client = _client(monkeypatch, ctx) monkeypatch.setattr(app_module.portal_db, "fetchone", lambda *args, **kwargs: None) resp = client.post( "/api/admin/access/requests/alice/approve", headers={"Authorization": "Bearer token"}, json={"flags": ["demo"]}, ) assert resp.status_code == 200 assert resp.json()["request_code"] == "" def test_access_request_deny(monkeypatch) -> None: ctx = AuthContext(username="bstein", email="", groups=["admin"], claims={}) client = _client(monkeypatch, ctx) monkeypatch.setattr(app_module.portal_db, "fetchone", lambda *args, **kwargs: {"request_code": "REQ2"}) resp = client.post( "/api/admin/access/requests/alice/deny", headers={"Authorization": "Bearer token"}, json={"note": "no"}, ) assert resp.status_code == 200 assert resp.json()["request_code"] == "REQ2" def test_access_request_deny_db_error(monkeypatch) -> None: ctx = AuthContext(username="bstein", email="", groups=["admin"], claims={}) client = _client(monkeypatch, ctx) monkeypatch.setattr( app_module.portal_db, "fetchone", lambda *args, **kwargs: (_ for _ in ()).throw(RuntimeError("fail")), ) resp = client.post( "/api/admin/access/requests/alice/deny", headers={"Authorization": "Bearer token"}, json={}, ) assert resp.status_code == 502 def test_access_request_deny_skipped(monkeypatch) -> None: ctx = AuthContext(username="bstein", email="", groups=["admin"], claims={}) client = _client(monkeypatch, ctx) monkeypatch.setattr(app_module.portal_db, "fetchone", lambda *args, **kwargs: None) resp = client.post( "/api/admin/access/requests/alice/deny", headers={"Authorization": "Bearer token"}, json={"note": "no"}, ) assert resp.status_code == 200 assert resp.json()["request_code"] == "" def test_rotate_mailu_password(monkeypatch) -> None: ctx = AuthContext(username="alice", email="", groups=["dev"], claims={}) client = _client(monkeypatch, ctx) monkeypatch.setattr(app_module.keycloak_admin, "ready", lambda: True) monkeypatch.setattr(app_module.keycloak_admin, "set_user_attribute", lambda *args, **kwargs: None) monkeypatch.setattr(app_module.mailu, "ready", lambda: True) monkeypatch.setattr(app_module.mailu, "sync", lambda *args, **kwargs: None) monkeypatch.setattr(app_module.nextcloud, "sync_mail", lambda *args, **kwargs: {"status": "ok"}) resp = client.post( "/api/account/mailu/rotate", headers={"Authorization": "Bearer token"}, ) assert resp.status_code == 200 payload = resp.json() assert payload["sync_ok"] is True assert payload["password"] def test_rotate_mailu_password_missing_config(monkeypatch) -> None: ctx = AuthContext(username="alice", email="", groups=["dev"], claims={}) client = _client(monkeypatch, ctx) monkeypatch.setattr(app_module.keycloak_admin, "ready", lambda: False) resp = client.post( "/api/account/mailu/rotate", headers={"Authorization": "Bearer token"}, ) assert resp.status_code == 503 def test_reset_wger_password(monkeypatch) -> None: ctx = AuthContext(username="alice", email="", groups=["dev"], claims={}) client = _client(monkeypatch, ctx) monkeypatch.setattr(app_module.keycloak_admin, "ready", lambda: True) monkeypatch.setattr(app_module.keycloak_admin, "find_user", lambda username: {"attributes": {"mailu_email": ["alice@bstein.dev"]}}) monkeypatch.setattr(app_module.keycloak_admin, "set_user_attribute", lambda *args, **kwargs: None) monkeypatch.setattr(app_module.wger, "sync_user", lambda *args, **kwargs: {"status": "ok"}) resp = client.post( "/api/account/wger/reset", headers={"Authorization": "Bearer token"}, ) assert resp.status_code == 200 payload = resp.json() assert payload["status"] == "ok" def test_reset_firefly_password(monkeypatch) -> None: ctx = AuthContext(username="alice", email="", groups=["dev"], claims={}) client = _client(monkeypatch, ctx) monkeypatch.setattr(app_module.keycloak_admin, "ready", lambda: True) monkeypatch.setattr(app_module.keycloak_admin, "find_user", lambda username: {"attributes": {"mailu_email": ["alice@bstein.dev"]}}) monkeypatch.setattr(app_module.keycloak_admin, "set_user_attribute", lambda *args, **kwargs: None) monkeypatch.setattr(app_module.firefly, "sync_user", lambda *args, **kwargs: {"status": "ok"}) resp = client.post( "/api/account/firefly/reset", headers={"Authorization": "Bearer token"}, ) assert resp.status_code == 200 payload = resp.json() assert payload["status"] == "ok" def test_nextcloud_mail_sync(monkeypatch) -> None: ctx = AuthContext(username="alice", email="", groups=["dev"], claims={}) client = _client(monkeypatch, ctx) monkeypatch.setattr(app_module.keycloak_admin, "ready", lambda: True) monkeypatch.setattr(app_module.nextcloud, "sync_mail", lambda *args, **kwargs: {"status": "ok"}) resp = client.post( "/api/account/nextcloud/mail/sync", headers={"Authorization": "Bearer token"}, json={"wait": True}, ) assert resp.status_code == 200 payload = resp.json() assert payload["status"] == "ok" def test_nextcloud_mail_sync_error(monkeypatch) -> None: ctx = AuthContext(username="alice", email="", groups=["dev"], claims={}) client = _client(monkeypatch, ctx) monkeypatch.setattr(app_module.keycloak_admin, "ready", lambda: True) monkeypatch.setattr(app_module.nextcloud, "sync_mail", lambda *args, **kwargs: (_ for _ in ()).throw(RuntimeError("fail"))) resp = client.post( "/api/account/nextcloud/mail/sync", headers={"Authorization": "Bearer token"}, json={"wait": True}, ) assert resp.status_code == 502 def test_require_admin_allows_group() -> None: ctx = AuthContext(username="alice", email="", groups=["admin"], claims={}) app_module._require_admin(ctx) def test_require_account_access_allows_when_disabled(monkeypatch) -> None: ctx = AuthContext(username="alice", email="", groups=[], claims={}) dummy_settings = type("S", (), {"account_allowed_groups": []})() monkeypatch.setattr(app_module, "settings", dummy_settings) app_module._require_account_access(ctx) def test_access_request_deny_bad_json(monkeypatch) -> None: ctx = AuthContext(username="bstein", email="", groups=["admin"], claims={}) client = _client(monkeypatch, ctx) monkeypatch.setattr(app_module.portal_db, "fetchone", lambda *args, **kwargs: {"request_code": "REQ2"}) resp = client.post( "/api/admin/access/requests/alice/deny", headers={"Authorization": "Bearer token", "Content-Type": "application/json"}, data="{bad}", ) assert resp.status_code == 200 def test_rotate_mailu_password_missing_username(monkeypatch) -> None: ctx = AuthContext(username="", email="", groups=["dev"], claims={}) client = _client(monkeypatch, ctx) monkeypatch.setattr(app_module.keycloak_admin, "ready", lambda: True) resp = client.post( "/api/account/mailu/rotate", headers={"Authorization": "Bearer token"}, ) assert resp.status_code == 400 def test_rotate_mailu_password_sync_error(monkeypatch) -> None: ctx = AuthContext(username="alice", email="", groups=["dev"], claims={}) client = _client(monkeypatch, ctx) monkeypatch.setattr(app_module.keycloak_admin, "ready", lambda: True) monkeypatch.setattr(app_module.keycloak_admin, "set_user_attribute", lambda *args, **kwargs: None) monkeypatch.setattr(app_module.mailu, "sync", lambda *args, **kwargs: (_ for _ in ()).throw(RuntimeError("fail"))) monkeypatch.setattr(app_module.nextcloud, "sync_mail", lambda *args, **kwargs: (_ for _ in ()).throw(RuntimeError("fail"))) resp = client.post( "/api/account/mailu/rotate", headers={"Authorization": "Bearer token"}, ) assert resp.status_code == 200 payload = resp.json() assert payload["sync_ok"] is False assert payload["nextcloud_sync"]["status"] == "error" def test_rotate_mailu_password_handles_storage_error(monkeypatch) -> None: ctx = AuthContext(username="alice", email="", groups=["dev"], claims={}) client = _client(monkeypatch, ctx) monkeypatch.setattr(app_module.keycloak_admin, "ready", lambda: True) monkeypatch.setattr(app_module.keycloak_admin, "set_user_attribute", lambda *args, **kwargs: None) monkeypatch.setattr(app_module.storage, "record_task_run", lambda *args, **kwargs: (_ for _ in ()).throw(RuntimeError("fail"))) resp = client.post( "/api/account/mailu/rotate", headers={"Authorization": "Bearer token"}, ) assert resp.status_code == 200 def test_rotate_mailu_password_failure(monkeypatch) -> None: ctx = AuthContext(username="alice", email="", groups=["dev"], claims={}) client = _client(monkeypatch, ctx) monkeypatch.setattr(app_module.keycloak_admin, "ready", lambda: True) monkeypatch.setattr(app_module.keycloak_admin, "set_user_attribute", lambda *args, **kwargs: (_ for _ in ()).throw(RuntimeError("fail"))) resp = client.post( "/api/account/mailu/rotate", headers={"Authorization": "Bearer token"}, ) assert resp.status_code == 502 def test_rotate_mailu_password_http_exception(monkeypatch) -> None: ctx = AuthContext(username="alice", email="", groups=["dev"], claims={}) client = _client(monkeypatch, ctx) monkeypatch.setattr(app_module.keycloak_admin, "ready", lambda: True) monkeypatch.setattr( app_module.keycloak_admin, "set_user_attribute", lambda *args, **kwargs: (_ for _ in ()).throw(HTTPException(status_code=409, detail="conflict")), ) resp = client.post( "/api/account/mailu/rotate", headers={"Authorization": "Bearer token"}, ) assert resp.status_code == 409 def test_wger_reset_missing_username(monkeypatch) -> None: ctx = AuthContext(username="", email="", groups=["dev"], claims={}) client = _client(monkeypatch, ctx) monkeypatch.setattr(app_module.keycloak_admin, "ready", lambda: True) resp = client.post( "/api/account/wger/reset", headers={"Authorization": "Bearer token"}, ) assert resp.status_code == 400 def test_wger_reset_unconfigured(monkeypatch) -> None: ctx = AuthContext(username="alice", email="", groups=["dev"], claims={}) client = _client(monkeypatch, ctx) monkeypatch.setattr(app_module.keycloak_admin, "ready", lambda: False) resp = client.post( "/api/account/wger/reset", headers={"Authorization": "Bearer token"}, ) assert resp.status_code == 503 def test_wger_reset_uses_mailu_string(monkeypatch) -> None: ctx = AuthContext(username="alice", email="", groups=["dev"], claims={}) client = _client(monkeypatch, ctx) captured = {} monkeypatch.setattr(app_module.keycloak_admin, "ready", lambda: True) monkeypatch.setattr( app_module.keycloak_admin, "find_user", lambda username: {"attributes": {"mailu_email": "alias@bstein.dev"}}, ) monkeypatch.setattr(app_module.keycloak_admin, "set_user_attribute", lambda *args, **kwargs: None) def fake_sync_user(username, email, password, wait=True): captured["email"] = email return {"status": "ok"} monkeypatch.setattr(app_module.wger, "sync_user", fake_sync_user) resp = client.post( "/api/account/wger/reset", headers={"Authorization": "Bearer token"}, ) assert resp.status_code == 200 assert captured["email"] == "alias@bstein.dev" def test_wger_reset_error(monkeypatch) -> None: ctx = AuthContext(username="alice", email="", groups=["dev"], claims={}) client = _client(monkeypatch, ctx) monkeypatch.setattr(app_module.keycloak_admin, "ready", lambda: True) monkeypatch.setattr(app_module.keycloak_admin, "find_user", lambda username: {"attributes": {"mailu_email": ["alice@bstein.dev"]}}) monkeypatch.setattr(app_module.wger, "sync_user", lambda *args, **kwargs: {"status": "error"}) resp = client.post( "/api/account/wger/reset", headers={"Authorization": "Bearer token"}, ) assert resp.status_code == 502 def test_wger_reset_http_exception(monkeypatch) -> None: ctx = AuthContext(username="alice", email="", groups=["dev"], claims={}) client = _client(monkeypatch, ctx) monkeypatch.setattr(app_module.keycloak_admin, "ready", lambda: True) monkeypatch.setattr(app_module.keycloak_admin, "find_user", lambda username: {"attributes": {"mailu_email": ["alice@bstein.dev"]}}) def raise_http(*_args, **_kwargs): raise HTTPException(status_code=409, detail="conflict") monkeypatch.setattr(app_module.wger, "sync_user", raise_http) resp = client.post( "/api/account/wger/reset", headers={"Authorization": "Bearer token"}, ) assert resp.status_code == 409 def test_wger_reset_handles_storage_error(monkeypatch) -> None: ctx = AuthContext(username="alice", email="", groups=["dev"], claims={}) client = _client(monkeypatch, ctx) monkeypatch.setattr(app_module.keycloak_admin, "ready", lambda: True) monkeypatch.setattr(app_module.keycloak_admin, "find_user", lambda username: {"attributes": {"mailu_email": ["alice@bstein.dev"]}}) monkeypatch.setattr(app_module.keycloak_admin, "set_user_attribute", lambda *args, **kwargs: None) monkeypatch.setattr(app_module.wger, "sync_user", lambda *args, **kwargs: {"status": "ok"}) monkeypatch.setattr( app_module.storage, "record_task_run", lambda *args, **kwargs: (_ for _ in ()).throw(RuntimeError("fail")), ) resp = client.post( "/api/account/wger/reset", headers={"Authorization": "Bearer token"}, ) assert resp.status_code == 200 def test_wger_reset_handles_find_user_error(monkeypatch) -> None: ctx = AuthContext(username="alice", email="", groups=["dev"], claims={}) client = _client(monkeypatch, ctx) monkeypatch.setattr(app_module.keycloak_admin, "ready", lambda: True) monkeypatch.setattr( app_module.keycloak_admin, "find_user", lambda *_args, **_kwargs: (_ for _ in ()).throw(RuntimeError("fail")), ) monkeypatch.setattr(app_module.keycloak_admin, "set_user_attribute", lambda *args, **kwargs: None) monkeypatch.setattr(app_module.wger, "sync_user", lambda *args, **kwargs: {"status": "ok"}) resp = client.post( "/api/account/wger/reset", headers={"Authorization": "Bearer token"}, ) assert resp.status_code == 200 def test_firefly_reset_missing_username(monkeypatch) -> None: ctx = AuthContext(username="", email="", groups=["dev"], claims={}) client = _client(monkeypatch, ctx) monkeypatch.setattr(app_module.keycloak_admin, "ready", lambda: True) resp = client.post( "/api/account/firefly/reset", headers={"Authorization": "Bearer token"}, ) assert resp.status_code == 400 def test_firefly_reset_unconfigured(monkeypatch) -> None: ctx = AuthContext(username="alice", email="", groups=["dev"], claims={}) client = _client(monkeypatch, ctx) monkeypatch.setattr(app_module.keycloak_admin, "ready", lambda: False) resp = client.post( "/api/account/firefly/reset", headers={"Authorization": "Bearer token"}, ) assert resp.status_code == 503 def test_firefly_reset_uses_mailu_string(monkeypatch) -> None: ctx = AuthContext(username="alice", email="", groups=["dev"], claims={}) client = _client(monkeypatch, ctx) captured = {} monkeypatch.setattr(app_module.keycloak_admin, "ready", lambda: True) monkeypatch.setattr( app_module.keycloak_admin, "find_user", lambda username: {"attributes": {"mailu_email": "alias@bstein.dev"}}, ) monkeypatch.setattr(app_module.keycloak_admin, "set_user_attribute", lambda *args, **kwargs: None) def fake_sync_user(email, password, wait=True): captured["email"] = email return {"status": "ok"} monkeypatch.setattr(app_module.firefly, "sync_user", fake_sync_user) resp = client.post( "/api/account/firefly/reset", headers={"Authorization": "Bearer token"}, ) assert resp.status_code == 200 assert captured["email"] == "alias@bstein.dev" def test_firefly_reset_error(monkeypatch) -> None: ctx = AuthContext(username="alice", email="", groups=["dev"], claims={}) client = _client(monkeypatch, ctx) monkeypatch.setattr(app_module.keycloak_admin, "ready", lambda: True) monkeypatch.setattr(app_module.keycloak_admin, "find_user", lambda username: {"attributes": {"mailu_email": ["alice@bstein.dev"]}}) monkeypatch.setattr(app_module.firefly, "sync_user", lambda *args, **kwargs: {"status": "error"}) resp = client.post( "/api/account/firefly/reset", headers={"Authorization": "Bearer token"}, ) assert resp.status_code == 502 def test_firefly_reset_http_exception(monkeypatch) -> None: ctx = AuthContext(username="alice", email="", groups=["dev"], claims={}) client = _client(monkeypatch, ctx) monkeypatch.setattr(app_module.keycloak_admin, "ready", lambda: True) monkeypatch.setattr(app_module.keycloak_admin, "find_user", lambda username: {"attributes": {"mailu_email": ["alice@bstein.dev"]}}) def raise_http(*_args, **_kwargs): raise HTTPException(status_code=409, detail="conflict") monkeypatch.setattr(app_module.firefly, "sync_user", raise_http) resp = client.post( "/api/account/firefly/reset", headers={"Authorization": "Bearer token"}, ) assert resp.status_code == 409 def test_firefly_reset_handles_storage_error(monkeypatch) -> None: ctx = AuthContext(username="alice", email="", groups=["dev"], claims={}) client = _client(monkeypatch, ctx) monkeypatch.setattr(app_module.keycloak_admin, "ready", lambda: True) monkeypatch.setattr(app_module.keycloak_admin, "find_user", lambda username: {"attributes": {"mailu_email": ["alice@bstein.dev"]}}) monkeypatch.setattr(app_module.keycloak_admin, "set_user_attribute", lambda *args, **kwargs: None) monkeypatch.setattr(app_module.firefly, "sync_user", lambda *args, **kwargs: {"status": "ok"}) monkeypatch.setattr( app_module.storage, "record_task_run", lambda *args, **kwargs: (_ for _ in ()).throw(RuntimeError("fail")), ) resp = client.post( "/api/account/firefly/reset", headers={"Authorization": "Bearer token"}, ) assert resp.status_code == 200 def test_firefly_reset_handles_find_user_error(monkeypatch) -> None: ctx = AuthContext(username="alice", email="", groups=["dev"], claims={}) client = _client(monkeypatch, ctx) monkeypatch.setattr(app_module.keycloak_admin, "ready", lambda: True) monkeypatch.setattr( app_module.keycloak_admin, "find_user", lambda *_args, **_kwargs: (_ for _ in ()).throw(RuntimeError("fail")), ) monkeypatch.setattr(app_module.keycloak_admin, "set_user_attribute", lambda *args, **kwargs: None) monkeypatch.setattr(app_module.firefly, "sync_user", lambda *args, **kwargs: {"status": "ok"}) resp = client.post( "/api/account/firefly/reset", headers={"Authorization": "Bearer token"}, ) assert resp.status_code == 200 def test_nextcloud_mail_sync_bad_json(monkeypatch) -> None: ctx = AuthContext(username="alice", email="", groups=["dev"], claims={}) client = _client(monkeypatch, ctx) monkeypatch.setattr(app_module.keycloak_admin, "ready", lambda: True) monkeypatch.setattr(app_module.nextcloud, "sync_mail", lambda *args, **kwargs: {"status": "ok"}) resp = client.post( "/api/account/nextcloud/mail/sync", headers={"Authorization": "Bearer token", "Content-Type": "application/json"}, data="{bad}", ) assert resp.status_code == 200 def test_nextcloud_mail_sync_unconfigured(monkeypatch) -> None: ctx = AuthContext(username="alice", email="", groups=["dev"], claims={}) client = _client(monkeypatch, ctx) monkeypatch.setattr(app_module.keycloak_admin, "ready", lambda: False) resp = client.post( "/api/account/nextcloud/mail/sync", headers={"Authorization": "Bearer token"}, json={"wait": True}, ) assert resp.status_code == 503 def test_nextcloud_mail_sync_missing_username(monkeypatch) -> None: ctx = AuthContext(username="", email="", groups=["dev"], claims={}) client = _client(monkeypatch, ctx) monkeypatch.setattr(app_module.keycloak_admin, "ready", lambda: True) resp = client.post( "/api/account/nextcloud/mail/sync", headers={"Authorization": "Bearer token"}, json={"wait": True}, ) assert resp.status_code == 400 def test_nextcloud_mail_sync_http_exception(monkeypatch) -> None: ctx = AuthContext(username="alice", email="", groups=["dev"], claims={}) client = _client(monkeypatch, ctx) monkeypatch.setattr(app_module.keycloak_admin, "ready", lambda: True) monkeypatch.setattr( app_module.nextcloud, "sync_mail", lambda *args, **kwargs: (_ for _ in ()).throw(HTTPException(status_code=409, detail="conflict")), ) resp = client.post( "/api/account/nextcloud/mail/sync", headers={"Authorization": "Bearer token"}, json={"wait": True}, ) assert resp.status_code == 409 def test_nextcloud_mail_sync_handles_storage_error(monkeypatch) -> None: ctx = AuthContext(username="alice", email="", groups=["dev"], claims={}) client = _client(monkeypatch, ctx) monkeypatch.setattr(app_module.keycloak_admin, "ready", lambda: True) monkeypatch.setattr(app_module.nextcloud, "sync_mail", lambda *args, **kwargs: {"status": "ok"}) monkeypatch.setattr( app_module.storage, "record_task_run", lambda *args, **kwargs: (_ for _ in ()).throw(RuntimeError("fail")), ) resp = client.post( "/api/account/nextcloud/mail/sync", headers={"Authorization": "Bearer token"}, json={"wait": True}, ) assert resp.status_code == 200