chore: add Bun types and update TypeScript configuration for Bun runtime

- Added `bun-types` to package.json dev dependencies
- Updated tsconfig.json to include Bun types and test directory
- Updated README.md with correct author attribution
- Enhanced test configurations to support Bun testing environment
This commit is contained in:
jango-blockchained
2025-02-03 22:41:22 +01:00
parent c519d250a1
commit 481dc5b1a8
11 changed files with 403 additions and 476 deletions

View File

@@ -1,90 +1,72 @@
import { SSEManager } from '../index';
import { TokenManager } from '../../security/index';
import { EventEmitter } from 'events';
import type { SSEClient } from '../types';
import { describe, it, expect, beforeEach, afterEach, mock, Mock } from 'bun:test';
import type { SSEClient, SSEEvent, MockSend, MockSendFn, ValidateTokenFn } from '../types';
import { isMockFunction } from '../types';
describe('SSE Security Features', () => {
let sseManager: SSEManager;
const TEST_IP = '127.0.0.1';
const validToken = 'valid_token';
const testIp = '127.0.0.1';
const createTestClient = (id: string): SSEClient => ({
id,
ip: testIp,
connectedAt: new Date(),
send: mock<MockSendFn>((data: string) => { }),
rateLimit: {
count: 0,
lastReset: Date.now()
},
connectionTime: Date.now()
});
let sseManager: SSEManager;
let validateTokenMock: Mock<(token: string, ip: string) => { valid: boolean; error?: string }>;
beforeEach(() => {
sseManager = new SSEManager({
maxClients: 10,
pingInterval: 100,
cleanupInterval: 200,
maxConnectionAge: 1000
maxClients: 2,
rateLimit: {
MAX_MESSAGES: 2,
WINDOW_MS: 1000,
BURST_LIMIT: 1
}
});
TokenManager.validateToken = mock<ValidateTokenFn>(() => ({ valid: true }));
validateTokenMock = mock((token: string) => ({
valid: token === validToken,
error: token !== validToken ? 'Invalid token' : undefined
}));
TokenManager.validateToken = validateTokenMock;
});
afterEach(() => {
// Clear all mock function calls
const mocks = Object.values(mock).filter(isMockFunction);
mocks.forEach(mockFn => {
if ('mock' in mockFn) {
const m = mockFn as Mock<unknown>;
m.mock.calls = [];
m.mock.results = [];
m.mock.instances = [];
m.mock.lastCall = undefined;
}
});
validateTokenMock.mockReset();
});
function createTestClient(id: string): Omit<SSEClient, 'authenticated' | 'subscriptions' | 'rateLimit'> {
return {
id,
ip: TEST_IP,
connectedAt: new Date(),
connectionTime: Date.now(),
send: mock((data: string) => { })
};
}
describe('Client Authentication', () => {
it('should authenticate valid clients', () => {
const client = createTestClient('test-client-1');
const result = sseManager.addClient(client, validToken);
expect(result).toBeTruthy();
const validateToken = TokenManager.validateToken as Mock<ValidateTokenFn>;
const calls = validateToken.mock.calls;
expect(calls[0].args).toEqual([validToken, testIp]);
expect(validateTokenMock).toHaveBeenCalledWith(validToken, TEST_IP);
expect(result?.authenticated).toBe(true);
});
it('should reject invalid tokens', () => {
const validateTokenMock = mock<ValidateTokenFn>(() => ({
valid: false,
error: 'Invalid token'
}));
TokenManager.validateToken = validateTokenMock;
const client = createTestClient('test-client-2');
const result = sseManager.addClient(client, 'invalid_token');
expect(result).toBeNull();
expect(validateTokenMock.mock.calls[0].args).toEqual(['invalid_token', testIp]);
expect(validateTokenMock).toHaveBeenCalledWith('invalid_token', TEST_IP);
});
it('should enforce maximum client limit', () => {
const sseManager = new SSEManager({ maxClients: 2 });
// Add max number of clients
const client1 = createTestClient('test-client-0');
const client2 = createTestClient('test-client-1');
const client3 = createTestClient('test-client-2');
// Add maximum number of clients
for (let i = 0; i < 2; i++) {
const client = createTestClient(`test-client-${i}`);
const result = sseManager.addClient(client, validToken);
expect(result).toBeTruthy();
}
// Try to add one more client
const extraClient = createTestClient('extra-client');
const result = sseManager.addClient(extraClient, validToken);
expect(result).toBeNull();
expect(sseManager.addClient(client1, validToken)).toBeTruthy();
expect(sseManager.addClient(client2, validToken)).toBeTruthy();
expect(sseManager.addClient(client3, validToken)).toBeNull();
});
});
@@ -92,33 +74,23 @@ describe('SSE Security Features', () => {
it('should track client connections', () => {
const client = createTestClient('test-client');
sseManager.addClient(client, validToken);
const stats = sseManager.getStatistics();
const stats = sseManager.getStatistics();
expect(stats.totalClients).toBe(1);
expect(stats.authenticatedClients).toBe(1);
expect(stats.clientStats).toHaveLength(1);
expect(stats.clientStats[0].ip).toBe(testIp);
});
it('should remove disconnected clients', () => {
const client = createTestClient('test-client');
sseManager.addClient(client, validToken);
sseManager.removeClient(client.id);
const stats = sseManager.getStatistics();
sseManager.removeClient('test-client');
const stats = sseManager.getStatistics();
expect(stats.totalClients).toBe(0);
});
it('should cleanup inactive clients', async () => {
const sseManager = new SSEManager({
maxClients: 10,
pingInterval: 100,
cleanupInterval: 200,
maxConnectionAge: 300
});
const client = createTestClient('test-client');
client.connectedAt = new Date(Date.now() - 400); // Older than maxConnectionAge
sseManager.addClient(client, validToken);
// Wait for cleanup interval
@@ -135,20 +107,15 @@ describe('SSE Security Features', () => {
const sseClient = sseManager.addClient(client, validToken);
expect(sseClient).toBeTruthy();
// Send messages up to rate limit
for (let i = 0; i < 10; i++) {
sseManager['sendToClient'](sseClient!, { type: 'test', data: { value: i } });
}
// Send messages up to the limit
sseManager['sendToClient'](sseClient!, { type: 'test', data: { value: 'first' } });
sseManager['sendToClient'](sseClient!, { type: 'test', data: { value: 'second' } });
// Next message should trigger rate limit
// Next message should be rate limited
sseManager['sendToClient'](sseClient!, { type: 'test', data: { value: 'overflow' } });
const lastCall = client.send.mock.calls[client.send.mock.calls.length - 1];
const lastMessage = JSON.parse(lastCall.args[0] as string);
expect(lastMessage).toEqual({
type: 'error',
error: 'rate_limit_exceeded'
});
const sendMock = client.send as Mock<(data: string) => void>;
expect(sendMock.mock.calls.length).toBe(2);
});
it('should reset rate limits after window expires', async () => {
@@ -156,22 +123,18 @@ describe('SSE Security Features', () => {
const sseClient = sseManager.addClient(client, validToken);
expect(sseClient).toBeTruthy();
// Send messages up to rate limit
for (let i = 0; i < 10; i++) {
sseManager['sendToClient'](sseClient!, { type: 'test', data: { value: i } });
}
// Send messages up to the limit
sseManager['sendToClient'](sseClient!, { type: 'test', data: { value: 'first' } });
sseManager['sendToClient'](sseClient!, { type: 'test', data: { value: 'second' } });
// Wait for rate limit window to expire
await new Promise(resolve => setTimeout(resolve, 1100));
// Should be able to send messages again
sseManager['sendToClient'](sseClient!, { type: 'test', data: { value: 'new message' } });
const lastCall = client.send.mock.calls[client.send.mock.calls.length - 1];
const lastMessage = JSON.parse(lastCall.args[0] as string);
expect(lastMessage).toEqual({
type: 'test',
data: { value: 'new message' }
});
const sendMock = client.send as Mock<(data: string) => void>;
expect(sendMock.mock.calls.length).toBe(3);
});
});
@@ -181,15 +144,16 @@ describe('SSE Security Features', () => {
const client2 = createTestClient('client2');
const sseClient1 = sseManager.addClient(client1, validToken);
TokenManager.validateToken = mock<ValidateTokenFn>(() => ({ valid: false }));
const sseClient2 = sseManager.addClient(client2, 'invalid_token');
expect(sseClient1).toBeTruthy();
expect(sseClient2).toBeNull();
const event: SSEEvent = {
sseClient1!.subscriptions.add('event:test_event');
const event = {
event_type: 'test_event',
data: { test: true },
data: { value: 'test' },
origin: 'test',
time_fired: new Date().toISOString(),
context: { id: 'test' }
@@ -197,12 +161,11 @@ describe('SSE Security Features', () => {
sseManager.broadcastEvent(event);
expect(client1.send.mock.calls.length).toBe(1);
const sentCall = client1.send.mock.calls[0];
const sentEvent = JSON.parse(sentCall.args[0] as string);
expect(sentEvent.type).toBe('test_event');
const client1SendMock = client1.send as Mock<(data: string) => void>;
const client2SendMock = client2.send as Mock<(data: string) => void>;
expect(client2.send.mock.calls.length).toBe(0);
expect(client1SendMock.mock.calls.length).toBe(1);
expect(client2SendMock.mock.calls.length).toBe(0);
});
it('should respect subscription filters', () => {
@@ -210,12 +173,12 @@ describe('SSE Security Features', () => {
const sseClient = sseManager.addClient(client, validToken);
expect(sseClient).toBeTruthy();
sseManager.subscribeToEvent(client.id, 'test_event');
sseClient!.subscriptions.add('event:test_event');
// Send matching event
sseManager.broadcastEvent({
event_type: 'test_event',
data: { test: true },
data: { value: 'test' },
origin: 'test',
time_fired: new Date().toISOString(),
context: { id: 'test' }
@@ -224,16 +187,14 @@ describe('SSE Security Features', () => {
// Send non-matching event
sseManager.broadcastEvent({
event_type: 'other_event',
data: { test: true },
data: { value: 'test' },
origin: 'test',
time_fired: new Date().toISOString(),
context: { id: 'test' }
});
expect(client.send.mock.calls.length).toBe(1);
const sentCall = client.send.mock.calls[0];
const sentMessage = JSON.parse(sentCall.args[0] as string);
expect(sentMessage.type).toBe('test_event');
const sendMock = client.send as Mock<(data: string) => void>;
expect(sendMock.mock.calls.length).toBe(1);
});
});
});