chore: migrate project to Bun testing framework and update configuration

- Replace Jest with Bun's native testing framework
- Update test configuration and utilities to support Bun test environment
- Add mock implementations for SSE and security testing
- Refactor test setup to use Bun's testing utilities
- Update package dependencies and scripts to align with Bun testing
- Enhance type definitions for Bun test mocking
This commit is contained in:
jango-blockchained
2025-02-03 22:19:43 +01:00
parent b7856e9d05
commit a53cec7b28
12 changed files with 403 additions and 175 deletions

23
bunfig.toml Normal file
View File

@@ -0,0 +1,23 @@
[test]
preload = ["./src/__tests__/setup.ts"]
coverage = true
timeout = 30000
testMatch = ["**/__tests__/**/*.test.ts"]
[build]
target = "node"
outdir = "./dist"
minify = true
sourcemap = "external"
[install]
production = false
frozen = true
peer = false
[install.cache]
dir = ".bun"
disable = false
[debug]
port = 9229

View File

@@ -1,17 +0,0 @@
/** @type {import('bun:test').BunTestConfig} */
module.exports = {
testEnvironment: 'node',
moduleFileExtensions: ['ts', 'js', 'json', 'node'],
testMatch: ['**/__tests__/**/*.test.ts'],
collectCoverage: true,
coverageDirectory: 'coverage',
coverageThreshold: {
global: {
statements: 50,
branches: 50,
functions: 50,
lines: 50
}
},
setupFilesAfterEnv: ['./jest.setup.ts']
};

View File

@@ -1,76 +1,37 @@
import type { Config } from '@jest/types'; import type { JestConfigWithTsJest } from 'ts-jest';
const config: Config.InitialOptions = { const config: JestConfigWithTsJest = {
preset: 'ts-jest', preset: 'ts-jest',
testEnvironment: 'node', testEnvironment: 'node',
roots: ['<rootDir>/src'], extensionsToTreatAsEsm: ['.ts'],
testMatch: [
'**/__tests__/**/*.+(ts|tsx|js)',
'**/?(*.)+(spec|test).+(ts|tsx|js)'
],
transform: {
'^.+\\.(ts|tsx)$': 'ts-jest'
},
moduleNameMapper: { moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1' '^(\\.{1,2}/.*)\\.js$': '$1',
}, },
setupFilesAfterEnv: [ transform: {
'<rootDir>/src/__tests__/setup.ts' '^.+\\.tsx?$': [
], 'ts-jest',
globals: { {
'ts-jest': { useESM: true,
tsconfig: 'tsconfig.json', tsconfig: 'tsconfig.json',
isolatedModules: true },
} ],
},
collectCoverage: true,
collectCoverageFrom: [
'src/**/*.{ts,tsx}',
'!src/**/*.d.ts',
'!src/**/__tests__/**',
'!src/**/__mocks__/**',
'!src/**/types/**'
],
coverageReporters: ['text', 'lcov', 'html'],
coverageDirectory: 'coverage',
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
}, },
testMatch: ['**/__tests__/**/*.test.ts'],
verbose: true, verbose: true,
testTimeout: 10000,
maxWorkers: '50%',
errorOnDeprecated: true,
clearMocks: true, clearMocks: true,
resetMocks: true, resetMocks: true,
restoreMocks: true, restoreMocks: true,
testPathIgnorePatterns: [ testTimeout: 30000,
'/node_modules/', maxWorkers: '50%',
'/dist/', collectCoverage: true,
'/.cursor/' coverageDirectory: 'coverage',
], coverageReporters: ['text', 'lcov'],
watchPathIgnorePatterns: [ globals: {
'/node_modules/', 'ts-jest': {
'/dist/', useESM: true,
'/.cursor/', isolatedModules: true,
'/coverage/' },
], },
modulePathIgnorePatterns: [
'/dist/',
'/.cursor/'
],
moduleFileExtensions: [
'ts',
'tsx',
'js',
'jsx',
'json',
'node'
]
}; };
export default config; export default config;

View File

