braid-transport/test/braid-peer-manager.test.ts

162 lines
4.6 KiB
TypeScript

/**
* 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();
}
});
});
});