/** * Tests for BraidPeerManager * * Verifies that the Braid-HTTP transport layer correctly replaces * the WebSocket PeerManager while maintaining the same event interface. */ import { describe, it, expect, beforeEach, vi } from 'vitest'; import express from 'express'; import { BraidPeerManager } from '../src/transport/braid-peer-manager.js'; describe('BraidPeerManager', () => { let manager: BraidPeerManager; beforeEach(() => { manager = new BraidPeerManager({ nodeId: 'test-node-1', publicKey: 'abc123', privateKey: new Uint8Array(32), host: 'localhost', port: 3007, redisUrl: 'redis://localhost:6379', }); }); describe('interface compatibility', () => { it('should have same public accessors as WebSocket PeerManager', () => { expect(manager.connectedPeers).toBe(0); expect(manager.connectedPeerIds).toEqual([]); expect(manager.getPeer('unknown')).toBeUndefined(); expect(manager.getPeerInfo('unknown')).toBeUndefined(); }); it('should emit peer:connected and peer:disconnected events', () => { const connected = vi.fn(); const disconnected = vi.fn(); manager.on('peer:connected', connected); manager.on('peer:disconnected', disconnected); // Events should be callable (they'll fire when peers connect via HTTP) expect(connected).not.toHaveBeenCalled(); expect(disconnected).not.toHaveBeenCalled(); }); it('should emit consensus message events', () => { const propose = vi.fn(); const vote = vi.fn(); const commit = vi.fn(); manager.on('message:propose', propose); manager.on('message:vote', vote); manager.on('message:commit', commit); expect(propose).not.toHaveBeenCalled(); }); }); describe('router creation', () => { it('should create an Express router with Braid endpoints', () => { const router = manager.createRouter(); expect(router).toBeDefined(); // Verify it can be mounted on an Express app const app = express(); app.use(router); }); }); describe('message handling', () => { it('should handle consensus messages via HTTP POST', async () => { const proposeFn = vi.fn(); manager.on('message:propose', proposeFn); const app = express(); app.use(express.json()); app.use(manager.createRouter()); // Simulate a POST to /braid/consensus const server = app.listen(0); const addr = server.address() as any; try { const response = await fetch(`http://localhost:${addr.port}/braid/consensus`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ type: 'PROPOSE', nodeId: 'test-node-2', timestamp: Date.now(), payload: { tx: {}, nodeSignature: {} }, }), }); expect(response.ok).toBe(true); expect(proposeFn).toHaveBeenCalledTimes(1); } finally { server.close(); } }); it('should reject messages without type', async () => { const app = express(); app.use(express.json()); app.use(manager.createRouter()); const server = app.listen(0); const addr = server.address() as any; try { const response = await fetch(`http://localhost:${addr.port}/braid/consensus`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ nodeId: 'test' }), }); expect(response.status).toBe(400); } finally { server.close(); } }); }); describe('Braid subscription', () => { it('should return peer info on GET /braid/peer-info', async () => { const app = express(); app.use(manager.createRouter()); const server = app.listen(0); const addr = server.address() as any; try { const response = await fetch(`http://localhost:${addr.port}/braid/peer-info`); const data = await response.json() as any; expect(data.nodeId).toBe('test-node-1'); expect(data.publicKey).toBe('abc123'); } finally { server.close(); } }); it('should require X-Node-Id header for sync subscription', async () => { const app = express(); app.use(manager.createRouter()); const server = app.listen(0); const addr = server.address() as any; try { const response = await fetch(`http://localhost:${addr.port}/braid/sync`, { headers: { 'Subscribe': 'true' }, }); expect(response.status).toBe(400); } finally { server.close(); } }); }); });