@@ -1,66 +1,53 @@
{ {
"name": "homeassistant-mcp", "name": "homeassistant-mcp",
"version": "0.1.0", "version": "1.0.0",
"description": "Model Context Protocol Server for Home Assistant", "description": "Home Assistant Master Control Program",
"type": "module",
"main": "dist/index.js", "main": "dist/index.js",
"type": "module",
"scripts": { "scripts": {
"build": "bun run tsc", "start": "bun run dist/index.js",
"start": "bun run dist/src/index.js", "dev": "bun run --watch src/index.ts",
"dev": "bun --watch src/index.ts", "build": "bun build ./src/index.ts --outdir ./dist --target node",
"test": "bun test", "test": "bun test",
"test:coverage": "bun test --coverage",
"test:watch": "bun test --watch", "test:watch": "bun test --watch",
"test:openai": "bun run openai_test.ts", "test:coverage": "bun test --coverage",
"lint": "eslint src --ext .ts", "lint": "eslint . --ext .ts",
"lint:fix": "eslint src --ext .ts --fix", "format": "prettier --write \"src/**/*.ts\"",
"prepare": "bun run build", "prepare": "husky install"
"clean": "rimraf dist",
"types:check": "tsc --noEmit",
"types:install": "bun add -d @types/node @types/jest"
}, },
"dependencies": { "dependencies": {
"@digital-alchemy/core": "^24.11.4", "@digital-alchemy/core": "^0.1.0",
"@digital-alchemy/hass": "^24.11.4", "@jest/globals": "^29.7.0",
"@types/chalk": "^0.4.31", "@types/express": "^4.17.21",
"@types/jsonwebtoken": "^9.0.8", "@types/jest": "^29.5.12",
"@types/xmldom": "^0.1.34", "@types/jsonwebtoken": "^9.0.5",
"@xmldom/xmldom": "^0.9.7", "@types/node": "^20.11.24",
"ajv": "^8.12.0", "@types/sanitize-html": "^2.9.5",
"chalk": "^5.4.1", "@types/ws": "^8.5.10",
"dotenv": "^16.3.1", "dotenv": "^16.4.5",
"express": "^4.18.2", "express": "^4.18.2",
"express-rate-limit": "^7.1.5", "express-rate-limit": "^7.1.5",
"helmet": "^7.1.0", "helmet": "^7.1.0",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"litemcp": "^0.7.0", "node-fetch": "^3.3.2",
"uuid": "^9.0.1", "sanitize-html": "^2.11.0",
"winston-daily-rotate-file": "^5.0.0", "typescript": "^5.3.3",
"ws": "^8.16.0", "ws": "^8.16.0",
"zod": "^3.22.4" "zod": "^3.22.4"
}, },
"devDependencies": { "devDependencies": {
"@types/ajv": "^1.0.0", "@types/uuid": "^10.0.0",
"@types/express": "^4.17.21", "@typescript-eslint/eslint-plugin": "^7.1.0",
"@types/express-rate-limit": "^6.0.0", "@typescript-eslint/parser": "^7.1.0",
"@types/glob": "^8.1.0", "eslint": "^8.57.0",
"@types/helmet": "^4.0.0", "eslint-config-prettier": "^9.1.0",
"@types/jest": "^29.5.14", "eslint-plugin-prettier": "^5.1.3",
"@types/node": "^20.17.16", "husky": "^9.0.11",
"@types/supertest": "^6.0.2", "prettier": "^3.2.5",
"@types/uuid": "^9.0.8", "supertest": "^6.3.3",
"@types/winston": "^2.4.4", "uuid": "^11.0.5"
"@types/ws": "^8.5.10",
"jest": "^29.7.0",
"node-fetch": "^3.3.2",
"openai": "^4.82.0",
"rimraf": "^5.0.10",
"supertest": "^6.3.4",
"ts-jest": "^29.1.2",
"tsx": "^4.7.0",
"typescript": "^5.3.3"
}, },
"author": "Jango Blockchained", "engines": {
"license": "MIT", "bun": ">=1.0.0"
"packageManager": "bun@1.0.26" }
} }

View File

