Filipe, gerade gemeldet. Es waren ZWEI Fehler, und beide erklaeren
genau das, was er gesehen hat.
--- 1. Die Benachrichtigung sagte nicht, dass es ein Anruf ist ------
Gemessen kam bei ihr an:
Nachricht von [object Object]
(kein Text)
`nachricht.von` ist beim Klingeln ein OBJEKT (`{id, name}`) und keine
Zeichenkette; einen `text` gibt es bei einem Anruf gar nicht. Moeglich
wurde beides, weil die ART des Ereignisses die Benachrichtigung nie
erreichte: `chatEreignis` nimmt sie als vierten Parameter entgegen,
reichte sie aber nur in den Ereignisstrom weiter. Fuer den Push galt
jedes Ereignis als Chatnachricht -- auch das Klingeln.
Jetzt steht dort "Filipe ruft an" / "Tippen zum Rangehen", und beim
Tippen landet man im richtigen Gespraech.
--- 2. Und dort klingelte es dann trotzdem nicht --------------------
Das Klingeln lief ausschliesslich ueber den offenen Ereignisstrom. Wer
zusieht, hoert es. Wer die Seite NICHT offen hat, bekommt die
Benachrichtigung, tippt darauf, die Seite laedt -- und bleibt still.
Das Ereignis war vorbei, bevor sie da war.
Der Anruf funktionierte damit ausgerechnet fuer die nicht, fuer die
die Benachrichtigung ueberhaupt gebaut wurde.
Neu: `GET /workspace/api/anruf/offen` -- beim Laden fragt die Seite
einmal nach, ob in einem ihrer Raeume jemand wartet. Nur was noch
klingelt (45 s), nur Raeume, in denen die Person drin ist, und nicht
beim Anrufer selbst.
--- Gepruefte Wege -------------------------------------------------
pruef-anruf 80 -> 95. Zwei neue Abschnitte, beide mit Gegenprobe:
Route stillgelegt -> 3 rot; der Text ist jetzt einzeln pruefbar
(`pushTextFuer`), weil er vorher tief in einer Funktion entstand, die
nur der Push-Weg aufruft -- von aussen nicht messbar.
Nebenbei gefunden: `istDrinFuerAnruf` braucht die PERSON, nicht ihre
Nummer, und sagt das mit einem eigenen TypeError. Meine erste Fassung
uebergab die Nummer.
Co-Authored-By: Claude Opus 5 <[email protected]>
837 lines
38 KiB
JavaScript
837 lines
38 KiB
JavaScript
/* =====================================================================
|
|
TELEFONIEREN IM CHAT -- die Riegel (18.09.2026)
|
|
|
|
Filipe: „kann man machen dass die modis, rechte hand und ich auch
|
|
telefonieren können im chat?"
|
|
|
|
DER WICHTIGSTE ABSCHNITT IST DER LETZTE. Ein Anruf ist der
|
|
vertraulichste Vorgang im ganzen Haus: Was gesagt wird, steht
|
|
nirgends, und wer mithört, hinterlässt keine Spur. Die Frage „kommt
|
|
jemand hinein, der nicht in den Raum gehört?" muss deshalb an JEDER
|
|
Route beantwortet sein und nicht an der Liste davor.
|
|
|
|
Besonders die Signalisierung: Sie reicht beliebiges JSON an eine
|
|
Personennummer weiter. Ohne Prüfung wäre das ein Weg, jedem
|
|
Angemeldeten etwas zu schicken -- an jeder Chatberechtigung vorbei.
|
|
|
|
Aufrufen mit: node server/pruef-anruf.mjs
|
|
===================================================================== */
|
|
import { mkdtempSync, rmSync, writeFileSync, unlinkSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { request as httpAnfrage } from "node:http";
|
|
import { portMussFreiSein } from "./helfer-port.mjs";
|
|
|
|
const PORT = await portMussFreiSein(4196, "pruef-anruf");
|
|
const ordner = mkdtempSync(join(tmpdir(), "ws-anruf-"));
|
|
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
|
|
process.env.PORT = String(PORT);
|
|
process.env.SITE_ACCESS_SECRET = "lokaler-test";
|
|
process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00";
|
|
/* Der Vermittlungsserver holt sein Geheimnis aus einer Datei. Hier muss
|
|
der Pfad UMGEBOGEN werden, bevor index.js geladen wird -- sonst
|
|
zeigte er auf die echte Datei unter /etc, und die Pruefung wuerde
|
|
entweder am echten Betrieb messen oder gar nichts sehen koennen. */
|
|
process.env.TURN_GEHEIMNIS_DATEI = join(ordner, "turn.geheimnis");
|
|
|
|
const express = (await import("express")).default;
|
|
const ec = express.response.cookie;
|
|
express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); };
|
|
import { notbremse } from "./helfer-notbremse.mjs";
|
|
await import("./index.js");
|
|
notbremse(180_000, "pruef-anruf");
|
|
await new Promise((r) => setTimeout(r, 800));
|
|
|
|
process.on("uncaughtException", (f) => { console.error("ABSTURZ:", f); process.exit(7); });
|
|
|
|
let fehler = 0, geprueft = 0, offen = 0;
|
|
const melde = (t) => console.log(t);
|
|
const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
|
|
const unklar = (t) => { offen++; console.log(" -- " + t); };
|
|
|
|
const CREW = "crew.dogfather-universe.com";
|
|
const HAUS = "workspace.dogfather-universe.com";
|
|
|
|
const { DatabaseSync } = await import("node:sqlite");
|
|
const { scryptSync, randomBytes } = await import("node:crypto");
|
|
const anrufModul = await import("./workspace-anruf.js");
|
|
|
|
/* ---- Leute anlegen ---- */
|
|
const d = new DatabaseSync(process.env.WORKSPACE_DB);
|
|
function person(name, rolle, code) {
|
|
const salt = randomBytes(16).toString("hex");
|
|
const hash = scryptSync(code, salt, 64,
|
|
{ N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex");
|
|
d.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n, aktiv, erstellt) VALUES (?,?,?,?,?,1,?)")
|
|
.run(name, rolle, hash, salt, 32768, new Date().toISOString());
|
|
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
|
|
}
|
|
const idDogi = person("Filipe", "admin", "CODE-DOGI-0001");
|
|
const idHand = person("Rieke", "hand", "CODE-HAND-0001");
|
|
const idModi = person("Kessi", "modi", "CODE-MODI-0001");
|
|
const idFremd = person("Tili", "creator", "CODE-TILI-0001");
|
|
d.close();
|
|
|
|
function roh(pfad, host, kopf = {}, koerper = null, methode = null) {
|
|
return new Promise((fertig, schief) => {
|
|
const a = httpAnfrage({
|
|
host: "127.0.0.1", port: PORT, path: pfad,
|
|
method: methode || (koerper ? "POST" : "GET"),
|
|
headers: { Host: host, ...(koerper ? { "Content-Type": "application/json" } : {}), ...kopf },
|
|
}, (antwort) => {
|
|
let text = "";
|
|
antwort.on("data", (s) => { text += s; });
|
|
antwort.on("end", () => fertig({ code: antwort.statusCode, text, kopf: antwort.headers }));
|
|
});
|
|
a.on("error", schief);
|
|
if (koerper) a.write(JSON.stringify(koerper));
|
|
a.end();
|
|
});
|
|
}
|
|
const json = (a) => { try { return JSON.parse(a.text); } catch { return {}; } };
|
|
async function anmelden(rolle, code, host) {
|
|
const r = await roh("/workspace/api/anmelden", host, {}, { rolle, code });
|
|
return [].concat(r.kopf?.["set-cookie"] || []).map((c) => c.split(";")[0]).join("; ");
|
|
}
|
|
const kDogi = await anmelden("admin", "CODE-DOGI-0001", HAUS);
|
|
const kHand = await anmelden("hand", "CODE-HAND-0001", CREW);
|
|
const kModi = await anmelden("modi", "CODE-MODI-0001", CREW);
|
|
const kFremd = await anmelden("creator", "CODE-TILI-0001", HAUS);
|
|
ok(!!kDogi && !!kHand && !!kModi && !!kFremd, "vier Leute sind angemeldet");
|
|
|
|
/* =======================================================================
|
|
1. DER KATALOG
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 1. Was fest steht ===");
|
|
ok(anrufModul.GRUPPE_MAX >= 2 && anrufModul.GRUPPE_MAX <= 8,
|
|
`die Gruppengrenze steht als Zahl im Code (${anrufModul.GRUPPE_MAX})`);
|
|
|
|
/* =======================================================================
|
|
2. EIN GESPRAECH UND EIN ANRUF
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 2. Anrufen ===");
|
|
|
|
/* Ein Zweier-Gespraech zwischen DogFather und der rechten Hand. */
|
|
let raumId = null;
|
|
{
|
|
/* Die Route heisst `direkt`, nicht `mit` -- nachgesehen statt
|
|
geraten. */
|
|
const r = await roh("/workspace/api/chat/direkt", HAUS, { cookie: kDogi },
|
|
{ mit: idHand });
|
|
raumId = json(r).raum?.id || json(r).raum_id || json(r).id;
|
|
ok(!!raumId, `ein Gespraech entsteht (${r.code}, Raum ${raumId})`);
|
|
}
|
|
|
|
{
|
|
const r = await roh(`/workspace/api/anruf/${raumId}`, HAUS, { cookie: kDogi });
|
|
ok(r.code === 200 && json(r).laeuft === false,
|
|
`am Anfang laeuft kein Anruf (${json(r).laeuft})`);
|
|
}
|
|
|
|
{
|
|
const r = await roh(`/workspace/api/anruf/${raumId}/start`, HAUS,
|
|
{ cookie: kDogi }, { video: false });
|
|
ok(r.code === 200, `anrufen geht (${r.code})`);
|
|
const l = json(await roh(`/workspace/api/anruf/${raumId}`, HAUS, { cookie: kDogi }));
|
|
ok(l.laeuft === true, "und danach laeuft einer");
|
|
ok(l.wer?.length === 1 && l.wer[0].id === idDogi,
|
|
`mit genau einem Teilnehmer (${l.wer?.map((w) => w.name).join(", ")})`);
|
|
}
|
|
|
|
{
|
|
const r = await roh(`/workspace/api/anruf/${raumId}/dabei`, CREW, { cookie: kHand }, {}, "POST");
|
|
ok(r.code === 200, `die rechte Hand geht ran (${r.code})`);
|
|
const l = json(await roh(`/workspace/api/anruf/${raumId}`, CREW, { cookie: kHand }));
|
|
ok(l.wer?.length === 2, `jetzt sind es zwei (${l.wer?.map((w) => w.name).join(", ")})`);
|
|
ok(l.ich_dabei === true, "und sie weiss, dass sie dabei ist");
|
|
}
|
|
|
|
/* =======================================================================
|
|
3. DIE SIGNALISIERUNG
|
|
|
|
Sie reicht beliebiges JSON an eine Personennummer weiter. Ohne
|
|
Pruefung waere das ein Weg, jedem Angemeldeten etwas zu schicken --
|
|
an jeder Chatberechtigung vorbei.
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 3. Die Signalisierung ===");
|
|
{
|
|
const r = await roh(`/workspace/api/anruf/${raumId}/signal`, HAUS, { cookie: kDogi },
|
|
{ an: idHand, inhalt: { angebot: { type: "offer", sdp: "v=0" } } });
|
|
ok(r.code === 200, `ein Angebot geht durch (${r.code})`);
|
|
}
|
|
{
|
|
const r = await roh(`/workspace/api/anruf/${raumId}/signal`, HAUS, { cookie: kDogi },
|
|
{ an: idFremd, inhalt: { angebot: {} } });
|
|
ok(r.code === 404,
|
|
`an jemanden AUSSERHALB des Raums geht nichts (${r.code}) -- sonst waere das `
|
|
+ "ein Weg, jedem Angemeldeten JSON zu schicken");
|
|
}
|
|
{
|
|
const r = await roh(`/workspace/api/anruf/${raumId}/signal`, HAUS, { cookie: kDogi },
|
|
{ an: idHand });
|
|
ok(r.code === 400, `ohne Inhalt wird abgewiesen (${r.code})`);
|
|
}
|
|
|
|
/* =======================================================================
|
|
4. WER NICHT HINEINGEHOERT
|
|
|
|
Der wichtigste Abschnitt. Ein Anruf hinterlaesst keine Spur -- wer
|
|
mithoert, faellt nicht auf. Die Frage muss an JEDER Route
|
|
beantwortet sein.
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 4. Wer nicht hineingehoert ===");
|
|
for (const [pfad, methode, koerper] of [
|
|
["", "GET", null],
|
|
["/start", "POST", { video: false }],
|
|
["/dabei", "POST", {}],
|
|
["/ende", "POST", {}],
|
|
["/signal", "POST", { an: idDogi, inhalt: { weg: {} } }],
|
|
]) {
|
|
const r = await roh(`/workspace/api/anruf/${raumId}${pfad}`, HAUS,
|
|
{ cookie: kFremd }, koerper, methode);
|
|
ok(r.code === 404,
|
|
`${(pfad || "/lage").padEnd(8)} ist fuer Fremde zu -- und zwar 404, `
|
|
+ `nicht „kein Zugriff" (${r.code})`);
|
|
}
|
|
{
|
|
const r = await roh(`/workspace/api/anruf/${raumId}`, HAUS, {});
|
|
ok(r.code === 401, `ohne Anmeldung gar nichts (${r.code})`);
|
|
}
|
|
{
|
|
const r = await roh("/workspace/api/anruf/999999/start", HAUS,
|
|
{ cookie: kDogi }, { video: false });
|
|
ok(r.code === 404, `ein erfundener Raum wird abgewiesen (${r.code})`);
|
|
}
|
|
|
|
/* =======================================================================
|
|
5. AUFLEGEN
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 5. Auflegen ===");
|
|
{
|
|
const r = await roh(`/workspace/api/anruf/${raumId}/ende`, CREW, { cookie: kHand }, {}, "POST");
|
|
ok(r.code === 200, `die rechte Hand legt auf (${r.code})`);
|
|
const l = json(await roh(`/workspace/api/anruf/${raumId}`, HAUS, { cookie: kDogi }));
|
|
ok(l.laeuft === true && l.wer?.length === 1,
|
|
`der Anruf laeuft weiter, solange noch jemand drin ist (${l.wer?.length})`);
|
|
await roh(`/workspace/api/anruf/${raumId}/ende`, HAUS, { cookie: kDogi }, {}, "POST");
|
|
const l2 = json(await roh(`/workspace/api/anruf/${raumId}`, HAUS, { cookie: kDogi }));
|
|
ok(l2.laeuft === false, "wenn der Letzte geht, ist er vorbei");
|
|
}
|
|
|
|
/* =======================================================================
|
|
6. DIE ADRESSEN
|
|
|
|
Sie enthalten Zugangsdaten (TURN braucht Benutzer und Passwort).
|
|
Deshalb darf nur DogFather sie setzen.
|
|
======================================================================= */
|
|
/* =======================================================================
|
|
5c. WER ERST DURCH DIE BENACHRICHTIGUNG KOMMT
|
|
|
|
Filipe, 18.09.2026: „wenn ich vanvan anrufe kriegt sie nur eine
|
|
benarichtigung aber keinen anruf was soll das."
|
|
|
|
Das war kein Missverstaendnis, sondern ein Loch. Das Klingeln lief
|
|
ausschliesslich ueber den offenen Ereignisstrom: Wer zusieht, hoert
|
|
es. Wer die Seite NICHT offen hat, bekommt die Push-Nachricht,
|
|
tippt darauf, die Seite laedt -- und bleibt still. Das Ereignis war
|
|
vorbei, bevor sie da war.
|
|
|
|
Der Anruf funktionierte damit ausgerechnet fuer die nicht, fuer die
|
|
die Benachrichtigung gebaut wurde.
|
|
|
|
DIESE PRUEFUNG BILDET GENAU DAS AB: Erst anrufen, DANN die Seite
|
|
oeffnen. Ohne sie beweist der Dreier-Test unten nur, dass es
|
|
funktioniert, wenn alle schon zusehen.
|
|
======================================================================= */
|
|
/* =======================================================================
|
|
5d. WAS IN DER BENACHRICHTIGUNG STEHT
|
|
|
|
Filipe: „wenn ich vanvan anrufe kriegt sie nur eine benarichtigung
|
|
aber keinen anruf was soll das."
|
|
|
|
Gemessen kam bei ihr an: „Nachricht von [object Object]", ohne Text.
|
|
|
|
Zwei Fehler in einer Zeile. `von` ist beim Klingeln ein OBJEKT und
|
|
keine Zeichenkette; und einen `text` gibt es bei einem Anruf gar
|
|
nicht. Moeglich wurde beides, weil die ART des Ereignisses die
|
|
Benachrichtigung nie erreichte -- jedes Ereignis galt dort als
|
|
Chatnachricht.
|
|
|
|
DIESE PRUEFUNG WAR VORHER NICHT MOEGLICH. Der Text entstand tief in
|
|
einer Funktion, die nur der Push-Weg aufruft. Er steht deshalb
|
|
jetzt in `pushTextFuer()` -- eine Zeile weniger Verschachtelung und
|
|
eine Frage, die sich beantworten laesst.
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 5d. Was in der Benachrichtigung steht ===");
|
|
{
|
|
const { pushTextFuer } = await import("./workspace-chat.js");
|
|
|
|
const a = pushTextFuer("anruf", { von: { id: 7, name: "Filipe" }, video: false }, 3);
|
|
ok(a.titel === "Filipe ruft an", `beim Anruf steht der Name und „ruft an" (${a.titel})`);
|
|
ok(!/\[object/i.test(a.titel + a.text),
|
|
"und nirgends [object Object] -- genau das kam vorher an");
|
|
ok(a.text.includes("Rangehen"), `der Text sagt, was zu tun ist (${a.text})`);
|
|
ok(a.ziel === "/workspace/chat.html?raum=3",
|
|
`und es fuehrt in das richtige Gespraech (${a.ziel})`);
|
|
|
|
const v = pushTextFuer("anruf", { von: { id: 7, name: "Filipe" }, video: true }, 3);
|
|
ok(v.text.toLowerCase().includes("video"),
|
|
`ein Videoanruf sagt das auch (${v.text})`);
|
|
|
|
/* UND EINE NORMALE NACHRICHT BLEIBT EINE NORMALE NACHRICHT. */
|
|
const n = pushTextFuer("neu", { von: "Rieke", text: "Hallo!" }, 3);
|
|
ok(n.titel === "Nachricht von Rieke" && n.text === "Hallo!",
|
|
`eine Nachricht meldet sich weiter als Nachricht (${n.titel})`);
|
|
|
|
/* Und wenn dort doch einmal ein Objekt steht -- dieselbe Falle,
|
|
nur an der anderen Stelle -- kommt trotzdem ein Name heraus. */
|
|
const o = pushTextFuer("neu", { von: { name: "Rieke" }, text: "Hallo!" }, 3);
|
|
ok(!/\[object/i.test(o.titel), `auch mit einem Objekt als Absender (${o.titel})`);
|
|
}
|
|
|
|
melde("");
|
|
melde("=== 5c. Wer erst durch die Benachrichtigung kommt ===");
|
|
{
|
|
const raum = json(await roh("/workspace/api/chat/raeume", HAUS,
|
|
{ cookie: kDogi })).raeume?.[0];
|
|
if (!raum) {
|
|
unklar("kein Gespraech vorhanden -- nicht nachgesehen");
|
|
} else {
|
|
/* Erst pruefen, dass nichts offen ist -- sonst misst der Rest
|
|
einen Rest von vorher. */
|
|
const vorher = json(await roh("/workspace/api/anruf/offen", CREW, { cookie: kHand }));
|
|
ok(Array.isArray(vorher.anrufe) && vorher.anrufe.length === 0,
|
|
`vorher klingelt nichts (${vorher.anrufe?.length})`);
|
|
|
|
const start = await roh(`/workspace/api/anruf/${raum.id}/start`, HAUS,
|
|
{ cookie: kDogi }, { video: false });
|
|
ok(start.code === 200 || start.code === 201, `DogFather ruft an (${start.code})`);
|
|
|
|
/* JETZT fragt die rechte Hand -- so, wie ihre Seite es beim Laden
|
|
tut, nachdem sie auf die Benachrichtigung getippt hat. */
|
|
const offen = json(await roh("/workspace/api/anruf/offen", CREW, { cookie: kHand }));
|
|
ok(offen.anrufe?.length === 1,
|
|
`sie erfaehrt beim Laden, dass es klingelt (${offen.anrufe?.length})`);
|
|
ok(offen.anrufe?.[0]?.von?.name && offen.anrufe[0].von.id !== undefined,
|
|
`und von wem (${offen.anrufe?.[0]?.von?.name})`);
|
|
ok(offen.anrufe?.[0]?.raum_id === raum.id,
|
|
"und in welchem Gespraech");
|
|
|
|
/* DER ANRUFER SELBST DARF NICHT KLINGELN. Er hat den Anruf
|
|
gestartet; ein zweites Fenster von ihm wuerde sonst mitlaeuten. */
|
|
const beimAnrufer = json(await roh("/workspace/api/anruf/offen", HAUS, { cookie: kDogi }));
|
|
ok(beimAnrufer.anrufe?.length === 0,
|
|
`beim Anrufer selbst klingelt nichts (${beimAnrufer.anrufe?.length})`);
|
|
|
|
/* UND WER NICHT IN DEN RAUM GEHOERT, ERFAEHRT NICHTS. Dieselbe
|
|
Frage wie ueberall sonst -- eine neue Route ist eine neue Tuer. */
|
|
const fremd = json(await roh("/workspace/api/anruf/offen", HAUS, { cookie: kFremd }));
|
|
ok(fremd.anrufe?.length === 0,
|
|
`ein Fremder erfaehrt nichts davon (${fremd.anrufe?.length})`);
|
|
|
|
await roh(`/workspace/api/anruf/${raum.id}/ende`, HAUS, { cookie: kDogi }, {});
|
|
const danach = json(await roh("/workspace/api/anruf/offen", CREW, { cookie: kHand }));
|
|
ok(danach.anrufe?.length === 0,
|
|
`nach dem Auflegen klingelt es nicht mehr (${danach.anrufe?.length})`);
|
|
}
|
|
}
|
|
|
|
melde("");
|
|
melde("=== 6. Die Verbindungsadressen ===");
|
|
{
|
|
const r = json(await roh("/workspace/api/anruf/adressen", HAUS, { cookie: kDogi }));
|
|
ok(Array.isArray(r.adressen), `die Liste kommt (${r.adressen?.length} Eintraege)`);
|
|
ok(r.nur_gleiches_netz === true,
|
|
"und sie sagt ehrlich, dass es ohne Eintrag nur im selben Netz geht");
|
|
}
|
|
{
|
|
const r = await roh("/workspace/api/anruf/adressen", CREW, { cookie: kHand },
|
|
{ adressen: [{ urls: "stun:beispiel.de:3478" }] }, "PUT");
|
|
ok(r.code === 404, `die rechte Hand darf sie nicht setzen (${r.code})`);
|
|
}
|
|
{
|
|
const r = await roh("/workspace/api/anruf/adressen", HAUS, { cookie: kDogi }, {
|
|
adressen: [
|
|
{ urls: "stun:turn.dogfather-universe.com:3478" },
|
|
{ urls: "turn:turn.dogfather-universe.com:3478", username: "a", credential: "b" },
|
|
/* DAS HIER MUSS RAUSFALLEN: kein STUN/TURN, sondern irgendetwas. */
|
|
{ urls: "https://beispiel.de/boese" },
|
|
],
|
|
}, "PUT");
|
|
ok(r.code === 200 && json(r).anzahl === 2,
|
|
`DogFather setzt sie, und nur echte Adressen bleiben (${json(r).anzahl} von 3)`);
|
|
const l = json(await roh("/workspace/api/anruf/adressen", CREW, { cookie: kModi }));
|
|
ok(l.adressen?.length === 2, `alle bekommen sie zu sehen (${l.adressen?.length})`);
|
|
ok(l.nur_gleiches_netz === false, "und der Hinweis verschwindet");
|
|
|
|
/* UND WIEDER WEG. Die Adressen oben sind erfunden -- es gibt unter
|
|
`turn.dogfather-universe.com` keinen STUN-Dienst. Ein Browser
|
|
versucht sie trotzdem zu erreichen und wartet auf das Zeitlimit;
|
|
der Dreier-Test unten haette danach gegen diese Wartezeit
|
|
gekaempft statt gegen den Code.
|
|
|
|
Eine Pruefung, die der naechsten den Boden verstellt, macht deren
|
|
Ergebnis unlesbar -- genau das ist heute schon einmal passiert
|
|
(die Anfragen, Abschnitt 8b). */
|
|
await roh("/workspace/api/anruf/adressen", HAUS, { cookie: kDogi },
|
|
{ adressen: [] }, "PUT");
|
|
const zurueck = json(await roh("/workspace/api/anruf/adressen", HAUS, { cookie: kDogi }));
|
|
ok(zurueck.adressen?.length === 0,
|
|
"danach ist die Liste wieder leer -- der naechste Abschnitt faengt sauber an");
|
|
}
|
|
|
|
/* =======================================================================
|
|
6b. DIE ZUGANGSDATEN ZUM VERMITTLUNGSSERVER
|
|
|
|
Hier steht die Frage, an der sich entscheidet, ob coturn ein Dienst
|
|
fuer uns ist oder eine offene Weiterleitung fuer jeden: Das
|
|
Geheimnis, aus dem die Zugangsdaten gerechnet werden, darf den
|
|
Browser NIE erreichen. Wer es haette, koennte sich selbst beliebig
|
|
viele Zugaenge ausstellen -- dauerhaft, ohne Konto, ueber unsere
|
|
IP-Adresse.
|
|
|
|
Deshalb wird hier nicht nur geprueft, dass Zugangsdaten ANKOMMEN,
|
|
sondern vor allem, dass das Geheimnis es NICHT tut -- und zwar im
|
|
rohen Antworttext, nicht im ausgewerteten Objekt. Ein Feld, an das
|
|
niemand denkt, faellt beim Auswerten nicht auf.
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 6b. Zugangsdaten fuer die Vermittlung ===");
|
|
{
|
|
const turn = await import("./workspace-turn.js");
|
|
const datei = process.env.TURN_GEHEIMNIS_DATEI;
|
|
/* Erfunden, nur fuer diesen Lauf, und am Ende wieder weg. Lang genug,
|
|
dass die Laengenpruefung nicht zufaellig anschlaegt. */
|
|
const GEHEIM = "pruef-geheimnis-nur-fuer-diesen-lauf-0123456789";
|
|
|
|
/* --- Die drei Ausgaenge beim Lesen ------------------------------- */
|
|
{
|
|
const g = turn.turnGeheimnisLesen(datei);
|
|
ok(g.wert === "" && g.grund === "fehlt",
|
|
`ohne Datei sagt es "fehlt" statt stillschweigend nichts (${g.grund})`);
|
|
}
|
|
{
|
|
writeFileSync(datei, "zu-kurz");
|
|
const g = turn.turnGeheimnisLesen(datei);
|
|
ok(g.wert === "" && g.grund === "zu_kurz",
|
|
`ein zu kurzes Geheimnis wird abgelehnt, nicht benutzt (${g.grund})`);
|
|
}
|
|
{
|
|
writeFileSync(datei, " \n");
|
|
const g = turn.turnGeheimnisLesen(datei);
|
|
ok(g.wert === "" && g.grund === "leer", `eine leere Datei meldet "leer" (${g.grund})`);
|
|
}
|
|
{
|
|
writeFileSync(datei, GEHEIM + "\n");
|
|
const g = turn.turnGeheimnisLesen(datei);
|
|
ok(g.wert === GEHEIM && g.grund === "",
|
|
"mit Datei kommt das Geheimnis an (Zeilenumbruch abgeschnitten)");
|
|
}
|
|
|
|
/* --- Ein WECHSEL muss auffallen ---------------------------------- */
|
|
{
|
|
writeFileSync(datei, GEHEIM + "-zweite-fassung-mit-anderer-laenge");
|
|
const g = turn.turnGeheimnisLesen(datei);
|
|
ok(g.wert.endsWith("-zweite-fassung-mit-anderer-laenge"),
|
|
"wird die Datei getauscht, wird der neue Wert gelesen (kein Festhaengen)");
|
|
writeFileSync(datei, GEHEIM + "\n");
|
|
}
|
|
|
|
/* --- Die Rechnung ------------------------------------------------ */
|
|
{
|
|
/* Eine FESTE Zeit als Eingabe, nicht die Wanduhr: Sonst waere die
|
|
Pruefung eine Zeitbombe, die irgendwann etwas anderes misst. */
|
|
const JETZT = 1_700_000_000_000;
|
|
const z = turn.turnZugang("w7", GEHEIM, JETZT);
|
|
const [ablaufStr, kennung] = z.username.split(":");
|
|
ok(kennung === "w7", `im Benutzernamen steht die Kennung (${kennung})`);
|
|
ok(Number(ablaufStr) === Math.floor(JETZT / 1000) + turn.TURN_GUELTIG_STUNDEN * 3600,
|
|
`und davor der Ablauf, genau ${turn.TURN_GUELTIG_STUNDEN} Stunden spaeter`);
|
|
|
|
/* FESTER VERGLEICHSWERT, ausgerechnet und nicht behauptet. Er
|
|
beweist NICHT, dass coturn ihn akzeptiert -- das beweist nur ein
|
|
Versuch am laufenden coturn (turnutils_uclient). Er beweist,
|
|
dass sich die Rechenvorschrift nicht unbemerkt aendert; genau
|
|
das wuerde jeden Anruf ueber die Vermittlung scheitern lassen,
|
|
ohne dass irgendwo ein Fehler steht. */
|
|
ok(z.credential === "9AbMBJeTDL6uebcR6KRpYVTFwt8=",
|
|
`das Passwort ist die bekannte HMAC-SHA1-Rechnung (${z.credential})`);
|
|
|
|
const nochmal = turn.turnZugang("w7", GEHEIM, JETZT);
|
|
ok(nochmal.credential === z.credential, "dieselbe Eingabe ergibt dasselbe Passwort");
|
|
const anderer = turn.turnZugang("w8", GEHEIM, JETZT);
|
|
ok(anderer.credential !== z.credential,
|
|
"eine andere Person bekommt ein anderes Passwort");
|
|
const spaeter = turn.turnZugang("w7", GEHEIM, JETZT + 60_000);
|
|
ok(spaeter.credential !== z.credential,
|
|
"und eine Minute spaeter ein anderes -- die Daten verfallen wirklich");
|
|
}
|
|
|
|
/* --- Wer bekommt Zugangsdaten und wer nicht ---------------------- */
|
|
{
|
|
const liste = [
|
|
{ urls: "stun:turn.dogfather-universe.com:3478" },
|
|
{ urls: "turn:turn.dogfather-universe.com:3478" },
|
|
{ urls: "turn:fremder-dienst.example:3478", username: "fremd", credential: "eigenes" },
|
|
];
|
|
const e = turn.mitZugangsdaten(liste, "w7", 1_700_000_000_000, datei);
|
|
ok(!e.adressen[0].credential, "ein STUN-Eintrag bekommt keine Zugangsdaten");
|
|
ok(!!e.adressen[1].credential && e.adressen[1].username.includes(":w7"),
|
|
"unser TURN-Eintrag bekommt gerechnete");
|
|
ok(e.adressen[2].credential === "eigenes" && e.adressen[2].username === "fremd",
|
|
"ein fremder Dienst behaelt seine eigenen -- wir ueberschreiben nichts");
|
|
ok(e.turn_gesamt === 2 && e.turn_bereit === 2,
|
|
`und die Zahlen stimmen (${e.turn_bereit} von ${e.turn_gesamt} bereit)`);
|
|
}
|
|
{
|
|
/* OHNE Geheimnis: Der Eintrag bleibt stehen, aber es wird gesagt. */
|
|
unlinkSync(datei);
|
|
const e = turn.mitZugangsdaten(
|
|
[{ urls: "turn:turn.dogfather-universe.com:3478" }], "w7", Date.now(), datei);
|
|
ok(e.turn_gesamt === 1 && e.turn_bereit === 0 && e.grund === "fehlt",
|
|
`ohne Geheimnis ist nichts bereit, und der Grund steht dabei (${e.grund})`);
|
|
writeFileSync(datei, GEHEIM + "\n");
|
|
}
|
|
|
|
/* --- Und jetzt ueber die Schnittstelle, wie der Browser sie sieht - */
|
|
{
|
|
await roh("/workspace/api/anruf/adressen", HAUS, { cookie: kDogi }, {
|
|
adressen: [{ urls: "turn:turn.dogfather-universe.com:3478" }],
|
|
}, "PUT");
|
|
const a = await roh("/workspace/api/anruf/adressen", CREW, { cookie: kModi });
|
|
const r = json(a);
|
|
|
|
ok(r.turn_gesamt === 1 && r.turn_bereit === 1,
|
|
`der Modi bekommt einen einsatzbereiten Eintrag (${r.turn_bereit}/${r.turn_gesamt})`);
|
|
ok(!!r.adressen?.[0]?.credential && !!r.adressen?.[0]?.username,
|
|
"mit Benutzername und Passwort");
|
|
|
|
/* DIE WICHTIGSTE PRUEFUNG DES ABSCHNITTS. Im ROHEN Text gesucht,
|
|
nicht im ausgewerteten Objekt. */
|
|
ok(!a.text.includes(GEHEIM),
|
|
"und das Geheimnis selbst steht NIRGENDS in der Antwort");
|
|
|
|
/* Jeder bekommt EIGENE Zugangsdaten -- im coturn-Protokoll steht
|
|
damit die Nummer, nicht ein Sammelkonto. */
|
|
const rDogi = json(await roh("/workspace/api/anruf/adressen", HAUS, { cookie: kDogi }));
|
|
ok(rDogi.adressen?.[0]?.username !== r.adressen?.[0]?.username,
|
|
"DogFather und der Modi bekommen verschiedene Zugangsdaten");
|
|
|
|
/* Der Ablauf muss fuer den Browser LESBAR sein -- darauf beruht das
|
|
Auffrischen vor jedem Anruf in anruf.js. */
|
|
const ablauf = Number(String(r.adressen?.[0]?.username || "").split(":")[0]);
|
|
ok(ablauf > Math.floor(Date.now() / 1000) + 3600,
|
|
`der Ablauf steht vorn im Benutzernamen und liegt in der Zukunft (${ablauf})`);
|
|
}
|
|
|
|
/* --- Aufraeumen: der naechste Abschnitt faengt sauber an ---------- */
|
|
{
|
|
await roh("/workspace/api/anruf/adressen", HAUS, { cookie: kDogi },
|
|
{ adressen: [] }, "PUT");
|
|
try { unlinkSync(datei); } catch { /* schon weg */ }
|
|
const zurueck = json(await roh("/workspace/api/anruf/adressen", HAUS, { cookie: kDogi }));
|
|
ok(zurueck.adressen?.length === 0 && !zurueck.turn_grund,
|
|
"danach ist wieder alles leer -- keine erfundene Adresse bleibt stehen");
|
|
}
|
|
}
|
|
|
|
/* =======================================================================
|
|
7. IM BROWSER
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 7. Im Browser ===");
|
|
{
|
|
let chromium;
|
|
try {
|
|
({ chromium } = await import(
|
|
"file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs"));
|
|
} catch { chromium = null; }
|
|
|
|
if (!chromium) {
|
|
unklar("kein Playwright gefunden -- die Oberflaeche wurde nicht angesehen");
|
|
} else {
|
|
const browser = await chromium.launch({
|
|
/* Mikrofon ohne Rueckfrage und mit einem Testton statt echter
|
|
Hardware -- sonst haengt getUserMedia im Prueflauf. */
|
|
args: ["--use-fake-ui-for-media-stream", "--use-fake-device-for-media-stream"],
|
|
});
|
|
try {
|
|
const sicht = await browser.newContext({
|
|
viewport: { width: 1280, height: 900 },
|
|
permissions: ["microphone", "camera"],
|
|
});
|
|
await sicht.addCookies(kDogi.split("; ").map((c) => {
|
|
const [name, ...rest] = c.split("=");
|
|
return { name, value: rest.join("="), domain: "127.0.0.1", path: "/" };
|
|
}));
|
|
const seite = await sicht.newPage();
|
|
const konsole = [];
|
|
seite.on("console", (m) => { if (m.type() === "error") konsole.push(m.text()); });
|
|
await seite.goto(`http://127.0.0.1:${PORT}/workspace/chat.html`,
|
|
{ waitUntil: "domcontentloaded" });
|
|
await seite.waitForTimeout(1800);
|
|
|
|
ok(await seite.evaluate(() => typeof window.anrufStarten === "function"),
|
|
"der Anruf ist geladen");
|
|
|
|
/* Ein Gespraech oeffnen -- erst dann gibt es Knoepfe. */
|
|
const raeume = await seite.locator("#raeume li").count();
|
|
ok(raeume > 0, `${raeume} Gespraech(e) in der Liste`);
|
|
await seite.locator("#raeume li").first().click();
|
|
await seite.waitForTimeout(1200);
|
|
|
|
const knoepfe = await seite.evaluate(() => ({
|
|
ton: !!document.getElementById("anruf-ton-start"),
|
|
video: !!document.getElementById("anruf-video-start"),
|
|
sichtbar: document.getElementById("anruf-ton-start")?.offsetHeight > 0,
|
|
}));
|
|
ok(knoepfe.ton && knoepfe.video, "beide Anrufknoepfe stehen im Kopf");
|
|
ok(knoepfe.sichtbar, "und man sieht sie");
|
|
|
|
/* ANRUFEN -- und zwar wirklich. Ein Test, der nur prueft, dass
|
|
ein Knopf da ist, prueft, dass jemand einen Knopf gebaut hat. */
|
|
await seite.click("#anruf-ton-start");
|
|
await seite.waitForTimeout(2500);
|
|
const kasten = await seite.evaluate(() => {
|
|
const k = document.getElementById("anruf");
|
|
return {
|
|
da: !!k && !k.hidden,
|
|
lage: document.getElementById("anruf-lage")?.textContent || "",
|
|
zeit: document.getElementById("anruf-zeit")?.textContent || "",
|
|
};
|
|
});
|
|
ok(kasten.da, `der Anrufkasten erscheint ("${kasten.lage}")`);
|
|
ok(/klingelt|Verbind/i.test(kasten.lage),
|
|
`und sagt, was gerade passiert ("${kasten.lage}")`);
|
|
ok(/^\d\d:\d\d$/.test(kasten.zeit), `die Uhr laeuft (${kasten.zeit})`);
|
|
|
|
/* Und der Server weiss davon. */
|
|
const l = json(await roh(`/workspace/api/anruf/${raumId}`, HAUS, { cookie: kDogi }));
|
|
ok(l.laeuft === true, "der Server kennt den Anruf");
|
|
|
|
await seite.screenshot({ path: "server/pruef-anruf.png", fullPage: false });
|
|
melde(" -- Bildschirmfoto: server/pruef-anruf.png");
|
|
|
|
await seite.click("#anruf-auflegen");
|
|
await seite.waitForTimeout(900);
|
|
ok(await seite.evaluate(() => document.getElementById("anruf")?.hidden === true),
|
|
"nach dem Auflegen ist der Kasten weg");
|
|
const l2 = json(await roh(`/workspace/api/anruf/${raumId}`, HAUS, { cookie: kDogi }));
|
|
ok(l2.laeuft === false, "und der Server auch");
|
|
|
|
ok(konsole.length === 0,
|
|
konsole.length ? `Konsolenfehler: ${konsole[0]}` : "kein Konsolenfehler");
|
|
} finally {
|
|
await browser.close();
|
|
}
|
|
}
|
|
}
|
|
|
|
/* =======================================================================
|
|
8. ZU DRITT -- UND ZWAR WIRKLICH (18.09.2026)
|
|
|
|
GEBAUT IST NICHT BEWIESEN. Der Anruf ist fuer bis zu vier Leute
|
|
geschrieben, geprueft war er zu zweit. Genau diese Luecke zwischen
|
|
Behauptung und Beleg ist die, an der im Haus schon mehrfach etwas
|
|
haengengeblieben ist -- zuletzt bei den Kachelfarben, wo ein
|
|
Werkzeug 21 Farben kannte und 37 gemeint waren.
|
|
|
|
DER ENTSCHEIDENDE PUNKT IST DIE ZAHL DER VERBINDUNGEN. Bei drei
|
|
Teilnehmern muss JEDER zwei haben -- eine zu jedem anderen. Haette
|
|
`verbindungen` eine einzelne Variable statt einer Karte, ginge zu
|
|
zweit alles gut und der Dritte ueberschriebe stillschweigend den
|
|
Ersten: Man sieht sich zu dritt und hoert nur einen.
|
|
|
|
UND DIE ANGEBOTSREGEL. Bei WebRTC muss genau EINER das Angebot
|
|
machen. Machen beide eins, heben sie sich auf ("glare") und es kommt
|
|
keine Verbindung zustande -- ohne Fehlermeldung, man wartet nur.
|
|
Die Regel „die kleinere Personennummer beginnt" ist auf beiden
|
|
Seiten ohne Absprache dieselbe Entscheidung. Ob sie traegt, sieht
|
|
man erst ab drei Leuten.
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 8. Zu dritt ===");
|
|
{
|
|
let chromium;
|
|
try {
|
|
({ chromium } = await import(
|
|
"file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs"));
|
|
} catch { chromium = null; }
|
|
|
|
if (!chromium) {
|
|
unklar("kein Playwright -- der Gruppenanruf wurde nicht ausprobiert");
|
|
} else {
|
|
/* Ein Gruppenraum mit allen dreien. */
|
|
const g = await roh("/workspace/api/chat/gruppe", HAUS, { cookie: kDogi },
|
|
{ name: "Testrunde", mit: [idHand, idModi] });
|
|
const gruppeId = json(g).raum?.id || json(g).raum_id || json(g).id;
|
|
ok(!!gruppeId, `ein Gruppengespraech entsteht (${g.code}, Raum ${gruppeId})`);
|
|
|
|
const browser = await chromium.launch({
|
|
args: ["--use-fake-ui-for-media-stream", "--use-fake-device-for-media-stream"],
|
|
});
|
|
try {
|
|
const machSeite = async (keks) => {
|
|
const k = await browser.newContext({
|
|
viewport: { width: 1100, height: 800 },
|
|
permissions: ["microphone"],
|
|
});
|
|
await k.addCookies(keks.split("; ").map((c) => {
|
|
const [name, ...rest] = c.split("=");
|
|
return { name, value: rest.join("="), domain: "127.0.0.1", path: "/" };
|
|
}));
|
|
const se = await k.newPage();
|
|
await se.goto(`http://127.0.0.1:${PORT}/workspace/chat.html`,
|
|
{ waitUntil: "domcontentloaded" });
|
|
await se.waitForTimeout(1600);
|
|
return { k, se };
|
|
};
|
|
|
|
const a = await machSeite(kDogi);
|
|
const b = await machSeite(kHand);
|
|
const c = await machSeite(kModi);
|
|
|
|
/* Alle drei oeffnen denselben Raum. */
|
|
for (const x of [a, b, c]) {
|
|
await x.se.evaluate((id) => {
|
|
const li = [...document.querySelectorAll("#raeume li")]
|
|
.find((e) => Number(e.dataset.raum || e.dataset.id) === id);
|
|
(li || document.querySelector("#raeume li"))?.click();
|
|
}, gruppeId);
|
|
await x.se.waitForTimeout(700);
|
|
}
|
|
|
|
/* DogFather ruft an. */
|
|
await a.se.evaluate((id) => window.anrufStarten(id, false), gruppeId);
|
|
await a.se.waitForTimeout(1500);
|
|
|
|
/* Die anderen beiden gehen ran -- ueber den Klingelknopf, nicht
|
|
ueber eine Abkuerzung: Geprueft werden soll der Weg, den ein
|
|
Mensch geht. */
|
|
for (const x of [b, c]) {
|
|
const da = await x.se.locator("#anruf-klingelt .anruf-klingelt__ja").count();
|
|
ok(da === 1, `es klingelt bei ${x === b ? "der rechten Hand" : "dem Modi"} (${da})`);
|
|
if (da) await x.se.click("#anruf-klingelt .anruf-klingelt__ja");
|
|
}
|
|
/* WebRTC braucht ein paar Sekunden, bis die Verbindungen stehen. */
|
|
await a.se.waitForTimeout(6000);
|
|
|
|
/* Der Server: drei drin. */
|
|
const lage = json(await roh(`/workspace/api/anruf/${gruppeId}`, HAUS, { cookie: kDogi }));
|
|
ok(lage.wer?.length === 3,
|
|
`der Server zaehlt drei Teilnehmer (${lage.wer?.map((w) => w.name).join(", ")})`);
|
|
|
|
/* DER EIGENTLICHE PUNKT: Jeder hat ZWEI Verbindungen. */
|
|
const zaehlen = (x) => x.se.evaluate(() => ({
|
|
/* Ueber die Oberflaeche nicht erreichbar -- deshalb ueber die
|
|
Zahl der Video-/Audio-Elemente im Kasten, die jede
|
|
Gegenstelle erzeugt. */
|
|
stroeme: document.querySelectorAll("#anruf-leute audio, #anruf-leute video").length,
|
|
lage: document.getElementById("anruf-lage")?.textContent || "",
|
|
}));
|
|
for (const [wer, x] of [["DogFather", a], ["die rechte Hand", b], ["der Modi", c]]) {
|
|
const z = await zaehlen(x);
|
|
ok(z.stroeme >= 2,
|
|
`${wer} hoert BEIDE anderen (${z.stroeme} Stroeme, „${z.lage}")`);
|
|
}
|
|
|
|
/* ---- DER LAUTSPRECHER (18.09.2026) -------------------------
|
|
Filipe: „die funktion lautsprecher fehlt."
|
|
|
|
Gegenstueck zum Mikrofon: „Mikro aus" heisst, die anderen
|
|
hoeren mich nicht; „Lautsprecher aus" heisst, ich hoere sie
|
|
nicht.
|
|
|
|
DIE PRUEFUNG, AUF DIE ES ANKOMMT, ist die dritte: Kommt
|
|
jemand dazu oder geht jemand, werden die Toenelemente NEU
|
|
GEBAUT -- und neue Elemente wissen nichts von einem Schalter,
|
|
der vorher umgelegt wurde. Genau daran scheitern solche
|
|
Knoepfe sonst: Sie wirken, bis sich die Runde aendert, und
|
|
dann hoert man ploetzlich wieder mit. */
|
|
const stumm = (x) => x.se.evaluate(() =>
|
|
[...document.querySelectorAll("#anruf-leute audio, #anruf-leute video")]
|
|
.filter((m) => !m.classList.contains("anruf__bild--ich"))
|
|
.map((m) => m.muted));
|
|
|
|
{
|
|
const vorher = await stumm(a);
|
|
/* Die ANZAHL gehoert in die Bedingung: `every` ist auf einer
|
|
leeren Liste wahr, und dann waere diese Pruefung gruen,
|
|
ohne ein einziges Element gesehen zu haben. */
|
|
ok(vorher.length >= 2 && vorher.every((m) => m === false),
|
|
`vorher hoert DogFather beide (${vorher.length} Stroeme, keiner stumm)`);
|
|
|
|
const knopf = await a.se.evaluate(() => {
|
|
const k = document.getElementById("anruf-hoeren");
|
|
return k ? { text: k.textContent.trim(), an: k.getAttribute("aria-pressed") } : null;
|
|
});
|
|
ok(knopf?.text === "Lautsprecher an" && knopf?.an === "true",
|
|
`der Lautsprecher-Knopf ist da und steht auf an („${knopf?.text}")`);
|
|
|
|
await a.se.click("#anruf-hoeren");
|
|
await a.se.waitForTimeout(300);
|
|
const nachher = await stumm(a);
|
|
ok(nachher.length >= 2 && nachher.every((m) => m === true),
|
|
`nach dem Druecken ist ALLES stumm (${nachher.length} Stroeme)`);
|
|
const knopf2 = await a.se.evaluate(() => {
|
|
const k = document.getElementById("anruf-hoeren");
|
|
return { text: k.textContent.trim(), an: k.getAttribute("aria-pressed") };
|
|
});
|
|
ok(knopf2.text === "Lautsprecher aus" && knopf2.an === "false",
|
|
`und der Knopf sagt es auch („${knopf2.text}")`);
|
|
|
|
/* NUR BEI IHM. Ein Schalter, der bei den anderen mitwirkt,
|
|
waere kein Lautsprecher, sondern eine Fernbedienung. */
|
|
const beiB = await stumm(b);
|
|
ok(beiB.length >= 2 && beiB.every((m) => m === false),
|
|
`bei der rechten Hand aendert sich nichts (${beiB.length} Stroeme, keiner stumm)`);
|
|
}
|
|
|
|
/* Einer legt auf -- fuer die anderen laeuft es weiter. */
|
|
await c.se.click("#anruf-auflegen");
|
|
await c.se.waitForTimeout(2000);
|
|
const lage2 = json(await roh(`/workspace/api/anruf/${gruppeId}`, HAUS, { cookie: kDogi }));
|
|
ok(lage2.laeuft === true && lage2.wer?.length === 2,
|
|
`einer geht, zwei telefonieren weiter (${lage2.wer?.length})`);
|
|
const zA = await zaehlen(a);
|
|
ok(zA.stroeme === 1,
|
|
`und bei DogFather ist genau ein Strom weniger (${zA.stroeme})`);
|
|
|
|
/* DIE FALLE: Die Runde hat sich geaendert, die Elemente wurden
|
|
neu gebaut. Bliebe der Schalter dabei auf der Strecke, wuerde
|
|
DogFather ploetzlich wieder mithoeren -- ohne dass irgendwo
|
|
etwas anders aussieht. */
|
|
const nachWechsel = await stumm(a);
|
|
ok(nachWechsel.length === 1 && nachWechsel[0] === true,
|
|
`und der Lautsprecher bleibt aus, obwohl neu gezeichnet wurde (${nachWechsel.length} Strom)`);
|
|
const knopfNoch = await a.se.evaluate(() =>
|
|
document.getElementById("anruf-hoeren")?.getAttribute("aria-pressed"));
|
|
ok(knopfNoch === "false", "auch der Knopf steht noch richtig");
|
|
|
|
await a.se.click("#anruf-auflegen");
|
|
await b.se.click("#anruf-auflegen");
|
|
await a.se.waitForTimeout(1200);
|
|
const lage3 = json(await roh(`/workspace/api/anruf/${gruppeId}`, HAUS, { cookie: kDogi }));
|
|
ok(lage3.laeuft === false, "wenn alle auflegen, ist der Anruf vorbei");
|
|
|
|
await a.k.close(); await b.k.close(); await c.k.close();
|
|
} finally {
|
|
await browser.close();
|
|
}
|
|
}
|
|
}
|
|
|
|
/* ------------------------------------------------------------------- */
|
|
melde("");
|
|
melde(`${geprueft} Pruefungen, ${fehler} Fehler`
|
|
+ (offen ? `, ${offen} nicht nachsehbar` : ""));
|
|
melde(fehler ? "NICHT IN ORDNUNG" : "ALLES IN ORDNUNG");
|
|
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Aufraeumrest */ }
|
|
process.exit(fehler ? 1 : 0);
|