Onyscribe Admin: Initialer Stand
Admin-Plattform für die Tenants von Onyscribe: Editor mit Live-Vorschau (echter Core in Sandbox), Versionen, KI-Agent (Cortecs), OIDC-Abgleich über die Login-Developer-API, Deploy-Engine (Mittwald, gesperrt ohne DEPLOY_LIVE, Freigabe pro Tenant), Core-Vertrag (Schema/Manifest), Core-Releases mit Kompatibilitätsprüfung, Canary-Rollout und Rollback, Zustandsabgleich. Runbook: docs/RUNBOOK_PARALLELTEST.md Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
59
tests/fakelogin.py
Normal file
59
tests/fakelogin.py
Normal file
@@ -0,0 +1,59 @@
|
||||
"""In-Memory-Nachbau von login.onyva.dev /api/v1 (Verhalten laut LoginSeite/docs/developer-api.md) für Tests und lokale Entwicklung."""
|
||||
|
||||
|
||||
class FakeLogin:
|
||||
"""In-Memory-Nachbau von login.onyva.dev /api/v1 (Verhalten laut LoginSeite/docs/developer-api.md)."""
|
||||
|
||||
def __init__(self, users=("anna@x.de", "bob@x.de")):
|
||||
self.apps, self.members, self.users, self.calls, self.n, self.rate_limit_once = {}, {}, set(users), [], 0, False
|
||||
|
||||
def __call__(self, method, url, headers, body):
|
||||
path = url.split("/api/v1")[1]
|
||||
self.calls.append((method, path, body))
|
||||
if headers.get("Authorization") != "Bearer onyva_testkey":
|
||||
return 401, {"detail": "invalid"}, {}
|
||||
if self.rate_limit_once:
|
||||
self.rate_limit_once = False
|
||||
return 429, {"detail": "slow down"}, {"Retry-After": "2"}
|
||||
parts = path.strip("/").split("/")
|
||||
if parts == ["apps"]:
|
||||
if method == "GET":
|
||||
return 200, {"apps": list(self.apps.values())}, {}
|
||||
self.n += 1
|
||||
cid = f"app_{self.n:04d}"
|
||||
app = {"client_id": cid, "name": body["name"], "slug": body["name"].lower(), "redirect_uris": body["redirect_uris"],
|
||||
"auth_mode": body.get("auth_mode", "required"), "totp_mode": body.get("totp_mode", "disabled"),
|
||||
"allow_registration": body.get("allow_registration", False), "backchannel_logout_uri": body.get("backchannel_logout_uri")}
|
||||
self.apps[cid], self.members[cid] = app, {}
|
||||
return 201, {**app, "client_secret": f"secret-{self.n}"}, {}
|
||||
cid = parts[1]
|
||||
if cid not in self.apps:
|
||||
return 404, {"detail": "not found"}, {}
|
||||
if len(parts) == 2:
|
||||
if method == "GET":
|
||||
return 200, self.apps[cid], {}
|
||||
if method == "PATCH":
|
||||
self.apps[cid].update(body)
|
||||
return 200, self.apps[cid], {}
|
||||
if method == "DELETE":
|
||||
del self.apps[cid]
|
||||
return 204, None, {}
|
||||
if parts[2] == "rotate-secret":
|
||||
self.n += 1
|
||||
return 200, {"client_id": cid, "client_secret": f"rotated-{self.n}"}, {}
|
||||
if parts[2] == "memberships":
|
||||
if method == "GET":
|
||||
return 200, {"memberships": [{"email": e, "display_name": e, "role": r} for e, r in self.members[cid].items()]}, {}
|
||||
if method == "PUT":
|
||||
if body["user_email"] not in self.users:
|
||||
return 404, {"detail": "user"}, {}
|
||||
self.members[cid][body["user_email"]] = body.get("role", "viewer")
|
||||
return 200, body, {}
|
||||
if method == "DELETE":
|
||||
self.members[cid].pop(parts[3], None)
|
||||
return 204, None, {}
|
||||
return 404, {}, {}
|
||||
|
||||
@property
|
||||
def writes(self):
|
||||
return [c for c in self.calls if c[0] != "GET"]
|
||||
Reference in New Issue
Block a user