game-stream: align Ariadne Wolf support with current app
This commit is contained in:
parent
2687ac441e
commit
dc0fccbbc6
1058
ariadne/app.py
1058
ariadne/app.py
File diff suppressed because it is too large
Load Diff
168
ariadne/app_game_routes.py
Normal file
168
ariadne/app_game_routes.py
Normal file
@ -0,0 +1,168 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
import secrets
|
||||
from typing import Any, Callable
|
||||
|
||||
from fastapi import Depends, FastAPI, HTTPException, Request
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
from .auth.keycloak import AuthContext
|
||||
from .db.storage import TaskRunRecord
|
||||
from .utils.errors import safe_error_detail
|
||||
|
||||
|
||||
def _game_from_payload(payload: dict[str, Any]) -> str:
|
||||
game = payload.get("game") if isinstance(payload, dict) else None
|
||||
return str(game).strip() if isinstance(game, str) and game.strip() else "wolf"
|
||||
|
||||
|
||||
def _record_simple_task(module: Any, task_name: str, started: datetime, status: str, detail: str | None = None) -> None:
|
||||
finished = datetime.now(timezone.utc)
|
||||
duration_sec = (finished - started).total_seconds()
|
||||
module.record_task_run(task_name, status, duration_sec)
|
||||
try:
|
||||
module.storage.record_task_run(
|
||||
TaskRunRecord(
|
||||
request_code=None,
|
||||
task=task_name,
|
||||
status=status,
|
||||
detail=detail,
|
||||
started_at=started,
|
||||
finished_at=finished,
|
||||
duration_ms=int(duration_sec * 1000),
|
||||
)
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _require_game_mode_hook(module: Any, request: Request) -> None:
|
||||
expected = module.settings.game_mode_hook_token
|
||||
if not expected:
|
||||
raise HTTPException(status_code=503, detail="game mode hook token not configured")
|
||||
token = request.headers.get("x-ariadne-game-mode-token", "")
|
||||
if not token and request.headers.get("authorization", "").lower().startswith("bearer "):
|
||||
token = request.headers.get("authorization", "")[7:].strip()
|
||||
if not secrets.compare_digest(token, expected):
|
||||
raise HTTPException(status_code=401, detail="invalid game mode hook token")
|
||||
|
||||
|
||||
async def _run_game_mode_action(module: Any, action: str, payload: dict[str, Any], actor: str) -> JSONResponse:
|
||||
started = datetime.now(timezone.utc)
|
||||
status = "ok"
|
||||
error_detail = ""
|
||||
game = _game_from_payload(payload)
|
||||
note = module._note_from_payload(payload)
|
||||
task_name = f"game_mode_{action}"
|
||||
try:
|
||||
if action == "start":
|
||||
result = module.game_mode.start(game, note=note)
|
||||
elif action == "stop":
|
||||
result = module.game_mode.stop(game, note=note)
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail="invalid action")
|
||||
module._record_event(task_name, {"actor": actor, "status": "ok", "game": game, "note": note or "", "result": result})
|
||||
return JSONResponse(result)
|
||||
except HTTPException:
|
||||
status = "error"
|
||||
raise
|
||||
except Exception as exc:
|
||||
status = "error"
|
||||
error_detail = safe_error_detail(exc, f"game mode {action} failed")
|
||||
module._record_event(task_name, {"actor": actor, "status": "error", "game": game, "error": error_detail})
|
||||
raise HTTPException(status_code=502, detail=error_detail)
|
||||
finally:
|
||||
_record_simple_task(module, task_name, started, status, error_detail or None)
|
||||
|
||||
|
||||
def _ensure_wolf_oauth2(module: Any, ctx: AuthContext) -> JSONResponse:
|
||||
module._require_admin(ctx)
|
||||
started = datetime.now(timezone.utc)
|
||||
status = "ok"
|
||||
detail = ""
|
||||
try:
|
||||
result = module.oauth2_proxy.ensure_wolf()
|
||||
if result.get("status") != "ok":
|
||||
status = "error"
|
||||
detail = str(result.get("detail") or "wolf oauth2 ensure failed")
|
||||
raise HTTPException(status_code=502, detail=detail)
|
||||
module._record_event("wolf_oidc_ensure", {"actor": ctx.username or "admin", **result})
|
||||
return JSONResponse(result)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as exc:
|
||||
status = "error"
|
||||
detail = safe_error_detail(exc, "wolf oauth2 ensure failed")
|
||||
module._record_event("wolf_oidc_ensure", {"actor": ctx.username or "admin", "status": "error", "detail": detail})
|
||||
raise HTTPException(status_code=502, detail=detail)
|
||||
finally:
|
||||
_record_simple_task(module, "wolf_oidc_ensure", started, status, detail or None)
|
||||
|
||||
|
||||
def _register_game_routes(app: FastAPI, require_auth: Callable, deps: Callable[[], Any]) -> None:
|
||||
@app.get("/api/game-stream/me")
|
||||
def get_game_stream_profile(ctx: AuthContext = Depends(require_auth)) -> JSONResponse:
|
||||
"""Return the Wolf profile policy for the authenticated Keycloak user."""
|
||||
|
||||
module = deps()
|
||||
return JSONResponse(module.game_stream_profiles.profile_for(ctx.username or "", ctx.groups))
|
||||
|
||||
@app.get("/api/admin/game-mode/status")
|
||||
def get_game_mode_status(ctx: AuthContext = Depends(require_auth)) -> JSONResponse:
|
||||
"""Return the current game-mode state for authenticated administrators."""
|
||||
|
||||
module = deps()
|
||||
module._require_admin(ctx)
|
||||
try:
|
||||
return JSONResponse(module.game_mode.status())
|
||||
except Exception:
|
||||
raise HTTPException(status_code=502, detail="failed to load game mode status")
|
||||
|
||||
@app.post("/api/admin/game-mode/start")
|
||||
async def start_game_mode(request: Request, ctx: AuthContext = Depends(require_auth)) -> JSONResponse:
|
||||
"""Scale infrastructure GPU workloads down for an administrator-triggered game session."""
|
||||
|
||||
module = deps()
|
||||
module._require_admin(ctx)
|
||||
payload = await module._read_json_payload(request)
|
||||
return await _run_game_mode_action(module, "start", payload, ctx.username or "admin")
|
||||
|
||||
@app.post("/api/admin/game-mode/stop")
|
||||
async def stop_game_mode(request: Request, ctx: AuthContext = Depends(require_auth)) -> JSONResponse:
|
||||
"""Restore infrastructure GPU workloads after an administrator-triggered game session."""
|
||||
|
||||
module = deps()
|
||||
module._require_admin(ctx)
|
||||
payload = await module._read_json_payload(request)
|
||||
return await _run_game_mode_action(module, "stop", payload, ctx.username or "admin")
|
||||
|
||||
@app.post("/api/game-mode/start")
|
||||
async def start_game_mode_hook(request: Request) -> JSONResponse:
|
||||
"""Scale infrastructure GPU workloads down for a trusted game-stream hook."""
|
||||
|
||||
module = deps()
|
||||
_require_game_mode_hook(module, request)
|
||||
payload = await module._read_json_payload(request)
|
||||
return await _run_game_mode_action(module, "start", payload, "game-stream-hook")
|
||||
|
||||
@app.post("/api/game-mode/stop")
|
||||
async def stop_game_mode_hook(request: Request) -> JSONResponse:
|
||||
"""Restore infrastructure GPU workloads for a trusted game-stream hook."""
|
||||
|
||||
module = deps()
|
||||
_require_game_mode_hook(module, request)
|
||||
payload = await module._read_json_payload(request)
|
||||
return await _run_game_mode_action(module, "stop", payload, "game-stream-hook")
|
||||
|
||||
@app.post("/api/admin/game-stream/wolf/oauth2/ensure")
|
||||
def ensure_wolf_oauth2(ctx: AuthContext = Depends(require_auth)) -> JSONResponse:
|
||||
"""Ensure Keycloak and Vault state for the Wolf oauth2-proxy."""
|
||||
|
||||
return _ensure_wolf_oauth2(deps(), ctx)
|
||||
|
||||
@app.post("/api/admin/game-stream/sunshine/oauth2/ensure")
|
||||
def ensure_sunshine_oauth2_alias(ctx: AuthContext = Depends(require_auth)) -> JSONResponse:
|
||||
"""Keep the old Sunshine route as a transition alias for Wolf."""
|
||||
|
||||
return _ensure_wolf_oauth2(deps(), ctx)
|
||||
@ -24,12 +24,7 @@ def _read_service_account() -> tuple[str, str]:
|
||||
return token, str(ca_path)
|
||||
|
||||
|
||||
def _k8s_request(
|
||||
method: str,
|
||||
path: str,
|
||||
payload: dict[str, Any] | None = None,
|
||||
extra_headers: dict[str, str] | None = None,
|
||||
) -> Any:
|
||||
def _k8s_request(method: str, path: str, payload: dict[str, Any] | None = None, extra_headers: dict[str, str] | None = None) -> Any:
|
||||
token, ca_path = _read_service_account()
|
||||
url = f"{_K8S_BASE_URL}{path}"
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
@ -62,12 +57,7 @@ def post_json(path: str, payload: dict[str, Any]) -> dict[str, Any]:
|
||||
def patch_json(path: str, payload: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Patch a Kubernetes API resource with a JSON merge patch."""
|
||||
|
||||
data = _k8s_request(
|
||||
"PATCH",
|
||||
path,
|
||||
payload,
|
||||
{"Content-Type": "application/merge-patch+json"},
|
||||
)
|
||||
data = _k8s_request("PATCH", path, payload, {"Content-Type": "application/merge-patch+json"})
|
||||
if not isinstance(data, dict):
|
||||
raise RuntimeError("unexpected kubernetes response")
|
||||
return data
|
||||
|
||||
@ -99,13 +99,7 @@ def record_task_run(task: str, status: str, duration_sec: float | None) -> None:
|
||||
TASK_DURATION_SECONDS.labels(task=task, status=status).observe(duration_sec)
|
||||
|
||||
|
||||
def record_schedule_state(
|
||||
task: str,
|
||||
last_run_ts: float | None,
|
||||
last_success_ts: float | None,
|
||||
next_run_ts: float | None,
|
||||
ok: bool | None,
|
||||
) -> None:
|
||||
def record_schedule_state(task: str, last_run_ts: float | None, last_success_ts: float | None, next_run_ts: float | None, ok: bool | None) -> None:
|
||||
"""Publish the latest scheduler timestamps and status for a task."""
|
||||
|
||||
if last_run_ts:
|
||||
@ -127,13 +121,7 @@ def set_access_request_counts(counts: dict[str, int]) -> None:
|
||||
ACCESS_REQUESTS.labels(status=status).set(count)
|
||||
|
||||
|
||||
def set_cluster_state_metrics(
|
||||
collected_at: datetime,
|
||||
nodes_total: int | None,
|
||||
nodes_ready: int | None,
|
||||
pods_running: float | None,
|
||||
kustomizations_not_ready: int | None,
|
||||
) -> None:
|
||||
def set_cluster_state_metrics(collected_at: datetime, nodes_total: int | None, nodes_ready: int | None, pods_running: float | None, kustomizations_not_ready: int | None) -> None:
|
||||
"""Set cluster-state gauges from the most recent collector snapshot."""
|
||||
|
||||
CLUSTER_STATE_LAST_TS.set(collected_at.timestamp())
|
||||
|
||||
@ -5,11 +5,7 @@ import threading
|
||||
from typing import Any
|
||||
|
||||
from ..k8s.client import get_json, patch_json
|
||||
from ..metrics.metrics import (
|
||||
record_game_mode_transition,
|
||||
set_game_mode_managed_replicas,
|
||||
set_game_mode_state,
|
||||
)
|
||||
from ..metrics.metrics import record_game_mode_transition, set_game_mode_managed_replicas, set_game_mode_state
|
||||
from ..settings import settings
|
||||
from ..utils.logging import get_logger
|
||||
|
||||
@ -45,14 +41,7 @@ class GameModeService:
|
||||
restore_replicas = int(replicas)
|
||||
except (TypeError, ValueError):
|
||||
restore_replicas = 1
|
||||
workloads.append(
|
||||
ManagedWorkload(
|
||||
kind=kind,
|
||||
namespace=namespace,
|
||||
name=name,
|
||||
restore_replicas=max(0, restore_replicas),
|
||||
)
|
||||
)
|
||||
workloads.append(ManagedWorkload(kind, namespace, name, max(0, restore_replicas)))
|
||||
return workloads
|
||||
|
||||
@staticmethod
|
||||
@ -107,13 +96,7 @@ class GameModeService:
|
||||
active = bool(workloads) and all(item["desired_replicas"] == 0 for item in workloads)
|
||||
game = self._current_game or "unknown"
|
||||
set_game_mode_state(settings.game_mode_node_name, game, active)
|
||||
return {
|
||||
"status": "active" if active else "idle",
|
||||
"active": active,
|
||||
"node": settings.game_mode_node_name,
|
||||
"game": game,
|
||||
"workloads": workloads,
|
||||
}
|
||||
return {"status": "active" if active else "idle", "active": active, "node": settings.game_mode_node_name, "game": game, "workloads": workloads}
|
||||
|
||||
def start(self, game: str | None = None, note: str | None = None) -> dict[str, Any]:
|
||||
game_name = self._game_name(game)
|
||||
@ -124,10 +107,7 @@ class GameModeService:
|
||||
self._current_game = game_name
|
||||
set_game_mode_state(settings.game_mode_node_name, game_name, True)
|
||||
record_game_mode_transition("start", "ok", game_name)
|
||||
logger.info(
|
||||
"game mode started",
|
||||
extra={"event": "game_mode_start", "game": game_name, "note": note or ""},
|
||||
)
|
||||
logger.info("game mode started", extra={"event": "game_mode_start", "game": game_name, "note": note or ""})
|
||||
result = self.status()
|
||||
result["action"] = "start"
|
||||
return result
|
||||
@ -145,10 +125,7 @@ class GameModeService:
|
||||
self._current_game = ""
|
||||
set_game_mode_state(settings.game_mode_node_name, game_name, False)
|
||||
record_game_mode_transition("stop", "ok", game_name)
|
||||
logger.info(
|
||||
"game mode stopped",
|
||||
extra={"event": "game_mode_stop", "game": game_name, "note": note or ""},
|
||||
)
|
||||
logger.info("game mode stopped", extra={"event": "game_mode_stop", "game": game_name, "note": note or ""})
|
||||
result = self.status()
|
||||
result["action"] = "stop"
|
||||
result["game"] = game_name
|
||||
|
||||
@ -18,8 +18,7 @@ def _group_values(groups: list[str]) -> set[str]:
|
||||
values: set[str] = set()
|
||||
for group in groups:
|
||||
stripped = group.strip()
|
||||
if not stripped:
|
||||
continue
|
||||
if stripped:
|
||||
values.add(stripped)
|
||||
values.add(stripped.lstrip("/"))
|
||||
return values
|
||||
@ -42,8 +41,7 @@ class GameStreamProfileService:
|
||||
if not group.startswith(prefix):
|
||||
continue
|
||||
suffix = group[len(prefix) :]
|
||||
if not suffix:
|
||||
continue
|
||||
if suffix:
|
||||
profile_group = group
|
||||
profile_id = _slug(suffix, profile_id)
|
||||
allowed = True
|
||||
|
||||
@ -160,9 +160,8 @@ class KeycloakAdminClient:
|
||||
|
||||
def find_client(self, client_id: str) -> dict[str, Any] | None:
|
||||
url = f"{settings.keycloak_admin_url}/admin/realms/{settings.keycloak_realm}/clients"
|
||||
params = {"clientId": client_id, "max": "1"}
|
||||
with httpx.Client(timeout=10.0) as client:
|
||||
resp = client.get(url, params=params, headers=self._headers())
|
||||
resp = client.get(url, params={"clientId": client_id, "max": "1"}, headers=self._headers())
|
||||
resp.raise_for_status()
|
||||
payload = resp.json()
|
||||
if not isinstance(payload, list) or not payload:
|
||||
@ -195,17 +194,14 @@ class KeycloakAdminClient:
|
||||
|
||||
def find_client_scope_id(self, scope_name: str) -> str | None:
|
||||
url = f"{settings.keycloak_admin_url}/admin/realms/{settings.keycloak_realm}/client-scopes"
|
||||
params = {"search": scope_name}
|
||||
with httpx.Client(timeout=10.0) as client:
|
||||
resp = client.get(url, params=params, headers=self._headers())
|
||||
resp = client.get(url, params={"search": scope_name}, headers=self._headers())
|
||||
resp.raise_for_status()
|
||||
payload = resp.json()
|
||||
if not isinstance(payload, list):
|
||||
return None
|
||||
for item in payload:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
if item.get("name") == scope_name and item.get("id"):
|
||||
if isinstance(item, dict) and item.get("name") == scope_name and item.get("id"):
|
||||
return str(item["id"])
|
||||
return None
|
||||
|
||||
|
||||
@ -3,10 +3,10 @@ from __future__ import annotations
|
||||
import secrets
|
||||
from typing import Any
|
||||
|
||||
from .keycloak_admin import keycloak_admin
|
||||
from .vault import vault
|
||||
from ..settings import settings
|
||||
from ..utils.logging import get_logger
|
||||
from .keycloak_admin import keycloak_admin
|
||||
from .vault import vault
|
||||
|
||||
|
||||
logger = get_logger(__name__)
|
||||
@ -67,22 +67,10 @@ class OAuth2ProxyService:
|
||||
cookie_secret = _valid_cookie_secret(existing.get("cookie_secret")) or secrets.token_hex(16)
|
||||
vault.write_kv_secret(
|
||||
settings.wolf_oidc_vault_path,
|
||||
{
|
||||
"client_id": client_id,
|
||||
"client_secret": client_secret,
|
||||
"cookie_secret": cookie_secret,
|
||||
},
|
||||
{"client_id": client_id, "client_secret": client_secret, "cookie_secret": cookie_secret},
|
||||
)
|
||||
logger.info(
|
||||
"wolf oauth2 proxy secret ensured",
|
||||
extra={"event": "wolf_oidc_ensure", "client_id": client_id, "vault_path": settings.wolf_oidc_vault_path},
|
||||
)
|
||||
return {
|
||||
"status": "ok",
|
||||
"client_id": client_id,
|
||||
"base_url": base_url,
|
||||
"vault_path": settings.wolf_oidc_vault_path,
|
||||
}
|
||||
logger.info("wolf oauth2 proxy secret ensured", extra={"event": "wolf_oidc_ensure", "client_id": client_id})
|
||||
return {"status": "ok", "client_id": client_id, "base_url": base_url, "vault_path": settings.wolf_oidc_vault_path}
|
||||
|
||||
def ensure_sunshine(self) -> dict[str, Any]:
|
||||
return self.ensure_wolf()
|
||||
|
||||
@ -167,12 +167,7 @@ def _parse_model_response(raw: str) -> tuple[dict[str, Any], str | None]:
|
||||
return (parsed if isinstance(parsed, dict) else {}, None)
|
||||
|
||||
|
||||
def _diagnosis_from_model(
|
||||
bundle: dict[str, Any],
|
||||
parsed: dict[str, Any],
|
||||
raw: str,
|
||||
parse_error: str | None,
|
||||
) -> dict[str, Any]:
|
||||
def _diagnosis_from_model(bundle: dict[str, Any], parsed: dict[str, Any], raw: str, parse_error: str | None) -> dict[str, Any]:
|
||||
summary = bundle.get("summary") if isinstance(bundle.get("summary"), dict) else {}
|
||||
unknowns = list(bundle.get("unknowns") or []) if isinstance(bundle.get("unknowns"), list) else []
|
||||
if parse_error:
|
||||
@ -264,13 +259,7 @@ def _text_value(value: Any, default: str) -> str:
|
||||
return default
|
||||
|
||||
|
||||
def _safe_text_value(
|
||||
value: Any,
|
||||
default: str,
|
||||
unknowns: list[Any],
|
||||
field: str,
|
||||
blocked_jobs: set[str],
|
||||
) -> str:
|
||||
def _safe_text_value(value: Any, default: str, unknowns: list[Any], field: str, blocked_jobs: set[str]) -> str:
|
||||
text = _text_value(value, default)
|
||||
if not _english_ascii(text):
|
||||
unknowns.append(f"model_{field}_non_english")
|
||||
|
||||
@ -8,6 +8,9 @@ import httpx
|
||||
|
||||
from ..settings import settings
|
||||
from ..utils.logging import get_logger
|
||||
from .vault_policies import DEV_KV_POLICY as _DEV_KV_POLICY
|
||||
from .vault_policies import K8S_ROLES as _K8S_ROLES
|
||||
from .vault_policies import VAULT_ADMIN_POLICY as _VAULT_ADMIN_POLICY
|
||||
|
||||
|
||||
logger = get_logger(__name__)
|
||||
@ -46,270 +49,6 @@ def _build_policy(read_paths: str, write_paths: str) -> str:
|
||||
)
|
||||
return "\n".join(policy_parts).strip() + "\n"
|
||||
|
||||
|
||||
_K8S_ROLES: list[dict[str, str]] = [
|
||||
{
|
||||
"role": "outline",
|
||||
"namespace": "outline",
|
||||
"service_accounts": "outline-vault",
|
||||
"read_paths": "outline/* shared/postmark-relay",
|
||||
"write_paths": "",
|
||||
},
|
||||
{
|
||||
"role": "planka",
|
||||
"namespace": "planka",
|
||||
"service_accounts": "planka-vault",
|
||||
"read_paths": "planka/* shared/postmark-relay",
|
||||
"write_paths": "",
|
||||
},
|
||||
{
|
||||
"role": "bstein-dev-home",
|
||||
"namespace": "bstein-dev-home",
|
||||
"service_accounts": "bstein-dev-home,bstein-dev-home-vault-sync",
|
||||
"read_paths": "portal/* shared/chat-ai-keys-runtime shared/portal-e2e-client shared/postmark-relay "
|
||||
"mailu/mailu-initial-account-secret shared/harbor-pull",
|
||||
"write_paths": "",
|
||||
},
|
||||
{
|
||||
"role": "gitea",
|
||||
"namespace": "gitea",
|
||||
"service_accounts": "gitea-vault",
|
||||
"read_paths": "gitea/*",
|
||||
"write_paths": "",
|
||||
},
|
||||
{
|
||||
"role": "vaultwarden",
|
||||
"namespace": "vaultwarden",
|
||||
"service_accounts": "vaultwarden-vault",
|
||||
"read_paths": "vaultwarden/* mailu/mailu-initial-account-secret",
|
||||
"write_paths": "",
|
||||
},
|
||||
{
|
||||
"role": "sso",
|
||||
"namespace": "sso",
|
||||
"service_accounts": "sso-vault,sso-vault-sync,mas-secrets-ensure",
|
||||
"read_paths": "sso/* portal/bstein-dev-home-keycloak-admin shared/keycloak-admin "
|
||||
"shared/portal-e2e-client shared/postmark-relay shared/harbor-pull",
|
||||
"write_paths": "",
|
||||
},
|
||||
{
|
||||
"role": "mailu-mailserver",
|
||||
"namespace": "mailu-mailserver",
|
||||
"service_accounts": "mailu-vault-sync",
|
||||
"read_paths": "mailu/* shared/postmark-relay shared/harbor-pull",
|
||||
"write_paths": "",
|
||||
},
|
||||
{
|
||||
"role": "harbor",
|
||||
"namespace": "harbor",
|
||||
"service_accounts": "harbor-vault-sync",
|
||||
"read_paths": "harbor/* shared/harbor-pull",
|
||||
"write_paths": "",
|
||||
},
|
||||
{
|
||||
"role": "nextcloud",
|
||||
"namespace": "nextcloud",
|
||||
"service_accounts": "nextcloud-vault",
|
||||
"read_paths": "nextcloud/* shared/keycloak-admin shared/postmark-relay",
|
||||
"write_paths": "",
|
||||
},
|
||||
{
|
||||
"role": "comms",
|
||||
"namespace": "comms",
|
||||
"service_accounts": "comms-vault,atlasbot",
|
||||
"read_paths": "comms/* shared/chat-ai-keys-runtime shared/harbor-pull",
|
||||
"write_paths": "",
|
||||
},
|
||||
{
|
||||
"role": "jenkins",
|
||||
"namespace": "jenkins",
|
||||
"service_accounts": "jenkins",
|
||||
"read_paths": "jenkins/*",
|
||||
"write_paths": "",
|
||||
},
|
||||
{
|
||||
"role": "monitoring",
|
||||
"namespace": "monitoring",
|
||||
"service_accounts": "monitoring-vault-sync",
|
||||
"read_paths": "monitoring/* shared/postmark-relay shared/harbor-pull",
|
||||
"write_paths": "",
|
||||
},
|
||||
{
|
||||
"role": "logging",
|
||||
"namespace": "logging",
|
||||
"service_accounts": "logging-vault-sync",
|
||||
"read_paths": "logging/* shared/harbor-pull",
|
||||
"write_paths": "",
|
||||
},
|
||||
{
|
||||
"role": "pegasus",
|
||||
"namespace": "jellyfin",
|
||||
"service_accounts": "pegasus-vault-sync",
|
||||
"read_paths": "pegasus/* shared/harbor-pull",
|
||||
"write_paths": "",
|
||||
},
|
||||
{
|
||||
"role": "crypto",
|
||||
"namespace": "crypto",
|
||||
"service_accounts": "crypto-vault-sync",
|
||||
"read_paths": "crypto/* shared/harbor-pull",
|
||||
"write_paths": "",
|
||||
},
|
||||
{
|
||||
"role": "health",
|
||||
"namespace": "health",
|
||||
"service_accounts": "health-vault-sync",
|
||||
"read_paths": "health/*",
|
||||
"write_paths": "",
|
||||
},
|
||||
{
|
||||
"role": "game-stream",
|
||||
"namespace": "game-stream",
|
||||
"service_accounts": "game-stream-vault",
|
||||
"read_paths": "game-stream/*",
|
||||
"write_paths": "",
|
||||
},
|
||||
{
|
||||
"role": "maintenance",
|
||||
"namespace": "maintenance",
|
||||
"service_accounts": "ariadne,maintenance-vault-sync",
|
||||
"read_paths": "maintenance/ariadne-db portal/bstein-dev-home-keycloak-admin mailu/mailu-db-secret "
|
||||
"mailu/mailu-initial-account-secret comms/synapse-admin shared/harbor-pull",
|
||||
"write_paths": "",
|
||||
},
|
||||
{
|
||||
"role": "finance",
|
||||
"namespace": "finance",
|
||||
"service_accounts": "finance-vault",
|
||||
"read_paths": "finance/* shared/postmark-relay",
|
||||
"write_paths": "",
|
||||
},
|
||||
{
|
||||
"role": "finance-secrets",
|
||||
"namespace": "finance",
|
||||
"service_accounts": "finance-secrets-ensure",
|
||||
"read_paths": "",
|
||||
"write_paths": "finance/*",
|
||||
},
|
||||
{
|
||||
"role": "longhorn",
|
||||
"namespace": "longhorn-system",
|
||||
"service_accounts": "longhorn-vault,longhorn-vault-sync",
|
||||
"read_paths": "longhorn/* shared/harbor-pull",
|
||||
"write_paths": "",
|
||||
},
|
||||
{
|
||||
"role": "postgres",
|
||||
"namespace": "postgres",
|
||||
"service_accounts": "postgres-vault",
|
||||
"read_paths": "postgres/postgres-db",
|
||||
"write_paths": "",
|
||||
},
|
||||
{
|
||||
"role": "vault",
|
||||
"namespace": "vault",
|
||||
"service_accounts": "vault",
|
||||
"read_paths": "vault/*",
|
||||
"write_paths": "",
|
||||
},
|
||||
{
|
||||
"role": "sso-secrets",
|
||||
"namespace": "sso",
|
||||
"service_accounts": "mas-secrets-ensure",
|
||||
"read_paths": "shared/keycloak-admin",
|
||||
"write_paths": "harbor/harbor-oidc vault/vault-oidc-config comms/synapse-oidc "
|
||||
"logging/oauth2-proxy-logs-oidc finance/actual-oidc",
|
||||
},
|
||||
{
|
||||
"role": "crypto-secrets",
|
||||
"namespace": "crypto",
|
||||
"service_accounts": "crypto-secrets-ensure",
|
||||
"read_paths": "",
|
||||
"write_paths": "crypto/wallet-monero-temp-rpc-auth",
|
||||
},
|
||||
{
|
||||
"role": "comms-secrets",
|
||||
"namespace": "comms",
|
||||
"service_accounts": "comms-secrets-ensure,mas-db-ensure,mas-admin-client-secret-writer,othrys-synapse-signingkey-job",
|
||||
"read_paths": "",
|
||||
"write_paths": "comms/turn-shared-secret comms/livekit-api comms/synapse-redis comms/synapse-macaroon "
|
||||
"comms/atlasbot-credentials-runtime comms/synapse-db comms/synapse-admin comms/synapse-registration "
|
||||
"comms/mas-db comms/mas-admin-client-runtime comms/mas-secrets-runtime comms/othrys-synapse-signingkey",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
_VAULT_ADMIN_POLICY = """
|
||||
path "sys/auth" {
|
||||
capabilities = ["read"]
|
||||
}
|
||||
path "sys/auth/*" {
|
||||
capabilities = ["create", "update", "delete", "sudo", "read"]
|
||||
}
|
||||
path "auth/kubernetes/*" {
|
||||
capabilities = ["create", "update", "read"]
|
||||
}
|
||||
path "auth/oidc/*" {
|
||||
capabilities = ["create", "update", "read"]
|
||||
}
|
||||
path "sys/policies/acl" {
|
||||
capabilities = ["list"]
|
||||
}
|
||||
path "sys/policies/acl/*" {
|
||||
capabilities = ["create", "update", "read"]
|
||||
}
|
||||
path "sys/internal/ui/mounts" {
|
||||
capabilities = ["read"]
|
||||
}
|
||||
path "sys/mounts" {
|
||||
capabilities = ["read"]
|
||||
}
|
||||
path "sys/mounts/auth/*" {
|
||||
capabilities = ["read", "update", "sudo"]
|
||||
}
|
||||
path "kv/data/atlas/vault/*" {
|
||||
capabilities = ["read"]
|
||||
}
|
||||
path "kv/metadata/atlas/vault/*" {
|
||||
capabilities = ["list"]
|
||||
}
|
||||
path "kv/data/*" {
|
||||
capabilities = ["create", "update", "read", "delete", "patch"]
|
||||
}
|
||||
path "kv/metadata" {
|
||||
capabilities = ["list"]
|
||||
}
|
||||
path "kv/metadata/*" {
|
||||
capabilities = ["read", "list", "delete"]
|
||||
}
|
||||
path "kv/data/atlas/shared/*" {
|
||||
capabilities = ["create", "update", "read", "patch"]
|
||||
}
|
||||
path "kv/metadata/atlas/shared/*" {
|
||||
capabilities = ["list"]
|
||||
}
|
||||
""".strip()
|
||||
|
||||
|
||||
_DEV_KV_POLICY = """
|
||||
path "kv/metadata" {
|
||||
capabilities = ["list"]
|
||||
}
|
||||
path "kv/metadata/atlas" {
|
||||
capabilities = ["list"]
|
||||
}
|
||||
path "kv/metadata/atlas/shared" {
|
||||
capabilities = ["list"]
|
||||
}
|
||||
path "kv/metadata/atlas/shared/*" {
|
||||
capabilities = ["list"]
|
||||
}
|
||||
path "kv/data/atlas/shared/*" {
|
||||
capabilities = ["read"]
|
||||
}
|
||||
""".strip()
|
||||
|
||||
|
||||
class VaultClient:
|
||||
"""Minimal HTTP client for Vault API requests."""
|
||||
|
||||
|
||||
@ -117,6 +117,13 @@ K8S_ROLES: list[dict[str, str]] = [
|
||||
"read_paths": "health/*",
|
||||
"write_paths": "",
|
||||
},
|
||||
{
|
||||
"role": "game-stream",
|
||||
"namespace": "game-stream",
|
||||
"service_accounts": "game-stream-vault",
|
||||
"read_paths": "game-stream/*",
|
||||
"write_paths": "",
|
||||
},
|
||||
{
|
||||
"role": "maintenance",
|
||||
"namespace": "maintenance",
|
||||
|
||||
@ -1,47 +1,30 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import json
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
|
||||
def _env(name: str, default: str = "") -> str:
|
||||
value = os.getenv(name, default)
|
||||
return value.strip() if isinstance(value, str) else default
|
||||
|
||||
|
||||
def _env_bool(name: str, default: str = "false") -> bool:
|
||||
return _env(name, default).lower() in {"1", "true", "yes", "y", "on"}
|
||||
|
||||
|
||||
def _env_int(name: str, default: int) -> int:
|
||||
raw = _env(name, str(default))
|
||||
try:
|
||||
return int(raw)
|
||||
except ValueError:
|
||||
return default
|
||||
|
||||
|
||||
def _env_float(name: str, default: float) -> float:
|
||||
raw = _env(name, str(default))
|
||||
try:
|
||||
return float(raw)
|
||||
except ValueError:
|
||||
return default
|
||||
|
||||
|
||||
def _env_json_list(name: str, default: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
raw = _env(name, "")
|
||||
if not raw:
|
||||
return default
|
||||
try:
|
||||
parsed = json.loads(raw)
|
||||
except json.JSONDecodeError:
|
||||
return default
|
||||
if not isinstance(parsed, list):
|
||||
return default
|
||||
return [item for item in parsed if isinstance(item, dict)]
|
||||
from .settings_env import _env, _env_bool, _env_float, _env_int
|
||||
from .settings_sections import (
|
||||
_cluster_state_config,
|
||||
_comms_config,
|
||||
_firefly_config,
|
||||
_game_stream_config,
|
||||
_image_sweeper_config,
|
||||
_jenkins_build_weather_config,
|
||||
_jenkins_workspace_cleanup_config,
|
||||
_keycloak_config,
|
||||
_mailu_config,
|
||||
_metis_config,
|
||||
_nextcloud_config,
|
||||
_opensearch_config,
|
||||
_platform_quality_probe_config,
|
||||
_portal_group_config,
|
||||
_schedule_config,
|
||||
_smtp_config,
|
||||
_testing_triage_config,
|
||||
_vault_config,
|
||||
_vaultwarden_config,
|
||||
_wger_config,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@ -191,6 +174,9 @@ class Settings:
|
||||
jenkins_workspace_cleanup_min_age_hours: float
|
||||
jenkins_workspace_cleanup_dry_run: bool
|
||||
jenkins_workspace_cleanup_max_deletions_per_run: int
|
||||
testing_triage_model_url: str
|
||||
testing_triage_model: str
|
||||
testing_triage_model_timeout_sec: float
|
||||
|
||||
vaultwarden_namespace: str
|
||||
vaultwarden_pod_label: str
|
||||
@ -245,9 +231,6 @@ class Settings:
|
||||
cluster_state_keep: int
|
||||
game_mode_node_name: str
|
||||
game_mode_displace_workloads: list[dict[str, Any]]
|
||||
game_mode_ollama_namespace: str
|
||||
game_mode_ollama_deployment: str
|
||||
game_mode_ollama_restore_replicas: int
|
||||
game_mode_hook_token: str
|
||||
wolf_oidc_client_id: str
|
||||
wolf_oidc_base_url: str
|
||||
@ -272,6 +255,7 @@ class Settings:
|
||||
platform_quality_suite_probe_cron: str
|
||||
jenkins_build_weather_cron: str
|
||||
jenkins_workspace_cleanup_cron: str
|
||||
testing_triage_cron: str
|
||||
|
||||
opensearch_url: str
|
||||
opensearch_limit_bytes: int
|
||||
@ -280,394 +264,28 @@ class Settings:
|
||||
|
||||
metrics_path: str
|
||||
|
||||
@classmethod
|
||||
def _keycloak_config(cls) -> dict[str, Any]:
|
||||
keycloak_url = _env("KEYCLOAK_URL", "https://sso.bstein.dev").rstrip("/")
|
||||
keycloak_realm = _env("KEYCLOAK_REALM", "atlas")
|
||||
keycloak_client_id = _env("KEYCLOAK_CLIENT_ID", "bstein-dev-home")
|
||||
keycloak_issuer = _env("KEYCLOAK_ISSUER", f"{keycloak_url}/realms/{keycloak_realm}").rstrip("/")
|
||||
keycloak_jwks_url = _env("KEYCLOAK_JWKS_URL", f"{keycloak_issuer}/protocol/openid-connect/certs").rstrip("/")
|
||||
return {
|
||||
"keycloak_url": keycloak_url,
|
||||
"keycloak_realm": keycloak_realm,
|
||||
"keycloak_client_id": keycloak_client_id,
|
||||
"keycloak_issuer": keycloak_issuer,
|
||||
"keycloak_jwks_url": keycloak_jwks_url,
|
||||
"keycloak_admin_url": _env("KEYCLOAK_ADMIN_URL", keycloak_url).rstrip("/"),
|
||||
"keycloak_admin_realm": _env("KEYCLOAK_ADMIN_REALM", keycloak_realm),
|
||||
"keycloak_admin_client_id": _env("KEYCLOAK_ADMIN_CLIENT_ID", ""),
|
||||
"keycloak_admin_client_secret": _env("KEYCLOAK_ADMIN_CLIENT_SECRET", ""),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def _portal_group_config(cls) -> dict[str, Any]:
|
||||
return {
|
||||
"portal_admin_users": [u for u in (_env("PORTAL_ADMIN_USERS", "bstein")).split(",") if u.strip()],
|
||||
"portal_admin_groups": [g for g in (_env("PORTAL_ADMIN_GROUPS", "admin")).split(",") if g.strip()],
|
||||
"account_allowed_groups": [
|
||||
g for g in (_env("ACCOUNT_ALLOWED_GROUPS", "dev,admin")).split(",") if g.strip()
|
||||
],
|
||||
"allowed_flag_groups": [g for g in (_env("ALLOWED_FLAG_GROUPS", "demo,test")).split(",") if g.strip()],
|
||||
"default_user_groups": [g for g in (_env("DEFAULT_USER_GROUPS", "dev")).split(",") if g.strip()],
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def _mailu_config(cls) -> dict[str, Any]:
|
||||
mailu_domain = _env("MAILU_DOMAIN", "bstein.dev")
|
||||
return {
|
||||
"mailu_domain": mailu_domain,
|
||||
"mailu_sync_url": _env(
|
||||
"MAILU_SYNC_URL",
|
||||
"http://mailu-sync-listener.mailu-mailserver.svc.cluster.local:8080/events",
|
||||
).rstrip("/"),
|
||||
"mailu_event_min_interval_sec": _env_float("MAILU_EVENT_MIN_INTERVAL_SEC", 10.0),
|
||||
"mailu_sync_wait_timeout_sec": _env_float("MAILU_SYNC_WAIT_TIMEOUT_SEC", 60.0),
|
||||
"mailu_mailbox_wait_timeout_sec": _env_float("MAILU_MAILBOX_WAIT_TIMEOUT_SEC", 60.0),
|
||||
"mailu_db_host": _env("MAILU_DB_HOST", "postgres-service.postgres.svc.cluster.local"),
|
||||
"mailu_db_port": _env_int("MAILU_DB_PORT", 5432),
|
||||
"mailu_db_name": _env("MAILU_DB_NAME", "mailu"),
|
||||
"mailu_db_user": _env("MAILU_DB_USER", "mailu"),
|
||||
"mailu_db_password": _env("MAILU_DB_PASSWORD", ""),
|
||||
"mailu_host": _env("MAILU_HOST", f"mail.{mailu_domain}"),
|
||||
"mailu_default_quota": _env_int("MAILU_DEFAULT_QUOTA", 20000000000),
|
||||
"mailu_system_users": [u for u in _env("MAILU_SYSTEM_USERS", "").split(",") if u.strip()],
|
||||
"mailu_system_password": _env("MAILU_SYSTEM_PASSWORD", ""),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def _smtp_config(cls, mailu_domain: str) -> dict[str, Any]:
|
||||
return {
|
||||
"smtp_host": _env("SMTP_HOST", ""),
|
||||
"smtp_port": _env_int("SMTP_PORT", 25),
|
||||
"smtp_username": _env("SMTP_USERNAME", ""),
|
||||
"smtp_password": _env("SMTP_PASSWORD", ""),
|
||||
"smtp_starttls": _env_bool("SMTP_STARTTLS", "false"),
|
||||
"smtp_use_tls": _env_bool("SMTP_USE_TLS", "false"),
|
||||
"smtp_from": _env("SMTP_FROM", f"postmaster@{mailu_domain}"),
|
||||
"smtp_timeout_sec": _env_float("SMTP_TIMEOUT_SEC", 10.0),
|
||||
"welcome_email_enabled": _env_bool("WELCOME_EMAIL_ENABLED", "true"),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def _nextcloud_config(cls) -> dict[str, Any]:
|
||||
return {
|
||||
"nextcloud_namespace": _env("NEXTCLOUD_NAMESPACE", "nextcloud"),
|
||||
"nextcloud_pod_label": _env("NEXTCLOUD_POD_LABEL", "app=nextcloud"),
|
||||
"nextcloud_container": _env("NEXTCLOUD_CONTAINER", "nextcloud"),
|
||||
"nextcloud_exec_timeout_sec": _env_float("NEXTCLOUD_EXEC_TIMEOUT_SEC", 120.0),
|
||||
"nextcloud_db_host": _env("NEXTCLOUD_DB_HOST", "postgres-service.postgres.svc.cluster.local"),
|
||||
"nextcloud_db_port": _env_int("NEXTCLOUD_DB_PORT", 5432),
|
||||
"nextcloud_db_name": _env("NEXTCLOUD_DB_NAME", "nextcloud"),
|
||||
"nextcloud_db_user": _env("NEXTCLOUD_DB_USER", "nextcloud"),
|
||||
"nextcloud_db_password": _env("NEXTCLOUD_DB_PASSWORD", ""),
|
||||
"nextcloud_url": _env("NEXTCLOUD_URL", "https://cloud.bstein.dev").rstrip("/"),
|
||||
"nextcloud_admin_user": _env("NEXTCLOUD_ADMIN_USER", ""),
|
||||
"nextcloud_admin_password": _env("NEXTCLOUD_ADMIN_PASSWORD", ""),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def _wger_config(cls) -> dict[str, Any]:
|
||||
return {
|
||||
"wger_namespace": _env("WGER_NAMESPACE", "health"),
|
||||
"wger_user_sync_wait_timeout_sec": _env_float("WGER_USER_SYNC_WAIT_TIMEOUT_SEC", 60.0),
|
||||
"wger_pod_label": _env("WGER_POD_LABEL", "app=wger"),
|
||||
"wger_container": _env("WGER_CONTAINER", "wger"),
|
||||
"wger_admin_username": _env("WGER_ADMIN_USERNAME", ""),
|
||||
"wger_admin_password": _env("WGER_ADMIN_PASSWORD", ""),
|
||||
"wger_admin_email": _env("WGER_ADMIN_EMAIL", ""),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def _firefly_config(cls) -> dict[str, Any]:
|
||||
return {
|
||||
"firefly_namespace": _env("FIREFLY_NAMESPACE", "finance"),
|
||||
"firefly_user_sync_wait_timeout_sec": _env_float("FIREFLY_USER_SYNC_WAIT_TIMEOUT_SEC", 90.0),
|
||||
"firefly_pod_label": _env("FIREFLY_POD_LABEL", "app=firefly"),
|
||||
"firefly_container": _env("FIREFLY_CONTAINER", "firefly"),
|
||||
"firefly_cron_base_url": _env(
|
||||
"FIREFLY_CRON_BASE_URL",
|
||||
"http://firefly.finance.svc.cluster.local/api/v1/cron",
|
||||
),
|
||||
"firefly_cron_token": _env("FIREFLY_CRON_TOKEN", ""),
|
||||
"firefly_cron_timeout_sec": _env_float("FIREFLY_CRON_TIMEOUT_SEC", 30.0),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def _vault_config(cls) -> dict[str, Any]:
|
||||
return {
|
||||
"vault_namespace": _env("VAULT_NAMESPACE", "vault"),
|
||||
"vault_addr": _env("VAULT_ADDR", "http://vault.vault.svc.cluster.local:8200").rstrip("/"),
|
||||
"vault_token": _env("VAULT_TOKEN", ""),
|
||||
"vault_k8s_role": _env("VAULT_K8S_ROLE", "vault"),
|
||||
"vault_k8s_role_ttl": _env("VAULT_K8S_ROLE_TTL", "1h"),
|
||||
"vault_k8s_token_reviewer_jwt": _env("VAULT_K8S_TOKEN_REVIEWER_JWT", ""),
|
||||
"vault_k8s_token_reviewer_jwt_file": _env("VAULT_K8S_TOKEN_REVIEWER_JWT_FILE", ""),
|
||||
"vault_oidc_discovery_url": _env("VAULT_OIDC_DISCOVERY_URL", ""),
|
||||
"vault_oidc_client_id": _env("VAULT_OIDC_CLIENT_ID", ""),
|
||||
"vault_oidc_client_secret": _env("VAULT_OIDC_CLIENT_SECRET", ""),
|
||||
"vault_oidc_default_role": _env("VAULT_OIDC_DEFAULT_ROLE", "admin"),
|
||||
"vault_oidc_scopes": _env("VAULT_OIDC_SCOPES", "openid profile email groups"),
|
||||
"vault_oidc_user_claim": _env("VAULT_OIDC_USER_CLAIM", "preferred_username"),
|
||||
"vault_oidc_groups_claim": _env("VAULT_OIDC_GROUPS_CLAIM", "groups"),
|
||||
"vault_oidc_token_policies": _env("VAULT_OIDC_TOKEN_POLICIES", ""),
|
||||
"vault_oidc_admin_group": _env("VAULT_OIDC_ADMIN_GROUP", "admin"),
|
||||
"vault_oidc_admin_policies": _env("VAULT_OIDC_ADMIN_POLICIES", "default,vault-admin"),
|
||||
"vault_oidc_dev_group": _env("VAULT_OIDC_DEV_GROUP", "dev"),
|
||||
"vault_oidc_dev_policies": _env("VAULT_OIDC_DEV_POLICIES", "default,dev-kv"),
|
||||
"vault_oidc_user_group": _env("VAULT_OIDC_USER_GROUP", ""),
|
||||
"vault_oidc_user_policies": _env("VAULT_OIDC_USER_POLICIES", ""),
|
||||
"vault_oidc_redirect_uris": _env(
|
||||
"VAULT_OIDC_REDIRECT_URIS",
|
||||
"https://secret.bstein.dev/ui/vault/auth/oidc/oidc/callback",
|
||||
),
|
||||
"vault_oidc_bound_audiences": _env("VAULT_OIDC_BOUND_AUDIENCES", ""),
|
||||
"vault_oidc_bound_claims_type": _env("VAULT_OIDC_BOUND_CLAIMS_TYPE", "string"),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def _comms_config(cls) -> dict[str, Any]:
|
||||
return {
|
||||
"comms_namespace": _env("COMMS_NAMESPACE", "comms"),
|
||||
"comms_synapse_base": _env(
|
||||
"COMMS_SYNAPSE_BASE",
|
||||
"http://othrys-synapse-matrix-synapse:8008",
|
||||
).rstrip("/"),
|
||||
"comms_auth_base": _env(
|
||||
"COMMS_AUTH_BASE",
|
||||
"http://matrix-authentication-service:8080",
|
||||
).rstrip("/"),
|
||||
"comms_mas_admin_api_base": _env(
|
||||
"COMMS_MAS_ADMIN_API_BASE",
|
||||
"http://matrix-authentication-service:8081/api/admin/v1",
|
||||
).rstrip("/"),
|
||||
"comms_mas_token_url": _env(
|
||||
"COMMS_MAS_TOKEN_URL",
|
||||
"http://matrix-authentication-service:8080/oauth2/token",
|
||||
),
|
||||
"comms_mas_admin_client_id": _env("COMMS_MAS_ADMIN_CLIENT_ID", "01KDXMVQBQ5JNY6SEJPZW6Z8BM"),
|
||||
"comms_mas_admin_client_secret": _env("COMMS_MAS_ADMIN_CLIENT_SECRET", ""),
|
||||
"comms_server_name": _env("COMMS_SERVER_NAME", "live.bstein.dev"),
|
||||
"comms_room_alias": _env("COMMS_ROOM_ALIAS", "#othrys:live.bstein.dev"),
|
||||
"comms_room_name": _env("COMMS_ROOM_NAME", "Othrys"),
|
||||
"comms_pin_message": _env(
|
||||
"COMMS_PIN_MESSAGE",
|
||||
"Invite guests: share https://live.bstein.dev/#/room/#othrys:live.bstein.dev?action=join and choose 'Continue' -> 'Join as guest'.",
|
||||
),
|
||||
"comms_seeder_user": _env("COMMS_SEEDER_USER", "othrys-seeder"),
|
||||
"comms_seeder_password": _env("COMMS_SEEDER_PASSWORD", ""),
|
||||
"comms_bot_user": _env("COMMS_BOT_USER", "atlasbot"),
|
||||
"comms_bot_password": _env("COMMS_BOT_PASSWORD", ""),
|
||||
"comms_synapse_db_host": _env(
|
||||
"COMMS_SYNAPSE_DB_HOST",
|
||||
"postgres-service.postgres.svc.cluster.local",
|
||||
),
|
||||
"comms_synapse_db_port": _env_int("COMMS_SYNAPSE_DB_PORT", 5432),
|
||||
"comms_synapse_db_name": _env("COMMS_SYNAPSE_DB_NAME", "synapse"),
|
||||
"comms_synapse_db_user": _env("COMMS_SYNAPSE_DB_USER", "synapse"),
|
||||
"comms_synapse_db_password": _env("COMMS_SYNAPSE_DB_PASSWORD", ""),
|
||||
"comms_synapse_admin_token": _env("COMMS_SYNAPSE_ADMIN_TOKEN", ""),
|
||||
"comms_timeout_sec": _env_float("COMMS_TIMEOUT_SEC", 30.0),
|
||||
"comms_guest_stale_days": _env_int("COMMS_GUEST_STALE_DAYS", 14),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def _image_sweeper_config(cls) -> dict[str, Any]:
|
||||
return {
|
||||
"image_sweeper_namespace": _env("IMAGE_SWEEPER_NAMESPACE", "maintenance"),
|
||||
"image_sweeper_service_account": _env("IMAGE_SWEEPER_SERVICE_ACCOUNT", "node-image-sweeper"),
|
||||
"image_sweeper_job_ttl_sec": _env_int("IMAGE_SWEEPER_JOB_TTL_SEC", 3600),
|
||||
"image_sweeper_wait_timeout_sec": _env_float("IMAGE_SWEEPER_WAIT_TIMEOUT_SEC", 1200.0),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def _platform_quality_probe_config(cls) -> dict[str, Any]:
|
||||
return {
|
||||
"platform_quality_probe_namespace": _env("PLATFORM_QUALITY_PROBE_NAMESPACE", "monitoring"),
|
||||
"platform_quality_probe_script_configmap": _env(
|
||||
"PLATFORM_QUALITY_PROBE_SCRIPT_CONFIGMAP",
|
||||
"platform-quality-suite-probe-script",
|
||||
),
|
||||
"platform_quality_probe_image": _env("PLATFORM_QUALITY_PROBE_IMAGE", "curlimages/curl:8.12.1"),
|
||||
"platform_quality_probe_job_ttl_sec": _env_int("PLATFORM_QUALITY_PROBE_JOB_TTL_SEC", 1800),
|
||||
"platform_quality_probe_wait_timeout_sec": _env_float("PLATFORM_QUALITY_PROBE_WAIT_TIMEOUT_SEC", 180.0),
|
||||
"platform_quality_probe_pushgateway_url": _env(
|
||||
"PLATFORM_QUALITY_PROBE_PUSHGATEWAY_URL",
|
||||
"http://platform-quality-gateway.monitoring.svc.cluster.local:9091",
|
||||
).rstrip("/"),
|
||||
"platform_quality_probe_http_timeout_sec": _env_int("PLATFORM_QUALITY_PROBE_HTTP_TIMEOUT_SECONDS", 12),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def _jenkins_build_weather_config(cls) -> dict[str, Any]:
|
||||
return {
|
||||
"jenkins_base_url": _env("JENKINS_BASE_URL", "https://ci.bstein.dev").rstrip("/"),
|
||||
"jenkins_api_user": _env("JENKINS_API_USER", ""),
|
||||
"jenkins_api_token": _env("JENKINS_API_TOKEN", ""),
|
||||
"jenkins_api_timeout_sec": _env_float("JENKINS_API_TIMEOUT_SEC", 10.0),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def _jenkins_workspace_cleanup_config(cls) -> dict[str, Any]:
|
||||
return {
|
||||
"jenkins_workspace_namespace": _env("JENKINS_WORKSPACE_NAMESPACE", "jenkins"),
|
||||
"jenkins_workspace_pvc_prefix": _env("JENKINS_WORKSPACE_PVC_PREFIX", "pvc-workspace-"),
|
||||
"jenkins_workspace_cleanup_min_age_hours": _env_float("JENKINS_WORKSPACE_CLEANUP_MIN_AGE_HOURS", 12.0),
|
||||
"jenkins_workspace_cleanup_dry_run": _env_bool("JENKINS_WORKSPACE_CLEANUP_DRY_RUN", "false"),
|
||||
"jenkins_workspace_cleanup_max_deletions_per_run": _env_int(
|
||||
"JENKINS_WORKSPACE_CLEANUP_MAX_DELETIONS_PER_RUN",
|
||||
20,
|
||||
),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def _vaultwarden_config(cls) -> dict[str, Any]:
|
||||
return {
|
||||
"vaultwarden_namespace": _env("VAULTWARDEN_NAMESPACE", "vaultwarden"),
|
||||
"vaultwarden_pod_label": _env("VAULTWARDEN_POD_LABEL", "app=vaultwarden"),
|
||||
"vaultwarden_pod_port": _env_int("VAULTWARDEN_POD_PORT", 80),
|
||||
"vaultwarden_service_host": _env(
|
||||
"VAULTWARDEN_SERVICE_HOST",
|
||||
"vaultwarden-service.vaultwarden.svc.cluster.local",
|
||||
),
|
||||
"vaultwarden_admin_secret_name": _env("VAULTWARDEN_ADMIN_SECRET_NAME", "vaultwarden-admin"),
|
||||
"vaultwarden_admin_secret_key": _env("VAULTWARDEN_ADMIN_SECRET_KEY", "ADMIN_TOKEN"),
|
||||
"vaultwarden_admin_session_ttl_sec": _env_float("VAULTWARDEN_ADMIN_SESSION_TTL_SEC", 300.0),
|
||||
"vaultwarden_admin_rate_limit_backoff_sec": _env_float("VAULTWARDEN_ADMIN_RATE_LIMIT_BACKOFF_SEC", 600.0),
|
||||
"vaultwarden_retry_cooldown_sec": _env_float("VAULTWARDEN_RETRY_COOLDOWN_SEC", 1800.0),
|
||||
"vaultwarden_failure_bailout": _env_int("VAULTWARDEN_FAILURE_BAILOUT", 2),
|
||||
"vaultwarden_invite_refresh_sec": _env_float("VAULTWARDEN_INVITE_REFRESH_SEC", 86400.0),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def _schedule_config(cls) -> dict[str, Any]:
|
||||
return {
|
||||
"mailu_sync_cron": _env("ARIADNE_SCHEDULE_MAILU_SYNC", "30 4 * * *"),
|
||||
"nextcloud_sync_cron": _env("ARIADNE_SCHEDULE_NEXTCLOUD_SYNC", "0 5 * * *"),
|
||||
"nextcloud_cron": _env("ARIADNE_SCHEDULE_NEXTCLOUD_CRON", "*/5 * * * *"),
|
||||
"nextcloud_maintenance_cron": _env("ARIADNE_SCHEDULE_NEXTCLOUD_MAINTENANCE", "30 4 * * *"),
|
||||
"vaultwarden_sync_cron": _env("ARIADNE_SCHEDULE_VAULTWARDEN_SYNC", "0 * * * *"),
|
||||
"wger_user_sync_cron": _env("ARIADNE_SCHEDULE_WGER_USER_SYNC", "0 5 * * *"),
|
||||
"wger_admin_cron": _env("ARIADNE_SCHEDULE_WGER_ADMIN", "15 3 * * *"),
|
||||
"firefly_user_sync_cron": _env("ARIADNE_SCHEDULE_FIREFLY_USER_SYNC", "0 6 * * *"),
|
||||
"firefly_cron": _env("ARIADNE_SCHEDULE_FIREFLY_CRON", "0 3 * * *"),
|
||||
"pod_cleaner_cron": _env("ARIADNE_SCHEDULE_POD_CLEANER", "0 * * * *"),
|
||||
"opensearch_prune_cron": _env("ARIADNE_SCHEDULE_OPENSEARCH_PRUNE", "23 3 * * *"),
|
||||
"image_sweeper_cron": _env("ARIADNE_SCHEDULE_IMAGE_SWEEPER", "30 4 * * 0"),
|
||||
"vault_k8s_auth_cron": _env("ARIADNE_SCHEDULE_VAULT_K8S_AUTH", "0 * * * *"),
|
||||
"vault_oidc_cron": _env("ARIADNE_SCHEDULE_VAULT_OIDC", "0 * * * *"),
|
||||
"comms_guest_name_cron": _env("ARIADNE_SCHEDULE_COMMS_GUEST_NAME", "*/5 * * * *"),
|
||||
"comms_pin_invite_cron": _env("ARIADNE_SCHEDULE_COMMS_PIN_INVITE", "*/30 * * * *"),
|
||||
"comms_reset_room_cron": _env("ARIADNE_SCHEDULE_COMMS_RESET_ROOM", "0 0 1 1 *"),
|
||||
"comms_seed_room_cron": _env("ARIADNE_SCHEDULE_COMMS_SEED_ROOM", "*/10 * * * *"),
|
||||
"keycloak_profile_cron": _env("ARIADNE_SCHEDULE_KEYCLOAK_PROFILE", "0 */6 * * *"),
|
||||
"metis_k3s_token_sync_cron": _env("ARIADNE_SCHEDULE_METIS_K3S_TOKEN_SYNC", "11 */6 * * *"),
|
||||
"platform_quality_suite_probe_cron": _env(
|
||||
"ARIADNE_SCHEDULE_PLATFORM_QUALITY_SUITE_PROBE",
|
||||
"*/15 * * * *",
|
||||
),
|
||||
"jenkins_build_weather_cron": _env(
|
||||
"ARIADNE_SCHEDULE_JENKINS_BUILD_WEATHER",
|
||||
"*/10 * * * *",
|
||||
),
|
||||
"jenkins_workspace_cleanup_cron": _env(
|
||||
"ARIADNE_SCHEDULE_JENKINS_WORKSPACE_CLEANUP",
|
||||
"45 */6 * * *",
|
||||
),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def _cluster_state_config(cls) -> dict[str, Any]:
|
||||
return {
|
||||
"vm_url": _env(
|
||||
"ARIADNE_VM_URL",
|
||||
"http://victoria-metrics-single-server.monitoring.svc.cluster.local:8428",
|
||||
).rstrip("/"),
|
||||
"cluster_state_vm_timeout_sec": _env_float("ARIADNE_CLUSTER_STATE_VM_TIMEOUT_SEC", 5.0),
|
||||
"alertmanager_url": _env("ARIADNE_ALERTMANAGER_URL", "").rstrip("/"),
|
||||
"cluster_state_cron": _env("ARIADNE_SCHEDULE_CLUSTER_STATE", "*/15 * * * *"),
|
||||
"cluster_state_keep": _env_int("ARIADNE_CLUSTER_STATE_KEEP", 168),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def _game_mode_config(cls) -> dict[str, Any]:
|
||||
legacy_ollama = {
|
||||
"kind": "Deployment",
|
||||
"namespace": _env("GAME_MODE_OLLAMA_NAMESPACE", "openclaw"),
|
||||
"name": _env("GAME_MODE_OLLAMA_DEPLOYMENT", "openclaw-ollama"),
|
||||
"restoreReplicas": _env_int("GAME_MODE_OLLAMA_RESTORE_REPLICAS", 1),
|
||||
}
|
||||
return {
|
||||
"game_mode_node_name": _env("GAME_MODE_NODE_NAME", "titan-24"),
|
||||
"game_mode_displace_workloads": _env_json_list("GAME_MODE_DISPLACE_WORKLOADS", [legacy_ollama]),
|
||||
"game_mode_ollama_namespace": legacy_ollama["namespace"],
|
||||
"game_mode_ollama_deployment": legacy_ollama["name"],
|
||||
"game_mode_ollama_restore_replicas": legacy_ollama["restoreReplicas"],
|
||||
"game_mode_hook_token": _env("GAME_MODE_HOOK_TOKEN", ""),
|
||||
"wolf_oidc_client_id": _env("WOLF_OIDC_CLIENT_ID", _env("SUNSHINE_OIDC_CLIENT_ID", "wolf")),
|
||||
"wolf_oidc_base_url": _env(
|
||||
"WOLF_OIDC_BASE_URL",
|
||||
_env("SUNSHINE_OIDC_BASE_URL", "https://wolf.bstein.dev"),
|
||||
).rstrip("/"),
|
||||
"wolf_oidc_vault_path": _env("WOLF_OIDC_VAULT_PATH", _env("SUNSHINE_OIDC_VAULT_PATH", "game-stream/wolf-oidc")),
|
||||
"wolf_oidc_cron": _env("ARIADNE_SCHEDULE_WOLF_OIDC", _env("ARIADNE_SCHEDULE_SUNSHINE_OIDC", "17 */6 * * *")),
|
||||
"game_stream_user_group": _env("GAME_STREAM_USER_GROUP", "game-stream-users"),
|
||||
"game_stream_admin_group": _env("GAME_STREAM_ADMIN_GROUP", "admin"),
|
||||
"game_stream_profile_group_prefix": _env("GAME_STREAM_PROFILE_GROUP_PREFIX", "game-stream-profile-"),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def _metis_config(cls) -> dict[str, Any]:
|
||||
return {
|
||||
"metis_base_url": _env("METIS_BASE_URL", "http://metis.maintenance.svc.cluster.local").rstrip("/"),
|
||||
"metis_watch_url": _env("METIS_WATCH_URL", "").rstrip("/"),
|
||||
"metis_timeout_sec": _env_float("METIS_TIMEOUT_SEC", 10.0),
|
||||
"metis_sentinel_watch_cron": _env("ARIADNE_SCHEDULE_METIS_SENTINEL_WATCH", "*/15 * * * *"),
|
||||
"metis_token_sync_namespace": _env("METIS_TOKEN_SYNC_NAMESPACE", "maintenance"),
|
||||
"metis_token_sync_service_account": _env("METIS_TOKEN_SYNC_SERVICE_ACCOUNT", "metis-token-sync"),
|
||||
"metis_token_sync_node_name": _env("METIS_TOKEN_SYNC_NODE_NAME", "titan-0a"),
|
||||
"metis_token_sync_image": _env("METIS_TOKEN_SYNC_IMAGE", "hashicorp/vault:1.17.6"),
|
||||
"metis_token_sync_job_ttl_sec": _env_int("METIS_TOKEN_SYNC_JOB_TTL_SEC", 1800),
|
||||
"metis_token_sync_wait_timeout_sec": _env_float("METIS_TOKEN_SYNC_WAIT_TIMEOUT_SEC", 180.0),
|
||||
"metis_token_sync_vault_addr": _env(
|
||||
"METIS_TOKEN_SYNC_VAULT_ADDR",
|
||||
"http://vault.vault.svc.cluster.local:8200",
|
||||
).rstrip("/"),
|
||||
"metis_token_sync_vault_k8s_role": _env("METIS_TOKEN_SYNC_VAULT_K8S_ROLE", "maintenance-metis-token-sync"),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def _opensearch_config(cls) -> dict[str, Any]:
|
||||
return {
|
||||
"opensearch_url": _env(
|
||||
"OPENSEARCH_URL",
|
||||
"http://opensearch-master.logging.svc.cluster.local:9200",
|
||||
).rstrip("/"),
|
||||
"opensearch_limit_bytes": _env_int("OPENSEARCH_LIMIT_BYTES", 1024**4),
|
||||
"opensearch_index_patterns": _env("OPENSEARCH_INDEX_PATTERNS", "kube-*,journald-*"),
|
||||
"opensearch_timeout_sec": _env_float("OPENSEARCH_TIMEOUT_SEC", 30.0),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_env(cls) -> "Settings":
|
||||
keycloak_cfg = cls._keycloak_config()
|
||||
portal_cfg = cls._portal_group_config()
|
||||
mailu_cfg = cls._mailu_config()
|
||||
smtp_cfg = cls._smtp_config(mailu_cfg["mailu_domain"])
|
||||
nextcloud_cfg = cls._nextcloud_config()
|
||||
wger_cfg = cls._wger_config()
|
||||
firefly_cfg = cls._firefly_config()
|
||||
vault_cfg = cls._vault_config()
|
||||
comms_cfg = cls._comms_config()
|
||||
image_cfg = cls._image_sweeper_config()
|
||||
platform_quality_probe_cfg = cls._platform_quality_probe_config()
|
||||
jenkins_build_weather_cfg = cls._jenkins_build_weather_config()
|
||||
jenkins_workspace_cleanup_cfg = cls._jenkins_workspace_cleanup_config()
|
||||
vaultwarden_cfg = cls._vaultwarden_config()
|
||||
schedule_cfg = cls._schedule_config()
|
||||
cluster_cfg = cls._cluster_state_config()
|
||||
game_mode_cfg = cls._game_mode_config()
|
||||
metis_cfg = cls._metis_config()
|
||||
opensearch_cfg = cls._opensearch_config()
|
||||
keycloak_cfg = _keycloak_config()
|
||||
portal_cfg = _portal_group_config()
|
||||
mailu_cfg = _mailu_config()
|
||||
smtp_cfg = _smtp_config(mailu_cfg["mailu_domain"])
|
||||
nextcloud_cfg = _nextcloud_config()
|
||||
wger_cfg = _wger_config()
|
||||
firefly_cfg = _firefly_config()
|
||||
vault_cfg = _vault_config()
|
||||
comms_cfg = _comms_config()
|
||||
image_cfg = _image_sweeper_config()
|
||||
platform_quality_probe_cfg = _platform_quality_probe_config()
|
||||
jenkins_build_weather_cfg = _jenkins_build_weather_config()
|
||||
jenkins_workspace_cleanup_cfg = _jenkins_workspace_cleanup_config()
|
||||
testing_triage_cfg = _testing_triage_config()
|
||||
vaultwarden_cfg = _vaultwarden_config()
|
||||
schedule_cfg = _schedule_config()
|
||||
cluster_cfg = _cluster_state_config()
|
||||
game_stream_cfg = _game_stream_config()
|
||||
metis_cfg = _metis_config()
|
||||
opensearch_cfg = _opensearch_config()
|
||||
|
||||
portal_db = _env("PORTAL_DATABASE_URL", "")
|
||||
ariadne_db = _env("ARIADNE_DATABASE_URL", portal_db)
|
||||
@ -705,10 +323,11 @@ class Settings:
|
||||
**platform_quality_probe_cfg,
|
||||
**jenkins_build_weather_cfg,
|
||||
**jenkins_workspace_cleanup_cfg,
|
||||
**testing_triage_cfg,
|
||||
**vaultwarden_cfg,
|
||||
**schedule_cfg,
|
||||
**cluster_cfg,
|
||||
**game_mode_cfg,
|
||||
**game_stream_cfg,
|
||||
**metis_cfg,
|
||||
**opensearch_cfg,
|
||||
)
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
from .settings_env import _env, _env_bool, _env_float, _env_int
|
||||
@ -307,6 +308,7 @@ def _schedule_config() -> dict[str, Any]:
|
||||
"ARIADNE_SCHEDULE_TESTING_TRIAGE",
|
||||
"*/15 * * * *",
|
||||
),
|
||||
"wolf_oidc_cron": _env("ARIADNE_SCHEDULE_WOLF_OIDC", _env("ARIADNE_SCHEDULE_SUNSHINE_OIDC", "17 */6 * * *")),
|
||||
}
|
||||
|
||||
|
||||
@ -323,6 +325,29 @@ def _cluster_state_config() -> dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
def _game_stream_config() -> dict[str, Any]:
|
||||
raw_workloads = _env("GAME_MODE_DISPLACE_WORKLOADS", "[]")
|
||||
try:
|
||||
parsed_workloads = json.loads(raw_workloads)
|
||||
except json.JSONDecodeError:
|
||||
parsed_workloads = []
|
||||
workloads = parsed_workloads if isinstance(parsed_workloads, list) else []
|
||||
return {
|
||||
"game_mode_node_name": _env("GAME_MODE_NODE_NAME", "titan-24"),
|
||||
"game_mode_displace_workloads": [item for item in workloads if isinstance(item, dict)],
|
||||
"game_mode_hook_token": _env("GAME_MODE_HOOK_TOKEN", ""),
|
||||
"wolf_oidc_client_id": _env("WOLF_OIDC_CLIENT_ID", _env("SUNSHINE_OIDC_CLIENT_ID", "wolf")),
|
||||
"wolf_oidc_base_url": _env(
|
||||
"WOLF_OIDC_BASE_URL",
|
||||
_env("SUNSHINE_OIDC_BASE_URL", "https://wolf.bstein.dev"),
|
||||
).rstrip("/"),
|
||||
"wolf_oidc_vault_path": _env("WOLF_OIDC_VAULT_PATH", _env("SUNSHINE_OIDC_VAULT_PATH", "game-stream/wolf-oidc")),
|
||||
"game_stream_user_group": _env("GAME_STREAM_USER_GROUP", "game-stream-users"),
|
||||
"game_stream_admin_group": _env("GAME_STREAM_ADMIN_GROUP", "admin"),
|
||||
"game_stream_profile_group_prefix": _env("GAME_STREAM_PROFILE_GROUP_PREFIX", "game-stream-profile-"),
|
||||
}
|
||||
|
||||
|
||||
def _metis_config() -> dict[str, Any]:
|
||||
return {
|
||||
"metis_base_url": _env("METIS_BASE_URL", "http://metis.maintenance.svc.cluster.local").rstrip("/"),
|
||||
|
||||
1094
tests/test_app.py
1094
tests/test_app.py
File diff suppressed because it is too large
Load Diff
@ -6,17 +6,11 @@ from ariadne.services import game_mode as game_mode_module
|
||||
from ariadne.services.game_mode import GameModeService
|
||||
|
||||
|
||||
def _settings() -> SimpleNamespace:
|
||||
def _settings(workloads=None) -> SimpleNamespace:
|
||||
return SimpleNamespace(
|
||||
game_mode_node_name="titan-24",
|
||||
game_mode_displace_workloads=[
|
||||
{
|
||||
"kind": "Deployment",
|
||||
"namespace": "openclaw",
|
||||
"name": "openclaw-ollama",
|
||||
"restoreReplicas": 1,
|
||||
}
|
||||
],
|
||||
game_mode_displace_workloads=workloads
|
||||
or [{"kind": "Deployment", "namespace": "openclaw", "name": "openclaw-ollama", "restoreReplicas": 1}],
|
||||
)
|
||||
|
||||
|
||||
@ -41,62 +35,62 @@ def test_game_mode_start_and_stop_patch_scale(monkeypatch) -> None:
|
||||
monkeypatch.setattr(game_mode_module, "record_game_mode_transition", lambda *args, **kwargs: None)
|
||||
|
||||
svc = GameModeService()
|
||||
started = svc.start("Arc Raiders")
|
||||
assert started["active"] is True
|
||||
assert svc.start("Arc Raiders")["active"] is True
|
||||
assert calls[-1][1] == {"spec": {"replicas": 0}}
|
||||
|
||||
stopped = svc.stop()
|
||||
assert stopped["active"] is False
|
||||
assert svc.stop()["active"] is False
|
||||
assert calls[-1][1] == {"spec": {"replicas": 1}}
|
||||
|
||||
|
||||
def test_game_mode_status_reports_workload(monkeypatch) -> None:
|
||||
monkeypatch.setattr(game_mode_module, "settings", _settings())
|
||||
monkeypatch.setattr(
|
||||
game_mode_module,
|
||||
"get_json",
|
||||
lambda _path: {"spec": {"replicas": 0}, "status": {"replicas": 0}},
|
||||
)
|
||||
monkeypatch.setattr(game_mode_module, "set_game_mode_state", lambda *args, **kwargs: None)
|
||||
monkeypatch.setattr(game_mode_module, "set_game_mode_managed_replicas", lambda *args, **kwargs: None)
|
||||
|
||||
status = GameModeService().status()
|
||||
assert status["status"] == "active"
|
||||
assert status["workloads"][0]["name"] == "openclaw-ollama"
|
||||
|
||||
|
||||
def test_game_mode_supports_statefulset_workload(monkeypatch) -> None:
|
||||
monkeypatch.setattr(
|
||||
game_mode_module,
|
||||
"settings",
|
||||
SimpleNamespace(
|
||||
game_mode_node_name="titan-24",
|
||||
game_mode_displace_workloads=[
|
||||
{
|
||||
"kind": "StatefulSet",
|
||||
"namespace": "hermes",
|
||||
"name": "hermes-llm",
|
||||
"restoreReplicas": "2",
|
||||
}
|
||||
],
|
||||
),
|
||||
)
|
||||
workload = [{"kind": "StatefulSet", "namespace": "hermes", "name": "hermes-llm", "restoreReplicas": "2"}]
|
||||
monkeypatch.setattr(game_mode_module, "settings", _settings(workload))
|
||||
calls: list[str] = []
|
||||
|
||||
monkeypatch.setattr(
|
||||
game_mode_module,
|
||||
"get_json",
|
||||
lambda _path: {"spec": {"replicas": 2}, "status": {"replicas": 2}},
|
||||
)
|
||||
|
||||
def fake_patch_json(path, _payload):
|
||||
calls.append(path)
|
||||
return {"ok": True}
|
||||
|
||||
monkeypatch.setattr(game_mode_module, "patch_json", fake_patch_json)
|
||||
monkeypatch.setattr(game_mode_module, "get_json", lambda _path: {"spec": {"replicas": 2}, "status": {"replicas": 2}})
|
||||
monkeypatch.setattr(game_mode_module, "patch_json", lambda path, _payload: calls.append(path) or {"ok": True})
|
||||
monkeypatch.setattr(game_mode_module, "set_game_mode_state", lambda *args, **kwargs: None)
|
||||
monkeypatch.setattr(game_mode_module, "set_game_mode_managed_replicas", lambda *args, **kwargs: None)
|
||||
monkeypatch.setattr(game_mode_module, "record_game_mode_transition", lambda *args, **kwargs: None)
|
||||
|
||||
GameModeService().start("wolf")
|
||||
assert calls == ["/apis/apps/v1/namespaces/hermes/statefulsets/hermes-llm/scale"]
|
||||
|
||||
|
||||
def test_game_mode_ignores_invalid_workloads_and_fallback_replicas(monkeypatch) -> None:
|
||||
workloads = [
|
||||
{"kind": "Deployment", "namespace": "", "name": "missing"},
|
||||
{"kind": "Deployment", "namespace": "openclaw", "name": "ollama", "restoreReplicas": "bad"},
|
||||
]
|
||||
monkeypatch.setattr(game_mode_module, "settings", _settings(workloads))
|
||||
monkeypatch.setattr(game_mode_module, "get_json", lambda _path: {"spec": {"replicas": None}, "status": {}})
|
||||
monkeypatch.setattr(game_mode_module, "set_game_mode_state", lambda *args, **kwargs: None)
|
||||
monkeypatch.setattr(game_mode_module, "set_game_mode_managed_replicas", lambda *args, **kwargs: None)
|
||||
|
||||
status = GameModeService().status()
|
||||
assert status["workloads"][0]["restore_replicas"] == 1
|
||||
assert status["workloads"][0]["desired_replicas"] is None
|
||||
|
||||
|
||||
def test_game_mode_rejects_unsupported_kind(monkeypatch) -> None:
|
||||
monkeypatch.setattr(game_mode_module, "settings", _settings([{"kind": "Job", "namespace": "x", "name": "y"}]))
|
||||
monkeypatch.setattr(game_mode_module, "record_game_mode_transition", lambda *args, **kwargs: None)
|
||||
|
||||
try:
|
||||
GameModeService().start("arc")
|
||||
except ValueError as exc:
|
||||
assert "unsupported" in str(exc)
|
||||
else:
|
||||
raise AssertionError("unsupported kind should fail")
|
||||
|
||||
|
||||
def test_game_mode_records_stop_errors(monkeypatch) -> None:
|
||||
monkeypatch.setattr(game_mode_module, "settings", _settings())
|
||||
transitions = []
|
||||
monkeypatch.setattr(game_mode_module, "record_game_mode_transition", lambda *args: transitions.append(args))
|
||||
monkeypatch.setattr(game_mode_module, "patch_json", lambda *_args, **_kwargs: (_ for _ in ()).throw(RuntimeError("boom")))
|
||||
|
||||
try:
|
||||
GameModeService().stop("arc")
|
||||
except RuntimeError:
|
||||
pass
|
||||
|
||||
assert transitions[-1] == ("stop", "error", "arc")
|
||||
|
||||
10
tests/test_game_mode_metrics.py
Normal file
10
tests/test_game_mode_metrics.py
Normal file
@ -0,0 +1,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from ariadne.metrics import metrics
|
||||
|
||||
|
||||
def test_game_mode_metric_helpers() -> None:
|
||||
metrics.set_game_mode_state("", "", True)
|
||||
metrics.record_game_mode_transition("", "", "")
|
||||
metrics.set_game_mode_managed_replicas("openclaw", "ollama", 0)
|
||||
metrics.set_game_mode_managed_replicas("openclaw", "ollama", None)
|
||||
@ -16,19 +16,14 @@ def _settings() -> SimpleNamespace:
|
||||
|
||||
def test_profile_defaults_to_user_profile(monkeypatch) -> None:
|
||||
monkeypatch.setattr(profile_module, "settings", _settings())
|
||||
|
||||
profile = GameStreamProfileService().profile_for("Brad Stein", ["game-stream-users"])
|
||||
|
||||
assert profile["allowed"] is True
|
||||
assert profile["profile_id"] == "user-brad-stein"
|
||||
assert profile["profile_group"] == ""
|
||||
|
||||
|
||||
def test_profile_group_overrides_user_profile(monkeypatch) -> None:
|
||||
monkeypatch.setattr(profile_module, "settings", _settings())
|
||||
|
||||
profile = GameStreamProfileService().profile_for("brad", ["/game-stream-profile-family"])
|
||||
|
||||
assert profile["allowed"] is True
|
||||
assert profile["profile_id"] == "family"
|
||||
assert profile["profile_group"] == "game-stream-profile-family"
|
||||
@ -36,8 +31,6 @@ def test_profile_group_overrides_user_profile(monkeypatch) -> None:
|
||||
|
||||
def test_profile_denies_unlisted_user(monkeypatch) -> None:
|
||||
monkeypatch.setattr(profile_module, "settings", _settings())
|
||||
|
||||
profile = GameStreamProfileService().profile_for("guest", ["other"])
|
||||
|
||||
assert profile["allowed"] is False
|
||||
assert profile["profile_id"] == "user-guest"
|
||||
|
||||
@ -70,16 +70,23 @@ def test_post_json_success(monkeypatch) -> None:
|
||||
assert result == {"ok": True}
|
||||
|
||||
|
||||
def test_patch_json_success(monkeypatch) -> None:
|
||||
def test_patch_json_uses_merge_patch_header(monkeypatch) -> None:
|
||||
dummy_settings = types.SimpleNamespace(k8s_api_timeout_sec=5.0)
|
||||
observed_headers = {}
|
||||
|
||||
monkeypatch.setattr(k8s_client, "settings", dummy_settings)
|
||||
monkeypatch.setattr(k8s_client, "_read_service_account", lambda: ("token", "/tmp/ca"))
|
||||
client = DummyClient()
|
||||
monkeypatch.setattr(k8s_client.httpx, "Client", lambda *args, **kwargs: client)
|
||||
|
||||
class HeaderClient(DummyClient):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
observed_headers.update(kwargs.get("headers") or {})
|
||||
|
||||
monkeypatch.setattr(k8s_client.httpx, "Client", HeaderClient)
|
||||
|
||||
result = k8s_client.patch_json("/api/test", {"spec": {"replicas": 0}})
|
||||
assert result == {"ok": True}
|
||||
assert client.calls[0][0] == "PATCH"
|
||||
assert observed_headers["Content-Type"] == "application/merge-patch+json"
|
||||
|
||||
|
||||
def test_patch_json_rejects_non_dict(monkeypatch) -> None:
|
||||
@ -91,7 +98,7 @@ def test_patch_json_rejects_non_dict(monkeypatch) -> None:
|
||||
monkeypatch.setattr(k8s_client.httpx, "Client", lambda *args, **kwargs: client)
|
||||
|
||||
with pytest.raises(RuntimeError):
|
||||
k8s_client.patch_json("/api/test", {"spec": {"replicas": 0}})
|
||||
k8s_client.patch_json("/api/test", {"payload": "ok"})
|
||||
|
||||
|
||||
def test_get_json_rejects_non_dict(monkeypatch) -> None:
|
||||
|
||||
@ -1,768 +0,0 @@
|
||||
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()
|
||||
|
||||
|
||||
def test_set_user_attribute_preserves_profile(monkeypatch) -> None:
|
||||
client = KeycloakAdminClient()
|
||||
captured: dict[str, Any] = {}
|
||||
|
||||
def fake_find_user(username: str) -> dict[str, Any]:
|
||||
return {"id": "user-123"}
|
||||
|
||||
def fake_get_user(user_id: str) -> dict[str, Any]:
|
||||
return {
|
||||
"id": user_id,
|
||||
"username": "alice",
|
||||
"email": "alice@bstein.dev",
|
||||
"emailVerified": True,
|
||||
"enabled": True,
|
||||
"firstName": "Alice",
|
||||
"lastName": "Smith",
|
||||
"requiredActions": ["UPDATE_PASSWORD", 123],
|
||||
"attributes": {"existing": ["value"]},
|
||||
}
|
||||
|
||||
def fake_update_user(user_id: str, payload: dict[str, Any]) -> None:
|
||||
captured["user_id"] = user_id
|
||||
captured["payload"] = payload
|
||||
|
||||
monkeypatch.setattr(client, "find_user", fake_find_user)
|
||||
monkeypatch.setattr(client, "get_user", fake_get_user)
|
||||
monkeypatch.setattr(client, "update_user", fake_update_user)
|
||||
|
||||
client.set_user_attribute("alice", "mailu_app_password", "secret")
|
||||
|
||||
payload = captured.get("payload") or {}
|
||||
assert payload.get("username") == "alice"
|
||||
assert payload.get("email") == "alice@bstein.dev"
|
||||
assert payload.get("emailVerified") is True
|
||||
assert payload.get("enabled") is True
|
||||
assert payload.get("firstName") == "Alice"
|
||||
assert payload.get("lastName") == "Smith"
|
||||
assert payload.get("requiredActions") == ["UPDATE_PASSWORD"]
|
||||
assert payload.get("attributes") == {
|
||||
"existing": ["value"],
|
||||
"mailu_app_password": ["secret"],
|
||||
}
|
||||
|
||||
|
||||
def test_update_user_safe_merges_payload(monkeypatch) -> None:
|
||||
client = KeycloakAdminClient()
|
||||
captured: dict[str, Any] = {}
|
||||
|
||||
def fake_get_user(user_id: str) -> dict[str, Any]:
|
||||
return {
|
||||
"id": user_id,
|
||||
"username": "alice",
|
||||
"enabled": True,
|
||||
"attributes": {"existing": ["value"]},
|
||||
}
|
||||
|
||||
def fake_update_user(user_id: str, payload: dict[str, Any]) -> None:
|
||||
captured["user_id"] = user_id
|
||||
captured["payload"] = payload
|
||||
|
||||
monkeypatch.setattr(client, "get_user", fake_get_user)
|
||||
monkeypatch.setattr(client, "update_user", fake_update_user)
|
||||
|
||||
client.update_user_safe(
|
||||
"user-123",
|
||||
{"attributes": {"new": ["item"]}, "requiredActions": ["UPDATE_PASSWORD"]},
|
||||
)
|
||||
|
||||
payload = captured.get("payload") or {}
|
||||
assert payload.get("username") == "alice"
|
||||
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",
|
||||
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([{"email": "Alice@Example.com", "id": "1"}])])
|
||||
monkeypatch.setattr("ariadne.services.keycloak_admin.httpx.Client", lambda *args, **kwargs: dummy)
|
||||
|
||||
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",
|
||||
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.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",
|
||||
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.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",
|
||||
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.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",
|
||||
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.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] = {}
|
||||
|
||||
def fake_get_user(user_id: str) -> dict[str, Any]:
|
||||
return {"id": user_id, "username": "alice", "attributes": "bad"}
|
||||
|
||||
def fake_update_user(user_id: str, payload: dict[str, Any]) -> None:
|
||||
captured["payload"] = payload
|
||||
|
||||
monkeypatch.setattr(client, "get_user", fake_get_user)
|
||||
monkeypatch.setattr(client, "update_user", fake_update_user)
|
||||
|
||||
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()
|
||||
|
||||
def fake_find_user(username: str) -> dict[str, Any]:
|
||||
return {"id": "user-1"}
|
||||
|
||||
def fake_get_user(user_id: str) -> dict[str, Any]:
|
||||
return {"id": user_id, "username": "alice", "attributes": "bad"}
|
||||
|
||||
monkeypatch.setattr(client, "find_user", fake_find_user)
|
||||
monkeypatch.setattr(client, "get_user", fake_get_user)
|
||||
monkeypatch.setattr(client, "update_user", lambda *_args, **_kwargs: 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",
|
||||
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([{"id": "1"}, {"id": "2"}]),
|
||||
DummyResponse([{"id": "3"}]),
|
||||
]
|
||||
)
|
||||
monkeypatch.setattr("ariadne.services.keycloak_admin.httpx.Client", lambda *args, **kwargs: dummy)
|
||||
|
||||
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",
|
||||
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)
|
||||
|
||||
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_find_client(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([{"id": "abc", "clientId": "sunshine"}])])
|
||||
monkeypatch.setattr("ariadne.services.keycloak_admin.httpx.Client", lambda *args, **kwargs: dummy)
|
||||
|
||||
assert client.find_client("sunshine") == {"id": "abc", "clientId": "sunshine"}
|
||||
|
||||
|
||||
def test_find_client_missing(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)
|
||||
|
||||
assert client.find_client("sunshine") is None
|
||||
|
||||
|
||||
def test_client_crud_helpers(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({}),
|
||||
DummyResponse({}),
|
||||
DummyResponse({"value": "secret"}),
|
||||
DummyResponse([{"name": "groups", "id": "scope-id"}]),
|
||||
DummyResponse({}),
|
||||
]
|
||||
)
|
||||
monkeypatch.setattr("ariadne.services.keycloak_admin.httpx.Client", lambda *args, **kwargs: dummy)
|
||||
|
||||
client.create_client({"clientId": "sunshine"})
|
||||
client.update_client("uuid", {"clientId": "sunshine"})
|
||||
assert client.get_client_secret("uuid") == "secret"
|
||||
assert client.find_client_scope_id("groups") == "scope-id"
|
||||
client.attach_optional_client_scope("uuid", "scope-id")
|
||||
|
||||
|
||||
def test_get_client_secret_missing(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)
|
||||
|
||||
with pytest.raises(RuntimeError):
|
||||
client.get_client_secret("uuid")
|
||||
|
||||
|
||||
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",
|
||||
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)
|
||||
assert client.find_user("alice") 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("id")
|
||||
|
||||
|
||||
def test_get_user_success(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({"id": "1"})])
|
||||
monkeypatch.setattr("ariadne.services.keycloak_admin.httpx.Client", lambda *args, **kwargs: dummy)
|
||||
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",
|
||||
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_safe_coerces_bad_attrs(monkeypatch) -> None:
|
||||
client = KeycloakAdminClient()
|
||||
monkeypatch.setattr(client, "get_user", lambda *_args, **_kwargs: {"id": "user-1"})
|
||||
monkeypatch.setattr(client, "_safe_update_payload", lambda *_args, **_kwargs: {"attributes": "bad"})
|
||||
monkeypatch.setattr(client, "update_user", lambda *_args, **_kwargs: 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"})
|
||||
monkeypatch.setattr(client, "get_user", lambda *_args, **_kwargs: {"id": "user-1"})
|
||||
monkeypatch.setattr(client, "_safe_update_payload", lambda *_args, **_kwargs: {"attributes": "bad"})
|
||||
monkeypatch.setattr(client, "update_user", lambda *_args, **_kwargs: 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",
|
||||
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(123)])
|
||||
monkeypatch.setattr("ariadne.services.keycloak_admin.httpx.Client", lambda *args, **kwargs: dummy)
|
||||
with pytest.raises(RuntimeError) as exc:
|
||||
client.get_user("user-1")
|
||||
assert "unexpected user payload" in str(exc.value)
|
||||
@ -10,7 +10,6 @@ def test_oauth_client_payload() -> None:
|
||||
payload = _oauth_client_payload("wolf", "https://wolf.bstein.dev")
|
||||
assert payload["clientId"] == "wolf"
|
||||
assert payload["redirectUris"] == ["https://wolf.bstein.dev/oauth2/callback"]
|
||||
assert payload["webOrigins"] == ["https://wolf.bstein.dev"]
|
||||
|
||||
|
||||
def test_valid_cookie_secret() -> None:
|
||||
@ -23,11 +22,7 @@ def test_ensure_wolf_creates_client_and_writes_vault(monkeypatch) -> None:
|
||||
monkeypatch.setattr(
|
||||
oauth_module,
|
||||
"settings",
|
||||
SimpleNamespace(
|
||||
wolf_oidc_client_id="wolf",
|
||||
wolf_oidc_base_url="https://wolf.bstein.dev",
|
||||
wolf_oidc_vault_path="game-stream/wolf-oidc",
|
||||
),
|
||||
SimpleNamespace(wolf_oidc_client_id="wolf", wolf_oidc_base_url="https://wolf.bstein.dev", wolf_oidc_vault_path="game-stream/wolf-oidc"),
|
||||
)
|
||||
calls: list[str] = []
|
||||
written = {}
|
||||
@ -40,9 +35,7 @@ def test_ensure_wolf_creates_client_and_writes_vault(monkeypatch) -> None:
|
||||
return True
|
||||
|
||||
def find_client(self, client_id):
|
||||
if not self.created:
|
||||
return None
|
||||
return {"id": "client-uuid", "clientId": client_id}
|
||||
return {"id": "client-uuid", "clientId": client_id} if self.created else None
|
||||
|
||||
def create_client(self, _payload):
|
||||
self.created = True
|
||||
@ -51,8 +44,7 @@ def test_ensure_wolf_creates_client_and_writes_vault(monkeypatch) -> None:
|
||||
def update_client(self, _client_uuid, _payload):
|
||||
calls.append("update")
|
||||
|
||||
def find_client_scope_id(self, name):
|
||||
assert name == "groups"
|
||||
def find_client_scope_id(self, _name):
|
||||
return "scope-uuid"
|
||||
|
||||
def attach_optional_client_scope(self, _client_uuid, _scope_id):
|
||||
@ -62,8 +54,7 @@ def test_ensure_wolf_creates_client_and_writes_vault(monkeypatch) -> None:
|
||||
return "client-secret"
|
||||
|
||||
class DummyVault:
|
||||
def read_kv_secret(self, path):
|
||||
assert path == "game-stream/wolf-oidc"
|
||||
def read_kv_secret(self, _path):
|
||||
return {"cookie_secret": "a" * 32}
|
||||
|
||||
def write_kv_secret(self, path, data):
|
||||
@ -73,8 +64,7 @@ def test_ensure_wolf_creates_client_and_writes_vault(monkeypatch) -> None:
|
||||
monkeypatch.setattr(oauth_module, "keycloak_admin", DummyKeycloak())
|
||||
monkeypatch.setattr(oauth_module, "vault", DummyVault())
|
||||
|
||||
result = OAuth2ProxyService().ensure_wolf()
|
||||
assert result["status"] == "ok"
|
||||
assert OAuth2ProxyService().ensure_wolf()["status"] == "ok"
|
||||
assert calls == ["create", "update", "scope"]
|
||||
assert written["data"]["client_secret"] == "client-secret"
|
||||
assert written["data"]["cookie_secret"] == "a" * 32
|
||||
@ -84,12 +74,47 @@ def test_ensure_wolf_reports_missing_keycloak(monkeypatch) -> None:
|
||||
monkeypatch.setattr(
|
||||
oauth_module,
|
||||
"settings",
|
||||
SimpleNamespace(
|
||||
wolf_oidc_client_id="wolf",
|
||||
wolf_oidc_base_url="https://wolf.bstein.dev",
|
||||
wolf_oidc_vault_path="game-stream/wolf-oidc",
|
||||
),
|
||||
SimpleNamespace(wolf_oidc_client_id="wolf", wolf_oidc_base_url="https://wolf.bstein.dev", wolf_oidc_vault_path="game-stream/wolf-oidc"),
|
||||
)
|
||||
monkeypatch.setattr(oauth_module, "keycloak_admin", SimpleNamespace(ready=lambda: False))
|
||||
|
||||
assert OAuth2ProxyService().ensure_wolf()["status"] == "error"
|
||||
|
||||
|
||||
def test_ensure_wolf_rejects_missing_client_after_create(monkeypatch) -> None:
|
||||
monkeypatch.setattr(
|
||||
oauth_module,
|
||||
"settings",
|
||||
SimpleNamespace(wolf_oidc_client_id="wolf", wolf_oidc_base_url="https://wolf.bstein.dev", wolf_oidc_vault_path="game-stream/wolf-oidc"),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
oauth_module,
|
||||
"keycloak_admin",
|
||||
SimpleNamespace(ready=lambda: True, find_client=lambda _client_id: None, create_client=lambda _payload: None),
|
||||
)
|
||||
|
||||
try:
|
||||
OAuth2ProxyService().ensure_wolf()
|
||||
except RuntimeError as exc:
|
||||
assert "not found" in str(exc)
|
||||
else:
|
||||
raise AssertionError("missing client should fail")
|
||||
|
||||
|
||||
def test_ensure_wolf_rejects_missing_client_uuid(monkeypatch) -> None:
|
||||
monkeypatch.setattr(
|
||||
oauth_module,
|
||||
"settings",
|
||||
SimpleNamespace(wolf_oidc_client_id="wolf", wolf_oidc_base_url="https://wolf.bstein.dev", wolf_oidc_vault_path="game-stream/wolf-oidc"),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
oauth_module,
|
||||
"keycloak_admin",
|
||||
SimpleNamespace(ready=lambda: True, find_client=lambda _client_id: {"id": ""}, create_client=lambda _payload: None),
|
||||
)
|
||||
|
||||
try:
|
||||
OAuth2ProxyService().ensure_sunshine()
|
||||
except RuntimeError as exc:
|
||||
assert "id missing" in str(exc)
|
||||
else:
|
||||
raise AssertionError("missing client id should fail")
|
||||
|
||||
@ -48,3 +48,26 @@ def test_from_env_includes_jenkins_weather_settings(monkeypatch) -> None:
|
||||
assert cfg.testing_triage_model_url == "http://openclaw-ollama:11434"
|
||||
assert cfg.testing_triage_model == "local-model"
|
||||
assert cfg.testing_triage_model_timeout_sec == 33.5
|
||||
|
||||
|
||||
def test_from_env_includes_game_stream_settings(monkeypatch) -> None:
|
||||
monkeypatch.setenv("GAME_MODE_NODE_NAME", "titan-24")
|
||||
monkeypatch.setenv(
|
||||
"GAME_MODE_DISPLACE_WORKLOADS",
|
||||
'[{"kind":"StatefulSet","namespace":"hermes","name":"hermes-llm","restoreReplicas":2}]',
|
||||
)
|
||||
monkeypatch.setenv("GAME_MODE_HOOK_TOKEN", "hook")
|
||||
monkeypatch.setenv("WOLF_OIDC_CLIENT_ID", "wolf")
|
||||
monkeypatch.setenv("WOLF_OIDC_BASE_URL", "https://wolf.bstein.dev/")
|
||||
monkeypatch.setenv("WOLF_OIDC_VAULT_PATH", "game-stream/wolf-oidc")
|
||||
monkeypatch.setenv("ARIADNE_SCHEDULE_WOLF_OIDC", "*/13 * * * *")
|
||||
|
||||
cfg = Settings.from_env()
|
||||
|
||||
assert cfg.game_mode_node_name == "titan-24"
|
||||
assert cfg.game_mode_displace_workloads[0]["namespace"] == "hermes"
|
||||
assert cfg.game_mode_hook_token == "hook"
|
||||
assert cfg.wolf_oidc_client_id == "wolf"
|
||||
assert cfg.wolf_oidc_base_url == "https://wolf.bstein.dev"
|
||||
assert cfg.wolf_oidc_vault_path == "game-stream/wolf-oidc"
|
||||
assert cfg.wolf_oidc_cron == "*/13 * * * *"
|
||||
|
||||
@ -154,8 +154,38 @@ def test_diagnose_testing_triage_handles_disabled_and_bad_json(monkeypatch) -> N
|
||||
assert "model_json_parse_failed" in diagnosis["unknowns"][0]
|
||||
|
||||
|
||||
def test_diagnose_testing_triage_handles_empty_model_response(monkeypatch) -> None:
|
||||
class EmptyResponse:
|
||||
def raise_for_status(self) -> None:
|
||||
return None
|
||||
|
||||
def json(self): # type: ignore[no-untyped-def]
|
||||
return {"response": ""}
|
||||
|
||||
class EmptyClient:
|
||||
def __init__(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def]
|
||||
return None
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *args) -> None: # type: ignore[no-untyped-def]
|
||||
return None
|
||||
|
||||
def post(self, url, json=None): # type: ignore[no-untyped-def]
|
||||
return EmptyResponse()
|
||||
|
||||
monkeypatch.setattr(testing_triage_diagnosis, "settings", SettingsStub(testing_triage_model_url="http://ollama"))
|
||||
monkeypatch.setattr(testing_triage_diagnosis.httpx, "Client", EmptyClient)
|
||||
diagnosis = testing_triage_diagnosis.diagnose_testing_triage({"summary": {"status": "ok", "problem_count": 0}})
|
||||
|
||||
assert diagnosis["status"] == "ok"
|
||||
assert "empty_model_response" in diagnosis["unknowns"]
|
||||
|
||||
|
||||
def test_latest_testing_triage_diagnosis_decodes_stored_json() -> None:
|
||||
storage = DummyStorage()
|
||||
assert testing_triage_diagnosis.latest_testing_triage_diagnosis(storage) is None # type: ignore[arg-type]
|
||||
payload = {"kind": "testing_triage_diagnosis", "status": "ok"}
|
||||
storage.events.append((testing_triage_diagnosis.TRIAGE_DIAGNOSIS_EVENT_TYPE, json.dumps(payload)))
|
||||
|
||||
@ -167,6 +197,9 @@ def test_latest_testing_triage_diagnosis_decodes_stored_json() -> None:
|
||||
storage.events.append((testing_triage_diagnosis.TRIAGE_DIAGNOSIS_EVENT_TYPE, json.dumps(["not", "a", "dict"])))
|
||||
assert testing_triage_diagnosis.latest_testing_triage_diagnosis(storage) is None # type: ignore[arg-type]
|
||||
|
||||
storage.events.append((testing_triage_diagnosis.TRIAGE_DIAGNOSIS_EVENT_TYPE, 7))
|
||||
assert testing_triage_diagnosis.latest_testing_triage_diagnosis(storage) is None # type: ignore[arg-type]
|
||||
|
||||
|
||||
def test_model_configuration_helpers_normalize_settings(monkeypatch) -> None:
|
||||
monkeypatch.setattr(
|
||||
@ -303,6 +336,33 @@ def test_diagnosis_from_model_rejects_non_english_and_out_of_scope_jobs(monkeypa
|
||||
assert "model_evidence_refs_out_of_scope" in diagnosis["unknowns"]
|
||||
|
||||
|
||||
def test_diagnosis_helpers_cover_boolean_and_defaults() -> None:
|
||||
assert testing_triage_diagnosis._bool_value("yes", False) is True # noqa: SLF001
|
||||
assert testing_triage_diagnosis._bool_value("no", True) is False # noqa: SLF001
|
||||
assert testing_triage_diagnosis._bool_value("maybe", True) is True # noqa: SLF001
|
||||
assert testing_triage_diagnosis._allowed_suite_jobs({"failed_suites": ["", "bstein_home", "data_prepper"]}) == { # noqa: SLF001
|
||||
"bstein_home",
|
||||
"bstein-home",
|
||||
"bstein-dev-home",
|
||||
"data_prepper",
|
||||
"data-prepper",
|
||||
}
|
||||
assert testing_triage_diagnosis._default_next_actions({"problem_count": 0}) == [ # noqa: SLF001
|
||||
"No action required unless a fresh bundle changes the status."
|
||||
]
|
||||
assert testing_triage_diagnosis._default_next_actions({"problem_count": 1}) == [ # noqa: SLF001
|
||||
"Review the evidence bundle sections with non-empty problem lists.",
|
||||
"Check the named Jenkins build logs and Flux Kustomizations before changing manifests.",
|
||||
]
|
||||
|
||||
|
||||
def test_safe_evidence_refs_rejects_non_ascii_refs() -> None:
|
||||
unknowns = []
|
||||
refs = testing_triage_diagnosis._safe_evidence_refs(["référence"], {}, unknowns) # noqa: SLF001
|
||||
assert refs == []
|
||||
assert unknowns == ["model_evidence_refs_non_english"]
|
||||
|
||||
|
||||
def test_default_evidence_refs_include_failed_suites() -> None:
|
||||
refs = testing_triage_diagnosis._default_evidence_refs( # noqa: SLF001
|
||||
{"status": "needs_attention", "problem_count": 3, "failed_suites": ["a", "b", "c", "d", "e", "f", "g"]}
|
||||
|
||||
@ -188,7 +188,7 @@ def test_vault_ensure_token_login(monkeypatch) -> None:
|
||||
assert svc._ensure_token() == "tok"
|
||||
|
||||
|
||||
def test_vault_read_write_kv(monkeypatch) -> None:
|
||||
def test_vault_read_write_kv_secret(monkeypatch) -> None:
|
||||
dummy_settings = types.SimpleNamespace(
|
||||
vault_addr="http://vault",
|
||||
vault_token="token",
|
||||
@ -197,41 +197,18 @@ def test_vault_read_write_kv(monkeypatch) -> None:
|
||||
vault_k8s_token_reviewer_jwt_file="",
|
||||
k8s_api_timeout_sec=5.0,
|
||||
)
|
||||
calls = []
|
||||
monkeypatch.setattr(vault_module, "settings", dummy_settings)
|
||||
calls: list[tuple[str, str, dict | None]] = []
|
||||
|
||||
def fake_request(self, method: str, path: str, json=None):
|
||||
calls.append((method, path, json))
|
||||
if method == "GET":
|
||||
return DummyResponse({"data": {"data": {"client_id": "sunshine"}}})
|
||||
return DummyResponse({"data": {"data": {"client_id": "wolf"}}})
|
||||
return DummyResponse({})
|
||||
|
||||
monkeypatch.setattr(vault_module.VaultClient, "request", fake_request)
|
||||
|
||||
svc = VaultService()
|
||||
assert svc.read_kv_secret("game-stream/sunshine-oidc") == {"client_id": "sunshine"}
|
||||
svc.write_kv_secret("game-stream/sunshine-oidc", {"client_id": "sunshine"})
|
||||
assert calls[-1] == (
|
||||
"POST",
|
||||
"/v1/kv/data/atlas/game-stream/sunshine-oidc",
|
||||
{"data": {"client_id": "sunshine"}},
|
||||
)
|
||||
|
||||
|
||||
def test_vault_read_kv_missing(monkeypatch) -> None:
|
||||
dummy_settings = types.SimpleNamespace(
|
||||
vault_addr="http://vault",
|
||||
vault_token="token",
|
||||
vault_k8s_role="vault",
|
||||
vault_k8s_token_reviewer_jwt="jwt",
|
||||
vault_k8s_token_reviewer_jwt_file="",
|
||||
k8s_api_timeout_sec=5.0,
|
||||
)
|
||||
monkeypatch.setattr(vault_module, "settings", dummy_settings)
|
||||
monkeypatch.setattr(
|
||||
vault_module.VaultClient,
|
||||
"request",
|
||||
lambda *args, **kwargs: DummyResponse({}, status_code=404),
|
||||
)
|
||||
|
||||
assert VaultService().read_kv_secret("missing") is None
|
||||
assert svc.read_kv_secret("game-stream/wolf-oidc") == {"client_id": "wolf"}
|
||||
svc.write_kv_secret("game-stream/wolf-oidc", {"client_id": "wolf"})
|
||||
assert calls[-1] == ("POST", "/v1/kv/data/atlas/game-stream/wolf-oidc", {"data": {"client_id": "wolf"}})
|
||||
|
||||
121
tests/unit/app/test_app_game_routes.py
Normal file
121
tests/unit/app/test_app_game_routes.py
Normal file
@ -0,0 +1,121 @@
|
||||
from tests.unit.app.app_route_helpers import *
|
||||
|
||||
|
||||
def test_game_stream_profile_me(monkeypatch) -> None:
|
||||
ctx = AuthContext(username="brad", email="", groups=["game-stream-users"], claims={})
|
||||
client = _client(monkeypatch, ctx)
|
||||
|
||||
resp = client.get("/api/game-stream/me", headers={"Authorization": "Bearer token"})
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["allowed"] is True
|
||||
|
||||
|
||||
def test_game_mode_admin_start_and_stop(monkeypatch) -> None:
|
||||
ctx = AuthContext(username="bstein", email="", groups=["admin"], claims={})
|
||||
client = _client(monkeypatch, ctx)
|
||||
calls = []
|
||||
|
||||
monkeypatch.setattr(app_module.game_mode, "start", lambda game, note=None: calls.append(("start", game, note)) or {"status": "active"})
|
||||
monkeypatch.setattr(app_module.game_mode, "stop", lambda game, note=None: calls.append(("stop", game, note)) or {"status": "idle"})
|
||||
monkeypatch.setattr(app_module.storage, "record_task_run", lambda *args, **kwargs: None)
|
||||
monkeypatch.setattr(app_module, "_record_event", lambda *args, **kwargs: None)
|
||||
|
||||
start = client.post("/api/admin/game-mode/start", headers={"Authorization": "Bearer token"}, json={"game": "arc", "note": "now"})
|
||||
stop = client.post("/api/admin/game-mode/stop", headers={"Authorization": "Bearer token"}, json={"game": "arc"})
|
||||
assert start.status_code == 200
|
||||
assert stop.status_code == 200
|
||||
assert calls[0] == ("start", "arc", "now")
|
||||
|
||||
|
||||
def test_game_mode_hook_requires_token(monkeypatch) -> None:
|
||||
ctx = AuthContext(username="", email="", groups=[], claims={})
|
||||
client = _client(monkeypatch, ctx)
|
||||
monkeypatch.setattr(app_module, "settings", dataclasses.replace(app_module.settings, game_mode_hook_token="secret"))
|
||||
|
||||
resp = client.post("/api/game-mode/start", json={"game": "arc"})
|
||||
assert resp.status_code == 401
|
||||
|
||||
|
||||
def test_game_mode_hook_requires_configured_token(monkeypatch) -> None:
|
||||
ctx = AuthContext(username="", email="", groups=[], claims={})
|
||||
client = _client(monkeypatch, ctx)
|
||||
monkeypatch.setattr(app_module, "settings", dataclasses.replace(app_module.settings, game_mode_hook_token=""))
|
||||
|
||||
resp = client.post("/api/game-mode/start", headers={"Authorization": "Bearer secret"}, json={"game": "arc"})
|
||||
assert resp.status_code == 503
|
||||
|
||||
|
||||
def test_game_mode_hook_start_and_stop(monkeypatch) -> None:
|
||||
ctx = AuthContext(username="", email="", groups=[], claims={})
|
||||
client = _client(monkeypatch, ctx)
|
||||
monkeypatch.setattr(app_module, "settings", dataclasses.replace(app_module.settings, game_mode_hook_token="secret"))
|
||||
monkeypatch.setattr(app_module.game_mode, "start", lambda game, note=None: {"status": "active", "game": game})
|
||||
monkeypatch.setattr(app_module.game_mode, "stop", lambda game, note=None: {"status": "idle", "game": game})
|
||||
monkeypatch.setattr(app_module.storage, "record_task_run", lambda *args, **kwargs: None)
|
||||
monkeypatch.setattr(app_module, "_record_event", lambda *args, **kwargs: None)
|
||||
|
||||
start = client.post("/api/game-mode/start", headers={"Authorization": "Bearer secret"}, json={"game": "arc"})
|
||||
stop = client.post("/api/game-mode/stop", headers={"x-ariadne-game-mode-token": "secret"}, json={"game": "arc"})
|
||||
assert start.status_code == 200
|
||||
assert stop.status_code == 200
|
||||
|
||||
|
||||
def test_game_mode_status_error(monkeypatch) -> None:
|
||||
ctx = AuthContext(username="bstein", email="", groups=["admin"], claims={})
|
||||
client = _client(monkeypatch, ctx)
|
||||
monkeypatch.setattr(app_module.game_mode, "status", lambda: (_ for _ in ()).throw(RuntimeError("boom")))
|
||||
|
||||
resp = client.get("/api/admin/game-mode/status", headers={"Authorization": "Bearer token"})
|
||||
assert resp.status_code == 502
|
||||
|
||||
|
||||
def test_game_mode_action_error_records(monkeypatch) -> None:
|
||||
ctx = AuthContext(username="bstein", email="", groups=["admin"], claims={})
|
||||
client = _client(monkeypatch, ctx)
|
||||
recorded = []
|
||||
monkeypatch.setattr(app_module.game_mode, "start", lambda *_args, **_kwargs: (_ for _ in ()).throw(RuntimeError("boom")))
|
||||
monkeypatch.setattr(app_module.storage, "record_task_run", lambda *args, **kwargs: recorded.append(args))
|
||||
monkeypatch.setattr(app_module, "_record_event", lambda *args, **kwargs: None)
|
||||
|
||||
resp = client.post("/api/admin/game-mode/start", headers={"Authorization": "Bearer token"}, json={"game": "arc"})
|
||||
assert resp.status_code == 502
|
||||
assert recorded
|
||||
|
||||
|
||||
def test_wolf_oauth2_ensure(monkeypatch) -> None:
|
||||
ctx = AuthContext(username="bstein", email="", groups=["admin"], claims={})
|
||||
client = _client(monkeypatch, ctx)
|
||||
|
||||
monkeypatch.setattr(app_module.oauth2_proxy, "ensure_wolf", lambda: {"status": "ok", "client_id": "wolf"})
|
||||
monkeypatch.setattr(app_module.storage, "record_task_run", lambda *args, **kwargs: None)
|
||||
monkeypatch.setattr(app_module, "_record_event", lambda *args, **kwargs: None)
|
||||
|
||||
resp = client.post("/api/admin/game-stream/wolf/oauth2/ensure", headers={"Authorization": "Bearer token"})
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["client_id"] == "wolf"
|
||||
|
||||
|
||||
def test_wolf_oauth2_ensure_error_paths(monkeypatch) -> None:
|
||||
ctx = AuthContext(username="bstein", email="", groups=["admin"], claims={})
|
||||
client = _client(monkeypatch, ctx)
|
||||
monkeypatch.setattr(app_module.storage, "record_task_run", lambda *args, **kwargs: None)
|
||||
monkeypatch.setattr(app_module, "_record_event", lambda *args, **kwargs: None)
|
||||
|
||||
monkeypatch.setattr(app_module.oauth2_proxy, "ensure_wolf", lambda: {"status": "error", "detail": "missing"})
|
||||
resp = client.post("/api/admin/game-stream/wolf/oauth2/ensure", headers={"Authorization": "Bearer token"})
|
||||
assert resp.status_code == 502
|
||||
|
||||
monkeypatch.setattr(app_module.oauth2_proxy, "ensure_wolf", lambda: (_ for _ in ()).throw(RuntimeError("boom")))
|
||||
alias = client.post("/api/admin/game-stream/sunshine/oauth2/ensure", headers={"Authorization": "Bearer token"})
|
||||
assert alias.status_code == 502
|
||||
|
||||
|
||||
def test_record_simple_task_swallows_storage_errors(monkeypatch) -> None:
|
||||
ctx = AuthContext(username="bstein", email="", groups=["admin"], claims={})
|
||||
client = _client(monkeypatch, ctx)
|
||||
monkeypatch.setattr(app_module.game_mode, "start", lambda game, note=None: {"status": "active", "game": game})
|
||||
monkeypatch.setattr(app_module.storage, "record_task_run", lambda *args, **kwargs: (_ for _ in ()).throw(RuntimeError("db")))
|
||||
monkeypatch.setattr(app_module, "_record_event", lambda *args, **kwargs: None)
|
||||
|
||||
resp = client.post("/api/admin/game-mode/start", headers={"Authorization": "Bearer token"}, json={"game": "arc"})
|
||||
assert resp.status_code == 200
|
||||
@ -43,6 +43,7 @@ def test_startup_registers_metis_watch(monkeypatch) -> None:
|
||||
assert any(name == "schedule.jenkins_build_weather" for name, _cron in tasks)
|
||||
assert any(name == "schedule.jenkins_workspace_cleanup" for name, _cron in tasks)
|
||||
assert any(name == "schedule.testing_triage" for name, _cron in tasks)
|
||||
assert any(name == "schedule.wolf_oidc" 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")))
|
||||
|
||||
@ -56,3 +56,57 @@ def test_reset_password_raises_on_error(monkeypatch) -> None:
|
||||
|
||||
with pytest.raises(httpx.HTTPStatusError):
|
||||
client.reset_password("user", "pw", temporary=True)
|
||||
|
||||
|
||||
def test_client_lifecycle_helpers(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([{"id": "uuid", "clientId": "wolf"}]),
|
||||
DummyResponse({}),
|
||||
DummyResponse({}),
|
||||
DummyResponse({"value": "secret"}),
|
||||
DummyResponse([{"id": "scope", "name": "groups"}]),
|
||||
DummyResponse({}),
|
||||
]
|
||||
)
|
||||
monkeypatch.setattr("ariadne.services.keycloak_admin.httpx.Client", lambda *args, **kwargs: dummy)
|
||||
|
||||
assert client.find_client("wolf")["id"] == "uuid"
|
||||
client.create_client({"clientId": "wolf"})
|
||||
client.update_client("uuid", {"clientId": "wolf"})
|
||||
assert client.get_client_secret("uuid") == "secret"
|
||||
assert client.find_client_scope_id("groups") == "scope"
|
||||
client.attach_optional_client_scope("uuid", "scope")
|
||||
assert dummy.calls[-1][0] == "put"
|
||||
|
||||
|
||||
def test_client_helpers_handle_missing_payloads(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([]), DummyResponse([]), DummyResponse({})])
|
||||
monkeypatch.setattr("ariadne.services.keycloak_admin.httpx.Client", lambda *args, **kwargs: dummy)
|
||||
|
||||
assert client.find_client("wolf") is None
|
||||
assert client.find_client_scope_id("groups") is None
|
||||
with pytest.raises(RuntimeError):
|
||||
client.get_client_secret("uuid")
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user