/* Rauchtest mit echtem Zugangstoken gegen die laufende HA-Testinstanz. Ergänzt scripts/smoke.ts um alles, was ohne Anmeldung nicht prüfbar ist: echte Lesevorgänge, ein echter Dienstaufruf und ein vollständiger Umlauf durch die Warteschlange. Zugangsdaten kommen aus companion-app/.env.local (schreibt testumgebung/aufsetzen.sh). Ohne sie bricht der Test mit einem Hinweis ab, ohne zu scheitern - damit er in einer Umgebung ohne Testinstanz nicht fälschlich rot wird. Aufruf: npm run smoke:auth */ import { readFileSync } from "node:fs" import { fileURLToPath } from "node:url" import { dirname, resolve } from "node:path" import { DIENST_DOMAIN, ENTITAETEN } from "../src/api/types.ts" import { HassRest } from "../src/api/rest.ts" import { Warteschlange } from "../src/api/warteschlange.ts" import { ablageSetzen, websocketUrl, type Ablage } from "../src/api/umgebung.ts" /* ------------------------------------------------------ Zugangsdaten */ const hier = dirname(fileURLToPath(import.meta.url)) const umgebungsdatei = resolve(hier, "../.env.local") function umgebungLesen(): Record { try { const inhalt = readFileSync(umgebungsdatei, "utf8") const werte: Record = {} for (const zeile of inhalt.split("\n")) { const treffer = /^([A-Z_]+)=(.*)$/.exec(zeile.trim()) if (treffer?.[1] && treffer[2] !== undefined) werte[treffer[1]] = treffer[2] } return werte } catch { return {} } } const umgebung = umgebungLesen() const BASIS = umgebung["VITE_TEST_URL"] ?? "http://localhost:18123" const TOKEN = umgebung["VITE_TEST_TOKEN"] ?? process.env["VITE_TEST_TOKEN"] ?? "" if (!TOKEN) { console.log("übersprungen: kein Token in companion-app/.env.local") console.log(" testumgebung/aufsetzen.sh ausführen, dann erneut versuchen.") process.exit(0) } /* ------------------------------------------------------------ Rahmen */ let fehler = 0 function pruefe(bezeichnung: string, bedingung: boolean, zusatz = ""): void { console.log(`${bedingung ? "PASS" : "FAIL"} ${bezeichnung}${zusatz ? ` (${zusatz})` : ""}`) if (!bedingung) fehler += 1 } /** Ablage im Arbeitsspeicher — Node hat kein localStorage. */ class SpeicherAblage implements Ablage { #werte = new Map() async lesen(schluessel: string) { return this.#werte.get(schluessel) ?? null } async schreiben(schluessel: string, wert: string) { this.#werte.set(schluessel, wert) } async loeschen(schluessel: string) { this.#werte.delete(schluessel) } } ablageSetzen(new SpeicherAblage()) const zugang = { basisUrl: BASIS, token: TOKEN } const rest = new HassRest(zugang) /* ------------------------------------------------------------- Lesen */ try { const antwort = await rest.verbindungPruefen() pruefe("verbindungPruefen mit Token", typeof antwort.message === "string", antwort.message) } catch (e) { pruefe("verbindungPruefen mit Token", false, e instanceof Error ? e.message : String(e)) } try { const profil = await rest.datenLesen>(ENTITAETEN.profil) pruefe( "Profil liefert die erwarteten Abschnitte", Boolean(profil["fahrzeug"] && profil["einstellungen"] && profil["reifen"]), Object.keys(profil).join(", "), ) } catch (e) { pruefe("Profil lesen", false, e instanceof Error ? e.message : String(e)) } try { const status = await rest.datenLesen>(ENTITAETEN.fahrzeugstatus) /* Die Feldnamen sind der Grund für diesen Test: ein früherer Entwurf der Typen hatte tank_prozent/sicher_abgestellt/sicherheit angenommen, das Backend schreibt aber tankprozent/gesichert/sicherheitscheck. */ pruefe( "Fahrzeugstatus benutzt die erwarteten Feldnamen", "tankprozent" in status && "gesichert" in status && "sicherheitscheck" in status, Object.keys(status).slice(0, 6).join(", "), ) /* Wie viele Punkte es sind, hängt an der Sensor-Zuordnung der Instanz: vier Türen, vier Fenster, Heckklappe, Motorhaube — unbelegte Rollen liefern gar keinen Eintrag. Eine feste Zahl zu erwarten hieße, die Zuordnung der Testinstanz zu prüfen statt das Backend; genau daran scheiterte diese Prüfung seit dem Entfernen der Türschloss-Erkennung (16 → 10), ohne dass irgendetwas kaputt gewesen wäre. Geprüft wird deshalb die vereinbarte Form: label ist Text, ok ist true/false/null (null = Sensor unbekannt). */ const punkte = (status["sicherheitscheck"] ?? []) as { label?: unknown; ok?: unknown }[] pruefe( "Sicherheitscheck liefert Punkte in der vereinbarten Form", Array.isArray(punkte) && punkte.every((p) => typeof p.label === "string" && (p.ok === null || typeof p.ok === "boolean")), `${punkte.length} Punkte`, ) } catch (e) { pruefe("Fahrzeugstatus lesen", false, e instanceof Error ? e.message : String(e)) } for (const [name, id] of [ ["Fahrten", ENTITAETEN.fahrten], ["Tankvorgänge", ENTITAETEN.tankvorgaenge], ] as const) { try { const liste = await rest.datenLesen(id) pruefe(`${name} sind eine Liste`, Array.isArray(liste), `${liste.length} Einträge`) } catch (e) { pruefe(`${name} lesen`, false, e instanceof Error ? e.message : String(e)) } } /* -------------------------------------------------------- Dienstaufruf */ try { await rest.dienstAufrufen(DIENST_DOMAIN, "jetzt_aktualisieren") pruefe("Dienstaufruf audi_dashboard.jetzt_aktualisieren", true) } catch (e) { pruefe("Dienstaufruf audi_dashboard.jetzt_aktualisieren", false, e instanceof Error ? e.message : String(e)) } /* ------------------------------------------------------ Warteschlange */ try { const warteschlange = new Warteschlange(rest) await warteschlange.einreihen(DIENST_DOMAIN, "jetzt_aktualisieren", {}, "Rauchtest") /* einreihen() stößt die Abarbeitung selbst an, ohne sie abzuwarten - ein sofortiger eigener abarbeiten()-Aufruf liefe deshalb ins Leere (der Lauf ist schon aktiv). Also warten, bis die Schlange tatsächlich leer ist. */ const frist = Date.now() + 10_000 while (warteschlange.anzahl > 0 && Date.now() < frist) { await new Promise((weiter) => setTimeout(weiter, 100)) } pruefe( "Warteschlange arbeitet einen Auftrag vollständig ab", warteschlange.anzahl === 0, `${warteschlange.anzahl} verblieben`, ) } catch (e) { pruefe("Warteschlangen-Umlauf", false, e instanceof Error ? e.message : String(e)) } /* ----------------------------------------- WebSocket mit Anmeldung */ try { const ergebnis = await new Promise((aufloesen, ablehnen) => { const ws = new WebSocket(websocketUrl(BASIS)) const frist = setTimeout(() => { ablehnen(new Error("Zeitüberschreitung")) ws.close() }, 10_000) ws.onmessage = (nachricht) => { const daten = JSON.parse(String(nachricht.data)) as { type: string } if (daten.type === "auth_required") { ws.send(JSON.stringify({ type: "auth", access_token: TOKEN })) } else if (daten.type === "auth_ok" || daten.type === "auth_invalid") { clearTimeout(frist) aufloesen(daten.type) ws.close() } } ws.onerror = () => { clearTimeout(frist) ablehnen(new Error("WebSocket-Fehler")) } }) pruefe("WebSocket meldet sich mit dem Token an", ergebnis === "auth_ok", ergebnis) } catch (e) { pruefe("WebSocket-Anmeldung", false, e instanceof Error ? e.message : String(e)) } console.log(fehler === 0 ? "\nAlle Prüfungen bestanden." : `\n${fehler} Prüfung(en) fehlgeschlagen.`) process.exitCode = fehler === 0 ? 0 : 1