@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.
@@ -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();