@ruvector/edge-net 0.4.4 → 0.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/credits.js +631 -0
- package/package.json +9 -1
- package/task-execution-handler.js +868 -0
- package/tests/webrtc-datachannel-e2e-test.js +1081 -0
- package/webrtc.js +15 -16
|
@@ -0,0 +1,1081 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* WebRTC Data Channel End-to-End Test
|
|
4
|
+
*
|
|
5
|
+
* Comprehensive verification of ACTUAL P2P data flow (not just signaling).
|
|
6
|
+
*
|
|
7
|
+
* Tests:
|
|
8
|
+
* 1. WebRTC peer connection establishment via Genesis signaling server
|
|
9
|
+
* 2. Data channel creation and bidirectional messaging
|
|
10
|
+
* 3. Latency measurement for P2P messages
|
|
11
|
+
* 4. Throughput measurement (messages per second, bytes per second)
|
|
12
|
+
* 5. Reconnection after disconnect
|
|
13
|
+
* 6. Large message handling
|
|
14
|
+
*
|
|
15
|
+
* Usage:
|
|
16
|
+
* node tests/webrtc-datachannel-e2e-test.js
|
|
17
|
+
*
|
|
18
|
+
* Environment:
|
|
19
|
+
* GENESIS_SERVER=wss://edge-net-genesis-875130704813.us-central1.run.app
|
|
20
|
+
* TURN_URL=turn:34.72.154.225:3478
|
|
21
|
+
* TURN_USERNAME=edgenet
|
|
22
|
+
* TURN_CREDENTIAL=ruvector2024turn
|
|
23
|
+
*
|
|
24
|
+
* @module @ruvector/edge-net/tests/webrtc-datachannel-e2e-test
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { EventEmitter } from 'events';
|
|
28
|
+
import { randomBytes, createHash } from 'crypto';
|
|
29
|
+
|
|
30
|
+
// ============================================
|
|
31
|
+
// TEST CONFIGURATION
|
|
32
|
+
// ============================================
|
|
33
|
+
|
|
34
|
+
const TEST_CONFIG = {
|
|
35
|
+
// Signaling server
|
|
36
|
+
signalingServer: process.env.GENESIS_SERVER ||
|
|
37
|
+
'wss://edge-net-genesis-875130704813.us-central1.run.app',
|
|
38
|
+
|
|
39
|
+
// TURN server configuration
|
|
40
|
+
turnServer: {
|
|
41
|
+
urls: process.env.TURN_URL || 'turn:34.72.154.225:3478',
|
|
42
|
+
username: process.env.TURN_USERNAME || 'edgenet',
|
|
43
|
+
credential: process.env.TURN_CREDENTIAL || 'ruvector2024turn',
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
// STUN servers (backup)
|
|
47
|
+
stunServers: [
|
|
48
|
+
{ urls: 'stun:34.72.154.225:3478' },
|
|
49
|
+
{ urls: 'stun:stun.l.google.com:19302' },
|
|
50
|
+
],
|
|
51
|
+
|
|
52
|
+
// Test parameters
|
|
53
|
+
connectionTimeout: 30000, // 30 seconds to establish connection
|
|
54
|
+
messageCount: 20, // Number of test messages
|
|
55
|
+
largeMessageSize: 16384, // 16KB large messages
|
|
56
|
+
throughputTestDuration: 5000, // 5 seconds for throughput test
|
|
57
|
+
reconnectTestDelay: 3000, // 3 seconds before reconnect test
|
|
58
|
+
|
|
59
|
+
// Timeouts
|
|
60
|
+
overallTimeout: 120000, // 2 minutes overall test timeout
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// ============================================
|
|
64
|
+
// TEST RESULT TRACKING
|
|
65
|
+
// ============================================
|
|
66
|
+
|
|
67
|
+
class TestResults {
|
|
68
|
+
constructor() {
|
|
69
|
+
this.tests = [];
|
|
70
|
+
this.startTime = Date.now();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
addResult(name, passed, details = {}) {
|
|
74
|
+
this.tests.push({
|
|
75
|
+
name,
|
|
76
|
+
passed,
|
|
77
|
+
details,
|
|
78
|
+
timestamp: Date.now(),
|
|
79
|
+
});
|
|
80
|
+
const icon = passed ? 'PASS' : 'FAIL';
|
|
81
|
+
console.log(` [${icon}] ${name}`);
|
|
82
|
+
if (details.error) {
|
|
83
|
+
console.log(` Error: ${details.error}`);
|
|
84
|
+
}
|
|
85
|
+
if (details.metrics) {
|
|
86
|
+
for (const [key, value] of Object.entries(details.metrics)) {
|
|
87
|
+
console.log(` ${key}: ${value}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
getSummary() {
|
|
93
|
+
const passed = this.tests.filter(t => t.passed).length;
|
|
94
|
+
const failed = this.tests.filter(t => !t.passed).length;
|
|
95
|
+
const duration = Date.now() - this.startTime;
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
total: this.tests.length,
|
|
99
|
+
passed,
|
|
100
|
+
failed,
|
|
101
|
+
duration,
|
|
102
|
+
success: failed === 0,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
printSummary() {
|
|
107
|
+
const summary = this.getSummary();
|
|
108
|
+
console.log('\n' + '='.repeat(60));
|
|
109
|
+
console.log(' TEST SUMMARY');
|
|
110
|
+
console.log('='.repeat(60));
|
|
111
|
+
console.log(` Total tests: ${summary.total}`);
|
|
112
|
+
console.log(` Passed: ${summary.passed}`);
|
|
113
|
+
console.log(` Failed: ${summary.failed}`);
|
|
114
|
+
console.log(` Duration: ${summary.duration}ms`);
|
|
115
|
+
console.log('='.repeat(60));
|
|
116
|
+
console.log(` OVERALL: ${summary.success ? 'PASS' : 'FAIL'}`);
|
|
117
|
+
console.log('='.repeat(60) + '\n');
|
|
118
|
+
|
|
119
|
+
return summary.success;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ============================================
|
|
124
|
+
// WEBRTC PEER CLASS (Simplified for testing)
|
|
125
|
+
// ============================================
|
|
126
|
+
|
|
127
|
+
class TestPeer extends EventEmitter {
|
|
128
|
+
constructor(peerId, isInitiator) {
|
|
129
|
+
super();
|
|
130
|
+
this.peerId = peerId;
|
|
131
|
+
this.isInitiator = isInitiator;
|
|
132
|
+
this.pc = null;
|
|
133
|
+
this.dataChannel = null;
|
|
134
|
+
this.signalingSocket = null;
|
|
135
|
+
this.remotePeerId = null;
|
|
136
|
+
|
|
137
|
+
// Metrics
|
|
138
|
+
this.metrics = {
|
|
139
|
+
messagesSent: 0,
|
|
140
|
+
messagesReceived: 0,
|
|
141
|
+
bytesSent: 0,
|
|
142
|
+
bytesReceived: 0,
|
|
143
|
+
latencies: [],
|
|
144
|
+
connectionStartTime: null,
|
|
145
|
+
connectionEstablishedTime: null,
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// Pending ICE candidates (received before remote description)
|
|
149
|
+
this.pendingCandidates = [];
|
|
150
|
+
|
|
151
|
+
// WebRTC classes
|
|
152
|
+
this._wrtc = null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async loadWebRTC() {
|
|
156
|
+
// Try browser globals first
|
|
157
|
+
if (globalThis.RTCPeerConnection) {
|
|
158
|
+
return {
|
|
159
|
+
RTCPeerConnection: globalThis.RTCPeerConnection,
|
|
160
|
+
RTCSessionDescription: globalThis.RTCSessionDescription,
|
|
161
|
+
RTCIceCandidate: globalThis.RTCIceCandidate,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Load wrtc for Node.js
|
|
166
|
+
try {
|
|
167
|
+
const wrtc = await import('wrtc');
|
|
168
|
+
this._wrtc = wrtc.default || wrtc;
|
|
169
|
+
return {
|
|
170
|
+
RTCPeerConnection: this._wrtc.RTCPeerConnection,
|
|
171
|
+
RTCSessionDescription: this._wrtc.RTCSessionDescription,
|
|
172
|
+
RTCIceCandidate: this._wrtc.RTCIceCandidate,
|
|
173
|
+
};
|
|
174
|
+
} catch (err) {
|
|
175
|
+
throw new Error(`WebRTC not available: ${err.message}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async initialize() {
|
|
180
|
+
const webrtc = await this.loadWebRTC();
|
|
181
|
+
|
|
182
|
+
// Build ICE configuration
|
|
183
|
+
const iceConfig = {
|
|
184
|
+
iceServers: [
|
|
185
|
+
...TEST_CONFIG.stunServers,
|
|
186
|
+
{
|
|
187
|
+
urls: TEST_CONFIG.turnServer.urls,
|
|
188
|
+
username: TEST_CONFIG.turnServer.username,
|
|
189
|
+
credential: TEST_CONFIG.turnServer.credential,
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
urls: TEST_CONFIG.turnServer.urls + '?transport=tcp',
|
|
193
|
+
username: TEST_CONFIG.turnServer.username,
|
|
194
|
+
credential: TEST_CONFIG.turnServer.credential,
|
|
195
|
+
},
|
|
196
|
+
],
|
|
197
|
+
iceTransportPolicy: 'all',
|
|
198
|
+
iceCandidatePoolSize: 10,
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
console.log(` [${this.peerId.slice(0, 8)}] Initializing with ICE config:`,
|
|
202
|
+
iceConfig.iceServers.map(s => s.urls).join(', '));
|
|
203
|
+
|
|
204
|
+
this.pc = new webrtc.RTCPeerConnection(iceConfig);
|
|
205
|
+
this._RTCSessionDescription = webrtc.RTCSessionDescription;
|
|
206
|
+
this._RTCIceCandidate = webrtc.RTCIceCandidate;
|
|
207
|
+
|
|
208
|
+
this.setupEventHandlers();
|
|
209
|
+
|
|
210
|
+
if (this.isInitiator) {
|
|
211
|
+
this.createDataChannel();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
this.metrics.connectionStartTime = Date.now();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
setupEventHandlers() {
|
|
218
|
+
this.pc.onicecandidate = (event) => {
|
|
219
|
+
if (event.candidate) {
|
|
220
|
+
this.emit('ice-candidate', event.candidate);
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
this.pc.oniceconnectionstatechange = () => {
|
|
225
|
+
const state = this.pc.iceConnectionState;
|
|
226
|
+
console.log(` [${this.peerId.slice(0, 8)}] ICE state: ${state}`);
|
|
227
|
+
|
|
228
|
+
if (state === 'connected' || state === 'completed') {
|
|
229
|
+
this.metrics.connectionEstablishedTime = Date.now();
|
|
230
|
+
this.emit('ice-connected');
|
|
231
|
+
} else if (state === 'disconnected' || state === 'failed') {
|
|
232
|
+
this.emit('ice-disconnected', state);
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
this.pc.ondatachannel = (event) => {
|
|
237
|
+
console.log(` [${this.peerId.slice(0, 8)}] Received data channel`);
|
|
238
|
+
this.dataChannel = event.channel;
|
|
239
|
+
this.setupDataChannel();
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
createDataChannel() {
|
|
244
|
+
console.log(` [${this.peerId.slice(0, 8)}] Creating data channel`);
|
|
245
|
+
this.dataChannel = this.pc.createDataChannel('edge-net-e2e-test', {
|
|
246
|
+
ordered: true,
|
|
247
|
+
maxRetransmits: 3,
|
|
248
|
+
});
|
|
249
|
+
this.setupDataChannel();
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
setupDataChannel() {
|
|
253
|
+
if (!this.dataChannel) return;
|
|
254
|
+
|
|
255
|
+
this.dataChannel.onopen = () => {
|
|
256
|
+
console.log(` [${this.peerId.slice(0, 8)}] Data channel OPEN`);
|
|
257
|
+
this.emit('channel-open');
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
this.dataChannel.onclose = () => {
|
|
261
|
+
console.log(` [${this.peerId.slice(0, 8)}] Data channel CLOSED`);
|
|
262
|
+
this.emit('channel-close');
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
this.dataChannel.onerror = (error) => {
|
|
266
|
+
console.error(` [${this.peerId.slice(0, 8)}] Data channel error:`, error);
|
|
267
|
+
this.emit('channel-error', error);
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
this.dataChannel.onmessage = (event) => {
|
|
271
|
+
this.metrics.messagesReceived++;
|
|
272
|
+
this.metrics.bytesReceived += event.data.length;
|
|
273
|
+
this.handleMessage(event.data);
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
handleMessage(data) {
|
|
278
|
+
try {
|
|
279
|
+
const message = JSON.parse(data);
|
|
280
|
+
|
|
281
|
+
// Handle ping/pong for latency measurement
|
|
282
|
+
if (message.type === 'ping') {
|
|
283
|
+
this.send({
|
|
284
|
+
type: 'pong',
|
|
285
|
+
pingTimestamp: message.timestamp,
|
|
286
|
+
pongTimestamp: Date.now(),
|
|
287
|
+
});
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (message.type === 'pong') {
|
|
292
|
+
const latency = Date.now() - message.pingTimestamp;
|
|
293
|
+
this.metrics.latencies.push(latency);
|
|
294
|
+
this.emit('pong', latency);
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
this.emit('message', message);
|
|
299
|
+
} catch (err) {
|
|
300
|
+
// Raw string message
|
|
301
|
+
this.emit('message', data);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
async createOffer() {
|
|
306
|
+
const offer = await this.pc.createOffer();
|
|
307
|
+
await this.pc.setLocalDescription(offer);
|
|
308
|
+
return offer;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async handleOffer(offer) {
|
|
312
|
+
await this.pc.setRemoteDescription(
|
|
313
|
+
new this._RTCSessionDescription(offer)
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
// Process pending ICE candidates
|
|
317
|
+
for (const candidate of this.pendingCandidates) {
|
|
318
|
+
await this.pc.addIceCandidate(new this._RTCIceCandidate(candidate));
|
|
319
|
+
}
|
|
320
|
+
this.pendingCandidates = [];
|
|
321
|
+
|
|
322
|
+
const answer = await this.pc.createAnswer();
|
|
323
|
+
await this.pc.setLocalDescription(answer);
|
|
324
|
+
return answer;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
async handleAnswer(answer) {
|
|
328
|
+
await this.pc.setRemoteDescription(
|
|
329
|
+
new this._RTCSessionDescription(answer)
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
// Process pending ICE candidates
|
|
333
|
+
for (const candidate of this.pendingCandidates) {
|
|
334
|
+
await this.pc.addIceCandidate(new this._RTCIceCandidate(candidate));
|
|
335
|
+
}
|
|
336
|
+
this.pendingCandidates = [];
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async addIceCandidate(candidate) {
|
|
340
|
+
if (this.pc.remoteDescription) {
|
|
341
|
+
await this.pc.addIceCandidate(
|
|
342
|
+
new this._RTCIceCandidate(candidate)
|
|
343
|
+
);
|
|
344
|
+
} else {
|
|
345
|
+
// Queue for later
|
|
346
|
+
this.pendingCandidates.push(candidate);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
send(data) {
|
|
351
|
+
if (!this.dataChannel || this.dataChannel.readyState !== 'open') {
|
|
352
|
+
throw new Error('Data channel not ready');
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const message = typeof data === 'string' ? data : JSON.stringify(data);
|
|
356
|
+
this.dataChannel.send(message);
|
|
357
|
+
this.metrics.messagesSent++;
|
|
358
|
+
this.metrics.bytesSent += message.length;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
sendPing() {
|
|
362
|
+
this.send({
|
|
363
|
+
type: 'ping',
|
|
364
|
+
timestamp: Date.now(),
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
getAverageLatency() {
|
|
369
|
+
if (this.metrics.latencies.length === 0) return 0;
|
|
370
|
+
const sum = this.metrics.latencies.reduce((a, b) => a + b, 0);
|
|
371
|
+
return Math.round(sum / this.metrics.latencies.length);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
getConnectionTime() {
|
|
375
|
+
if (!this.metrics.connectionStartTime || !this.metrics.connectionEstablishedTime) {
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
return this.metrics.connectionEstablishedTime - this.metrics.connectionStartTime;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
close() {
|
|
382
|
+
if (this.dataChannel) {
|
|
383
|
+
this.dataChannel.close();
|
|
384
|
+
}
|
|
385
|
+
if (this.pc) {
|
|
386
|
+
this.pc.close();
|
|
387
|
+
}
|
|
388
|
+
if (this.signalingSocket) {
|
|
389
|
+
this.signalingSocket.close();
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// ============================================
|
|
395
|
+
// SIGNALING HELPER
|
|
396
|
+
// ============================================
|
|
397
|
+
|
|
398
|
+
class SignalingHelper {
|
|
399
|
+
constructor(serverUrl) {
|
|
400
|
+
this.serverUrl = serverUrl;
|
|
401
|
+
this.socket = null;
|
|
402
|
+
this.peerId = null;
|
|
403
|
+
this.emitter = new EventEmitter();
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
async connect(peerId) {
|
|
407
|
+
this.peerId = peerId;
|
|
408
|
+
|
|
409
|
+
// Load WebSocket for Node.js
|
|
410
|
+
const WebSocket = globalThis.WebSocket ||
|
|
411
|
+
(await import('ws')).default;
|
|
412
|
+
|
|
413
|
+
return new Promise((resolve, reject) => {
|
|
414
|
+
const timeout = setTimeout(() => {
|
|
415
|
+
reject(new Error('Signaling connection timeout'));
|
|
416
|
+
}, 10000);
|
|
417
|
+
|
|
418
|
+
try {
|
|
419
|
+
this.socket = new WebSocket(this.serverUrl);
|
|
420
|
+
|
|
421
|
+
this.socket.onopen = () => {
|
|
422
|
+
clearTimeout(timeout);
|
|
423
|
+
console.log(` [Signaling] Connected to ${this.serverUrl}`);
|
|
424
|
+
|
|
425
|
+
// Announce presence
|
|
426
|
+
this.socket.send(JSON.stringify({
|
|
427
|
+
type: 'announce',
|
|
428
|
+
piKey: peerId,
|
|
429
|
+
siteId: `e2e-test-${peerId.slice(0, 8)}`,
|
|
430
|
+
capabilities: ['e2e-test'],
|
|
431
|
+
}));
|
|
432
|
+
|
|
433
|
+
resolve(true);
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
this.socket.onerror = (err) => {
|
|
437
|
+
clearTimeout(timeout);
|
|
438
|
+
reject(new Error(`WebSocket error: ${err.message || 'connection failed'}`));
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
this.socket.onmessage = (event) => {
|
|
442
|
+
try {
|
|
443
|
+
const message = JSON.parse(event.data);
|
|
444
|
+
this.emitter.emit(message.type, message);
|
|
445
|
+
} catch (err) {
|
|
446
|
+
console.error(' [Signaling] Parse error:', err);
|
|
447
|
+
}
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
this.socket.onclose = () => {
|
|
451
|
+
this.emitter.emit('disconnected');
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
} catch (err) {
|
|
455
|
+
clearTimeout(timeout);
|
|
456
|
+
reject(err);
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
send(type, to, data) {
|
|
462
|
+
if (!this.socket || this.socket.readyState !== 1) {
|
|
463
|
+
throw new Error('Signaling socket not connected');
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
this.socket.send(JSON.stringify({
|
|
467
|
+
type,
|
|
468
|
+
to,
|
|
469
|
+
from: this.peerId,
|
|
470
|
+
...data,
|
|
471
|
+
}));
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
on(event, handler) {
|
|
475
|
+
this.emitter.on(event, handler);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
close() {
|
|
479
|
+
if (this.socket) {
|
|
480
|
+
this.socket.close();
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// ============================================
|
|
486
|
+
// E2E TEST RUNNER
|
|
487
|
+
// ============================================
|
|
488
|
+
|
|
489
|
+
async function runE2ETest() {
|
|
490
|
+
console.log('\n' + '='.repeat(60));
|
|
491
|
+
console.log(' WebRTC Data Channel E2E Test');
|
|
492
|
+
console.log(' Testing ACTUAL P2P data flow');
|
|
493
|
+
console.log('='.repeat(60));
|
|
494
|
+
console.log(` Signaling: ${TEST_CONFIG.signalingServer}`);
|
|
495
|
+
console.log(` TURN: ${TEST_CONFIG.turnServer.urls}`);
|
|
496
|
+
console.log('='.repeat(60) + '\n');
|
|
497
|
+
|
|
498
|
+
const results = new TestResults();
|
|
499
|
+
|
|
500
|
+
// Generate peer IDs
|
|
501
|
+
const peerAId = `peer-A-${randomBytes(6).toString('hex')}`;
|
|
502
|
+
const peerBId = `peer-B-${randomBytes(6).toString('hex')}`;
|
|
503
|
+
|
|
504
|
+
console.log(`Peer A: ${peerAId}`);
|
|
505
|
+
console.log(`Peer B: ${peerBId}`);
|
|
506
|
+
console.log('');
|
|
507
|
+
|
|
508
|
+
// Create peers
|
|
509
|
+
const peerA = new TestPeer(peerAId, true); // Initiator
|
|
510
|
+
const peerB = new TestPeer(peerBId, false); // Responder
|
|
511
|
+
|
|
512
|
+
// Create signaling helpers
|
|
513
|
+
const signalingA = new SignalingHelper(TEST_CONFIG.signalingServer);
|
|
514
|
+
const signalingB = new SignalingHelper(TEST_CONFIG.signalingServer);
|
|
515
|
+
|
|
516
|
+
// Track state
|
|
517
|
+
let channelAOpen = false;
|
|
518
|
+
let channelBOpen = false;
|
|
519
|
+
let messagesExchanged = 0;
|
|
520
|
+
let reconnectTested = false;
|
|
521
|
+
|
|
522
|
+
// Setup promise resolvers
|
|
523
|
+
let connectionResolve;
|
|
524
|
+
const connectionPromise = new Promise(r => connectionResolve = r);
|
|
525
|
+
|
|
526
|
+
// ==========================================
|
|
527
|
+
// TEST 1: Signaling Server Connection
|
|
528
|
+
// ==========================================
|
|
529
|
+
console.log('\n[TEST 1] Signaling Server Connection');
|
|
530
|
+
|
|
531
|
+
try {
|
|
532
|
+
await Promise.all([
|
|
533
|
+
signalingA.connect(peerAId),
|
|
534
|
+
signalingB.connect(peerBId),
|
|
535
|
+
]);
|
|
536
|
+
results.addResult('Signaling server connection', true, {
|
|
537
|
+
metrics: {
|
|
538
|
+
'Server': TEST_CONFIG.signalingServer,
|
|
539
|
+
}
|
|
540
|
+
});
|
|
541
|
+
} catch (err) {
|
|
542
|
+
results.addResult('Signaling server connection', false, {
|
|
543
|
+
error: err.message,
|
|
544
|
+
});
|
|
545
|
+
return results.printSummary();
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// ==========================================
|
|
549
|
+
// TEST 2: WebRTC Peer Initialization
|
|
550
|
+
// ==========================================
|
|
551
|
+
console.log('\n[TEST 2] WebRTC Peer Initialization');
|
|
552
|
+
|
|
553
|
+
try {
|
|
554
|
+
await Promise.all([
|
|
555
|
+
peerA.initialize(),
|
|
556
|
+
peerB.initialize(),
|
|
557
|
+
]);
|
|
558
|
+
results.addResult('WebRTC peer initialization', true);
|
|
559
|
+
} catch (err) {
|
|
560
|
+
results.addResult('WebRTC peer initialization', false, {
|
|
561
|
+
error: err.message,
|
|
562
|
+
});
|
|
563
|
+
cleanup();
|
|
564
|
+
return results.printSummary();
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// ==========================================
|
|
568
|
+
// Wire up signaling
|
|
569
|
+
// ==========================================
|
|
570
|
+
|
|
571
|
+
// ICE candidates from A to B
|
|
572
|
+
peerA.on('ice-candidate', (candidate) => {
|
|
573
|
+
signalingA.send('ice-candidate', peerBId, { candidate });
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
// ICE candidates from B to A
|
|
577
|
+
peerB.on('ice-candidate', (candidate) => {
|
|
578
|
+
signalingB.send('ice-candidate', peerAId, { candidate });
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
// Handle ICE candidates
|
|
582
|
+
signalingA.on('ice-candidate', async ({ from, candidate }) => {
|
|
583
|
+
if (from === peerBId) {
|
|
584
|
+
await peerA.addIceCandidate(candidate);
|
|
585
|
+
}
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
signalingB.on('ice-candidate', async ({ from, candidate }) => {
|
|
589
|
+
if (from === peerAId) {
|
|
590
|
+
await peerB.addIceCandidate(candidate);
|
|
591
|
+
}
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
// Handle offers at B
|
|
595
|
+
signalingB.on('offer', async ({ from, offer }) => {
|
|
596
|
+
if (from === peerAId) {
|
|
597
|
+
console.log(` [${peerBId.slice(0, 8)}] Received offer from ${from.slice(0, 8)}`);
|
|
598
|
+
peerB.remotePeerId = from;
|
|
599
|
+
const answer = await peerB.handleOffer(offer);
|
|
600
|
+
signalingB.send('answer', peerAId, { answer });
|
|
601
|
+
}
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
// Handle answers at A
|
|
605
|
+
signalingA.on('answer', async ({ from, answer }) => {
|
|
606
|
+
if (from === peerBId) {
|
|
607
|
+
console.log(` [${peerAId.slice(0, 8)}] Received answer from ${from.slice(0, 8)}`);
|
|
608
|
+
await peerA.handleAnswer(answer);
|
|
609
|
+
}
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
// Track channel state
|
|
613
|
+
peerA.on('channel-open', () => {
|
|
614
|
+
channelAOpen = true;
|
|
615
|
+
if (channelAOpen && channelBOpen) connectionResolve();
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
peerB.on('channel-open', () => {
|
|
619
|
+
channelBOpen = true;
|
|
620
|
+
if (channelAOpen && channelBOpen) connectionResolve();
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
// ==========================================
|
|
624
|
+
// TEST 3: WebRTC Connection Establishment
|
|
625
|
+
// ==========================================
|
|
626
|
+
console.log('\n[TEST 3] WebRTC Connection Establishment');
|
|
627
|
+
|
|
628
|
+
try {
|
|
629
|
+
// Create and send offer
|
|
630
|
+
const offer = await peerA.createOffer();
|
|
631
|
+
peerA.remotePeerId = peerBId;
|
|
632
|
+
signalingA.send('offer', peerBId, { offer });
|
|
633
|
+
|
|
634
|
+
// Wait for connection with timeout
|
|
635
|
+
const connectionTimeout = new Promise((_, reject) => {
|
|
636
|
+
setTimeout(() => reject(new Error('Connection timeout')), TEST_CONFIG.connectionTimeout);
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
await Promise.race([connectionPromise, connectionTimeout]);
|
|
640
|
+
|
|
641
|
+
const connectionTime = peerA.getConnectionTime();
|
|
642
|
+
results.addResult('WebRTC connection establishment', true, {
|
|
643
|
+
metrics: {
|
|
644
|
+
'Connection time': `${connectionTime}ms`,
|
|
645
|
+
}
|
|
646
|
+
});
|
|
647
|
+
} catch (err) {
|
|
648
|
+
results.addResult('WebRTC connection establishment', false, {
|
|
649
|
+
error: err.message,
|
|
650
|
+
});
|
|
651
|
+
cleanup();
|
|
652
|
+
return results.printSummary();
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// ==========================================
|
|
656
|
+
// TEST 4: Bidirectional Message Exchange
|
|
657
|
+
// ==========================================
|
|
658
|
+
console.log('\n[TEST 4] Bidirectional Message Exchange');
|
|
659
|
+
|
|
660
|
+
try {
|
|
661
|
+
const messagesFromA = [];
|
|
662
|
+
const messagesFromB = [];
|
|
663
|
+
|
|
664
|
+
peerA.on('message', (msg) => messagesFromB.push(msg));
|
|
665
|
+
peerB.on('message', (msg) => messagesFromA.push(msg));
|
|
666
|
+
|
|
667
|
+
// Send messages from A to B
|
|
668
|
+
for (let i = 0; i < TEST_CONFIG.messageCount; i++) {
|
|
669
|
+
peerA.send({
|
|
670
|
+
type: 'test',
|
|
671
|
+
from: 'A',
|
|
672
|
+
sequence: i,
|
|
673
|
+
timestamp: Date.now(),
|
|
674
|
+
payload: `Message ${i} from A`,
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// Send messages from B to A
|
|
679
|
+
for (let i = 0; i < TEST_CONFIG.messageCount; i++) {
|
|
680
|
+
peerB.send({
|
|
681
|
+
type: 'test',
|
|
682
|
+
from: 'B',
|
|
683
|
+
sequence: i,
|
|
684
|
+
timestamp: Date.now(),
|
|
685
|
+
payload: `Message ${i} from B`,
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// Wait for messages
|
|
690
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
691
|
+
|
|
692
|
+
const aReceived = messagesFromB.filter(m => m.from === 'B').length;
|
|
693
|
+
const bReceived = messagesFromA.filter(m => m.from === 'A').length;
|
|
694
|
+
|
|
695
|
+
results.addResult('Bidirectional message exchange',
|
|
696
|
+
aReceived >= TEST_CONFIG.messageCount && bReceived >= TEST_CONFIG.messageCount,
|
|
697
|
+
{
|
|
698
|
+
metrics: {
|
|
699
|
+
'A received': `${aReceived}/${TEST_CONFIG.messageCount}`,
|
|
700
|
+
'B received': `${bReceived}/${TEST_CONFIG.messageCount}`,
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
);
|
|
704
|
+
} catch (err) {
|
|
705
|
+
results.addResult('Bidirectional message exchange', false, {
|
|
706
|
+
error: err.message,
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// ==========================================
|
|
711
|
+
// TEST 5: Latency Measurement
|
|
712
|
+
// ==========================================
|
|
713
|
+
console.log('\n[TEST 5] Latency Measurement');
|
|
714
|
+
|
|
715
|
+
try {
|
|
716
|
+
// Send pings from A
|
|
717
|
+
const pingCount = 10;
|
|
718
|
+
for (let i = 0; i < pingCount; i++) {
|
|
719
|
+
peerA.sendPing();
|
|
720
|
+
await new Promise(r => setTimeout(r, 100));
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// Wait for pongs
|
|
724
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
725
|
+
|
|
726
|
+
const avgLatency = peerA.getAverageLatency();
|
|
727
|
+
const minLatency = Math.min(...peerA.metrics.latencies);
|
|
728
|
+
const maxLatency = Math.max(...peerA.metrics.latencies);
|
|
729
|
+
|
|
730
|
+
results.addResult('Latency measurement', peerA.metrics.latencies.length > 0, {
|
|
731
|
+
metrics: {
|
|
732
|
+
'Average latency': `${avgLatency}ms`,
|
|
733
|
+
'Min latency': `${minLatency}ms`,
|
|
734
|
+
'Max latency': `${maxLatency}ms`,
|
|
735
|
+
'Samples': peerA.metrics.latencies.length,
|
|
736
|
+
}
|
|
737
|
+
});
|
|
738
|
+
} catch (err) {
|
|
739
|
+
results.addResult('Latency measurement', false, {
|
|
740
|
+
error: err.message,
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// ==========================================
|
|
745
|
+
// TEST 6: Large Message Handling
|
|
746
|
+
// ==========================================
|
|
747
|
+
console.log('\n[TEST 6] Large Message Handling');
|
|
748
|
+
|
|
749
|
+
try {
|
|
750
|
+
let largeMessageReceived = false;
|
|
751
|
+
const largePayload = randomBytes(TEST_CONFIG.largeMessageSize).toString('hex');
|
|
752
|
+
const messageHash = createHash('sha256').update(largePayload).digest('hex');
|
|
753
|
+
|
|
754
|
+
const largeMessagePromise = new Promise((resolve) => {
|
|
755
|
+
const handler = (msg) => {
|
|
756
|
+
if (msg.type === 'large-test') {
|
|
757
|
+
const receivedHash = createHash('sha256')
|
|
758
|
+
.update(msg.payload)
|
|
759
|
+
.digest('hex');
|
|
760
|
+
if (receivedHash === messageHash) {
|
|
761
|
+
largeMessageReceived = true;
|
|
762
|
+
peerB.removeListener('message', handler);
|
|
763
|
+
resolve();
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
};
|
|
767
|
+
peerB.on('message', handler);
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
peerA.send({
|
|
771
|
+
type: 'large-test',
|
|
772
|
+
size: TEST_CONFIG.largeMessageSize,
|
|
773
|
+
payload: largePayload,
|
|
774
|
+
hash: messageHash,
|
|
775
|
+
});
|
|
776
|
+
|
|
777
|
+
await Promise.race([
|
|
778
|
+
largeMessagePromise,
|
|
779
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), 10000)),
|
|
780
|
+
]);
|
|
781
|
+
|
|
782
|
+
results.addResult('Large message handling', largeMessageReceived, {
|
|
783
|
+
metrics: {
|
|
784
|
+
'Message size': `${Math.round(TEST_CONFIG.largeMessageSize / 1024)}KB`,
|
|
785
|
+
'Integrity verified': 'SHA-256 hash matched',
|
|
786
|
+
}
|
|
787
|
+
});
|
|
788
|
+
} catch (err) {
|
|
789
|
+
results.addResult('Large message handling', false, {
|
|
790
|
+
error: err.message,
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// ==========================================
|
|
795
|
+
// TEST 7: Throughput Measurement
|
|
796
|
+
// ==========================================
|
|
797
|
+
console.log('\n[TEST 7] Throughput Measurement');
|
|
798
|
+
|
|
799
|
+
try {
|
|
800
|
+
const startBytes = peerA.metrics.bytesSent;
|
|
801
|
+
const startMessages = peerA.metrics.messagesSent;
|
|
802
|
+
const startTime = Date.now();
|
|
803
|
+
const testPayload = randomBytes(1024).toString('hex'); // 2KB message
|
|
804
|
+
|
|
805
|
+
// Send as many messages as possible in the duration
|
|
806
|
+
while (Date.now() - startTime < TEST_CONFIG.throughputTestDuration) {
|
|
807
|
+
peerA.send({
|
|
808
|
+
type: 'throughput-test',
|
|
809
|
+
payload: testPayload,
|
|
810
|
+
timestamp: Date.now(),
|
|
811
|
+
});
|
|
812
|
+
// Small delay to prevent overwhelming
|
|
813
|
+
await new Promise(r => setTimeout(r, 10));
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
const duration = (Date.now() - startTime) / 1000;
|
|
817
|
+
const messagesSent = peerA.metrics.messagesSent - startMessages;
|
|
818
|
+
const bytesSent = peerA.metrics.bytesSent - startBytes;
|
|
819
|
+
|
|
820
|
+
const messagesPerSecond = Math.round(messagesSent / duration);
|
|
821
|
+
const bytesPerSecond = Math.round(bytesSent / duration);
|
|
822
|
+
const kbPerSecond = Math.round(bytesPerSecond / 1024);
|
|
823
|
+
|
|
824
|
+
results.addResult('Throughput measurement', messagesSent > 0, {
|
|
825
|
+
metrics: {
|
|
826
|
+
'Duration': `${duration.toFixed(1)}s`,
|
|
827
|
+
'Messages sent': messagesSent,
|
|
828
|
+
'Throughput (msg)': `${messagesPerSecond} msg/s`,
|
|
829
|
+
'Throughput (data)': `${kbPerSecond} KB/s`,
|
|
830
|
+
}
|
|
831
|
+
});
|
|
832
|
+
} catch (err) {
|
|
833
|
+
results.addResult('Throughput measurement', false, {
|
|
834
|
+
error: err.message,
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// ==========================================
|
|
839
|
+
// TEST 8: Connection Metrics Summary
|
|
840
|
+
// ==========================================
|
|
841
|
+
console.log('\n[TEST 8] Connection Metrics Summary');
|
|
842
|
+
|
|
843
|
+
try {
|
|
844
|
+
const totalBytesSent = peerA.metrics.bytesSent + peerB.metrics.bytesSent;
|
|
845
|
+
const totalBytesReceived = peerA.metrics.bytesReceived + peerB.metrics.bytesReceived;
|
|
846
|
+
const totalMessagesSent = peerA.metrics.messagesSent + peerB.metrics.messagesSent;
|
|
847
|
+
const totalMessagesReceived = peerA.metrics.messagesReceived + peerB.metrics.messagesReceived;
|
|
848
|
+
|
|
849
|
+
results.addResult('Connection metrics summary', true, {
|
|
850
|
+
metrics: {
|
|
851
|
+
'Total messages sent': totalMessagesSent,
|
|
852
|
+
'Total messages received': totalMessagesReceived,
|
|
853
|
+
'Total bytes sent': `${Math.round(totalBytesSent / 1024)} KB`,
|
|
854
|
+
'Total bytes received': `${Math.round(totalBytesReceived / 1024)} KB`,
|
|
855
|
+
'Message delivery rate': `${Math.round(totalMessagesReceived / totalMessagesSent * 100)}%`,
|
|
856
|
+
}
|
|
857
|
+
});
|
|
858
|
+
} catch (err) {
|
|
859
|
+
results.addResult('Connection metrics summary', false, {
|
|
860
|
+
error: err.message,
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// ==========================================
|
|
865
|
+
// TEST 9: Graceful Disconnect
|
|
866
|
+
// ==========================================
|
|
867
|
+
console.log('\n[TEST 9] Graceful Disconnect');
|
|
868
|
+
|
|
869
|
+
try {
|
|
870
|
+
let disconnectDetected = false;
|
|
871
|
+
|
|
872
|
+
peerB.on('channel-close', () => {
|
|
873
|
+
disconnectDetected = true;
|
|
874
|
+
});
|
|
875
|
+
|
|
876
|
+
// Close peer A's data channel
|
|
877
|
+
peerA.dataChannel.close();
|
|
878
|
+
|
|
879
|
+
// Wait for disconnect detection
|
|
880
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
881
|
+
|
|
882
|
+
results.addResult('Graceful disconnect', disconnectDetected, {
|
|
883
|
+
metrics: {
|
|
884
|
+
'Disconnect detected by peer B': disconnectDetected ? 'Yes' : 'No',
|
|
885
|
+
}
|
|
886
|
+
});
|
|
887
|
+
} catch (err) {
|
|
888
|
+
results.addResult('Graceful disconnect', false, {
|
|
889
|
+
error: err.message,
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
// ==========================================
|
|
894
|
+
// TEST 10: ICE Candidate Types Analysis
|
|
895
|
+
// ==========================================
|
|
896
|
+
console.log('\n[TEST 10] ICE Candidate Types Analysis');
|
|
897
|
+
|
|
898
|
+
try {
|
|
899
|
+
// Get ICE candidate stats from peer connections if available
|
|
900
|
+
const statsA = await peerA.pc.getStats();
|
|
901
|
+
const statsB = await peerB.pc.getStats();
|
|
902
|
+
|
|
903
|
+
const candidateTypes = new Set();
|
|
904
|
+
const connectionTypes = [];
|
|
905
|
+
|
|
906
|
+
statsA.forEach((report) => {
|
|
907
|
+
if (report.type === 'candidate-pair' && report.state === 'succeeded') {
|
|
908
|
+
connectionTypes.push({
|
|
909
|
+
localType: report.localCandidateId,
|
|
910
|
+
remoteType: report.remoteCandidateId,
|
|
911
|
+
nominated: report.nominated,
|
|
912
|
+
});
|
|
913
|
+
}
|
|
914
|
+
if (report.type === 'local-candidate' || report.type === 'remote-candidate') {
|
|
915
|
+
candidateTypes.add(report.candidateType);
|
|
916
|
+
}
|
|
917
|
+
});
|
|
918
|
+
|
|
919
|
+
const candidateList = Array.from(candidateTypes);
|
|
920
|
+
const hasRelay = candidateList.includes('relay');
|
|
921
|
+
|
|
922
|
+
results.addResult('ICE candidate types analysis', candidateList.length > 0, {
|
|
923
|
+
metrics: {
|
|
924
|
+
'Candidate types found': candidateList.join(', ') || 'none',
|
|
925
|
+
'TURN relay used': hasRelay ? 'Yes' : 'No',
|
|
926
|
+
'Connection established': 'Yes',
|
|
927
|
+
}
|
|
928
|
+
});
|
|
929
|
+
} catch (err) {
|
|
930
|
+
results.addResult('ICE candidate types analysis', false, {
|
|
931
|
+
error: err.message,
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
// ==========================================
|
|
936
|
+
// Cleanup
|
|
937
|
+
// ==========================================
|
|
938
|
+
function cleanup() {
|
|
939
|
+
console.log('\n Cleaning up...');
|
|
940
|
+
peerA.close();
|
|
941
|
+
peerB.close();
|
|
942
|
+
signalingA.close();
|
|
943
|
+
signalingB.close();
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
cleanup();
|
|
947
|
+
|
|
948
|
+
// Print and return results
|
|
949
|
+
return results.printSummary();
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
// ==========================================
|
|
953
|
+
// QUICK CONNECTIVITY TEST
|
|
954
|
+
// ==========================================
|
|
955
|
+
|
|
956
|
+
async function runQuickTest() {
|
|
957
|
+
console.log('\n' + '='.repeat(60));
|
|
958
|
+
console.log(' Quick WebRTC Connectivity Test');
|
|
959
|
+
console.log('='.repeat(60) + '\n');
|
|
960
|
+
|
|
961
|
+
// Test 1: Check wrtc availability
|
|
962
|
+
console.log('[1] Checking wrtc module...');
|
|
963
|
+
try {
|
|
964
|
+
const wrtc = await import('wrtc');
|
|
965
|
+
console.log(' wrtc module loaded successfully');
|
|
966
|
+
} catch (err) {
|
|
967
|
+
console.log(' FAILED: wrtc not available -', err.message);
|
|
968
|
+
return false;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
// Test 2: Check signaling server
|
|
972
|
+
console.log('[2] Checking signaling server...');
|
|
973
|
+
try {
|
|
974
|
+
const WebSocket = (await import('ws')).default;
|
|
975
|
+
const ws = new WebSocket(TEST_CONFIG.signalingServer);
|
|
976
|
+
|
|
977
|
+
await new Promise((resolve, reject) => {
|
|
978
|
+
const timeout = setTimeout(() => reject(new Error('Timeout')), 5000);
|
|
979
|
+
ws.onopen = () => {
|
|
980
|
+
clearTimeout(timeout);
|
|
981
|
+
ws.close();
|
|
982
|
+
resolve();
|
|
983
|
+
};
|
|
984
|
+
ws.onerror = () => {
|
|
985
|
+
clearTimeout(timeout);
|
|
986
|
+
reject(new Error('Connection failed'));
|
|
987
|
+
};
|
|
988
|
+
});
|
|
989
|
+
console.log(' Signaling server reachable');
|
|
990
|
+
} catch (err) {
|
|
991
|
+
console.log(' WARNING: Signaling server unreachable -', err.message);
|
|
992
|
+
console.log(' (Test will use inline peer exchange)');
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
// Test 3: Basic peer connection
|
|
996
|
+
console.log('[3] Testing basic peer connection...');
|
|
997
|
+
try {
|
|
998
|
+
const wrtc = (await import('wrtc')).default;
|
|
999
|
+
const pc = new wrtc.RTCPeerConnection({
|
|
1000
|
+
iceServers: TEST_CONFIG.stunServers,
|
|
1001
|
+
});
|
|
1002
|
+
|
|
1003
|
+
const offer = await pc.createOffer();
|
|
1004
|
+
await pc.setLocalDescription(offer);
|
|
1005
|
+
pc.close();
|
|
1006
|
+
console.log(' Peer connection works');
|
|
1007
|
+
} catch (err) {
|
|
1008
|
+
console.log(' FAILED: Peer connection error -', err.message);
|
|
1009
|
+
return false;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
console.log('\n Quick test passed - ready for E2E test');
|
|
1013
|
+
return true;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
// ==========================================
|
|
1017
|
+
// MAIN ENTRY POINT
|
|
1018
|
+
// ==========================================
|
|
1019
|
+
|
|
1020
|
+
async function main() {
|
|
1021
|
+
const args = process.argv.slice(2);
|
|
1022
|
+
|
|
1023
|
+
// Set overall timeout
|
|
1024
|
+
const timeoutHandle = setTimeout(() => {
|
|
1025
|
+
console.error('\n\nOVERALL TEST TIMEOUT - Exiting');
|
|
1026
|
+
process.exit(1);
|
|
1027
|
+
}, TEST_CONFIG.overallTimeout);
|
|
1028
|
+
|
|
1029
|
+
try {
|
|
1030
|
+
if (args.includes('--quick')) {
|
|
1031
|
+
const success = await runQuickTest();
|
|
1032
|
+
clearTimeout(timeoutHandle);
|
|
1033
|
+
process.exit(success ? 0 : 1);
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
1037
|
+
console.log(`
|
|
1038
|
+
WebRTC Data Channel E2E Test
|
|
1039
|
+
|
|
1040
|
+
Usage:
|
|
1041
|
+
node tests/webrtc-datachannel-e2e-test.js [options]
|
|
1042
|
+
|
|
1043
|
+
Options:
|
|
1044
|
+
--quick Run quick connectivity check only
|
|
1045
|
+
--help Show this help message
|
|
1046
|
+
|
|
1047
|
+
Environment Variables:
|
|
1048
|
+
GENESIS_SERVER Signaling server URL (default: wss://edge-net-genesis-...)
|
|
1049
|
+
TURN_URL TURN server URL (default: turn:34.72.154.225:3478)
|
|
1050
|
+
TURN_USERNAME TURN username (default: edgenet)
|
|
1051
|
+
TURN_CREDENTIAL TURN credential (default: ruvector2024turn)
|
|
1052
|
+
|
|
1053
|
+
Tests Performed:
|
|
1054
|
+
1. Signaling server connection
|
|
1055
|
+
2. WebRTC peer initialization
|
|
1056
|
+
3. WebRTC connection establishment
|
|
1057
|
+
4. Bidirectional message exchange
|
|
1058
|
+
5. Latency measurement
|
|
1059
|
+
6. Large message handling (16KB)
|
|
1060
|
+
7. Throughput measurement
|
|
1061
|
+
8. Connection metrics summary
|
|
1062
|
+
9. Graceful disconnect
|
|
1063
|
+
10. ICE candidate types analysis
|
|
1064
|
+
`);
|
|
1065
|
+
clearTimeout(timeoutHandle);
|
|
1066
|
+
process.exit(0);
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
// Run full E2E test
|
|
1070
|
+
const success = await runE2ETest();
|
|
1071
|
+
clearTimeout(timeoutHandle);
|
|
1072
|
+
process.exit(success ? 0 : 1);
|
|
1073
|
+
|
|
1074
|
+
} catch (err) {
|
|
1075
|
+
clearTimeout(timeoutHandle);
|
|
1076
|
+
console.error('\nTest error:', err);
|
|
1077
|
+
process.exit(1);
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
main();
|