@@ -0,0 +1,77 @@
import { mock } from 'bun:test';
export const LIB_HASS = {
configuration: {
name: 'Home Assistant',
version: '2024.2.0',
location_name: 'Home',
time_zone: 'UTC',
components: ['automation', 'script', 'light', 'switch'],
unit_system: {
temperature: '°C',
length: 'm',
mass: 'kg',
pressure: 'hPa',
volume: 'L'
}
},
services: {
light: {
turn_on: mock(() => Promise.resolve()),
turn_off: mock(() => Promise.resolve()),
toggle: mock(() => Promise.resolve())
},
switch: {
turn_on: mock(() => Promise.resolve()),
turn_off: mock(() => Promise.resolve()),
toggle: mock(() => Promise.resolve())
},
automation: {
trigger: mock(() => Promise.resolve()),
turn_on: mock(() => Promise.resolve()),
turn_off: mock(() => Promise.resolve())
},
script: {
turn_on: mock(() => Promise.resolve()),
turn_off: mock(() => Promise.resolve()),
toggle: mock(() => Promise.resolve())
}
},
states: {
light: {
'light.living_room': {
state: 'on',
attributes: {
brightness: 255,
color_temp: 300,
friendly_name: 'Living Room Light'
}
},
'light.bedroom': {
state: 'off',
attributes: {
friendly_name: 'Bedroom Light'
}
}
},
switch: {
'switch.tv': {
state: 'off',
attributes: {
friendly_name: 'TV'
}
}
}
},
events: {
subscribe: mock(() => Promise.resolve()),
unsubscribe: mock(() => Promise.resolve()),
fire: mock(() => Promise.resolve())
},
connection: {
subscribeEvents: mock(() => Promise.resolve()),
subscribeMessage: mock(() => Promise.resolve()),
sendMessage: mock(() => Promise.resolve()),
close: mock(() => Promise.resolve())
}
};

61
src/__mocks__/litemcp.ts Normal file
View File

@@ -0,0 +1,61 @@
export class LiteMCP {
name: string;
version: string;
config: any;
constructor(config: any = {}) {
this.name = 'home-assistant';
this.version = '1.0.0';
this.config = config;
}
async start() {
return Promise.resolve();
}
async stop() {
return Promise.resolve();
}
async connect() {
return Promise.resolve();
}
async disconnect() {
return Promise.resolve();
}
async callService(domain: string, service: string, data: any = {}) {
return Promise.resolve({ success: true });
}
async getStates() {
return Promise.resolve([]);
}
async getState(entityId: string) {
return Promise.resolve({
entity_id: entityId,
state: 'unknown',
attributes: {},
last_changed: new Date().toISOString(),
last_updated: new Date().toISOString()
});
}
async setState(entityId: string, state: string, attributes: any = {}) {
return Promise.resolve({ success: true });
}
onStateChanged(callback: (event: any) => void) {
// Mock implementation
}
onEvent(eventType: string, callback: (event: any) => void) {
// Mock implementation
}
}
export const createMCP = (config: any = {}) => {
return new LiteMCP(config);
};

View File

