test(ariadne): split oversized unit suites

This commit is contained in:
codex 2026-04-21 02:05:59 -03:00
parent 152c19665e
commit f0e161ba8b
33 changed files with 4320 additions and 4569 deletions

View File

@ -1,5 +1 @@
# path reason
tests/test_provisioning.py test module split planned; broad provisioning coverage retained meanwhile
tests/test_services.py test module split planned; broad service contract coverage retained meanwhile
tests/test_app.py test module split planned; API coverage retained meanwhile
tests/test_keycloak_admin.py test module split planned; identity admin coverage retained meanwhile

1 # path reason
tests/test_provisioning.py test module split planned; broad provisioning coverage retained meanwhile
tests/test_services.py test module split planned; broad service contract coverage retained meanwhile
tests/test_app.py test module split planned; API coverage retained meanwhile
tests/test_keycloak_admin.py test module split planned; identity admin coverage retained meanwhile

0
tests/__init__.py Normal file
View File

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

0
tests/unit/__init__.py Normal file
View File

View File

View File

@ -0,0 +1,27 @@
from __future__ import annotations
import dataclasses
from datetime import datetime, timezone
from fastapi import HTTPException
from fastapi.testclient import TestClient
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.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)
__all__ = [name for name in globals() if not name.startswith("__")]

View File

