162 lines
4.6 KiB
TypeScript
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();
|
|
}
|
|
});
|
|
});
|
|
});
|