test(ariadne): split oversized unit suites
This commit is contained in:
parent
152c19665e
commit
f0e161ba8b
@ -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
|
||||
|
||||
|
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
1005
tests/test_app.py
1005
tests/test_app.py
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
0
tests/unit/__init__.py
Normal file
0
tests/unit/app/__init__.py
Normal file
0
tests/unit/app/__init__.py
Normal file
27
tests/unit/app/app_route_helpers.py
Normal file
27
tests/unit/app/app_route_helpers.py
Normal 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("__")]
|
||||
286
tests/unit/app/test_app_admin_routes.py
Normal file
286
tests/unit/app/test_app_admin_routes.py
Normal 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
|
||||
139
tests/unit/app/test_app_firefly_account_routes.py
Normal file
139
tests/unit/app/test_app_firefly_account_routes.py
Normal 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
|
||||
148
tests/unit/app/test_app_lifecycle.py
Normal file
148
tests/unit/app/test_app_lifecycle.py
Normal 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"
|
||||
112
tests/unit/app/test_app_mailu_account_routes.py
Normal file
112
tests/unit/app/test_app_mailu_account_routes.py
Normal 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
|
||||
107
tests/unit/app/test_app_nextcloud_account_routes.py
Normal file
107
tests/unit/app/test_app_nextcloud_account_routes.py
Normal 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
|
||||
139
tests/unit/app/test_app_wger_account_routes.py
Normal file
139
tests/unit/app/test_app_wger_account_routes.py
Normal 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
|
||||
0
tests/unit/manager/__init__.py
Normal file
0
tests/unit/manager/__init__.py
Normal file
104
tests/unit/manager/provisioning_helpers.py
Normal file
104
tests/unit/manager/provisioning_helpers.py
Normal 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("__")]
|
||||
336
tests/unit/manager/test_provisioning_failure_modes.py
Normal file
336
tests/unit/manager/test_provisioning_failure_modes.py
Normal 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)
|
||||
480
tests/unit/manager/test_provisioning_identity_flow.py
Normal file
480
tests/unit/manager/test_provisioning_identity_flow.py
Normal 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"
|
||||
269
tests/unit/manager/test_provisioning_run_loop.py
Normal file
269
tests/unit/manager/test_provisioning_run_loop.py
Normal 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
|
||||
329
tests/unit/manager/test_provisioning_status_flow.py
Normal file
329
tests/unit/manager/test_provisioning_status_flow.py
Normal 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"
|
||||
132
tests/unit/manager/test_provisioning_welcome_email.py
Normal file
132
tests/unit/manager/test_provisioning_welcome_email.py
Normal 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
|
||||
0
tests/unit/services/__init__.py
Normal file
0
tests/unit/services/__init__.py
Normal file
56
tests/unit/services/keycloak_admin_helpers.py
Normal file
56
tests/unit/services/keycloak_admin_helpers.py
Normal 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("__")]
|
||||
73
tests/unit/services/service_helpers.py
Normal file
73
tests/unit/services/service_helpers.py
Normal 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("__")]
|
||||
246
tests/unit/services/test_firefly_service.py
Normal file
246
tests/unit/services/test_firefly_service.py
Normal 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)
|
||||
94
tests/unit/services/test_keycloak_admin_groups.py
Normal file
94
tests/unit/services/test_keycloak_admin_groups.py
Normal 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"
|
||||
58
tests/unit/services/test_keycloak_admin_lifecycle.py
Normal file
58
tests/unit/services/test_keycloak_admin_lifecycle.py
Normal 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)
|
||||
54
tests/unit/services/test_keycloak_admin_tokens.py
Normal file
54
tests/unit/services/test_keycloak_admin_tokens.py
Normal 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"
|
||||
@ -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",
|
||||
369
tests/unit/services/test_mailu_service.py
Normal file
369
tests/unit/services/test_mailu_service.py
Normal 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
|
||||
99
tests/unit/services/test_nextcloud_service.py
Normal file
99
tests/unit/services/test_nextcloud_service.py
Normal 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"
|
||||
355
tests/unit/services/test_vaultwarden_service.py
Normal file
355
tests/unit/services/test_vaultwarden_service.py
Normal 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
|
||||
307
tests/unit/services/test_wger_service.py
Normal file
307
tests/unit/services/test_wger_service.py
Normal 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)
|
||||
Loading…
x
Reference in New Issue
Block a user