@ -0,0 +1,286 @@
from tests.unit.app.app_route_helpers import *
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_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_require_admin_allows_group() -> None:
ctx = AuthContext(username="alice", email="", groups=["admin"], claims={})
app_module._require_admin(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

View File

@ -0,0 +1,139 @@
from tests.unit.app.app_route_helpers import *
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_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

View File

@ -0,0 +1,148 @@
from tests.unit.app.app_route_helpers import *
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.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_startup_registers_metis_watch(monkeypatch) -> None:
tasks = []
monkeypatch.setattr(app_module.provisioning, "start", lambda: 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)
monkeypatch.setattr(
app_module.scheduler,
"add_task",
lambda name, cron_expr, runner: tasks.append((name, cron_expr)),
)
app_module._startup()
assert any(name == "schedule.metis_sentinel_watch" for name, _cron in tasks)
assert any(name == "schedule.metis_k3s_token_sync" for name, _cron in tasks)
assert any(name == "schedule.platform_quality_suite_probe" for name, _cron in tasks)
assert any(name == "schedule.jenkins_build_weather" for name, _cron in tasks)
assert any(name == "schedule.jenkins_workspace_cleanup" for name, _cron in tasks)
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_account_access_denied(monkeypatch) -> None:
ctx = AuthContext(username="alice", email="", groups=["guest"], claims={})
client = _client(monkeypatch, ctx)
resp = client.post(
"/api/account/firefly/reset",
headers={"Authorization": "Bearer token"},
)
assert resp.status_code == 403
def test_account_access_allows_missing_groups(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_retry_access_request_ok(monkeypatch) -> None:
ctx = AuthContext(username="", email="", groups=[], claims={})
client = _client(monkeypatch, ctx)
executed = []
invoked = {}
monkeypatch.setattr(app_module.keycloak_admin, "ready", lambda: True)
monkeypatch.setattr(app_module.portal_db, "fetchone", lambda *_args, **_kwargs: {"status": "accounts_building"})
monkeypatch.setattr(app_module.portal_db, "execute", lambda query, params=None: executed.append((query, params)))
monkeypatch.setattr(app_module.provisioning, "provision_access_request", lambda code: invoked.setdefault("code", code))
monkeypatch.setattr(app_module, "_record_event", lambda *args, **kwargs: None)
resp = client.post("/api/access/requests/REQ123/retry")
assert resp.status_code == 200
assert resp.json()["request_code"] == "REQ123"
assert invoked["code"] == "REQ123"
assert any("provision_attempted_at" in query for query, _params in executed)
def test_retry_access_request_not_found(monkeypatch) -> None:
ctx = AuthContext(username="", email="", groups=[], claims={})
client = _client(monkeypatch, ctx)
monkeypatch.setattr(app_module.keycloak_admin, "ready", lambda: True)
monkeypatch.setattr(app_module.portal_db, "fetchone", lambda *_args, **_kwargs: None)
resp = client.post("/api/access/requests/REQ123/retry")
assert resp.status_code == 404
def test_retry_access_request_not_retryable(monkeypatch) -> None:
ctx = AuthContext(username="", email="", groups=[], claims={})
client = _client(monkeypatch, ctx)
monkeypatch.setattr(app_module.keycloak_admin, "ready", lambda: True)
monkeypatch.setattr(app_module.portal_db, "fetchone", lambda *_args, **_kwargs: {"status": "ready"})
resp = client.post("/api/access/requests/REQ123/retry")
assert resp.status_code == 409
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"

View File

@ -0,0 +1,112 @@
from tests.unit.app.app_route_helpers import *
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_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_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

View File

@ -0,0 +1,107 @@
from tests.unit.app.app_route_helpers import *
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_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

View File

@ -0,0 +1,139 @@
from tests.unit.app.app_route_helpers import *
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_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

View File

View File

@ -0,0 +1,104 @@
from __future__ import annotations
from contextlib import contextmanager
from datetime import datetime, timezone
import types
from ariadne.manager import provisioning as prov
from ariadne.services.vaultwarden import VaultwardenInvite, VaultwardenLookup
class DummyResult:
def __init__(self, row=None):
self._row = row
def fetchone(self):
return self._row
def fetchall(self):
return []
class DummyConn:
def __init__(self, row, locked=True):
self._row = row
self._locked = locked
self.executed = []
def execute(self, query, params=None):
self.executed.append((query, params))
if "pg_try_advisory_lock" in query:
return DummyResult({"locked": self._locked})
if "SELECT username" in query:
return DummyResult(self._row)
return DummyResult()
class DummyDB:
def __init__(self, row, locked=True):
self._row = row
self._locked = locked
@contextmanager
def connection(self):
yield DummyConn(self._row, locked=self._locked)
def fetchone(self, query, params=None):
return None
def fetchall(self, query, params=None):
return []
class DummyStorage:
def record_task_run(self, *args, **kwargs):
return None
def record_event(self, *args, **kwargs):
return None
def mark_welcome_sent(self, *args, **kwargs):
return None
def list_provision_candidates(self):
return []
class DummyAdmin:
def __init__(self):
self.groups = []
def ready(self):
return True
def find_user(self, username):
return {"id": "1"}
def find_user_by_email(self, email):
return None
def get_user(self, user_id):
return {"id": "1", "attributes": {}}
def create_user(self, payload):
return "1"
def update_user(self, user_id, payload):
return None
def reset_password(self, user_id, password, temporary=False):
return None
def set_user_attribute(self, username, key, value):
return None
def get_group_id(self, group_name: str):
return group_name
def add_user_to_group(self, user_id, group_id):
self.groups.append(group_id)
def _patch_mailu_ready(monkeypatch, settings, value=None) -> None:
if value is None:
value = bool(getattr(settings, "mailu_sync_url", ""))
monkeypatch.setattr(prov.mailu, "ready", lambda: value)
__all__ = [name for name in globals() if not name.startswith("__")]

View File

@ -0,0 +1,336 @@
from tests.unit.manager.provisioning_helpers import *
def test_provisioning_missing_verified_email(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
mailu_domain="bstein.dev",
mailu_sync_url="",
mailu_mailbox_wait_timeout_sec=1.0,
nextcloud_namespace="",
nextcloud_mail_sync_cronjob="",
provision_retry_cooldown_sec=0.0,
default_user_groups=["dev"],
allowed_flag_groups=[],
welcome_email_enabled=False,
portal_public_base_url="https://bstein.dev",
)
monkeypatch.setattr(prov, "settings", dummy_settings)
_patch_mailu_ready(monkeypatch, dummy_settings)
class Admin(DummyAdmin):
def find_user(self, username):
return None
monkeypatch.setattr(prov, "keycloak_admin", Admin())
row = {
"username": "alice",
"contact_email": "alice@example.com",
"email_verified_at": None,
"status": "accounts_building",
"initial_password": "temp",
"initial_password_revealed_at": None,
"provision_attempted_at": None,
"approval_flags": [],
}
outcome = prov.ProvisioningManager(DummyDB(row), DummyStorage()).provision_access_request("REQ_MISSING")
assert outcome.status == "accounts_building"
def test_provisioning_email_conflict(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
mailu_domain="bstein.dev",
mailu_sync_url="",
mailu_mailbox_wait_timeout_sec=1.0,
nextcloud_namespace="",
nextcloud_mail_sync_cronjob="",
provision_retry_cooldown_sec=0.0,
default_user_groups=["dev"],
allowed_flag_groups=[],
welcome_email_enabled=False,
portal_public_base_url="https://bstein.dev",
)
monkeypatch.setattr(prov, "settings", dummy_settings)
_patch_mailu_ready(monkeypatch, dummy_settings)
class Admin(DummyAdmin):
def find_user(self, username):
return None
def find_user_by_email(self, email):
return {"username": "other"}
monkeypatch.setattr(prov, "keycloak_admin", Admin())
row = {
"username": "alice",
"contact_email": "alice@example.com",
"email_verified_at": datetime.now(timezone.utc),
"status": "accounts_building",
"initial_password": "temp",
"initial_password_revealed_at": None,
"provision_attempted_at": None,
"approval_flags": [],
}
outcome = prov.ProvisioningManager(DummyDB(row), DummyStorage()).provision_access_request("REQ_CONFLICT")
assert outcome.status == "accounts_building"
def test_provisioning_missing_contact_email(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
mailu_domain="bstein.dev",
mailu_sync_url="",
mailu_mailbox_wait_timeout_sec=1.0,
nextcloud_namespace="",
nextcloud_mail_sync_cronjob="",
provision_retry_cooldown_sec=0.0,
default_user_groups=["dev"],
allowed_flag_groups=[],
welcome_email_enabled=False,
portal_public_base_url="https://bstein.dev",
)
monkeypatch.setattr(prov, "settings", dummy_settings)
_patch_mailu_ready(monkeypatch, dummy_settings)
class Admin(DummyAdmin):
def find_user(self, username):
return None
monkeypatch.setattr(prov, "keycloak_admin", Admin())
row = {
"username": "alice",
"contact_email": "",
"email_verified_at": datetime.now(timezone.utc),
"status": "accounts_building",
"initial_password": "temp",
"initial_password_revealed_at": None,
"provision_attempted_at": None,
"approval_flags": [],
}
outcome = prov.ProvisioningManager(DummyDB(row), DummyStorage()).provision_access_request("REQ_EMPTY")
assert outcome.status == "accounts_building"
def test_provisioning_user_id_missing(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
mailu_domain="bstein.dev",
mailu_sync_url="",
mailu_mailbox_wait_timeout_sec=1.0,
nextcloud_namespace="",
nextcloud_mail_sync_cronjob="",
provision_retry_cooldown_sec=0.0,
default_user_groups=["dev"],
allowed_flag_groups=[],
welcome_email_enabled=False,
portal_public_base_url="https://bstein.dev",
)
monkeypatch.setattr(prov, "settings", dummy_settings)
_patch_mailu_ready(monkeypatch, dummy_settings)
class Admin(DummyAdmin):
def find_user(self, username):
return {"id": ""}
monkeypatch.setattr(prov, "keycloak_admin", Admin())
row = {
"username": "alice",
"contact_email": "alice@example.com",
"email_verified_at": datetime.now(timezone.utc),
"status": "accounts_building",
"initial_password": "temp",
"initial_password_revealed_at": None,
"provision_attempted_at": None,
"approval_flags": [],
}
outcome = prov.ProvisioningManager(DummyDB(row), DummyStorage()).provision_access_request("REQ_ID")
assert outcome.status == "accounts_building"
def test_provisioning_initial_password_revealed(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
mailu_domain="bstein.dev",
mailu_sync_url="",
mailu_mailbox_wait_timeout_sec=1.0,
nextcloud_namespace="",
nextcloud_mail_sync_cronjob="",
provision_retry_cooldown_sec=0.0,
default_user_groups=["dev"],
allowed_flag_groups=[],
welcome_email_enabled=False,
portal_public_base_url="https://bstein.dev",
)
monkeypatch.setattr(prov, "settings", dummy_settings)
_patch_mailu_ready(monkeypatch, dummy_settings)
monkeypatch.setattr(prov, "keycloak_admin", DummyAdmin())
monkeypatch.setattr(prov.mailu, "wait_for_mailbox", lambda email, timeout: True)
monkeypatch.setattr(prov.wger, "sync_user", lambda *args, **kwargs: {"status": "ok"})
monkeypatch.setattr(prov.firefly, "sync_user", lambda *args, **kwargs: {"status": "ok"})
monkeypatch.setattr(prov.vaultwarden, "invite_user", lambda email: VaultwardenInvite(True, "invited"))
monkeypatch.setattr(prov.ProvisioningManager, "_all_tasks_ok", lambda *args, **kwargs: False)
row = {
"username": "alice",
"contact_email": "alice@example.com",
"email_verified_at": datetime.now(timezone.utc),
"status": "accounts_building",
"initial_password": None,
"initial_password_revealed_at": datetime.now(timezone.utc),
"provision_attempted_at": None,
"approval_flags": [],
}
outcome = prov.ProvisioningManager(DummyDB(row), DummyStorage()).provision_access_request("REQ_REVEALED")
assert outcome.status == "accounts_building"
def test_provisioning_vaultwarden_attribute_failure(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
mailu_domain="bstein.dev",
mailu_sync_url="",
mailu_mailbox_wait_timeout_sec=1.0,
nextcloud_namespace="",
nextcloud_mail_sync_cronjob="",
provision_retry_cooldown_sec=0.0,
default_user_groups=["dev"],
allowed_flag_groups=[],
welcome_email_enabled=False,
portal_public_base_url="https://bstein.dev",
)
monkeypatch.setattr(prov, "settings", dummy_settings)
_patch_mailu_ready(monkeypatch, dummy_settings)
class Admin(DummyAdmin):
def set_user_attribute(self, username, key, value):
raise RuntimeError("fail")
monkeypatch.setattr(prov, "keycloak_admin", Admin())
monkeypatch.setattr(prov.mailu, "wait_for_mailbox", lambda email, timeout: True)
monkeypatch.setattr(prov.wger, "sync_user", lambda *args, **kwargs: {"status": "ok"})
monkeypatch.setattr(prov.firefly, "sync_user", lambda *args, **kwargs: {"status": "ok"})
monkeypatch.setattr(prov.vaultwarden, "invite_user", lambda email: VaultwardenInvite(True, "invited"))
monkeypatch.setattr(prov.ProvisioningManager, "_all_tasks_ok", lambda *args, **kwargs: False)
row = {
"username": "alice",
"contact_email": "alice@example.com",
"email_verified_at": datetime.now(timezone.utc),
"status": "accounts_building",
"initial_password": "temp",
"initial_password_revealed_at": None,
"provision_attempted_at": None,
"approval_flags": [],
}
outcome = prov.ProvisioningManager(DummyDB(row), DummyStorage()).provision_access_request("REQ_VAULT")
assert outcome.status == "accounts_building"
def test_provisioning_vaultwarden_rate_limited(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
mailu_domain="bstein.dev",
mailu_sync_url="",
mailu_mailbox_wait_timeout_sec=1.0,
vaultwarden_admin_rate_limit_backoff_sec=600.0,
provision_retry_cooldown_sec=0.0,
)
monkeypatch.setattr(prov, "settings", dummy_settings)
monkeypatch.setattr(prov, "keycloak_admin", DummyAdmin())
monkeypatch.setattr(prov.mailu, "wait_for_mailbox", lambda *_args, **_kwargs: True)
monkeypatch.setattr(
prov.vaultwarden,
"invite_user",
lambda _email: VaultwardenInvite(False, "rate_limited", "vaultwarden rate limited"),
)
row = {
"username": "alice",
"contact_email": "alice@example.com",
"email_verified_at": datetime.now(timezone.utc),
"status": "accounts_building",
"initial_password": "temp",
"initial_password_revealed_at": None,
"provision_attempted_at": None,
"approval_flags": [],
}
manager = prov.ProvisioningManager(DummyDB(row), DummyStorage())
ctx = prov.RequestContext(
request_code="REQ_RATE",
username="alice",
first_name="",
last_name="",
contact_email="alice@example.com",
email_verified_at=row["email_verified_at"],
status="accounts_building",
initial_password="temp",
revealed_at=None,
attempted_at=None,
approval_flags=[],
user_id="1",
mailu_email="alice@bstein.dev",
)
conn = DummyConn(row)
manager._ensure_vaultwarden_invite(conn, ctx)
inserts = [
params
for query, params in conn.executed
if "INSERT INTO access_request_tasks" in query and isinstance(params, tuple) and len(params) >= 4
]
assert any(params[2] == "pending" and "rate limited until" in (params[3] or "") for params in inserts)
def test_provisioning_vaultwarden_grandfathered(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
mailu_domain="bstein.dev",
mailu_sync_url="",
mailu_mailbox_wait_timeout_sec=1.0,
vaultwarden_admin_rate_limit_backoff_sec=600.0,
provision_retry_cooldown_sec=0.0,
)
monkeypatch.setattr(prov, "settings", dummy_settings)
monkeypatch.setattr(prov, "keycloak_admin", DummyAdmin())
monkeypatch.setattr(
prov.vaultwarden,
"find_user_by_email",
lambda _email: VaultwardenLookup(True, "present", "user found"),
)
monkeypatch.setattr(
prov.vaultwarden,
"invite_user",
lambda _email: (_ for _ in ()).throw(RuntimeError("invite should not run")),
)
row = {
"username": "legacy",
"contact_email": "legacy@example.com",
"email_verified_at": datetime.now(timezone.utc),
"status": "accounts_building",
"initial_password": "temp",
"initial_password_revealed_at": None,
"provision_attempted_at": None,
"approval_flags": [prov.VAULTWARDEN_GRANDFATHERED_FLAG],
}
manager = prov.ProvisioningManager(DummyDB(row), DummyStorage())
ctx = prov.RequestContext(
request_code="REQ_GRANDFATHER",
username="legacy",
first_name="",
last_name="",
contact_email="legacy@example.com",
email_verified_at=row["email_verified_at"],
status="accounts_building",
initial_password="temp",
revealed_at=None,
attempted_at=None,
approval_flags=row["approval_flags"],
user_id="1",
mailu_email="legacy@bstein.dev",
)
conn = DummyConn(row)
manager._ensure_vaultwarden_invite(conn, ctx)
inserts = [
params
for query, params in conn.executed
if "INSERT INTO access_request_tasks" in query and isinstance(params, tuple) and len(params) >= 4
]
assert any(params[2] == "ok" and params[3] == "grandfathered" for params in inserts)

View File

@ -0,0 +1,480 @@
from tests.unit.manager.provisioning_helpers import *
def test_provisioning_filters_flag_groups(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
mailu_domain="bstein.dev",
mailu_sync_url="http://mailu",
mailu_mailbox_wait_timeout_sec=1.0,
nextcloud_namespace="nextcloud",
nextcloud_mail_sync_cronjob="nextcloud-mail-sync",
provision_retry_cooldown_sec=0.0,
default_user_groups=["dev"],
allowed_flag_groups=["demo", "test"],
welcome_email_enabled=False,
portal_public_base_url="https://bstein.dev",
)
monkeypatch.setattr(prov, "settings", dummy_settings)
_patch_mailu_ready(monkeypatch, dummy_settings)
admin = DummyAdmin()
monkeypatch.setattr(prov, "keycloak_admin", admin)
monkeypatch.setattr(prov.mailu, "sync", lambda reason, force=False: None)
monkeypatch.setattr(prov.mailu, "wait_for_mailbox", lambda email, timeout: True)
monkeypatch.setattr(prov.nextcloud, "sync_mail", lambda username, wait=True: {"status": "ok"})
monkeypatch.setattr(prov.wger, "sync_user", lambda username, email, password, wait=True: {"status": "ok"})
monkeypatch.setattr(prov.firefly, "sync_user", lambda email, password, wait=True: {"status": "ok"})
monkeypatch.setattr(prov.vaultwarden, "invite_user", lambda email: VaultwardenInvite(True, "invited"))
monkeypatch.setattr(prov.ProvisioningManager, "_all_tasks_ok", lambda *args, **kwargs: True)
monkeypatch.setattr(prov.ProvisioningManager, "_send_welcome_email", lambda *args, **kwargs: None)
row = {
"username": "alice",
"contact_email": "alice@example.com",
"email_verified_at": None,
"status": "approved",
"initial_password": None,
"initial_password_revealed_at": None,
"provision_attempted_at": None,
"approval_flags": ["demo", "admin"],
}
db = DummyDB(row)
storage = DummyStorage()
manager = prov.ProvisioningManager(db, storage)
manager.provision_access_request("REQ123")
assert "dev" in admin.groups
assert "demo" in admin.groups
assert "admin" not in admin.groups
def test_provisioning_creates_user_and_password(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
mailu_domain="bstein.dev",
mailu_sync_url="http://mailu",
mailu_mailbox_wait_timeout_sec=1.0,
nextcloud_namespace="nextcloud",
nextcloud_mail_sync_cronjob="nextcloud-mail-sync",
provision_retry_cooldown_sec=0.0,
default_user_groups=["dev"],
allowed_flag_groups=["demo"],
welcome_email_enabled=False,
portal_public_base_url="https://bstein.dev",
)
monkeypatch.setattr(prov, "settings", dummy_settings)
_patch_mailu_ready(monkeypatch, dummy_settings)
class Admin(DummyAdmin):
def __init__(self):
super().__init__()
self.created_payload = None
self.reset_calls = []
def find_user(self, username):
return None
def get_user(self, user_id):
return {
"id": user_id,
"username": "alice",
"requiredActions": ["CONFIGURE_TOTP"],
"attributes": {},
}
def create_user(self, payload):
self.created_payload = payload
return "user-123"
def update_user(self, user_id, payload):
return None
def reset_password(self, user_id, password, temporary=False):
self.reset_calls.append((user_id, temporary))
admin = Admin()
monkeypatch.setattr(prov, "keycloak_admin", admin)
monkeypatch.setattr(prov.mailu, "sync", lambda reason, force=False: None)
monkeypatch.setattr(prov.mailu, "wait_for_mailbox", lambda email, timeout: True)
monkeypatch.setattr(prov.nextcloud, "sync_mail", lambda username, wait=True: {"status": "ok"})
monkeypatch.setattr(prov.wger, "sync_user", lambda username, email, password, wait=True: {"status": "ok"})
monkeypatch.setattr(prov.firefly, "sync_user", lambda email, password, wait=True: {"status": "ok"})
monkeypatch.setattr(prov.vaultwarden, "invite_user", lambda email: VaultwardenInvite(True, "invited"))
monkeypatch.setattr(prov.ProvisioningManager, "_all_tasks_ok", lambda *args, **kwargs: True)
monkeypatch.setattr(prov.ProvisioningManager, "_send_welcome_email", lambda *args, **kwargs: None)
row = {
"username": "alice",
"first_name": "Alice",
"last_name": "Atlas",
"contact_email": "alice@example.com",
"email_verified_at": datetime.now(timezone.utc),
"status": "approved",
"initial_password": None,
"initial_password_revealed_at": None,
"provision_attempted_at": None,
"approval_flags": ["demo"],
}
db = DummyDB(row)
storage = DummyStorage()
manager = prov.ProvisioningManager(db, storage)
outcome = manager.provision_access_request("REQ124")
assert outcome.status == "awaiting_onboarding"
assert admin.created_payload is not None
assert admin.created_payload.get("firstName") == "Alice"
assert admin.created_payload.get("lastName") == "Atlas"
assert admin.reset_calls
def test_provisioning_create_user_fallback(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
mailu_domain="bstein.dev",
mailu_sync_url="http://mailu",
mailu_mailbox_wait_timeout_sec=1.0,
nextcloud_namespace="nextcloud",
nextcloud_mail_sync_cronjob="nextcloud-mail-sync",
provision_retry_cooldown_sec=0.0,
default_user_groups=["dev"],
allowed_flag_groups=["demo"],
welcome_email_enabled=False,
portal_public_base_url="https://bstein.dev",
)
monkeypatch.setattr(prov, "settings", dummy_settings)
_patch_mailu_ready(monkeypatch, dummy_settings)
class Admin(DummyAdmin):
def __init__(self):
super().__init__()
self.find_calls = 0
self.reset_calls = []
def find_user(self, username):
self.find_calls += 1
if self.find_calls >= 2:
return {"id": "user-123", "username": username, "attributes": {}, "requiredActions": []}
return None
def get_user(self, user_id):
return {"id": user_id, "username": "alice", "attributes": {}, "requiredActions": []}
def create_user(self, payload):
raise RuntimeError("boom")
def reset_password(self, user_id, password, temporary=False):
self.reset_calls.append((user_id, temporary))
admin = Admin()
monkeypatch.setattr(prov, "keycloak_admin", admin)
monkeypatch.setattr(prov.mailu, "sync", lambda reason, force=False: None)
monkeypatch.setattr(prov.mailu, "wait_for_mailbox", lambda email, timeout: True)
monkeypatch.setattr(prov.nextcloud, "sync_mail", lambda username, wait=True: {"status": "ok"})
monkeypatch.setattr(prov.wger, "sync_user", lambda username, email, password, wait=True: {"status": "ok"})
monkeypatch.setattr(prov.firefly, "sync_user", lambda email, password, wait=True: {"status": "ok"})
monkeypatch.setattr(prov.vaultwarden, "invite_user", lambda email: VaultwardenInvite(True, "invited"))
monkeypatch.setattr(prov.ProvisioningManager, "_all_tasks_ok", lambda *args, **kwargs: True)
monkeypatch.setattr(prov.ProvisioningManager, "_send_welcome_email", lambda *args, **kwargs: None)
row = {
"username": "alice",
"contact_email": "alice@example.com",
"email_verified_at": datetime.now(timezone.utc),
"status": "approved",
"initial_password": None,
"initial_password_revealed_at": None,
"provision_attempted_at": None,
"approval_flags": ["demo"],
}
db = DummyDB(row)
storage = DummyStorage()
manager = prov.ProvisioningManager(db, storage)
outcome = manager.provision_access_request("REQ125")
assert outcome.status == "awaiting_onboarding"
assert admin.find_calls >= 2
assert admin.reset_calls
def test_extract_attr_variants() -> None:
assert prov._extract_attr("bad", "key") == ""
assert prov._extract_attr({"key": ["value"]}, "key") == "value"
assert prov._extract_attr({"key": ["", " "]}, "key") == ""
assert prov._extract_attr({"key": "value"}, "key") == "value"
assert prov._extract_attr({}, "key") == ""
def test_provisioning_empty_request_code(monkeypatch) -> None:
db = DummyDB({})
storage = DummyStorage()
manager = prov.ProvisioningManager(db, storage)
outcome = manager.provision_access_request("")
assert outcome.status == "unknown"
def test_provisioning_admin_not_ready(monkeypatch) -> None:
monkeypatch.setattr(prov.keycloak_admin, "ready", lambda: False)
db = DummyDB({})
storage = DummyStorage()
manager = prov.ProvisioningManager(db, storage)
outcome = manager.provision_access_request("REQ")
assert outcome.status == "accounts_building"
def test_provisioning_lock_not_acquired(monkeypatch) -> None:
monkeypatch.setattr(prov.keycloak_admin, "ready", lambda: True)
row = {
"username": "alice",
"contact_email": "alice@example.com",
"email_verified_at": datetime.now(timezone.utc),
"status": "approved",
"initial_password": None,
"initial_password_revealed_at": None,
"provision_attempted_at": None,
"approval_flags": [],
}
db = DummyDB(row, locked=False)
storage = DummyStorage()
manager = prov.ProvisioningManager(db, storage)
outcome = manager.provision_access_request("REQ")
assert outcome.status == "accounts_building"
def test_provisioning_cooldown_short_circuit(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
mailu_domain="bstein.dev",
mailu_sync_url="http://mailu",
mailu_mailbox_wait_timeout_sec=1.0,
nextcloud_namespace="nextcloud",
nextcloud_mail_sync_cronjob="nextcloud-mail-sync",
provision_retry_cooldown_sec=9999.0,
default_user_groups=["dev"],
allowed_flag_groups=["demo"],
welcome_email_enabled=False,
portal_public_base_url="https://bstein.dev",
provision_poll_interval_sec=1.0,
)
monkeypatch.setattr(prov, "settings", dummy_settings)
_patch_mailu_ready(monkeypatch, dummy_settings)
monkeypatch.setattr(prov.keycloak_admin, "ready", lambda: True)
row = {
"username": "alice",
"contact_email": "alice@example.com",
"email_verified_at": datetime.now(timezone.utc),
"status": "accounts_building",
"initial_password": None,
"initial_password_revealed_at": None,
"provision_attempted_at": datetime.now(timezone.utc),
"approval_flags": [],
}
db = DummyDB(row, locked=True)
storage = DummyStorage()
manager = prov.ProvisioningManager(db, storage)
outcome = manager.provision_access_request("REQ")
assert outcome.status == "accounts_building"
def test_provisioning_mailu_sync_disabled(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
mailu_domain="bstein.dev",
mailu_sync_url="",
mailu_mailbox_wait_timeout_sec=1.0,
nextcloud_namespace="",
nextcloud_mail_sync_cronjob="",
provision_retry_cooldown_sec=0.0,
default_user_groups=["dev"],
allowed_flag_groups=["demo"],
welcome_email_enabled=False,
portal_public_base_url="https://bstein.dev",
)
monkeypatch.setattr(prov, "settings", dummy_settings)
_patch_mailu_ready(monkeypatch, dummy_settings)
class Admin(DummyAdmin):
def __init__(self):
super().__init__()
self.updated_actions = []
def find_user(self, username):
return {"id": "user-1"}
def get_user(self, user_id):
return {
"id": user_id,
"username": "alice",
"requiredActions": ["CONFIGURE_TOTP"],
"attributes": {
"mailu_email": ["alice@bstein.dev"],
"mailu_enabled": ["false"],
"wger_password": ["pw"],
"wger_password_updated_at": ["done"],
"firefly_password": ["pw"],
"firefly_password_updated_at": ["done"],
},
}
def update_user_safe(self, user_id, payload):
self.updated_actions.append(payload)
admin = Admin()
monkeypatch.setattr(prov, "keycloak_admin", admin)
monkeypatch.setattr(prov.wger, "sync_user", lambda username, email, password, wait=True: {"status": "ok"})
monkeypatch.setattr(prov.firefly, "sync_user", lambda email, password, wait=True: {"status": "ok"})
monkeypatch.setattr(prov.vaultwarden, "invite_user", lambda email: VaultwardenInvite(True, "invited"))
monkeypatch.setattr(prov.ProvisioningManager, "_all_tasks_ok", lambda *args, **kwargs: False)
row = {
"username": "alice",
"contact_email": "alice@example.com",
"email_verified_at": datetime.now(timezone.utc),
"status": "accounts_building",
"initial_password": "temp",
"initial_password_revealed_at": None,
"provision_attempted_at": None,
"approval_flags": [],
}
db = DummyDB(row)
storage = DummyStorage()
manager = prov.ProvisioningManager(db, storage)
outcome = manager.provision_access_request("REQ125")
assert outcome.status == "accounts_building"
assert admin.updated_actions
def test_provisioning_sets_missing_email(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
mailu_domain="bstein.dev",
mailu_sync_url="http://mailu",
mailu_mailbox_wait_timeout_sec=1.0,
nextcloud_namespace="nextcloud",
nextcloud_mail_sync_cronjob="nextcloud-mail-sync",
provision_retry_cooldown_sec=0.0,
default_user_groups=["dev"],
allowed_flag_groups=[],
welcome_email_enabled=False,
portal_public_base_url="https://bstein.dev",
)
monkeypatch.setattr(prov, "settings", dummy_settings)
_patch_mailu_ready(monkeypatch, dummy_settings)
class Admin(DummyAdmin):
def __init__(self):
super().__init__()
self.updated_actions = []
def find_user(self, username):
return {"id": "user-1"}
def get_user(self, user_id):
return {"id": user_id, "username": "alice", "email": None, "attributes": {}}
def update_user_safe(self, user_id, payload):
self.updated_actions.append(payload)
admin = Admin()
monkeypatch.setattr(prov, "keycloak_admin", admin)
monkeypatch.setattr(prov.mailu, "sync", lambda reason, force=False: None)
monkeypatch.setattr(prov.mailu, "wait_for_mailbox", lambda email, timeout: True)
monkeypatch.setattr(prov.nextcloud, "sync_mail", lambda username, wait=True: {"status": "ok"})
monkeypatch.setattr(prov.wger, "sync_user", lambda username, email, password, wait=True: {"status": "ok"})
monkeypatch.setattr(prov.firefly, "sync_user", lambda email, password, wait=True: {"status": "ok"})
monkeypatch.setattr(prov.vaultwarden, "invite_user", lambda email: VaultwardenInvite(True, "invited"))
monkeypatch.setattr(prov.ProvisioningManager, "_all_tasks_ok", lambda *args, **kwargs: False)
row = {
"username": "alice",
"contact_email": "alice@example.com",
"email_verified_at": datetime.now(timezone.utc),
"status": "accounts_building",
"initial_password": "temp",
"initial_password_revealed_at": None,
"provision_attempted_at": None,
"approval_flags": [],
}
db = DummyDB(row)
storage = DummyStorage()
manager = prov.ProvisioningManager(db, storage)
outcome = manager.provision_access_request("REQ126")
assert outcome.status == "accounts_building"
assert any("email" in payload for payload in admin.updated_actions)
def test_provisioning_mailbox_not_ready(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
mailu_domain="bstein.dev",
mailu_sync_url="http://mailu",
mailu_mailbox_wait_timeout_sec=1.0,
nextcloud_namespace="nextcloud",
nextcloud_mail_sync_cronjob="nextcloud-mail-sync",
provision_retry_cooldown_sec=0.0,
default_user_groups=["dev"],
allowed_flag_groups=[],
welcome_email_enabled=False,
portal_public_base_url="https://bstein.dev",
)
monkeypatch.setattr(prov, "settings", dummy_settings)
_patch_mailu_ready(monkeypatch, dummy_settings)
admin = DummyAdmin()
monkeypatch.setattr(prov, "keycloak_admin", admin)
monkeypatch.setattr(prov.mailu, "sync", lambda reason, force=False: None)
monkeypatch.setattr(prov.mailu, "wait_for_mailbox", lambda email, timeout: False)
monkeypatch.setattr(prov.nextcloud, "sync_mail", lambda username, wait=True: {"status": "ok"})
monkeypatch.setattr(prov.wger, "sync_user", lambda username, email, password, wait=True: {"status": "ok"})
monkeypatch.setattr(prov.firefly, "sync_user", lambda email, password, wait=True: {"status": "ok"})
monkeypatch.setattr(prov.vaultwarden, "invite_user", lambda email: VaultwardenInvite(True, "invited"))
monkeypatch.setattr(prov.ProvisioningManager, "_all_tasks_ok", lambda *args, **kwargs: False)
row = {
"username": "alice",
"contact_email": "alice@example.com",
"email_verified_at": datetime.now(timezone.utc),
"status": "approved",
"initial_password": "temp",
"initial_password_revealed_at": None,
"provision_attempted_at": None,
"approval_flags": [],
}
db = DummyDB(row)
storage = DummyStorage()
manager = prov.ProvisioningManager(db, storage)
outcome = manager.provision_access_request("REQ126")
assert outcome.status == "accounts_building"
def test_provisioning_sync_errors(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
mailu_domain="bstein.dev",
mailu_sync_url="http://mailu",
mailu_mailbox_wait_timeout_sec=1.0,
nextcloud_namespace="nextcloud",
nextcloud_mail_sync_cronjob="nextcloud-mail-sync",
provision_retry_cooldown_sec=0.0,
default_user_groups=["dev"],
allowed_flag_groups=[],
welcome_email_enabled=False,
portal_public_base_url="https://bstein.dev",
)
monkeypatch.setattr(prov, "settings", dummy_settings)
_patch_mailu_ready(monkeypatch, dummy_settings)
admin = DummyAdmin()
monkeypatch.setattr(prov, "keycloak_admin", admin)
monkeypatch.setattr(prov.mailu, "sync", lambda reason, force=False: None)
monkeypatch.setattr(prov.mailu, "wait_for_mailbox", lambda email, timeout: True)
monkeypatch.setattr(prov.nextcloud, "sync_mail", lambda username, wait=True: {"status": "error"})
monkeypatch.setattr(prov.wger, "sync_user", lambda username, email, password, wait=True: {"status": "error"})
monkeypatch.setattr(prov.firefly, "sync_user", lambda email, password, wait=True: {"status": "error"})
monkeypatch.setattr(prov.vaultwarden, "invite_user", lambda email: VaultwardenInvite(False, "error", "fail"))
monkeypatch.setattr(prov.ProvisioningManager, "_all_tasks_ok", lambda *args, **kwargs: False)
row = {
"username": "alice",
"contact_email": "alice@example.com",
"email_verified_at": datetime.now(timezone.utc),
"status": "approved",
"initial_password": "temp",
"initial_password_revealed_at": None,
"provision_attempted_at": None,
"approval_flags": [],
}
db = DummyDB(row)
storage = DummyStorage()
manager = prov.ProvisioningManager(db, storage)
outcome = manager.provision_access_request("REQ127")
assert outcome.status == "accounts_building"

View File

@ -0,0 +1,269 @@
from tests.unit.manager.provisioning_helpers import *
def test_provisioning_run_loop_waits_for_admin(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(provision_poll_interval_sec=0.0)
monkeypatch.setattr(prov, "settings", dummy_settings)
_patch_mailu_ready(monkeypatch, dummy_settings)
monkeypatch.setattr(prov.keycloak_admin, "ready", lambda: False)
class DB:
def fetchall(self, *_args, **_kwargs):
return []
class Storage:
def list_provision_candidates(self):
raise AssertionError("should not list candidates")
manager = prov.ProvisioningManager(DB(), Storage())
def fake_sleep(_):
manager._stop_event.set()
monkeypatch.setattr(prov.time, "sleep", fake_sleep)
manager._stop_event.clear()
manager._run_loop()
def test_provisioning_initial_password_missing(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
mailu_domain="bstein.dev",
mailu_sync_url="",
mailu_mailbox_wait_timeout_sec=1.0,
nextcloud_namespace="",
nextcloud_mail_sync_cronjob="",
provision_retry_cooldown_sec=0.0,
default_user_groups=["dev"],
allowed_flag_groups=[],
welcome_email_enabled=False,
portal_public_base_url="https://bstein.dev",
)
monkeypatch.setattr(prov, "settings", dummy_settings)
_patch_mailu_ready(monkeypatch, dummy_settings)
admin = DummyAdmin()
monkeypatch.setattr(prov, "keycloak_admin", admin)
monkeypatch.setattr(prov.mailu, "wait_for_mailbox", lambda email, timeout: True)
monkeypatch.setattr(prov.wger, "sync_user", lambda username, email, password, wait=True: {"status": "ok"})
monkeypatch.setattr(prov.firefly, "sync_user", lambda email, password, wait=True: {"status": "ok"})
monkeypatch.setattr(prov.vaultwarden, "invite_user", lambda email: VaultwardenInvite(True, "invited"))
monkeypatch.setattr(prov.ProvisioningManager, "_all_tasks_ok", lambda *args, **kwargs: False)
row = {
"username": "alice",
"contact_email": "alice@example.com",
"email_verified_at": datetime.now(timezone.utc),
"status": "awaiting_onboarding",
"initial_password": None,
"initial_password_revealed_at": None,
"provision_attempted_at": None,
"approval_flags": [],
}
db = DummyDB(row)
storage = DummyStorage()
manager = prov.ProvisioningManager(db, storage)
outcome = manager.provision_access_request("REQ201")
assert outcome.status == "awaiting_onboarding"
def test_provisioning_group_and_mailu_errors(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
mailu_domain="bstein.dev",
mailu_sync_url="http://mailu",
mailu_mailbox_wait_timeout_sec=1.0,
nextcloud_namespace="nextcloud",
nextcloud_mail_sync_cronjob="nextcloud-mail-sync",
provision_retry_cooldown_sec=0.0,
default_user_groups=["dev"],
allowed_flag_groups=[],
welcome_email_enabled=False,
portal_public_base_url="https://bstein.dev",
)
monkeypatch.setattr(prov, "settings", dummy_settings)
_patch_mailu_ready(monkeypatch, dummy_settings)
class Admin(DummyAdmin):
def get_group_id(self, group_name: str):
return None
def set_user_attribute(self, username: str, key: str, value: str):
if key == "mailu_app_password":
raise RuntimeError("fail")
return None
admin = Admin()
monkeypatch.setattr(prov, "keycloak_admin", admin)
monkeypatch.setattr(prov.mailu, "sync", lambda reason, force=False: None)
monkeypatch.setattr(prov.mailu, "wait_for_mailbox", lambda email, timeout: True)
monkeypatch.setattr(prov.nextcloud, "sync_mail", lambda username, wait=True: (_ for _ in ()).throw(RuntimeError("fail")))
monkeypatch.setattr(prov.wger, "sync_user", lambda username, email, password, wait=True: {"status": "ok"})
monkeypatch.setattr(prov.firefly, "sync_user", lambda email, password, wait=True: {"status": "ok"})
monkeypatch.setattr(prov.vaultwarden, "invite_user", lambda email: VaultwardenInvite(True, "invited"))
monkeypatch.setattr(prov.ProvisioningManager, "_all_tasks_ok", lambda *args, **kwargs: False)
row = {
"username": "alice",
"contact_email": "alice@example.com",
"email_verified_at": datetime.now(timezone.utc),
"status": "accounts_building",
"initial_password": "temp",
"initial_password_revealed_at": None,
"provision_attempted_at": None,
"approval_flags": [],
}
db = DummyDB(row)
storage = DummyStorage()
manager = prov.ProvisioningManager(db, storage)
outcome = manager.provision_access_request("REQ202")
assert outcome.status == "accounts_building"
def test_provisioning_task_helpers() -> None:
class Conn:
def execute(self, *_args, **_kwargs):
class Result:
def fetchall(self):
return [
{"task": "a", "status": "ok"},
{"task": "b", "status": "error"},
"bad",
]
return Result()
manager = prov.ProvisioningManager(DummyDB({}), DummyStorage())
statuses = manager._task_statuses(Conn(), "REQ")
assert statuses == {"a": "ok", "b": "error"}
assert manager._all_tasks_ok(Conn(), "REQ", ["a"]) is True
assert manager._all_tasks_ok(Conn(), "REQ", ["b"]) is False
def test_provisioning_retryable_detail_detection() -> None:
manager = prov.ProvisioningManager(DummyDB({}, locked=True), DummyStorage())
assert manager._is_retryable_detail("timeout") is True
assert manager._is_retryable_detail("http 503: service unavailable") is True
assert manager._is_retryable_detail("mailbox not ready") is True
assert manager._is_retryable_detail("invalid credentials") is False
assert manager._retryable_detail("timeout").startswith("retryable:")
def test_provisioning_ensure_task_rows_empty() -> None:
manager = prov.ProvisioningManager(DummyDB({}), DummyStorage())
manager._ensure_task_rows(DummyConn({}, locked=True), "REQ", [])
def test_provisioning_record_task_ignores_storage_errors(monkeypatch) -> None:
class Storage:
def record_event(self, *args, **kwargs):
raise RuntimeError("fail")
def record_task_run(self, *args, **kwargs):
raise RuntimeError("fail")
db = DummyDB({})
manager = prov.ProvisioningManager(db, Storage())
manager._record_task("REQ", "task", "ok", None, datetime.now(timezone.utc))
def test_provisioning_send_welcome_email_variants(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
welcome_email_enabled=False,
portal_public_base_url="https://bstein.dev",
)
monkeypatch.setattr(prov, "settings", dummy_settings)
_patch_mailu_ready(monkeypatch, dummy_settings)
manager = prov.ProvisioningManager(DummyDB({}), DummyStorage())
manager._send_welcome_email("REQ", "alice", "alice@example.com")
dummy_settings = types.SimpleNamespace(
welcome_email_enabled=True,
portal_public_base_url="https://bstein.dev",
)
monkeypatch.setattr(prov, "settings", dummy_settings)
_patch_mailu_ready(monkeypatch, dummy_settings)
manager._send_welcome_email("REQ", "alice", "")
class DB(DummyDB):
def fetchone(self, *_args, **_kwargs):
return None
class Storage(DummyStorage):
def mark_welcome_sent(self, *args, **kwargs):
raise AssertionError("should not be called")
manager = prov.ProvisioningManager(DB({}), Storage())
monkeypatch.setattr(
prov.mailer,
"send_welcome",
lambda *args, **kwargs: (_ for _ in ()).throw(prov.MailerError("fail")),
)
manager._send_welcome_email("REQ", "alice", "alice@example.com")
def test_provisioning_start_stop(monkeypatch) -> None:
class DummyThread:
def __init__(self, target=None, name=None, daemon=None):
self.started = False
self.joined = False
def is_alive(self) -> bool:
return False
def start(self) -> None:
self.started = True
def join(self, timeout=None) -> None:
self.joined = True
monkeypatch.setattr(prov.threading, "Thread", DummyThread)
manager = prov.ProvisioningManager(DummyDB({}), DummyStorage())
manager.start()
assert manager._thread.started is True
manager.stop()
assert manager._thread.joined is True
def test_provisioning_start_skips_when_running() -> None:
class LiveThread:
def __init__(self):
self.started = False
def is_alive(self) -> bool:
return True
def start(self) -> None:
self.started = True
manager = prov.ProvisioningManager(DummyDB({}), DummyStorage())
manager._thread = LiveThread()
manager.start()
assert manager._thread.started is False
def test_provisioning_run_loop_skips_when_admin_not_ready(monkeypatch) -> None:
manager = prov.ProvisioningManager(DummyDB({}), DummyStorage())
monkeypatch.setattr(prov.keycloak_admin, "ready", lambda: False)
monkeypatch.setattr(manager, "_sync_status_metrics", lambda: (_ for _ in ()).throw(RuntimeError("fail")))
monkeypatch.setattr(
prov.time,
"sleep",
lambda *_args, **_kwargs: manager._stop_event.set(),
)
manager._run_loop()
def test_provisioning_run_loop_processes_candidates(monkeypatch) -> None:
manager = prov.ProvisioningManager(DummyDB({}), DummyStorage())
monkeypatch.setattr(prov.keycloak_admin, "ready", lambda: True)
monkeypatch.setattr(manager._storage, "list_provision_candidates", lambda: [types.SimpleNamespace(request_code="REQ")])
calls: list[str] = []
monkeypatch.setattr(manager, "provision_access_request", lambda code: calls.append(code))
monkeypatch.setattr(
prov.time,
"sleep",
lambda *_args, **_kwargs: manager._stop_event.set(),
)
manager._run_loop()
assert calls == ["REQ"]
def test_provisioning_sync_status_metrics(monkeypatch) -> None:
db = DummyDB({})
db.fetchall = lambda *_args, **_kwargs: [{"status": "pending", "count": 2}]
manager = prov.ProvisioningManager(db, DummyStorage())
captured = {}
monkeypatch.setattr(prov, "set_access_request_counts", lambda payload: captured.update(payload))
manager._sync_status_metrics()
assert captured["pending"] == 2

View File

@ -0,0 +1,329 @@
from tests.unit.manager.provisioning_helpers import *
def test_provisioning_locked_returns_accounts_building(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
mailu_domain="bstein.dev",
mailu_sync_url="",
mailu_mailbox_wait_timeout_sec=1.0,
nextcloud_namespace="",
nextcloud_mail_sync_cronjob="",
provision_retry_cooldown_sec=0.0,
default_user_groups=["dev"],
allowed_flag_groups=[],
welcome_email_enabled=False,
portal_public_base_url="https://bstein.dev",
)
monkeypatch.setattr(prov, "settings", dummy_settings)
_patch_mailu_ready(monkeypatch, dummy_settings)
monkeypatch.setattr(prov, "keycloak_admin", DummyAdmin())
db = DummyDB({"username": "alice"}, locked=False)
manager = prov.ProvisioningManager(db, DummyStorage())
outcome = manager.provision_access_request("REQ_LOCK")
assert outcome.status == "accounts_building"
def test_provisioning_missing_row_returns_unknown(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
mailu_domain="bstein.dev",
mailu_sync_url="",
mailu_mailbox_wait_timeout_sec=1.0,
nextcloud_namespace="",
nextcloud_mail_sync_cronjob="",
provision_retry_cooldown_sec=0.0,
default_user_groups=["dev"],
allowed_flag_groups=[],
welcome_email_enabled=False,
portal_public_base_url="https://bstein.dev",
)
monkeypatch.setattr(prov, "settings", dummy_settings)
_patch_mailu_ready(monkeypatch, dummy_settings)
monkeypatch.setattr(prov, "keycloak_admin", DummyAdmin())
db = DummyDB(None)
manager = prov.ProvisioningManager(db, DummyStorage())
outcome = manager.provision_access_request("REQ_NONE")
assert outcome.status == "unknown"
def test_provisioning_denied_status_returns_denied(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
mailu_domain="bstein.dev",
mailu_sync_url="",
mailu_mailbox_wait_timeout_sec=1.0,
nextcloud_namespace="",
nextcloud_mail_sync_cronjob="",
provision_retry_cooldown_sec=0.0,
default_user_groups=["dev"],
allowed_flag_groups=[],
welcome_email_enabled=False,
portal_public_base_url="https://bstein.dev",
)
monkeypatch.setattr(prov, "settings", dummy_settings)
_patch_mailu_ready(monkeypatch, dummy_settings)
monkeypatch.setattr(prov, "keycloak_admin", DummyAdmin())
row = {
"username": "alice",
"contact_email": "alice@example.com",
"email_verified_at": datetime.now(timezone.utc),
"status": "denied",
"initial_password": None,
"initial_password_revealed_at": None,
"provision_attempted_at": None,
"approval_flags": [],
}
db = DummyDB(row)
manager = prov.ProvisioningManager(db, DummyStorage())
outcome = manager.provision_access_request("REQ_DENIED")
assert outcome.status == "denied"
def test_provisioning_respects_retry_cooldown(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
mailu_domain="bstein.dev",
mailu_sync_url="",
mailu_mailbox_wait_timeout_sec=1.0,
nextcloud_namespace="",
nextcloud_mail_sync_cronjob="",
provision_retry_cooldown_sec=60.0,
default_user_groups=["dev"],
allowed_flag_groups=[],
welcome_email_enabled=False,
portal_public_base_url="https://bstein.dev",
)
monkeypatch.setattr(prov, "settings", dummy_settings)
_patch_mailu_ready(monkeypatch, dummy_settings)
monkeypatch.setattr(prov, "keycloak_admin", DummyAdmin())
row = {
"username": "alice",
"contact_email": "alice@example.com",
"email_verified_at": datetime.now(timezone.utc),
"status": "accounts_building",
"initial_password": "temp",
"initial_password_revealed_at": None,
"provision_attempted_at": datetime.now(),
"approval_flags": [],
}
db = DummyDB(row)
manager = prov.ProvisioningManager(db, DummyStorage())
outcome = manager.provision_access_request("REQ_COOLDOWN")
assert outcome.status == "accounts_building"
def test_provisioning_updates_existing_user_attrs(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
mailu_domain="bstein.dev",
mailu_sync_url="",
mailu_mailbox_wait_timeout_sec=1.0,
nextcloud_namespace="",
nextcloud_mail_sync_cronjob="",
provision_retry_cooldown_sec=0.0,
default_user_groups=["dev"],
allowed_flag_groups=[],
welcome_email_enabled=False,
portal_public_base_url="https://bstein.dev",
)
monkeypatch.setattr(prov, "settings", dummy_settings)
_patch_mailu_ready(monkeypatch, dummy_settings)
class Admin(DummyAdmin):
def __init__(self):
super().__init__()
self.update_payloads = []
self.attr_calls = []
def find_user(self, username):
return {"id": "1"}
def get_user(self, user_id):
return {
"id": user_id,
"username": "alice",
"email": "",
"requiredActions": ["CONFIGURE_TOTP"],
"attributes": {"mailu_enabled": ["false"]},
}
def update_user_safe(self, user_id, payload):
self.update_payloads.append(payload)
def set_user_attribute(self, username, key, value):
self.attr_calls.append((key, value))
admin = Admin()
monkeypatch.setattr(prov, "keycloak_admin", admin)
monkeypatch.setattr(prov.mailu, "sync", lambda reason, force=False: None)
monkeypatch.setattr(prov.mailu, "wait_for_mailbox", lambda email, timeout: True)
monkeypatch.setattr(prov.nextcloud, "sync_mail", lambda username, wait=True: {"status": "ok"})
monkeypatch.setattr(prov.wger, "sync_user", lambda username, email, password, wait=True: {"status": "ok"})
monkeypatch.setattr(prov.firefly, "sync_user", lambda email, password, wait=True: {"status": "ok"})
monkeypatch.setattr(prov.vaultwarden, "invite_user", lambda email: VaultwardenInvite(True, "invited"))
monkeypatch.setattr(prov.ProvisioningManager, "_all_tasks_ok", lambda *args, **kwargs: False)
row = {
"username": "alice",
"contact_email": "alice@example.com",
"email_verified_at": datetime.now(timezone.utc),
"status": "accounts_building",
"initial_password": "temp",
"initial_password_revealed_at": None,
"provision_attempted_at": None,
"approval_flags": [],
}
db = DummyDB(row)
storage = DummyStorage()
manager = prov.ProvisioningManager(db, storage)
outcome = manager.provision_access_request("REQ_ATTRS")
assert outcome.status == "accounts_building"
assert admin.update_payloads
assert any(key == "mailu_email" for key, _value in admin.attr_calls)
def test_provisioning_mailu_sync_failure(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
mailu_domain="bstein.dev",
mailu_sync_url="http://mailu",
mailu_mailbox_wait_timeout_sec=1.0,
nextcloud_namespace="",
nextcloud_mail_sync_cronjob="",
provision_retry_cooldown_sec=0.0,
default_user_groups=["dev"],
allowed_flag_groups=[],
welcome_email_enabled=False,
portal_public_base_url="https://bstein.dev",
)
monkeypatch.setattr(prov, "settings", dummy_settings)
_patch_mailu_ready(monkeypatch, dummy_settings)
monkeypatch.setattr(prov, "keycloak_admin", DummyAdmin())
monkeypatch.setattr(prov.mailu, "sync", lambda *args, **kwargs: (_ for _ in ()).throw(RuntimeError("fail")))
monkeypatch.setattr(prov.mailu, "wait_for_mailbox", lambda email, timeout: False)
monkeypatch.setattr(prov.ProvisioningManager, "_all_tasks_ok", lambda *args, **kwargs: False)
row = {
"username": "alice",
"contact_email": "alice@example.com",
"email_verified_at": datetime.now(timezone.utc),
"status": "accounts_building",
"initial_password": "temp",
"initial_password_revealed_at": None,
"provision_attempted_at": None,
"approval_flags": [],
}
db = DummyDB(row)
manager = prov.ProvisioningManager(db, DummyStorage())
outcome = manager.provision_access_request("REQ_MAILU")
assert outcome.status == "accounts_building"
def test_provisioning_nextcloud_sync_error(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
mailu_domain="bstein.dev",
mailu_sync_url="",
mailu_mailbox_wait_timeout_sec=1.0,
nextcloud_namespace="nextcloud",
nextcloud_mail_sync_cronjob="nextcloud-mail-sync",
provision_retry_cooldown_sec=0.0,
default_user_groups=["dev"],
allowed_flag_groups=[],
welcome_email_enabled=False,
portal_public_base_url="https://bstein.dev",
)
monkeypatch.setattr(prov, "settings", dummy_settings)
_patch_mailu_ready(monkeypatch, dummy_settings)
monkeypatch.setattr(prov, "keycloak_admin", DummyAdmin())
monkeypatch.setattr(prov.mailu, "wait_for_mailbox", lambda email, timeout: True)
monkeypatch.setattr(prov.nextcloud, "sync_mail", lambda *args, **kwargs: {"status": "error"})
monkeypatch.setattr(prov.wger, "sync_user", lambda username, email, password, wait=True: {"status": "ok"})
monkeypatch.setattr(prov.firefly, "sync_user", lambda email, password, wait=True: {"status": "ok"})
monkeypatch.setattr(prov.vaultwarden, "invite_user", lambda email: VaultwardenInvite(True, "invited"))
monkeypatch.setattr(prov.ProvisioningManager, "_all_tasks_ok", lambda *args, **kwargs: False)
row = {
"username": "alice",
"contact_email": "alice@example.com",
"email_verified_at": datetime.now(timezone.utc),
"status": "accounts_building",
"initial_password": "temp",
"initial_password_revealed_at": None,
"provision_attempted_at": None,
"approval_flags": [],
}
db = DummyDB(row)
manager = prov.ProvisioningManager(db, DummyStorage())
outcome = manager.provision_access_request("REQ_NC")
assert outcome.status == "accounts_building"
def test_provisioning_wger_firefly_errors(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
mailu_domain="bstein.dev",
mailu_sync_url="",
mailu_mailbox_wait_timeout_sec=1.0,
nextcloud_namespace="",
nextcloud_mail_sync_cronjob="",
provision_retry_cooldown_sec=0.0,
default_user_groups=["dev"],
allowed_flag_groups=[],
welcome_email_enabled=False,
portal_public_base_url="https://bstein.dev",
)
monkeypatch.setattr(prov, "settings", dummy_settings)
_patch_mailu_ready(monkeypatch, dummy_settings)
monkeypatch.setattr(prov, "keycloak_admin", DummyAdmin())
monkeypatch.setattr(prov.mailu, "wait_for_mailbox", lambda email, timeout: True)
monkeypatch.setattr(prov.wger, "sync_user", lambda username, email, password, wait=True: {"status": "error"})
monkeypatch.setattr(prov.firefly, "sync_user", lambda email, password, wait=True: {"status": "error"})
monkeypatch.setattr(prov.vaultwarden, "invite_user", lambda email: VaultwardenInvite(True, "invited"))
monkeypatch.setattr(prov.ProvisioningManager, "_all_tasks_ok", lambda *args, **kwargs: False)
row = {
"username": "alice",
"contact_email": "alice@example.com",
"email_verified_at": datetime.now(timezone.utc),
"status": "accounts_building",
"initial_password": "temp",
"initial_password_revealed_at": None,
"provision_attempted_at": None,
"approval_flags": [],
}
db = DummyDB(row)
manager = prov.ProvisioningManager(db, DummyStorage())
outcome = manager.provision_access_request("REQ_WGER")
assert outcome.status == "accounts_building"
def test_provisioning_start_event_failure(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
mailu_domain="bstein.dev",
mailu_sync_url="",
mailu_mailbox_wait_timeout_sec=1.0,
nextcloud_namespace="",
nextcloud_mail_sync_cronjob="",
provision_retry_cooldown_sec=0.0,
default_user_groups=["dev"],
allowed_flag_groups=[],
welcome_email_enabled=False,
portal_public_base_url="https://bstein.dev",
)
monkeypatch.setattr(prov, "settings", dummy_settings)
_patch_mailu_ready(monkeypatch, dummy_settings)
monkeypatch.setattr(prov, "keycloak_admin", DummyAdmin())
monkeypatch.setattr(prov.mailu, "wait_for_mailbox", lambda email, timeout: True)
monkeypatch.setattr(prov.wger, "sync_user", lambda *args, **kwargs: {"status": "ok"})
monkeypatch.setattr(prov.firefly, "sync_user", lambda *args, **kwargs: {"status": "ok"})
monkeypatch.setattr(prov.vaultwarden, "invite_user", lambda email: VaultwardenInvite(True, "invited"))
monkeypatch.setattr(prov.ProvisioningManager, "_all_tasks_ok", lambda *args, **kwargs: False)
class Storage(DummyStorage):
def record_event(self, *args, **kwargs):
raise RuntimeError("fail")
row = {
"username": "alice",
"contact_email": "alice@example.com",
"email_verified_at": datetime.now(timezone.utc),
"status": "accounts_building",
"initial_password": "temp",
"initial_password_revealed_at": None,
"provision_attempted_at": None,
"approval_flags": [],
}
manager = prov.ProvisioningManager(DummyDB(row), Storage())
outcome = manager.provision_access_request("REQ_EVENT")
assert outcome.status == "accounts_building"

View File

@ -0,0 +1,132 @@
from tests.unit.manager.provisioning_helpers import *
def test_provisioning_complete_event_failure(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
mailu_domain="bstein.dev",
mailu_sync_url="",
mailu_mailbox_wait_timeout_sec=1.0,
nextcloud_namespace="",
nextcloud_mail_sync_cronjob="",
provision_retry_cooldown_sec=0.0,
default_user_groups=["dev"],
allowed_flag_groups=[],
welcome_email_enabled=False,
portal_public_base_url="https://bstein.dev",
)
monkeypatch.setattr(prov, "settings", dummy_settings)
_patch_mailu_ready(monkeypatch, dummy_settings)
monkeypatch.setattr(prov, "keycloak_admin", DummyAdmin())
monkeypatch.setattr(prov.mailu, "wait_for_mailbox", lambda email, timeout: True)
monkeypatch.setattr(prov.wger, "sync_user", lambda *args, **kwargs: {"status": "ok"})
monkeypatch.setattr(prov.firefly, "sync_user", lambda *args, **kwargs: {"status": "ok"})
monkeypatch.setattr(prov.vaultwarden, "invite_user", lambda email: VaultwardenInvite(True, "invited"))
monkeypatch.setattr(prov.ProvisioningManager, "_all_tasks_ok", lambda *args, **kwargs: True)
class Storage(DummyStorage):
def record_event(self, event_type, detail):
if event_type == "provision_complete":
raise RuntimeError("fail")
return None
row = {
"username": "alice",
"contact_email": "alice@example.com",
"email_verified_at": datetime.now(timezone.utc),
"status": "accounts_building",
"initial_password": "temp",
"initial_password_revealed_at": None,
"provision_attempted_at": None,
"approval_flags": [],
}
outcome = prov.ProvisioningManager(DummyDB(row), Storage()).provision_access_request("REQ_DONE")
assert outcome.status == "awaiting_onboarding"
def test_provisioning_pending_event_failure(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
mailu_domain="bstein.dev",
mailu_sync_url="",
mailu_mailbox_wait_timeout_sec=1.0,
nextcloud_namespace="",
nextcloud_mail_sync_cronjob="",
provision_retry_cooldown_sec=0.0,
default_user_groups=["dev"],
allowed_flag_groups=[],
welcome_email_enabled=False,
portal_public_base_url="https://bstein.dev",
)
monkeypatch.setattr(prov, "settings", dummy_settings)
_patch_mailu_ready(monkeypatch, dummy_settings)
monkeypatch.setattr(prov, "keycloak_admin", DummyAdmin())
monkeypatch.setattr(prov.mailu, "wait_for_mailbox", lambda email, timeout: True)
monkeypatch.setattr(prov.wger, "sync_user", lambda *args, **kwargs: {"status": "ok"})
monkeypatch.setattr(prov.firefly, "sync_user", lambda *args, **kwargs: {"status": "ok"})
monkeypatch.setattr(prov.vaultwarden, "invite_user", lambda email: VaultwardenInvite(True, "invited"))
monkeypatch.setattr(prov.ProvisioningManager, "_all_tasks_ok", lambda *args, **kwargs: False)
class Storage(DummyStorage):
def record_event(self, event_type, detail):
if event_type == "provision_pending":
raise RuntimeError("fail")
return None
row = {
"username": "alice",
"contact_email": "alice@example.com",
"email_verified_at": datetime.now(timezone.utc),
"status": "accounts_building",
"initial_password": "temp",
"initial_password_revealed_at": None,
"provision_attempted_at": None,
"approval_flags": [],
}
outcome = prov.ProvisioningManager(DummyDB(row), Storage()).provision_access_request("REQ_PENDING")
assert outcome.status == "accounts_building"
def test_send_welcome_email_already_sent(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
welcome_email_enabled=True,
portal_public_base_url="https://bstein.dev",
)
monkeypatch.setattr(prov, "settings", dummy_settings)
_patch_mailu_ready(monkeypatch, dummy_settings)
class DB(DummyDB):
def fetchone(self, query, params=None):
return {"welcome_email_sent_at": datetime.now(timezone.utc)}
sent = {"called": False}
def mark_sent(_code):
sent["called"] = True
manager = prov.ProvisioningManager(DB({}), DummyStorage())
monkeypatch.setattr(manager._storage, "mark_welcome_sent", mark_sent)
monkeypatch.setattr(prov.mailer, "send_welcome", lambda *args, **kwargs: None)
manager._send_welcome_email("REQ_WELCOME", "alice", "alice@example.com")
assert sent["called"] is False
def test_send_welcome_email_marks_sent(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
welcome_email_enabled=True,
portal_public_base_url="https://bstein.dev",
)
monkeypatch.setattr(prov, "settings", dummy_settings)
_patch_mailu_ready(monkeypatch, dummy_settings)
class DB(DummyDB):
def fetchone(self, query, params=None):
return None
sent = {"called": False}
def mark_sent(_code):
sent["called"] = True
manager = prov.ProvisioningManager(DB({}), DummyStorage())
monkeypatch.setattr(manager._storage, "mark_welcome_sent", mark_sent)
monkeypatch.setattr(prov.mailer, "send_welcome", lambda *args, **kwargs: None)
manager._send_welcome_email("REQ_WELCOME", "alice", "alice@example.com")
assert sent["called"] is True

View File

View File

@ -0,0 +1,56 @@
from __future__ import annotations
from typing import Any
import types
import httpx
import pytest
from ariadne.services.keycloak_admin import KeycloakAdminClient
class DummyResponse:
def __init__(self, payload=None, status_code=200, headers=None):
self._payload = payload
self.status_code = status_code
self.headers = headers or {}
def json(self):
return self._payload
def raise_for_status(self):
if self.status_code >= 400:
request = httpx.Request("GET", "https://example.com")
response = httpx.Response(self.status_code, request=request)
raise httpx.HTTPStatusError("error", request=request, response=response)
class DummyClient:
def __init__(self, responses):
self._responses = list(responses)
self.calls = []
def __enter__(self):
return self
def __exit__(self, exc_type, exc, tb):
return False
def _next(self):
if not self._responses:
raise RuntimeError("missing response")
return self._responses.pop(0)
def get(self, url, params=None, headers=None):
self.calls.append(("get", url, params))
return self._next()
def post(self, url, data=None, json=None, headers=None):
self.calls.append(("post", url, data, json))
return self._next()
def put(self, url, headers=None, json=None):
self.calls.append(("put", url, json))
return self._next()
__all__ = [name for name in globals() if not name.startswith("__")]

View File

@ -0,0 +1,73 @@
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="", json_data=None):
self.status_code = status_code
self.text = text
self._json_data = json_data
def raise_for_status(self):
return None
def json(self):
if self._json_data is None:
raise ValueError("no json data")
return self._json_data
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 get(self, path):
self.calls.append((path, None, None))
resp = self.responses.get(path)
if resp is None:
resp = DummyResponse(200, "", json_data=[])
return resp
def close(self):
return None
__all__ = [name for name in globals() if not name.startswith("__")]

View File

@ -0,0 +1,246 @@
from tests.unit.services.service_helpers import *
def test_firefly_check_rotation_marks_rotated(monkeypatch) -> None:
dummy = types.SimpleNamespace(
firefly_namespace="finance",
firefly_pod_label="app=firefly",
firefly_container="firefly",
firefly_user_sync_wait_timeout_sec=60.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 find_user(self, username: str):
return {"id": "1", "username": username, "attributes": {}}
def get_user(self, user_id: str):
return {
"id": user_id,
"username": "alice",
"attributes": {
"mailu_email": ["alice@bstein.dev"],
"firefly_password": ["pw"],
},
}
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())
svc = FireflyService()
monkeypatch.setattr(svc, "check_password", lambda *_args, **_kwargs: {"status": "mismatch"})
result = svc.check_rotation_for_user("alice")
assert result["status"] == "ok"
assert result["rotated"] is True
assert any(key == "firefly_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)

View File

@ -0,0 +1,94 @@
from tests.unit.services.keycloak_admin_helpers import *
def test_list_group_names_filters(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
keycloak_admin_url="http://kc",
keycloak_admin_realm="atlas",
keycloak_admin_client_id="client",
keycloak_admin_client_secret="secret",
keycloak_realm="atlas",
)
monkeypatch.setattr("ariadne.services.keycloak_admin.settings", dummy_settings)
client = KeycloakAdminClient()
client._token = "token"
client._expires_at = 9999999999
dummy = DummyClient([DummyResponse([{"name": "demo"}, {"name": "admin"}, {"name": "test"}])])
monkeypatch.setattr("ariadne.services.keycloak_admin.httpx.Client", lambda *args, **kwargs: dummy)
assert client.list_group_names(exclude={"admin"}) == ["demo", "test"]
def test_get_group_id_skips_non_dict(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
keycloak_admin_url="http://kc",
keycloak_admin_realm="atlas",
keycloak_admin_client_id="client",
keycloak_admin_client_secret="secret",
keycloak_realm="atlas",
)
monkeypatch.setattr("ariadne.services.keycloak_admin.settings", dummy_settings)
client = KeycloakAdminClient()
client._token = "token"
client._expires_at = 9999999999
dummy = DummyClient([DummyResponse(["bad"])])
monkeypatch.setattr("ariadne.services.keycloak_admin.httpx.Client", lambda *args, **kwargs: dummy)
assert client.get_group_id("demo") is None
def test_get_group_id_cached(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
keycloak_admin_url="http://kc",
keycloak_admin_realm="atlas",
keycloak_admin_client_id="client",
keycloak_admin_client_secret="secret",
keycloak_realm="atlas",
)
monkeypatch.setattr("ariadne.services.keycloak_admin.settings", dummy_settings)
client = KeycloakAdminClient()
client._token = "token"
client._expires_at = 9999999999
dummy = DummyClient([DummyResponse([{"name": "demo", "id": "gid"}])])
monkeypatch.setattr("ariadne.services.keycloak_admin.httpx.Client", lambda *args, **kwargs: dummy)
assert client.get_group_id("demo") == "gid"
monkeypatch.setattr("ariadne.services.keycloak_admin.httpx.Client", lambda *args, **kwargs: (_ for _ in ()).throw(RuntimeError("no call")))
assert client.get_group_id("demo") == "gid"
def test_get_group_id_invalid_payload(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
keycloak_admin_url="http://kc",
keycloak_admin_realm="atlas",
keycloak_admin_client_id="client",
keycloak_admin_client_secret="secret",
keycloak_realm="atlas",
)
monkeypatch.setattr("ariadne.services.keycloak_admin.settings", dummy_settings)
client = KeycloakAdminClient()
client._token = "token"
client._expires_at = 9999999999
dummy = DummyClient([DummyResponse({"bad": "payload"})])
monkeypatch.setattr("ariadne.services.keycloak_admin.httpx.Client", lambda *args, **kwargs: dummy)
assert client.get_group_id("demo") is None
def test_add_user_to_group(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
keycloak_admin_url="http://kc",
keycloak_admin_realm="atlas",
keycloak_admin_client_id="client",
keycloak_admin_client_secret="secret",
keycloak_realm="atlas",
)
monkeypatch.setattr("ariadne.services.keycloak_admin.settings", dummy_settings)
client = KeycloakAdminClient()
client._token = "token"
client._expires_at = 9999999999
dummy = DummyClient([DummyResponse({})])
monkeypatch.setattr("ariadne.services.keycloak_admin.httpx.Client", lambda *args, **kwargs: dummy)
client.add_user_to_group("user", "group")
assert dummy.calls[0][0] == "put"

View File

@ -0,0 +1,58 @@
from tests.unit.services.keycloak_admin_helpers import *
def test_create_user_parses_location(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
keycloak_admin_url="http://kc",
keycloak_admin_realm="atlas",
keycloak_admin_client_id="client",
keycloak_admin_client_secret="secret",
keycloak_realm="atlas",
)
monkeypatch.setattr("ariadne.services.keycloak_admin.settings", dummy_settings)
client = KeycloakAdminClient()
client._token = "token"
client._expires_at = 9999999999
dummy = DummyClient([DummyResponse({}, headers={"Location": "http://kc/admin/realms/atlas/users/abc"})])
monkeypatch.setattr("ariadne.services.keycloak_admin.httpx.Client", lambda *args, **kwargs: dummy)
assert client.create_user({"username": "alice"}) == "abc"
def test_create_user_missing_location(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
keycloak_admin_url="http://kc",
keycloak_admin_realm="atlas",
keycloak_admin_client_id="client",
keycloak_admin_client_secret="secret",
keycloak_realm="atlas",
)
monkeypatch.setattr("ariadne.services.keycloak_admin.settings", dummy_settings)
client = KeycloakAdminClient()
client._token = "token"
client._expires_at = 9999999999
dummy = DummyClient([DummyResponse({}, headers={})])
monkeypatch.setattr("ariadne.services.keycloak_admin.httpx.Client", lambda *args, **kwargs: dummy)
with pytest.raises(RuntimeError):
client.create_user({"username": "alice"})
def test_reset_password_raises_on_error(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
keycloak_admin_url="http://kc",
keycloak_admin_realm="atlas",
keycloak_admin_client_id="client",
keycloak_admin_client_secret="secret",
keycloak_realm="atlas",
)
monkeypatch.setattr("ariadne.services.keycloak_admin.settings", dummy_settings)
client = KeycloakAdminClient()
client._token = "token"
client._expires_at = 9999999999
dummy = DummyClient([DummyResponse({}, status_code=400)])
monkeypatch.setattr("ariadne.services.keycloak_admin.httpx.Client", lambda *args, **kwargs: dummy)
with pytest.raises(httpx.HTTPStatusError):
client.reset_password("user", "pw", temporary=True)

View File

@ -0,0 +1,54 @@
from tests.unit.services.keycloak_admin_helpers import *
def test_get_token_fetches_once(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
keycloak_admin_url="http://kc",
keycloak_admin_realm="atlas",
keycloak_admin_client_id="client",
keycloak_admin_client_secret="secret",
keycloak_realm="atlas",
)
monkeypatch.setattr("ariadne.services.keycloak_admin.settings", dummy_settings)
client = KeycloakAdminClient()
dummy = DummyClient([DummyResponse({"access_token": "token", "expires_in": 120})])
monkeypatch.setattr("ariadne.services.keycloak_admin.httpx.Client", lambda *args, **kwargs: dummy)
assert client._get_token() == "token"
monkeypatch.setattr("ariadne.services.keycloak_admin.httpx.Client", lambda *args, **kwargs: (_ for _ in ()).throw(RuntimeError("should not call")))
assert client._get_token() == "token"
def test_get_token_missing_access_token(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
keycloak_admin_url="http://kc",
keycloak_admin_realm="atlas",
keycloak_admin_client_id="client",
keycloak_admin_client_secret="secret",
keycloak_realm="atlas",
)
monkeypatch.setattr("ariadne.services.keycloak_admin.settings", dummy_settings)
client = KeycloakAdminClient()
dummy = DummyClient([DummyResponse({"expires_in": 120})])
monkeypatch.setattr("ariadne.services.keycloak_admin.httpx.Client", lambda *args, **kwargs: dummy)
with pytest.raises(RuntimeError):
client._get_token()
def test_get_token_requires_config(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
keycloak_admin_url="http://kc",
keycloak_admin_realm="atlas",
keycloak_admin_client_id="",
keycloak_admin_client_secret="",
keycloak_realm="atlas",
)
monkeypatch.setattr("ariadne.services.keycloak_admin.settings", dummy_settings)
client = KeycloakAdminClient()
with pytest.raises(RuntimeError):
client._get_token()
def test_headers_includes_bearer(monkeypatch) -> None:
client = KeycloakAdminClient()
monkeypatch.setattr(client, "_get_token", lambda: "token")
headers = client.headers()
assert headers["Authorization"] == "Bearer token"

View File

@ -1,57 +1,4 @@
from __future__ import annotations
from typing import Any
import types
import httpx
import pytest
from ariadne.services.keycloak_admin import KeycloakAdminClient
class DummyResponse:
def __init__(self, payload=None, status_code=200, headers=None):
self._payload = payload
self.status_code = status_code
self.headers = headers or {}
def json(self):
return self._payload
def raise_for_status(self):
if self.status_code >= 400:
request = httpx.Request("GET", "https://example.com")
response = httpx.Response(self.status_code, request=request)
raise httpx.HTTPStatusError("error", request=request, response=response)
class DummyClient:
def __init__(self, responses):
self._responses = list(responses)
self.calls = []
def __enter__(self):
return self
def __exit__(self, exc_type, exc, tb):
return False
def _next(self):
if not self._responses:
raise RuntimeError("missing response")
return self._responses.pop(0)
def get(self, url, params=None, headers=None):
self.calls.append(("get", url, params))
return self._next()
def post(self, url, data=None, json=None, headers=None):
self.calls.append(("post", url, data, json))
return self._next()
def put(self, url, headers=None, json=None):
self.calls.append(("put", url, json))
return self._next()
from tests.unit.services.keycloak_admin_helpers import *
def test_set_user_attribute_preserves_profile(monkeypatch) -> None:
@ -97,7 +44,6 @@ def test_set_user_attribute_preserves_profile(monkeypatch) -> None:
"mailu_app_password": ["secret"],
}
def test_update_user_safe_merges_payload(monkeypatch) -> None:
client = KeycloakAdminClient()
captured: dict[str, Any] = {}
@ -127,25 +73,6 @@ def test_update_user_safe_merges_payload(monkeypatch) -> None:
assert payload.get("attributes") == {"existing": ["value"], "new": ["item"]}
assert payload.get("requiredActions") == ["UPDATE_PASSWORD"]
def test_get_token_fetches_once(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
keycloak_admin_url="http://kc",
keycloak_admin_realm="atlas",
keycloak_admin_client_id="client",
keycloak_admin_client_secret="secret",
keycloak_realm="atlas",
)
monkeypatch.setattr("ariadne.services.keycloak_admin.settings", dummy_settings)
client = KeycloakAdminClient()
dummy = DummyClient([DummyResponse({"access_token": "token", "expires_in": 120})])
monkeypatch.setattr("ariadne.services.keycloak_admin.httpx.Client", lambda *args, **kwargs: dummy)
assert client._get_token() == "token"
monkeypatch.setattr("ariadne.services.keycloak_admin.httpx.Client", lambda *args, **kwargs: (_ for _ in ()).throw(RuntimeError("should not call")))
assert client._get_token() == "token"
def test_find_user_by_email_case_insensitive(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
keycloak_admin_url="http://kc",
@ -165,7 +92,6 @@ def test_find_user_by_email_case_insensitive(monkeypatch) -> None:
user = client.find_user_by_email("alice@example.com")
assert user["id"] == "1"
def test_find_user_invalid_payload(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
keycloak_admin_url="http://kc",
@ -183,12 +109,10 @@ def test_find_user_invalid_payload(monkeypatch) -> None:
monkeypatch.setattr("ariadne.services.keycloak_admin.httpx.Client", lambda *args, **kwargs: dummy)
assert client.find_user("alice") is None
def test_find_user_by_email_empty() -> None:
client = KeycloakAdminClient()
assert client.find_user_by_email("") is None
def test_find_user_by_email_invalid_payload(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
keycloak_admin_url="http://kc",
@ -206,26 +130,6 @@ def test_find_user_by_email_invalid_payload(monkeypatch) -> None:
monkeypatch.setattr("ariadne.services.keycloak_admin.httpx.Client", lambda *args, **kwargs: dummy)
assert client.find_user_by_email("alice@example.com") is None
def test_list_group_names_filters(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
keycloak_admin_url="http://kc",
keycloak_admin_realm="atlas",
keycloak_admin_client_id="client",
keycloak_admin_client_secret="secret",
keycloak_realm="atlas",
)
monkeypatch.setattr("ariadne.services.keycloak_admin.settings", dummy_settings)
client = KeycloakAdminClient()
client._token = "token"
client._expires_at = 9999999999
dummy = DummyClient([DummyResponse([{"name": "demo"}, {"name": "admin"}, {"name": "test"}])])
monkeypatch.setattr("ariadne.services.keycloak_admin.httpx.Client", lambda *args, **kwargs: dummy)
assert client.list_group_names(exclude={"admin"}) == ["demo", "test"]
def test_find_user_by_email_skips_non_dict(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
keycloak_admin_url="http://kc",
@ -243,26 +147,6 @@ def test_find_user_by_email_skips_non_dict(monkeypatch) -> None:
monkeypatch.setattr("ariadne.services.keycloak_admin.httpx.Client", lambda *args, **kwargs: dummy)
assert client.find_user_by_email("alice@example.com") is None
def test_get_user_invalid_payload(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
keycloak_admin_url="http://kc",
keycloak_admin_realm="atlas",
keycloak_admin_client_id="client",
keycloak_admin_client_secret="secret",
keycloak_realm="atlas",
)
monkeypatch.setattr("ariadne.services.keycloak_admin.settings", dummy_settings)
client = KeycloakAdminClient()
client._token = "token"
client._expires_at = 9999999999
dummy = DummyClient([DummyResponse("bad")])
monkeypatch.setattr("ariadne.services.keycloak_admin.httpx.Client", lambda *args, **kwargs: dummy)
with pytest.raises(RuntimeError):
client.get_user("user-1")
def test_update_user_calls_put(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
keycloak_admin_url="http://kc",
@ -281,7 +165,6 @@ def test_update_user_calls_put(monkeypatch) -> None:
client.update_user("user-1", {"enabled": True})
assert dummy.calls
def test_update_user_safe_handles_bad_attrs(monkeypatch) -> None:
client = KeycloakAdminClient()
captured: dict[str, Any] = {}
@ -298,18 +181,6 @@ def test_update_user_safe_handles_bad_attrs(monkeypatch) -> None:
client.update_user_safe("user-1", {"attributes": {"new": ["item"]}})
assert captured["payload"]["attributes"] == {"new": ["item"]}
def test_set_user_attribute_user_id_missing(monkeypatch) -> None:
client = KeycloakAdminClient()
def fake_find_user(username: str) -> dict[str, Any]:
return {"id": ""}
monkeypatch.setattr(client, "find_user", fake_find_user)
with pytest.raises(RuntimeError):
client.set_user_attribute("alice", "attr", "val")
def test_set_user_attribute_handles_bad_attrs(monkeypatch) -> None:
client = KeycloakAdminClient()
@ -325,63 +196,6 @@ def test_set_user_attribute_handles_bad_attrs(monkeypatch) -> None:
client.set_user_attribute("alice", "attr", "val")
def test_get_group_id_skips_non_dict(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
keycloak_admin_url="http://kc",
keycloak_admin_realm="atlas",
keycloak_admin_client_id="client",
keycloak_admin_client_secret="secret",
keycloak_realm="atlas",
)
monkeypatch.setattr("ariadne.services.keycloak_admin.settings", dummy_settings)
client = KeycloakAdminClient()
client._token = "token"
client._expires_at = 9999999999
dummy = DummyClient([DummyResponse(["bad"])])
monkeypatch.setattr("ariadne.services.keycloak_admin.httpx.Client", lambda *args, **kwargs: dummy)
assert client.get_group_id("demo") is None
def test_get_group_id_cached(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
keycloak_admin_url="http://kc",
keycloak_admin_realm="atlas",
keycloak_admin_client_id="client",
keycloak_admin_client_secret="secret",
keycloak_realm="atlas",
)
monkeypatch.setattr("ariadne.services.keycloak_admin.settings", dummy_settings)
client = KeycloakAdminClient()
client._token = "token"
client._expires_at = 9999999999
dummy = DummyClient([DummyResponse([{"name": "demo", "id": "gid"}])])
monkeypatch.setattr("ariadne.services.keycloak_admin.httpx.Client", lambda *args, **kwargs: dummy)
assert client.get_group_id("demo") == "gid"
monkeypatch.setattr("ariadne.services.keycloak_admin.httpx.Client", lambda *args, **kwargs: (_ for _ in ()).throw(RuntimeError("no call")))
assert client.get_group_id("demo") == "gid"
def test_get_group_id_invalid_payload(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
keycloak_admin_url="http://kc",
keycloak_admin_realm="atlas",
keycloak_admin_client_id="client",
keycloak_admin_client_secret="secret",
keycloak_realm="atlas",
)
monkeypatch.setattr("ariadne.services.keycloak_admin.settings", dummy_settings)
client = KeycloakAdminClient()
client._token = "token"
client._expires_at = 9999999999
dummy = DummyClient([DummyResponse({"bad": "payload"})])
monkeypatch.setattr("ariadne.services.keycloak_admin.httpx.Client", lambda *args, **kwargs: dummy)
assert client.get_group_id("demo") is None
def test_iter_users_paginates(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
keycloak_admin_url="http://kc",
@ -406,7 +220,6 @@ def test_iter_users_paginates(monkeypatch) -> None:
users = client.iter_users(page_size=2, brief=True)
assert [u["id"] for u in users] == ["1", "2", "3"]
def test_iter_users_empty(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
keycloak_admin_url="http://kc",
@ -425,104 +238,6 @@ def test_iter_users_empty(monkeypatch) -> None:
assert client.iter_users(page_size=2) == []
def test_create_user_parses_location(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
keycloak_admin_url="http://kc",
keycloak_admin_realm="atlas",
keycloak_admin_client_id="client",
keycloak_admin_client_secret="secret",
keycloak_realm="atlas",
)
monkeypatch.setattr("ariadne.services.keycloak_admin.settings", dummy_settings)
client = KeycloakAdminClient()
client._token = "token"
client._expires_at = 9999999999
dummy = DummyClient([DummyResponse({}, headers={"Location": "http://kc/admin/realms/atlas/users/abc"})])
monkeypatch.setattr("ariadne.services.keycloak_admin.httpx.Client", lambda *args, **kwargs: dummy)
assert client.create_user({"username": "alice"}) == "abc"
def test_create_user_missing_location(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
keycloak_admin_url="http://kc",
keycloak_admin_realm="atlas",
keycloak_admin_client_id="client",
keycloak_admin_client_secret="secret",
keycloak_realm="atlas",
)
monkeypatch.setattr("ariadne.services.keycloak_admin.settings", dummy_settings)
client = KeycloakAdminClient()
client._token = "token"
client._expires_at = 9999999999
dummy = DummyClient([DummyResponse({}, headers={})])
monkeypatch.setattr("ariadne.services.keycloak_admin.httpx.Client", lambda *args, **kwargs: dummy)
with pytest.raises(RuntimeError):
client.create_user({"username": "alice"})
def test_get_token_missing_access_token(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
keycloak_admin_url="http://kc",
keycloak_admin_realm="atlas",
keycloak_admin_client_id="client",
keycloak_admin_client_secret="secret",
keycloak_realm="atlas",
)
monkeypatch.setattr("ariadne.services.keycloak_admin.settings", dummy_settings)
client = KeycloakAdminClient()
dummy = DummyClient([DummyResponse({"expires_in": 120})])
monkeypatch.setattr("ariadne.services.keycloak_admin.httpx.Client", lambda *args, **kwargs: dummy)
with pytest.raises(RuntimeError):
client._get_token()
def test_reset_password_raises_on_error(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
keycloak_admin_url="http://kc",
keycloak_admin_realm="atlas",
keycloak_admin_client_id="client",
keycloak_admin_client_secret="secret",
keycloak_realm="atlas",
)
monkeypatch.setattr("ariadne.services.keycloak_admin.settings", dummy_settings)
client = KeycloakAdminClient()
client._token = "token"
client._expires_at = 9999999999
dummy = DummyClient([DummyResponse({}, status_code=400)])
monkeypatch.setattr("ariadne.services.keycloak_admin.httpx.Client", lambda *args, **kwargs: dummy)
with pytest.raises(httpx.HTTPStatusError):
client.reset_password("user", "pw", temporary=True)
def test_get_token_requires_config(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
keycloak_admin_url="http://kc",
keycloak_admin_realm="atlas",
keycloak_admin_client_id="",
keycloak_admin_client_secret="",
keycloak_realm="atlas",
)
monkeypatch.setattr("ariadne.services.keycloak_admin.settings", dummy_settings)
client = KeycloakAdminClient()
with pytest.raises(RuntimeError):
client._get_token()
def test_headers_includes_bearer(monkeypatch) -> None:
client = KeycloakAdminClient()
monkeypatch.setattr(client, "_get_token", lambda: "token")
headers = client.headers()
assert headers["Authorization"] == "Bearer token"
def test_find_user_returns_none(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
keycloak_admin_url="http://kc",
@ -540,7 +255,6 @@ def test_find_user_returns_none(monkeypatch) -> None:
monkeypatch.setattr("ariadne.services.keycloak_admin.httpx.Client", lambda *args, **kwargs: dummy)
assert client.find_user("alice") is None
def test_get_user_invalid_payload(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
keycloak_admin_url="http://kc",
@ -560,7 +274,6 @@ def test_get_user_invalid_payload(monkeypatch) -> None:
with pytest.raises(RuntimeError):
client.get_user("id")
def test_get_user_success(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
keycloak_admin_url="http://kc",
@ -579,41 +292,18 @@ def test_get_user_success(monkeypatch) -> None:
user = client.get_user("id")
assert user["id"] == "1"
def test_set_user_attribute_user_missing(monkeypatch) -> None:
client = KeycloakAdminClient()
monkeypatch.setattr(client, "find_user", lambda username: None)
with pytest.raises(RuntimeError):
client.set_user_attribute("alice", "attr", "value")
def test_set_user_attribute_user_id_missing(monkeypatch) -> None:
client = KeycloakAdminClient()
monkeypatch.setattr(client, "find_user", lambda username: {})
with pytest.raises(RuntimeError):
client.set_user_attribute("alice", "attr", "value")
def test_add_user_to_group(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
keycloak_admin_url="http://kc",
keycloak_admin_realm="atlas",
keycloak_admin_client_id="client",
keycloak_admin_client_secret="secret",
keycloak_realm="atlas",
)
monkeypatch.setattr("ariadne.services.keycloak_admin.settings", dummy_settings)
client = KeycloakAdminClient()
client._token = "token"
client._expires_at = 9999999999
dummy = DummyClient([DummyResponse({})])
monkeypatch.setattr("ariadne.services.keycloak_admin.httpx.Client", lambda *args, **kwargs: dummy)
client.add_user_to_group("user", "group")
assert dummy.calls[0][0] == "put"
def test_get_user_raises_on_non_dict_payload(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
keycloak_admin_url="http://kc",
@ -632,7 +322,6 @@ def test_get_user_raises_on_non_dict_payload(monkeypatch) -> None:
with pytest.raises(RuntimeError):
client.get_user("user-1")
def test_update_user_safe_coerces_bad_attrs(monkeypatch) -> None:
client = KeycloakAdminClient()
monkeypatch.setattr(client, "get_user", lambda *_args, **_kwargs: {"id": "user-1"})
@ -641,7 +330,6 @@ def test_update_user_safe_coerces_bad_attrs(monkeypatch) -> None:
client.update_user_safe("user-1", {"attributes": {"new": ["item"]}})
def test_set_user_attribute_coerces_bad_attrs(monkeypatch) -> None:
client = KeycloakAdminClient()
monkeypatch.setattr(client, "find_user", lambda username: {"id": "user-1"})
@ -651,14 +339,12 @@ def test_set_user_attribute_coerces_bad_attrs(monkeypatch) -> None:
client.set_user_attribute("alice", "attr", "value")
def test_set_user_attribute_user_id_missing_raises(monkeypatch) -> None:
client = KeycloakAdminClient()
monkeypatch.setattr(client, "find_user", lambda username: {"id": ""})
with pytest.raises(RuntimeError):
client.set_user_attribute("alice", "attr", "value")
def test_get_user_rejects_non_dict_payload(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
keycloak_admin_url="http://kc",

View File

@ -0,0 +1,369 @@
from tests.unit.services.service_helpers import *
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_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

View File

@ -0,0 +1,99 @@
from tests.unit.services.service_helpers import *
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_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_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"

View File

@ -0,0 +1,355 @@
from tests.unit.services.service_helpers import *
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_lookup_user_present(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/users"] = DummyResponse(200, "", json_data=[{"email": "alice@bstein.dev"}])
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.find_user_by_email("alice@bstein.dev")
assert result.status == "present"
def test_vaultwarden_lookup_user_missing(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/users"] = DummyResponse(200, "", json_data=[{"email": "bob@bstein.dev"}])
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.find_user_by_email("alice@bstein.dev")
assert result.status == "missing"
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

View File

@ -0,0 +1,307 @@
from tests.unit.services.service_helpers import *
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_check_rotation_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 find_user(self, username: str):
return {"id": "1", "username": username, "attributes": {}}
def get_user(self, user_id: str):
return {
"id": user_id,
"username": "alice",
"attributes": {
"mailu_email": ["alice@bstein.dev"],
"wger_password": ["pw"],
},
}
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())
svc = WgerService()
monkeypatch.setattr(svc, "check_password", lambda *_args, **_kwargs: {"status": "mismatch"})
result = svc.check_rotation_for_user("alice")
assert result["status"] == "ok"
assert result["rotated"] is True
assert any(key == "wger_password_rotated_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_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)