// E2E test: Two clients connect to relay node and exchange messages const net = require('net'); const http = require('http'); const SERVER = 'http://localhost:3001'; const RELAY_HOST = '127.0.0.1'; const RELAY_PORT = 4101; function api(path, method = 'GET', body, token) { return new Promise((resolve, reject) => { const url = new URL(path, SERVER); const data = body ? JSON.stringify(body) : undefined; const opts = { hostname: url.hostname, port: url.port, path: url.pathname, method, headers: { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}), ...(data ? { 'Content-Length': Buffer.byteLength(data) } : {}), }, }; const req = http.request(opts, (res) => { let b = ''; res.on('data', c => b += c); res.on('end', () => { try { resolve(JSON.parse(b)) } catch { resolve({error: b}) } }); }); req.on('error', reject); if (data) req.write(data); req.end(); }); } function frameParser() { let buf = Buffer.alloc(0); return { push(chunk) { buf = Buffer.concat([buf, chunk]); const msgs = []; while (buf.length >= 4) { const len = buf.readUInt32BE(0); if (buf.length < 4 + len) break; try { msgs.push(JSON.parse(buf.subarray(4, 4 + len).toString())); } catch {} buf = buf.subarray(4 + len); } return msgs; } }; } function encodeMsg(msg) { const json = JSON.stringify(msg); const payload = Buffer.from(json, 'utf-8'); const header = Buffer.alloc(4); header.writeUInt32BE(payload.length, 0); return Buffer.concat([header, payload]); } async function main() { console.log('=== E2E Relay Test ===\n'); // 1. Register & login console.log('1. Register user...'); const reg = await api('/api/auth/register', 'POST', { email: 'e2e@test.com', password: '123456' }); const token = reg.data.token; console.log(' OK, user_id:', reg.data.user.id); // 2. Register two devices console.log('2. Register devices...'); const d1 = await api('/api/devices/register', 'POST', { device_name: 'Client-A', device_fingerprint: 'e2e-fp-a', public_key: '' }, token); const d2 = await api('/api/devices/register', 'POST', { device_name: 'Client-B', device_fingerprint: 'e2e-fp-b', public_key: '' }, token); const devA = d1.data.id, devB = d2.data.id; console.log(' OK, Device-A:', devA, 'Device-B:', devB); // 3. Get relay node (assume already running) console.log('3. Get relay node...'); const nodes = await api('/api/nodes/available', 'GET', null, token); if (!nodes.data || nodes.data.length === 0) { console.error(' FAIL: No relay nodes available. Make sure relay node is running.'); process.exit(1); } const node = nodes.data[0]; console.log(' OK, Node:', node.name, node.host + ':' + node.port); // 4. Request connection token for Device-A console.log('4. Request connection token...'); const conn = await api('/api/connections/request', 'POST', { device_id: devA, node_id: node.id }, token); const connToken = conn.data.token; console.log(' OK, Token obtained'); // 5. Connect Device-A to relay console.log('5. Connect Device-A to relay...'); const socketA = new net.Socket(); const parserA = frameParser(); await new Promise((resolve, reject) => { socketA.connect(node.port, node.host, () => { // Send auth socketA.write(encodeMsg({ type: 'auth', from_device_id: devA, to_device_id: 0, payload: JSON.stringify({ action: 'connect', device_id: devA, token: connToken }), timestamp: Date.now(), message_id: 'auth-1', })); }); socketA.on('data', (chunk) => { for (const msg of parserA.push(chunk)) { if (msg.type === 'control') { const ctrl = JSON.parse(msg.payload); if (ctrl.action === 'connect_ok') { console.log(' OK, Device-A authenticated'); resolve(null); } } } }); socketA.on('error', reject); setTimeout(() => reject(new Error('Auth timeout')), 5000); }); // 6. Connect Device-B to relay (simulate another client using same token pattern) console.log('6. Request token for Device-B and connect...'); const conn2 = await api('/api/connections/request', 'POST', { device_id: devB, node_id: node.id }, token); const connToken2 = conn2.data.token; const socketB = new net.Socket(); const parserB = frameParser(); await new Promise((resolve, reject) => { socketB.connect(node.port, node.host, () => { socketB.write(encodeMsg({ type: 'auth', from_device_id: devB, to_device_id: 0, payload: JSON.stringify({ action: 'connect', device_id: devB, token: connToken2 }), timestamp: Date.now(), message_id: 'auth-2', })); }); socketB.on('data', (chunk) => { for (const msg of parserB.push(chunk)) { if (msg.type === 'control') { if (JSON.parse(msg.payload).action === 'connect_ok') { console.log(' OK, Device-B authenticated'); resolve(null); } } } }); socketB.on('error', reject); setTimeout(() => reject(new Error('Auth timeout')), 5000); }); // 7. Device-A sends message to Device-B console.log('7. Device-A -> Device-B: "Hello from A!"'); let msgReceived = false; socketB.on('data', (chunk) => { for (const msg of parserB.push(chunk)) { if (msg.type === 'data' && msg.payload === 'Hello from A!') { console.log(' OK, Device-B received: "' + msg.payload + '"'); msgReceived = true; } } }); socketA.write(encodeMsg({ type: 'data', from_device_id: devA, to_device_id: devB, payload: 'Hello from A!', timestamp: Date.now(), message_id: 'msg-1', })); // Wait for message delivery await new Promise(r => setTimeout(r, 1000)); // 8. Device-B sends reply console.log('8. Device-B -> Device-A: "Hello from B!"'); let replyReceived = false; socketA.on('data', (chunk) => { for (const msg of parserA.push(chunk)) { if (msg.type === 'data' && msg.payload === 'Hello from B!') { console.log(' OK, Device-A received: "' + msg.payload + '"'); replyReceived = true; } } }); socketB.write(encodeMsg({ type: 'data', from_device_id: devB, to_device_id: devA, payload: 'Hello from B!', timestamp: Date.now(), message_id: 'msg-2', })); await new Promise(r => setTimeout(r, 1000)); // Cleanup socketA.destroy(); socketB.destroy(); console.log('\n=== Results ==='); console.log('Message A->B:', msgReceived ? '✅ PASS' : '❌ FAIL'); console.log('Message B->A:', replyReceived ? '✅ PASS' : '❌ FAIL'); console.log('\n=== E2E Test Complete ==='); process.exit(msgReceived && replyReceived ? 0 : 1); } main().catch(err => { console.error('Test failed:', err); process.exit(1); });