This commit is contained in:
parent
becd1593b6
commit
fc414239f8
2 changed files with 511 additions and 0 deletions
48
CRYPTO-UPDATE.md
Normal file
48
CRYPTO-UPDATE.md
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
KONTEXT
|
||||||
|
Vanilla-ECMAScript-Projekt. Mein aktuelles Protokoll committet Geheimnisse
|
||||||
|
per H(secret) und teilt einen Schlüssel n-of-n auf. Ich ersetze das durch ein
|
||||||
|
2-von-3 Threshold-Schema: Pedersen-DKG + Threshold-ElGamal mit verifizierbarer
|
||||||
|
verteilter Entschlüsselung (Chaum-Pedersen-Beweise statt blankem Hash).
|
||||||
|
Die fertige npm-Library threshold-elgamal scheidet aus: sie funktioniert
|
||||||
|
zuverlässig nur mit threshold == Teilnehmerzahl, nicht mit 2-von-3.
|
||||||
|
|
||||||
|
AUFGABE
|
||||||
|
1. Inspiziere zuerst das Repo: finde den bestehenden Commit-/Keypart-Code,
|
||||||
|
bestimme die öffentliche Schnittstelle (welche Funktionen rufen Aufrufer
|
||||||
|
auf), und liste sie mir auf, BEVOR du etwas änderst.
|
||||||
|
|
||||||
|
2. Füge @noble/curves als Fundament hinzu (auditiert, dependency-frei).
|
||||||
|
Verifiziere die aktuelle Version aus dem npm-Registry, bevor du pinst.
|
||||||
|
KEINE selbstgebaute Feldarithmetik, KEIN eigenes BigInt-Modular-Inverse –
|
||||||
|
alles über noble.
|
||||||
|
|
||||||
|
3. Implementiere mit threshold=2, parties=3 auf einer Prime-Order-Gruppe
|
||||||
|
(Ristretto255 via @noble/curves bevorzugt):
|
||||||
|
- Pedersen-VSS / Pedersen-DKG: zwei Polynome Grad 1, Koeffizienten-
|
||||||
|
Commitments C_k = g^a_k · h^b_k, h mit unbekanntem log_g(h).
|
||||||
|
- Verteilte Schlüsselerzeugung: jede Partei dealt einen Beitrag, jede
|
||||||
|
verifiziert jeden empfangenen Share gegen die Commitments.
|
||||||
|
- Threshold-ElGamal als KEM: Payload NICHT direkt mit ElGamal. Rekonstruiere
|
||||||
|
per 2 partiellen Entschlüsselungen das gemeinsame Gruppenelement, leite
|
||||||
|
daraus per HKDF-SHA256 einen 256-bit Key ab, verschlüssele das Payload mit
|
||||||
|
AES-256-GCM (WebCrypto subtle, nicht selbst gebaut).
|
||||||
|
- Jede partielle Entschlüsselung trägt einen Chaum-Pedersen-Beweis der
|
||||||
|
DL-Gleichheit; verifiziere VOR der Lagrange-Kombination.
|
||||||
|
|
||||||
|
4. Behalte die bestehende öffentliche Schnittstelle bei (gleiche Signaturen),
|
||||||
|
damit Aufrufer unverändert bleiben. Markiere den alten Code deprecated statt
|
||||||
|
ihn zu löschen.
|
||||||
|
|
||||||
|
5. Tests: (a) 2 von 3 entschlüsseln erfolgreich, (b) 1 allein scheitert,
|
||||||
|
(c) manipulierte partielle Entschlüsselung wird durch die Verifikation
|
||||||
|
abgelehnt, (d) manipulierter VSS-Share fliegt bei der DKG-Verifikation auf,
|
||||||
|
(e) echtes Payload Round-Trip.
|
||||||
|
|
||||||
|
EINSCHRÄNKUNGEN / WARNUNG
|
||||||
|
- Das ist selbstgebaute Threshold-Krypto. Setze einen unübersehbaren Kommentar
|
||||||
|
an den Kern: nicht auditiert, Komposition über noble ist ungeprüft.
|
||||||
|
- Nutze ausschließlich noble + WebCrypto für alle Primitiven. Erfinde keine
|
||||||
|
Kurven-, Hash- oder Symmetrie-Operationen selbst.
|
||||||
|
- Konstante-Zeit-Verhalten kannst du in reinem JS nicht garantieren – halte das
|
||||||
|
im Kommentar fest.
|
||||||
|
- Ändere nichts an meiner Geheimnis-/Key-Persistenz, ohne vorher zu fragen.
|
||||||
463
test-pairing.html
Normal file
463
test-pairing.html
Normal file
|
|
@ -0,0 +1,463 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>randomp2p – Pairing-Test</title>
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', monospace; background: #0d1117; color: #c9d1d9; padding: 24px; max-width: 800px; margin: 0 auto; }
|
||||||
|
h1 { color: #f0c040; font-size: 1.4rem; margin-bottom: 4px; }
|
||||||
|
.sub { color: #8b949e; font-size: 0.85rem; margin-bottom: 20px; }
|
||||||
|
button { padding: 10px 24px; background: #238636; color: #fff; border: none; border-radius: 8px; cursor: pointer; font-size: 1rem; font-weight: 600; }
|
||||||
|
button:hover { background: #2ea043; }
|
||||||
|
button:disabled { opacity: 0.5; cursor: wait; }
|
||||||
|
.test { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 14px; margin: 12px 0; }
|
||||||
|
.test h3 { margin: 0 0 6px; font-size: 1rem; }
|
||||||
|
.test .status { font-weight: 600; }
|
||||||
|
.test .detail { font-size: 0.85rem; color: #8b949e; white-space: pre-wrap; margin-top: 4px; max-height: 400px; overflow-y: auto; }
|
||||||
|
.pass { color: #2ea043; }
|
||||||
|
.fail { color: #f85149; }
|
||||||
|
.skip { color: #8b949e; }
|
||||||
|
.running { color: #f0c040; }
|
||||||
|
.summary { font-size: 1.1rem; margin: 16px 0; padding: 12px; border-radius: 8px; background: #161b22; border: 1px solid #30363d; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>randomp2p Pairing-Test</h1>
|
||||||
|
<p class="sub">Mock-SimplePeer statt WebRTC – testet QR-Pairing, Mesh-Aufbau und Chat</p>
|
||||||
|
<button id="run">Tests ausführen</button>
|
||||||
|
<div id="summary" class="summary" style="display:none"></div>
|
||||||
|
<div id="output"></div>
|
||||||
|
|
||||||
|
<!-- Load dependencies first, with SimplePeer mock injected before p2p.js -->
|
||||||
|
<script src="crypto.js"></script>
|
||||||
|
<script>
|
||||||
|
/* ─── Mock SimplePeer (injected before p2p.js) ─── */
|
||||||
|
const mockPeers = [];
|
||||||
|
|
||||||
|
function clearMockPeers() {
|
||||||
|
for (const p of mockPeers) p._destroyed = true;
|
||||||
|
mockPeers.length = 0;
|
||||||
|
window.SimplePeer = MockSimplePeer;
|
||||||
|
}
|
||||||
|
|
||||||
|
class MockSimplePeer {
|
||||||
|
constructor(opts) {
|
||||||
|
this._initiator = !!opts.initiator;
|
||||||
|
this._destroyed = false;
|
||||||
|
this._connected = false;
|
||||||
|
this._pairKey = null;
|
||||||
|
this._remotePeer = null;
|
||||||
|
this._callbacks = { signal: [], connect: [], data: [], close: [], error: [] };
|
||||||
|
|
||||||
|
mockPeers.push(this);
|
||||||
|
this._mockId = mockPeers.length;
|
||||||
|
|
||||||
|
if (this._initiator) {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (this._destroyed) return;
|
||||||
|
const sdp = 'mock-offer-' + Date.now() + '-' + Math.random().toString(36).slice(2, 8);
|
||||||
|
this._pairKey = sdp;
|
||||||
|
this._emit('signal', { type: 'offer', sdp });
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_emit(event, arg) {
|
||||||
|
for (const cb of this._callbacks[event]) cb(arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
on(event, cb) {
|
||||||
|
this._callbacks[event].push(cb);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
signal(data) {
|
||||||
|
if (this._destroyed) return;
|
||||||
|
|
||||||
|
if (data.type === 'offer' && !this._initiator) {
|
||||||
|
this._pairKey = data.sdp;
|
||||||
|
setTimeout(() => {
|
||||||
|
if (this._destroyed) return;
|
||||||
|
this._emit('signal', { type: 'answer', sdp: 'mock-answer-' + Date.now() + '-' + Math.random().toString(36).slice(2, 8) });
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
} else if (data.type === 'answer' && this._initiator) {
|
||||||
|
const nonInitiator = mockPeers.find(p =>
|
||||||
|
p._pairKey === this._pairKey && p !== this && !p._destroyed
|
||||||
|
);
|
||||||
|
if (nonInitiator) {
|
||||||
|
this._remotePeer = nonInitiator;
|
||||||
|
nonInitiator._remotePeer = this;
|
||||||
|
this._connected = true;
|
||||||
|
nonInitiator._connected = true;
|
||||||
|
setTimeout(() => {
|
||||||
|
if (this._destroyed) return;
|
||||||
|
this._emit('connect');
|
||||||
|
if (!nonInitiator._destroyed) nonInitiator._emit('connect');
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
send(data) {
|
||||||
|
if (this._destroyed || !this._connected || !this._remotePeer) return;
|
||||||
|
const remote = this._remotePeer;
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!remote._destroyed) remote._emit('data', data);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
if (this._destroyed) return;
|
||||||
|
this._destroyed = true;
|
||||||
|
if (this._remotePeer) {
|
||||||
|
this._remotePeer._connected = false;
|
||||||
|
this._remotePeer._remotePeer = null;
|
||||||
|
}
|
||||||
|
this._emit('close');
|
||||||
|
const idx = mockPeers.indexOf(this);
|
||||||
|
if (idx >= 0) mockPeers.splice(idx, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clearMockPeers();
|
||||||
|
</script>
|
||||||
|
<script src="p2p.js"></script>
|
||||||
|
<script>
|
||||||
|
(function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const $ = id => document.getElementById(id);
|
||||||
|
|
||||||
|
/* ─── MeshNet wrapper with event recording ─── */
|
||||||
|
function createTestMesh(name) {
|
||||||
|
const mesh = new MeshNet();
|
||||||
|
mesh.playerName = name;
|
||||||
|
mesh.names.set(mesh.myPeerId, name);
|
||||||
|
mesh._testName = name;
|
||||||
|
mesh._events = [];
|
||||||
|
mesh._chatReceived = [];
|
||||||
|
mesh._qrData = null;
|
||||||
|
mesh._answerData = null;
|
||||||
|
|
||||||
|
mesh.onqrupdate = d => { mesh._qrData = d; };
|
||||||
|
mesh.onanswerready = d => { mesh._answerData = d; };
|
||||||
|
mesh.onchat = (peerId, text) => {
|
||||||
|
mesh._chatReceived.push({ from: peerId, text });
|
||||||
|
};
|
||||||
|
mesh.onpeerconnect = pid => {
|
||||||
|
mesh._events.push({ t: 'connect', pid });
|
||||||
|
};
|
||||||
|
mesh.onpeerdisconnect = pid => {
|
||||||
|
mesh._events.push({ t: 'disconnect', pid });
|
||||||
|
};
|
||||||
|
|
||||||
|
return mesh;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearLogs(meshes) {
|
||||||
|
for (const m of meshes) {
|
||||||
|
m._chatReceived = [];
|
||||||
|
m._events = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function delay(ms) {
|
||||||
|
return new Promise(r => setTimeout(r, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Test: QR-Daten format (encode45/decode45 roundtrip) ─── */
|
||||||
|
async function testQRRoundtrip() {
|
||||||
|
const testData = { v: 1, id: 'aabbccdd', s: { type: 'offer', sdp: 'mock-sdp' } };
|
||||||
|
const packed = await pack(testData);
|
||||||
|
const encoded = encode45(packed);
|
||||||
|
const decoded = decode45(encoded);
|
||||||
|
const unpacked = await unpack(decoded);
|
||||||
|
|
||||||
|
const pass = unpacked.v === 1 && unpacked.id === 'aabbccdd' && unpacked.s.type === 'offer';
|
||||||
|
return {
|
||||||
|
pass,
|
||||||
|
detail: pass
|
||||||
|
? `pack → encode45 → decode45 → unpack OK\nEncoded: ${encoded.slice(0, 40)}…`
|
||||||
|
: `Roundtrip fehlgeschlagen: ${JSON.stringify(unpacked)}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Test: QR-Pairing Host ↔ Peer ─── */
|
||||||
|
async function testBasicPairing() {
|
||||||
|
clearMockPeers();
|
||||||
|
const host = createTestMesh('Host');
|
||||||
|
const peer = createTestMesh('Peer');
|
||||||
|
|
||||||
|
// 1. Host creates room → generates QR
|
||||||
|
host.createRoom();
|
||||||
|
await delay(50);
|
||||||
|
if (!host._qrData) return { pass: false, detail: 'Host QR wurde nicht generiert' };
|
||||||
|
|
||||||
|
let parsed;
|
||||||
|
try {
|
||||||
|
parsed = await unpack(decode45(host._qrData));
|
||||||
|
} catch (e) {
|
||||||
|
return { pass: false, detail: 'Host QR parsing failed: ' + e.message };
|
||||||
|
}
|
||||||
|
if (!parsed.s || parsed.s.type !== 'offer') return { pass: false, detail: 'Host QR enthält kein Offer-Signal' };
|
||||||
|
if (parsed.id !== host.myPeerId) return { pass: false, detail: 'Host QR enthält falsche ID' };
|
||||||
|
|
||||||
|
// 2. Peer joins from QR → generates answer
|
||||||
|
peer.joinFromQR(host._qrData);
|
||||||
|
await delay(50);
|
||||||
|
if (!peer._answerData) return { pass: false, detail: 'Peer Answer QR wurde nicht generiert' };
|
||||||
|
|
||||||
|
try {
|
||||||
|
parsed = await unpack(decode45(peer._answerData));
|
||||||
|
} catch (e) {
|
||||||
|
return { pass: false, detail: 'Peer Answer QR parsing failed: ' + e.message };
|
||||||
|
}
|
||||||
|
if (parsed.s.type !== 'answer') return { pass: false, detail: 'Peer QR ist kein Answer' };
|
||||||
|
if (parsed.to !== host.myPeerId) return { pass: false, detail: 'Peer Answer QR target falsch' };
|
||||||
|
|
||||||
|
// 3. Host feeds answer → connection established
|
||||||
|
await host.feedAnswer(peer._answerData);
|
||||||
|
await delay(50);
|
||||||
|
|
||||||
|
if (host.getPeerIds().length < 1) return { pass: false, detail: 'Host hat keine Verbindung' };
|
||||||
|
|
||||||
|
const pid = Array.from(host.connections.keys())[0];
|
||||||
|
if (host.getPeerName(pid).length === 0) return { pass: false, detail: 'Host kennt Peer-Namen nicht' };
|
||||||
|
if (peer.getPeerName(host.myPeerId).length === 0) return { pass: false, detail: 'Peer kennt Host-Namen nicht' };
|
||||||
|
|
||||||
|
return { pass: true, detail: `Host ↔ Peer: ${host.myPeerId} ↔ ${pid}\nHost peers: ${host.getPeerIds().length}\nPeer peers: ${peer.getPeerIds().length}\nNames: ${host.getPeerName(pid)}, ${peer.getPeerName(host.myPeerId)}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Test: Chat über Pairing-Verbindung ─── */
|
||||||
|
async function testChatAfterPairing() {
|
||||||
|
clearMockPeers();
|
||||||
|
const host = createTestMesh('Host');
|
||||||
|
const peer = createTestMesh('Peer');
|
||||||
|
|
||||||
|
host.createRoom();
|
||||||
|
await delay(30);
|
||||||
|
peer.joinFromQR(host._qrData);
|
||||||
|
await delay(30);
|
||||||
|
await host.feedAnswer(peer._answerData);
|
||||||
|
await delay(60);
|
||||||
|
clearLogs([host, peer]);
|
||||||
|
|
||||||
|
// Host sends chat → peer receives
|
||||||
|
host.broadcast({ type: 'chat', text: 'Hallo von Host' });
|
||||||
|
await delay(20);
|
||||||
|
if (peer._chatReceived.length !== 1) return { pass: false, detail: `Peer empfing ${peer._chatReceived.length} Chats (erwartet: 1)` };
|
||||||
|
if (peer._chatReceived[0].text !== 'Hallo von Host') return { pass: false, detail: 'Falscher Chat-Text bei Peer' };
|
||||||
|
|
||||||
|
// Peer sends chat → host receives
|
||||||
|
clearLogs([host, peer]);
|
||||||
|
peer.broadcast({ type: 'chat', text: 'Hallo von Peer' });
|
||||||
|
await delay(20);
|
||||||
|
if (host._chatReceived.length !== 1) return { pass: false, detail: `Host empfing ${host._chatReceived.length} Chats (erwartet: 1)` };
|
||||||
|
if (host._chatReceived[0].text !== 'Hallo von Peer') return { pass: false, detail: 'Falscher Chat-Text bei Host' };
|
||||||
|
|
||||||
|
return { pass: true, detail: 'Host → Peer: "Hallo von Host" ✓\nPeer → Host: "Hallo von Peer" ✓' };
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Test: onchat Callback ─── */
|
||||||
|
async function testOnchatCallback() {
|
||||||
|
clearMockPeers();
|
||||||
|
const host = createTestMesh('Host');
|
||||||
|
const peer = createTestMesh('Peer');
|
||||||
|
|
||||||
|
host.createRoom();
|
||||||
|
await delay(30);
|
||||||
|
peer.joinFromQR(host._qrData);
|
||||||
|
await delay(30);
|
||||||
|
await host.feedAnswer(peer._answerData);
|
||||||
|
await delay(60);
|
||||||
|
clearLogs([host, peer]);
|
||||||
|
|
||||||
|
const hostId = Array.from(peer.connections.keys())[0];
|
||||||
|
|
||||||
|
peer.sendTo(hostId, { type: 'chat', text: 'Direkter Chat via sendTo' });
|
||||||
|
await delay(20);
|
||||||
|
|
||||||
|
if (host._chatReceived.length !== 1) return { pass: false, detail: `onchat nicht aufgerufen (${host._chatReceived.length} empfangen)` };
|
||||||
|
if (host._chatReceived[0].text !== 'Direkter Chat via sendTo') return { pass: false, detail: `Falscher Text: "${host._chatReceived[0].text}"` };
|
||||||
|
|
||||||
|
return { pass: true, detail: `onchat ausgelöst: "${host._chatReceived[0].text}" von ${host._chatReceived[0].from}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Test: 3 Peers nacheinander paaren + Chat ─── */
|
||||||
|
async function testSequentialPairing() {
|
||||||
|
clearMockPeers();
|
||||||
|
const host = createTestMesh('Host');
|
||||||
|
const peers = [createTestMesh('Peer1'), createTestMesh('Peer2'), createTestMesh('Peer3')];
|
||||||
|
|
||||||
|
host.createRoom();
|
||||||
|
await delay(50);
|
||||||
|
if (!host._qrData) return { pass: false, detail: 'Host QR wurde nicht generiert' };
|
||||||
|
|
||||||
|
for (let i = 0; i < peers.length; i++) {
|
||||||
|
const p = peers[i];
|
||||||
|
p.joinFromQR(host._qrData);
|
||||||
|
await delay(50);
|
||||||
|
if (!p._answerData) return { pass: false, detail: `Peer${i + 1} hat keinen Answer generiert` };
|
||||||
|
await host.feedAnswer(p._answerData);
|
||||||
|
// Wait for identity exchange + host creates new pending peer
|
||||||
|
await delay(500);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (host.getPeerIds().length !== 3) return { pass: false, detail: `Host hat ${host.getPeerIds().length} Peers (erwartet: 3)` };
|
||||||
|
|
||||||
|
let allConnected = true;
|
||||||
|
for (const p of peers) {
|
||||||
|
if (p.getPeerIds().length === 0) { allConnected = false; break; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Broadcast chat to all
|
||||||
|
clearLogs([host, ...peers]);
|
||||||
|
host.broadcast({ type: 'chat', text: 'An alle' });
|
||||||
|
await delay(30);
|
||||||
|
|
||||||
|
const detail = [
|
||||||
|
`Host Peers: ${host.getPeerIds().length}`,
|
||||||
|
...peers.map((p, i) => `Peer${i + 1} connections: ${p.getPeerIds().length}, chats: ${p._chatReceived.length}`),
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
for (let i = 0; i < peers.length; i++) {
|
||||||
|
if (peers[i]._chatReceived.length === 0) {
|
||||||
|
return { pass: false, detail: `Peer${i + 1} hat keinen Chat empfangen\n${detail}` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { pass: true, detail };
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Test: Chat nach Verbindungsabbruch ─── */
|
||||||
|
async function testChatAfterDisconnect() {
|
||||||
|
clearMockPeers();
|
||||||
|
const host = createTestMesh('Host');
|
||||||
|
const peer = createTestMesh('Peer');
|
||||||
|
|
||||||
|
host.createRoom();
|
||||||
|
await delay(30);
|
||||||
|
peer.joinFromQR(host._qrData);
|
||||||
|
await delay(30);
|
||||||
|
await host.feedAnswer(peer._answerData);
|
||||||
|
await delay(60);
|
||||||
|
|
||||||
|
const pid = Array.from(host.connections.keys())[0];
|
||||||
|
host._cleanupPeer(host.connections.get(pid));
|
||||||
|
await delay(20);
|
||||||
|
|
||||||
|
try {
|
||||||
|
host.broadcast({ type: 'chat', text: 'Nach Verbindungsabbruch' });
|
||||||
|
} catch (e) {
|
||||||
|
return { pass: false, detail: 'Chat warf Exception: ' + e.message };
|
||||||
|
}
|
||||||
|
await delay(20);
|
||||||
|
|
||||||
|
return { pass: true, detail: `Chat nach disconnect ohne Fehler\nHost connections: ${host.getPeerIds().length}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Test: Chat broadcast via onchat callback chain ─── */
|
||||||
|
async function testChatBroadcastFlow() {
|
||||||
|
clearMockPeers();
|
||||||
|
const host = createTestMesh('Host');
|
||||||
|
const peer1 = createTestMesh('Peer1');
|
||||||
|
const peer2 = createTestMesh('Peer2');
|
||||||
|
|
||||||
|
// Pair peer1
|
||||||
|
host.createRoom();
|
||||||
|
await delay(30);
|
||||||
|
peer1.joinFromQR(host._qrData);
|
||||||
|
await delay(30);
|
||||||
|
await host.feedAnswer(peer1._answerData);
|
||||||
|
await delay(500);
|
||||||
|
|
||||||
|
// Pair peer2
|
||||||
|
if (!host._qrData) return { pass: false, detail: 'Nach peer1 kein neues QR' };
|
||||||
|
peer2.joinFromQR(host._qrData);
|
||||||
|
await delay(30);
|
||||||
|
await host.feedAnswer(peer2._answerData);
|
||||||
|
await delay(500);
|
||||||
|
|
||||||
|
clearLogs([host, peer1, peer2]);
|
||||||
|
|
||||||
|
// Peer1 sends chat → should reach host (direct) and peer2 (via host broadcast)
|
||||||
|
peer1.broadcast({ type: 'chat', text: 'Von Peer1 an alle' });
|
||||||
|
await delay(30);
|
||||||
|
|
||||||
|
const detail = [
|
||||||
|
`Host chats: ${host._chatReceived.length} (von Peer1)` +
|
||||||
|
(host._chatReceived.length > 0 ? `: "${host._chatReceived[0].text}"` : ''),
|
||||||
|
`Peer2 chats: ${peer2._chatReceived.length}` +
|
||||||
|
(peer2._chatReceived.length > 0 ? `: "${peer2._chatReceived[0].text}"` : ''),
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
if (host._chatReceived.length === 0) return { pass: false, detail: 'Host hat Chat von Peer1 nicht erhalten\n' + detail };
|
||||||
|
if (peer2._chatReceived.length === 0) return { pass: false, detail: 'Peer2 hat Chat von Peer1 nicht erhalten\n' + detail };
|
||||||
|
|
||||||
|
return { pass: true, detail };
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── UI helpers ─── */
|
||||||
|
function addResult(id, label, status, detail) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'test';
|
||||||
|
div.id = id;
|
||||||
|
div.innerHTML = `<h3>${label}</h3><div class="status ${status}">${status === 'pass' ? 'PASS' : status === 'fail' ? 'FAIL' : status === 'skip' ? 'SKIP' : 'RUNNING'}</div><div class="detail">${detail || ''}</div>`;
|
||||||
|
$('output').appendChild(div);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateResult(id, status, detail) {
|
||||||
|
const div = document.getElementById(id);
|
||||||
|
if (div) {
|
||||||
|
div.querySelector('.status').className = 'status ' + status;
|
||||||
|
div.querySelector('.status').textContent = status === 'pass' ? 'PASS' : status === 'fail' ? 'FAIL' : 'RUNNING';
|
||||||
|
if (detail !== undefined) div.querySelector('.detail').textContent = detail;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runTests() {
|
||||||
|
$('run').disabled = true;
|
||||||
|
$('run').textContent = 'Läuft...';
|
||||||
|
$('output').innerHTML = '';
|
||||||
|
$('summary').style.display = 'none';
|
||||||
|
|
||||||
|
const tests = [
|
||||||
|
{ id: 't-qr-roundtrip', label: 'QR-Format: pack/encode45/decode45/unpack', fn: testQRRoundtrip },
|
||||||
|
{ id: 't-basic-pairing', label: 'Basis-Pairing: Host → QR → Peer → Answer → Feed', fn: testBasicPairing },
|
||||||
|
{ id: 't-chat-pairing', label: 'Chat via Pairing: Host ↔ Peer', fn: testChatAfterPairing },
|
||||||
|
{ id: 't-onchat', label: 'onchat Callback bei Chat-Empfang', fn: testOnchatCallback },
|
||||||
|
{ id: 't-sequential', label: '3 Peers nacheinander paaren + Chat', fn: testSequentialPairing },
|
||||||
|
{ id: 't-chat-disconnect', label: 'Chat nach Verbindungsabbruch', fn: testChatAfterDisconnect },
|
||||||
|
{ id: 't-broadcast', label: 'Chat Broadcast zu allen Peers', fn: testChatBroadcastFlow },
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const t of tests) addResult(t.id, t.label, 'running', '');
|
||||||
|
|
||||||
|
let passed = 0, failed = 0;
|
||||||
|
for (const t of tests) {
|
||||||
|
try {
|
||||||
|
const { pass, detail } = await t.fn();
|
||||||
|
updateResult(t.id, pass ? 'pass' : 'fail', detail);
|
||||||
|
if (pass) passed++; else failed++;
|
||||||
|
} catch (e) {
|
||||||
|
updateResult(t.id, 'fail', `Exception: ${e.message}`);
|
||||||
|
failed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const summary = $('summary');
|
||||||
|
summary.style.display = 'block';
|
||||||
|
summary.textContent = `${passed}/${passed + failed} Tests bestanden`;
|
||||||
|
summary.className = 'summary ' + (failed === 0 ? 'pass' : 'fail');
|
||||||
|
$('run').disabled = false;
|
||||||
|
$('run').textContent = 'Tests ausführen';
|
||||||
|
}
|
||||||
|
|
||||||
|
$('run').addEventListener('click', runTests);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
Reference in a new issue