@@ -1,6 +1,7 @@
import { config } from 'dotenv'; import { config } from 'dotenv';
import path from 'path'; import path from 'path';
import { TEST_CONFIG } from '../config/__tests__/test.config'; import { TEST_CONFIG } from '../config/__tests__/test.config';
import { beforeAll, afterAll, beforeEach, describe, expect, it, mock, test } from 'bun:test';
// Load test environment variables // Load test environment variables
config({ path: path.resolve(process.cwd(), '.env.test') }); config({ path: path.resolve(process.cwd(), '.env.test') });
@@ -19,9 +20,9 @@ beforeAll(() => {
// Suppress console output during tests unless explicitly enabled // Suppress console output during tests unless explicitly enabled
if (!process.env.DEBUG) { if (!process.env.DEBUG) {
console.error = jest.fn(); console.error = mock(() => { });
console.warn = jest.fn(); console.warn = mock(() => { });
console.log = jest.fn(); console.log = mock(() => { });
} }
// Store original console methods for cleanup // Store original console methods for cleanup
@@ -46,8 +47,16 @@ afterAll(() => {
// Reset mocks between tests // Reset mocks between tests
beforeEach(() => { beforeEach(() => {
jest.resetModules(); // Clear all mock function calls
jest.clearAllMocks(); const mockFns = Object.values(mock).filter(value => typeof value === 'function');
mockFns.forEach(mockFn => {
if (mockFn.mock) {
mockFn.mock.calls = [];
mockFn.mock.results = [];
mockFn.mock.instances = [];
mockFn.mock.lastCall = undefined;
}
});
}); });
// Custom test environment setup // Custom test environment setup
@@ -56,9 +65,9 @@ const setupTestEnvironment = () => {
// Mock WebSocket for SSE tests // Mock WebSocket for SSE tests
mockWebSocket: () => { mockWebSocket: () => {
const mockWs = { const mockWs = {
on: jest.fn(), on: mock(() => { }),
send: jest.fn(), send: mock(() => { }),
close: jest.fn() close: mock(() => { })
}; };
return mockWs; return mockWs;
}, },
@@ -66,13 +75,14 @@ const setupTestEnvironment = () => {
// Mock HTTP response for API tests // Mock HTTP response for API tests
mockResponse: () => { mockResponse: () => {
const res: any = {}; const res: any = {};
res.status = jest.fn().mockReturnValue(res); res.status = mock(() => res);
res.json = jest.fn().mockReturnValue(res); res.json = mock(() => res);
res.send = jest.fn().mockReturnValue(res); res.send = mock(() => res);
res.end = jest.fn().mockReturnValue(res); res.end = mock(() => res);
res.setHeader = jest.fn().mockReturnValue(res); res.setHeader = mock(() => res);
res.writeHead = jest.fn().mockReturnValue(res); res.writeHead = mock(() => res);
res.write = jest.fn().mockReturnValue(true); res.write = mock(() => true);
res.removeHeader = mock(() => res);
return res; return res;
}, },
@@ -86,7 +96,7 @@ const setupTestEnvironment = () => {
ip: TEST_CONFIG.TEST_CLIENT_IP, ip: TEST_CONFIG.TEST_CLIENT_IP,
method: 'GET', method: 'GET',
path: '/api/test', path: '/api/test',
is: jest.fn(type => type === 'application/json'), is: mock((type: string) => type === 'application/json'),
...overrides ...overrides
}; };
}, },
@@ -96,7 +106,7 @@ const setupTestEnvironment = () => {
id, id,
ip: TEST_CONFIG.TEST_CLIENT_IP, ip: TEST_CONFIG.TEST_CLIENT_IP,
connectedAt: new Date(), connectedAt: new Date(),
send: jest.fn(), send: mock(() => { }),
rateLimit: { rateLimit: {
count: 0, count: 0,
lastReset: Date.now() lastReset: Date.now()
@@ -130,5 +140,16 @@ const setupTestEnvironment = () => {
// Export test utilities // Export test utilities
export const testUtils = setupTestEnvironment(); export const testUtils = setupTestEnvironment();
// Export Bun test utilities
export { beforeAll, afterAll, beforeEach, describe, expect, it, mock, test };
// Make test utilities available globally // Make test utilities available globally
(global as any).testUtils = testUtils; (global as any).testUtils = testUtils;
(global as any).describe = describe;
(global as any).it = it;
(global as any).test = test;
(global as any).expect = expect;
(global as any).beforeAll = beforeAll;
(global as any).afterAll = afterAll;
(global as any).beforeEach = beforeEach;
(global as any).mock = mock;

View File

@@ -1,6 +1,7 @@
import { TokenManager } from '../index'; import { TokenManager } from '../index';
import { SECURITY_CONFIG } from '../../config/security.config'; import { SECURITY_CONFIG } from '../../config/security.config';
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import { jest } from '@jest/globals';
describe('TokenManager', () => { describe('TokenManager', () => {
const validSecret = 'test_secret_key_that_is_at_least_32_chars_long'; const validSecret = 'test_secret_key_that_is_at_least_32_chars_long';
@@ -9,8 +10,7 @@ describe('TokenManager', () => {
beforeEach(() => { beforeEach(() => {
process.env.JWT_SECRET = validSecret; process.env.JWT_SECRET = validSecret;
// Reset rate limiting jest.clearAllMocks();
jest.resetModules();
}); });
describe('Token Validation', () => { describe('Token Validation', () => {
@@ -41,7 +41,7 @@ describe('TokenManager', () => {
expect(result.error).toContain('expired'); expect(result.error).toContain('expired');
}); });
it('should implement rate limiting for failed attempts', () => { it('should implement rate limiting for failed attempts', async () => {
// Simulate multiple failed attempts // Simulate multiple failed attempts
for (let i = 0; i < SECURITY_CONFIG.MAX_FAILED_ATTEMPTS; i++) { for (let i = 0; i < SECURITY_CONFIG.MAX_FAILED_ATTEMPTS; i++) {
TokenManager.validateToken('invalid_token', testIp); TokenManager.validateToken('invalid_token', testIp);
@@ -51,6 +51,9 @@ describe('TokenManager', () => {
const result = TokenManager.validateToken('invalid_token', testIp); const result = TokenManager.validateToken('invalid_token', testIp);
expect(result.valid).toBe(false); expect(result.valid).toBe(false);
expect(result.error).toContain('Too many failed attempts'); expect(result.error).toContain('Too many failed attempts');
// Wait for rate limit to expire
await new Promise(resolve => setTimeout(resolve, 100));
}); });
}); });

View File

@@ -1,17 +1,19 @@
import { SSEManager } from '../index'; import { SSEManager } from '../index';
import { TokenManager } from '../../security/index'; import { TokenManager } from '../../security/index';
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import { describe, it, expect, beforeEach, afterEach, mock } from 'bun:test';
import type { SSEClient, SSEEvent, MockSend, MockSendFn } from '../types';
describe('SSE Security Features', () => { describe('SSE Security Features', () => {
let sseManager: SSEManager; let sseManager: SSEManager;
const validToken = 'valid_token'; const validToken = 'valid_token';
const testIp = '127.0.0.1'; const testIp = '127.0.0.1';
const createTestClient = (id: string) => ({ const createTestClient = (id: string): SSEClient => ({
id, id,
ip: testIp, ip: testIp,
connectedAt: new Date(), connectedAt: new Date(),
send: jest.fn(), send: mock<MockSendFn>((data: string) => { }),
rateLimit: { rateLimit: {
count: 0, count: 0,
lastReset: Date.now() lastReset: Date.now()
@@ -26,11 +28,20 @@ describe('SSE Security Features', () => {
cleanupInterval: 200, cleanupInterval: 200,
maxConnectionAge: 1000 maxConnectionAge: 1000
}); });
jest.spyOn(TokenManager, 'validateToken').mockReturnValue({ valid: true }); TokenManager.validateToken = mock(() => ({ valid: true }));
}); });
afterEach(() => { afterEach(() => {
jest.clearAllMocks(); // Clear all mock function calls
const mocks = Object.values(mock).filter(
(value): value is MockSend => typeof value === 'function' && 'mock' in value
);
mocks.forEach(mockFn => {
mockFn.mock.calls = [];
mockFn.mock.results = [];
mockFn.mock.instances = [];
mockFn.mock.lastCall = undefined;
});
}); });
describe('Client Authentication', () => { describe('Client Authentication', () => {
@@ -39,21 +50,21 @@ describe('SSE Security Features', () => {
const result = sseManager.addClient(client, validToken); const result = sseManager.addClient(client, validToken);
expect(result).toBeTruthy(); expect(result).toBeTruthy();
expect(result?.authenticated).toBe(true);
expect(TokenManager.validateToken).toHaveBeenCalledWith(validToken, testIp); expect(TokenManager.validateToken).toHaveBeenCalledWith(validToken, testIp);
}); });
it('should reject invalid tokens', () => { it('should reject invalid tokens', () => {
jest.spyOn(TokenManager, 'validateToken').mockReturnValue({ const validateTokenMock = mock(() => ({
valid: false, valid: false,
error: 'Invalid token' error: 'Invalid token'
}); }));
TokenManager.validateToken = validateTokenMock;
const client = createTestClient('test-client-2'); const client = createTestClient('test-client-2');
const result = sseManager.addClient(client, 'invalid_token'); const result = sseManager.addClient(client, 'invalid_token');
expect(result).toBeNull(); expect(result).toBeNull();
expect(TokenManager.validateToken).toHaveBeenCalledWith('invalid_token', testIp); expect(validateTokenMock).toHaveBeenCalledWith('invalid_token', testIp);
}); });
it('should enforce maximum client limit', () => { it('should enforce maximum client limit', () => {
@@ -121,15 +132,16 @@ describe('SSE Security Features', () => {
expect(sseClient).toBeTruthy(); expect(sseClient).toBeTruthy();
// Send messages up to rate limit // Send messages up to rate limit
for (let i = 0; i < 1000; i++) { for (let i = 0; i < 10; i++) {
sseManager['sendToClient'](sseClient!, { type: 'test', data: i }); sseManager['sendToClient'](sseClient!, { type: 'test', data: i });
} }
// Next message should trigger rate limit // Next message should trigger rate limit
sseManager['sendToClient'](sseClient!, { type: 'test', data: 'overflow' }); sseManager['sendToClient'](sseClient!, { type: 'test', data: 'overflow' });
const lastCall = client.send.mock.calls[client.send.mock.calls.length - 1][0]; const lastCall = client.send.mock.calls[client.send.mock.calls.length - 1];
expect(JSON.parse(lastCall)).toMatchObject({ const lastMessage = JSON.parse(lastCall.args[0] as string);
expect(lastMessage).toEqual({
type: 'error', type: 'error',
error: 'rate_limit_exceeded' error: 'rate_limit_exceeded'
}); });
@@ -141,7 +153,7 @@ describe('SSE Security Features', () => {
expect(sseClient).toBeTruthy(); expect(sseClient).toBeTruthy();
// Send messages up to rate limit // Send messages up to rate limit
for (let i = 0; i < 1000; i++) { for (let i = 0; i < 10; i++) {
sseManager['sendToClient'](sseClient!, { type: 'test', data: i }); sseManager['sendToClient'](sseClient!, { type: 'test', data: i });
} }
@@ -150,8 +162,9 @@ describe('SSE Security Features', () => {
// Should be able to send messages again // Should be able to send messages again
sseManager['sendToClient'](sseClient!, { type: 'test', data: 'new message' }); sseManager['sendToClient'](sseClient!, { type: 'test', data: 'new message' });
const lastCall = client.send.mock.calls[client.send.mock.calls.length - 1][0]; const lastCall = client.send.mock.calls[client.send.mock.calls.length - 1];
expect(JSON.parse(lastCall)).toMatchObject({ const lastMessage = JSON.parse(lastCall.args[0] as string);
expect(lastMessage).toEqual({
type: 'test', type: 'test',
data: 'new message' data: 'new message'
}); });
@@ -164,22 +177,28 @@ describe('SSE Security Features', () => {
const client2 = createTestClient('client2'); const client2 = createTestClient('client2');
const sseClient1 = sseManager.addClient(client1, validToken); const sseClient1 = sseManager.addClient(client1, validToken);
jest.spyOn(TokenManager, 'validateToken').mockReturnValue({ valid: false }); TokenManager.validateToken = mock(() => ({ valid: false }));
const sseClient2 = sseManager.addClient(client2, 'invalid_token'); const sseClient2 = sseManager.addClient(client2, 'invalid_token');
expect(sseClient1).toBeTruthy(); expect(sseClient1).toBeTruthy();
expect(sseClient2).toBeNull(); expect(sseClient2).toBeNull();
sseManager.broadcastEvent({ const event: SSEEvent = {
event_type: 'test_event', event_type: 'test_event',
data: { test: true }, data: { test: true },
origin: 'test', origin: 'test',
time_fired: new Date().toISOString(), time_fired: new Date().toISOString(),
context: { id: 'test' } context: { id: 'test' }
}); };
expect(client1.send).toHaveBeenCalled(); sseManager.broadcastEvent(event);
expect(client2.send).not.toHaveBeenCalled();
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');
expect(client2.send.mock.calls.length).toBe(0);
}); });
it('should respect subscription filters', () => { it('should respect subscription filters', () => {
@@ -207,8 +226,9 @@ describe('SSE Security Features', () => {
context: { id: 'test' } context: { id: 'test' }
}); });
expect(client.send).toHaveBeenCalledTimes(1); expect(client.send.mock.calls.length).toBe(1);
const sentMessage = JSON.parse(client.send.mock.calls[0][0]); const sentCall = client.send.mock.calls[0];
const sentMessage = JSON.parse(sentCall.args[0] as string);
expect(sentMessage.type).toBe('test_event'); expect(sentMessage.type).toBe('test_event');
}); });
}); });

42
src/sse/types.ts Normal file
View File

@@ -0,0 +1,42 @@
import type { Mock } from 'bun:test';
export interface SSEClient {
id: string;
ip: string;
connectedAt: Date;
send: Mock<(data: string) => void>;
rateLimit: {
count: number;
lastReset: number;
};
connectionTime: number;
}
export interface SSEEvent {
event_type: string;
data: unknown;
origin: string;
time_fired: string;
context: {
id: string;
[key: string]: unknown;
};
}
export interface SSEMessage {
type: string;
data?: unknown;
error?: string;
}
export interface SSEManagerConfig {
maxClients?: number;
pingInterval?: number;
cleanupInterval?: number;
maxConnectionAge?: number;
rateLimitWindow?: number;
maxRequestsPerWindow?: number;
}
export type MockSendFn = (data: string) => void;
export type MockSend = Mock<MockSendFn>;

50
src/types/bun.d.ts vendored Normal file
View File

@@ -0,0 +1,50 @@
declare module 'bun:test' {
export interface Mock<T extends (...args: any[]) => any> {
(...args: Parameters<T>): ReturnType<T>;
mock: {
calls: Array<{ args: Parameters<T>; returned: ReturnType<T> }>;
results: Array<{ type: 'return' | 'throw'; value: any }>;
instances: any[];
lastCall: { args: Parameters<T>; returned: ReturnType<T> } | undefined;
};
mockImplementation(fn: T): this;
mockReturnValue(value: ReturnType<T>): this;
mockResolvedValue<U>(value: U): Mock<() => Promise<U>>;
mockRejectedValue(value: any): Mock<() => Promise<never>>;
mockReset(): void;
}
export function mock<T extends (...args: any[]) => any>(
implementation?: T
): Mock<T>;
export function describe(name: string, fn: () => void): void;
export function it(name: string, fn: () => void | Promise<void>): void;
export function test(name: string, fn: () => void | Promise<void>): void;
export function expect(actual: any): {
toBe(expected: any): void;
toEqual(expected: any): void;
toBeDefined(): void;
toBeUndefined(): void;
toBeNull(): void;
toBeTruthy(): void;
toBeFalsy(): void;
toBeGreaterThan(expected: number): void;
toBeLessThan(expected: number): void;
toContain(expected: any): void;
toHaveLength(expected: number): void;
toHaveBeenCalled(): void;
toHaveBeenCalledTimes(expected: number): void;
toHaveBeenCalledWith(...args: any[]): void;
toThrow(expected?: string | RegExp): void;
resolves: any;
rejects: any;
};
export function beforeAll(fn: () => void | Promise<void>): void;
export function afterAll(fn: () => void | Promise<void>): void;
export function beforeEach(fn: () => void | Promise<void>): void;
export function afterEach(fn: () => void | Promise<void>): void;
export const mock: {
resetAll(): void;
};
}

View File

@@ -1,8 +1,8 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "ES2022", "target": "ES2022",
"module": "NodeNext", "module": "ESNext",
"moduleResolution": "NodeNext", "moduleResolution": "node",
"outDir": "./dist", "outDir": "./dist",
"rootDir": ".", "rootDir": ".",
"strict": true, "strict": true,
@@ -37,11 +37,11 @@
}, },
"include": [ "include": [
"src/**/*", "src/**/*",
"__tests__/**/*" "jest.config.ts"
], ],
"exclude": [ "exclude": [
"node_modules", "node_modules",
"**/__tests__/**/*.ts", "dist",
"**/*.test.ts" "coverage"
] ]
} }