randomp2p/test-pairing.html
Niels Göttsch fc414239f8
All checks were successful
Pin to IPFS / pin (push) Successful in 12s
[IMP] add next cryto task
2026-06-14 18:52:31 +02:00

463 lines
17 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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>