Automatischer Push: 6 Datei(en) geändert
- .vault-tools/plan-kopie/LIESMICH.md - .vault-tools/plan-kopie/rot_nachweis.py - .vault-tools/trading-plattform-pruefung/LIESMICH.md - .vault-tools/trading-plattform-pruefung/browser_r.py - .vault-tools/trading-plattform-pruefung/start_probe_w.py - .vault-tools/trading-plattform-pruefung/start_probe_zugang.py Erzeugt vom PostToolUse-Hook .vault-tools/auto-push/auto_push.py. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
1 parent
593acdd122
commit
d7ffc9a5b5
6 files changed
+233
-5
No files matched your search
@@ -0,0 +1,180 @@
|
||||
"""Browserlauf zu Paket R: der Red Button mit virtuellem Sicherheitsschluessel gegen eine Probe-Instanz.
|
||||
|
||||
Koordinator-Pruefcode, nicht Teil des Repos. Laeuft unter Windows mit Playwright (Chromium) und dem
|
||||
virtuellen Authenticator des DevTools-Protokolls, wie browser_w.py. Ablauf: mit Passwort und Code
|
||||
anmelden, in "Zugang" einen Schluessel registrieren, zurueck zur Uebersicht, den Red Button druecken,
|
||||
Grund eintragen, Stick (virtuell) beruehren -- danach muss der Killswitch gesperrt sein, im Register
|
||||
genau ein Ereignis "glattstellen" mit Ausloeser "pascal" stehen und der Zaehler des Authenticators
|
||||
gestiegen sein. Dazu: kein sichtbares "Glattstellen" mehr, kein Wortfeld mit Schluessel, unter
|
||||
127.0.0.1 der Satz "nur unter" ohne POST, und ein Screenshot des Dialogs.
|
||||
|
||||
Was der Lauf nicht misst: eine abgelehnte Signatur (der virtuelle Authenticator signiert immer gueltig),
|
||||
das Bestaetigungswort ohne Schluessel, die Protokollbremse. Das deckt tests/test_dashboard_red_button.py.
|
||||
|
||||
Aufruf: python -X utf8 browser_r.py <port> <ausgabeordner-der-probe> <ordner-fuer-screenshots>
|
||||
"""
|
||||
|
||||
import json
|
||||
import sqlite3
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
HIER = Path(__file__).resolve().parent
|
||||
sys.path.insert(0, str(HIER))
|
||||
import browser_w # noqa: E402 -- totp, schritt, melde_mit_code_an, ZUGANG, PORT
|
||||
|
||||
PORT = int(sys.argv[1])
|
||||
ORDNER = Path(sys.argv[2])
|
||||
BILDER = Path(sys.argv[3])
|
||||
BILDER.mkdir(parents=True, exist_ok=True)
|
||||
schritt = browser_w.schritt
|
||||
ZUGANG = browser_w.ZUGANG
|
||||
|
||||
|
||||
def ereignisse() -> list[tuple]:
|
||||
db = ORDNER / "registry.db"
|
||||
with sqlite3.connect(f"file:{db}?mode=ro", uri=True) as verbindung:
|
||||
spalten = [z[1] for z in verbindung.execute("PRAGMA table_info(killswitch_ereignisse)")]
|
||||
zeilen = verbindung.execute("SELECT * FROM killswitch_ereignisse ORDER BY rowid").fetchall()
|
||||
return [dict(zip(spalten, z)) for z in zeilen]
|
||||
|
||||
|
||||
def freigeben_falls_gesperrt(seite) -> None:
|
||||
"""Der Red Button ist nur im Zustand aktiv oder angehalten bedienbar; nach einem Notaus erst freigeben."""
|
||||
if "Killswitch gesperrt" in seite.inner_text("body"):
|
||||
seite.get_by_role("button", name="Freigeben").click()
|
||||
# Nach dem Freigeben ist der Zustand aktiv (oder angehalten) -- massgeblich ist, dass der
|
||||
# Red Button wieder bedienbar ist; auf den Text eines bestimmten Zustands zu warten, war falsch.
|
||||
knopf = seite.get_by_role("button", name="Red Button")
|
||||
for _ in range(30):
|
||||
if knopf.is_enabled():
|
||||
break
|
||||
seite.wait_for_timeout(500)
|
||||
else:
|
||||
raise AssertionError("Red Button nach dem Freigeben nicht bedienbar")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
try:
|
||||
return _lauf()
|
||||
finally:
|
||||
print("\n".join(browser_w.schritte))
|
||||
print(f"\n{len(browser_w.schritte) - len(browser_w.befunde)} von {len(browser_w.schritte)} ok; Befunde: {len(browser_w.befunde)}")
|
||||
print(f"Screenshots unter {BILDER}")
|
||||
|
||||
|
||||
def _lauf() -> int:
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch(headless=True)
|
||||
kontext = browser.new_context(viewport={"width": 1280, "height": 900})
|
||||
seite = kontext.new_page()
|
||||
konsole: list[str] = []
|
||||
api: list[str] = []
|
||||
seite.on("console", lambda m: konsole.append(f"{m.type}: {m.text}") if m.type in ("error", "warning") else None)
|
||||
seite.on("pageerror", lambda e: konsole.append(f"pageerror: {e}"))
|
||||
seite.on("response", lambda r: api.append(f"{r.status} {r.request.method} {r.url.split(str(PORT), 1)[-1]}") if "/api/" in r.url else None)
|
||||
|
||||
cdp = kontext.new_cdp_session(seite)
|
||||
cdp.send("WebAuthn.enable")
|
||||
authenticator = cdp.send("WebAuthn.addVirtualAuthenticator", {"options": {
|
||||
"protocol": "ctap2", "transport": "usb", "hasResidentKey": False,
|
||||
"hasUserVerification": True, "isUserVerified": True, "automaticPresenceSimulation": True,
|
||||
}})["authenticatorId"]
|
||||
|
||||
def zugaenge() -> list[dict]:
|
||||
return cdp.send("WebAuthn.getCredentials", {"authenticatorId": authenticator})["credentials"]
|
||||
|
||||
seite.goto(f"http://localhost:{PORT}/")
|
||||
seite.locator("#anmeldung-benutzer").wait_for(timeout=15000)
|
||||
zaehler = browser_w.melde_mit_code_an(seite, -1)
|
||||
schritt("Anmeldung mit Passwort und Code", True)
|
||||
|
||||
# Vor der Registrierung: der Knopf heisst Red Button, "Glattstellen" steht nirgends sichtbar.
|
||||
knopf = seite.get_by_role("button", name="Red Button")
|
||||
schritt("Der Knopf in der Kopfzeile heisst Red Button", knopf.count() == 1)
|
||||
schritt("Kein sichtbares 'Glattstellen' auf der Seite", "Glattstellen" not in seite.inner_text("body"))
|
||||
|
||||
# Schluessel registrieren (wie in browser_w.py).
|
||||
seite.get_by_text("Zugang", exact=True).first.click()
|
||||
seite.locator("#zugang-bezeichnung").wait_for(timeout=10000)
|
||||
seite.fill("#zugang-bezeichnung", "Virtueller Schluessel")
|
||||
seite.fill("#zugang-passwort", ZUGANG["passwort"])
|
||||
seite.get_by_role("button", name="Schlüssel hinzufügen").click()
|
||||
liste = seite.get_by_role("list", name="Sicherheitsschlüssel")
|
||||
liste.wait_for(timeout=15000)
|
||||
schritt("Schluessel registriert", "Virtueller Schluessel" in liste.inner_text())
|
||||
vorher = zugaenge()
|
||||
zaehler_vorher = vorher[0]["signCount"] if vorher else -1
|
||||
|
||||
# Erst die Gegenprobe unter 127.0.0.1, solange der Killswitch noch angehalten (bedienbar) ist:
|
||||
# der Satz "nur unter", kein Wortfeld, kein POST an den Red Button.
|
||||
zweite = kontext.new_page()
|
||||
zwei_api: list[str] = []
|
||||
zweite.on("response", lambda r: zwei_api.append(f"{r.status} {r.request.method} {r.url.split(str(PORT), 1)[-1]}") if "/api/killswitch" in r.url else None)
|
||||
zweite.goto(f"http://127.0.0.1:{PORT}/")
|
||||
zweite.locator("#anmeldung-benutzer").wait_for(timeout=15000)
|
||||
zaehler = browser_w.melde_mit_code_an(zweite, zaehler)
|
||||
zweite.get_by_role("button", name="Red Button").click()
|
||||
zweite.get_by_label("Grund").wait_for(timeout=10000)
|
||||
zweite.wait_for_timeout(1000)
|
||||
text2 = zweite.inner_text("body")
|
||||
schritt("Unter 127.0.0.1: Hinweis 'nur unter' mit der richtigen Adresse", "nur unter" in text2 and f"localhost:{PORT}" in text2)
|
||||
# Der Knopf "Bestaetigen" (aria-label Ausfuehren) bleibt im Dialog stehen, ist aber gesperrt
|
||||
# (bereit ist ohne Weg falsch) -- gemessen am 2026-09-22; ein fehlender Knopf war zu streng verlangt.
|
||||
ausfuehren = zweite.get_by_label("Ausfuehren")
|
||||
schritt("Unter 127.0.0.1: kein Wortfeld, und der Knopf ist gesperrt",
|
||||
zweite.get_by_label("Bestaetigungswort").count() == 0
|
||||
and (ausfuehren.count() == 0 or not ausfuehren.is_enabled()))
|
||||
schritt(f"Unter 127.0.0.1: kein POST an den Red Button ({zwei_api})", not any("POST" in a and "glattstellen" in a for a in zwei_api))
|
||||
zweite.screenshot(path=str(BILDER / "red-button-127.png"))
|
||||
zweite.close()
|
||||
|
||||
seite.get_by_text("Übersicht", exact=True).first.click()
|
||||
seite.get_by_role("button", name="Red Button").wait_for(timeout=10000)
|
||||
vorher_ereignisse = [e for e in ereignisse() if e.get("stufe") == "glattstellen"]
|
||||
api.clear()
|
||||
|
||||
seite.get_by_role("button", name="Red Button").click()
|
||||
seite.get_by_label("Grund").wait_for(timeout=10000)
|
||||
text = seite.inner_text("body")
|
||||
schritt("Der Dialog nennt den Stick und kein Wortfeld", "Stick" in text and seite.get_by_label("Bestaetigungswort").count() == 0)
|
||||
schritt("Der Dialog sagt, was passiert (Paper-Konto)", "Paper-Konto" in text)
|
||||
seite.fill("input[aria-label='Grund']", "Notaus im Browserlauf")
|
||||
seite.screenshot(path=str(BILDER / "red-button-dialog.png"))
|
||||
seite.get_by_label("Ausfuehren").click()
|
||||
|
||||
# Der Dialog schliesst sich nach der Freigabe; der Zustand wird gesperrt.
|
||||
seite.get_by_label("Grund").wait_for(state="hidden", timeout=20000)
|
||||
seite.wait_for_timeout(1500)
|
||||
aufrufe = [a for a in api if "/api/killswitch/glattstellen" in a]
|
||||
schritt(f"Zwei Aufrufe: beginn und Abschluss mit 200 ({aufrufe})",
|
||||
any(a.startswith("200 POST") and a.endswith("/api/killswitch/glattstellen/beginn") for a in aufrufe)
|
||||
and any(a.startswith("200 POST") and a.endswith("/api/killswitch/glattstellen") for a in aufrufe))
|
||||
nachher = zugaenge()
|
||||
zaehler_nachher = nachher[0]["signCount"] if nachher else -1
|
||||
schritt(f"Der Authenticator wurde gefragt (Zaehler {zaehler_vorher} -> {zaehler_nachher})", zaehler_nachher > zaehler_vorher)
|
||||
neu = [e for e in ereignisse() if e.get("stufe") == "glattstellen"]
|
||||
schritt(f"Genau ein neues Registerereignis glattstellen ({len(neu) - len(vorher_ereignisse)})", len(neu) - len(vorher_ereignisse) == 1)
|
||||
if neu:
|
||||
letztes = neu[-1]
|
||||
schritt(f"Ausloeser pascal und der Grund im Register ({letztes.get('ausloeser')!r})",
|
||||
letztes.get("ausloeser") == "pascal" and "Browserlauf" in str(letztes.get("grund")))
|
||||
schritt("Kopfzeile zeigt gesperrt", "gesperrt" in seite.inner_text("body"))
|
||||
seite.screenshot(path=str(BILDER / "red-button-danach.png"))
|
||||
|
||||
# Das Anmeldeprotokoll: keine Zeile mit weg red-button (nichts wurde abgelehnt), das Wort nirgends.
|
||||
protokoll = ORDNER / "anmeldungen.jsonl"
|
||||
if protokoll.exists():
|
||||
zeilen = protokoll.read_text(encoding="utf-8").splitlines()
|
||||
schritt("Keine abgelehnte Freigabe im Anmeldeprotokoll", not any('"red-button"' in z for z in zeilen))
|
||||
|
||||
schritt(f"Keine Konsolenfehler ({konsole[:3]})", not konsole)
|
||||
browser.close()
|
||||
|
||||
return 2 if browser_w.befunde else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in new issue
Block a user