test: add comprehensive test suite for security and SSE components

- Implemented detailed Jest test configurations for project
- Added test configuration with robust environment setup
- Created comprehensive test suites for:
  * Security middleware
  * Token management
  * SSE security features
- Configured test utilities with mock request/response objects
- Implemented extensive test scenarios covering authentication, rate limiting, and error handling
This commit is contained in:
jango-blockchained
2025-02-03 22:08:16 +01:00
parent a814c427e9
commit 7891115ebe
6 changed files with 978 additions and 0 deletions

134
src/__tests__/setup.ts Normal file
View File

@@ -0,0 +1,134 @@
import { config } from 'dotenv';
import path from 'path';
import { TEST_CONFIG } from '../config/__tests__/test.config';
// Load test environment variables
config({ path: path.resolve(process.cwd(), '.env.test') });
// Global test setup
beforeAll(() => {
// Set required environment variables
process.env.NODE_ENV = 'test';
process.env.JWT_SECRET = TEST_CONFIG.TEST_JWT_SECRET;
process.env.TEST_TOKEN = TEST_CONFIG.TEST_TOKEN;
// Configure console output for tests
const originalConsoleError = console.error;
const originalConsoleWarn = console.warn;
const originalConsoleLog = console.log;
// Suppress console output during tests unless explicitly enabled
if (!process.env.DEBUG) {
console.error = jest.fn();
console.warn = jest.fn();
console.log = jest.fn();
}
// Store original console methods for cleanup
(global as any).__ORIGINAL_CONSOLE__ = {
error: originalConsoleError,
warn: originalConsoleWarn,
log: originalConsoleLog
};
});
// Global test teardown
afterAll(() => {
// Restore original console methods
const originalConsole = (global as any).__ORIGINAL_CONSOLE__;
if (originalConsole) {
console.error = originalConsole.error;
console.warn = originalConsole.warn;
console.log = originalConsole.log;
delete (global as any).__ORIGINAL_CONSOLE__;
}
});
// Reset mocks between tests
beforeEach(() => {
jest.resetModules();
jest.clearAllMocks();
});
// Custom test environment setup
const setupTestEnvironment = () => {
return {
// Mock WebSocket for SSE tests
mockWebSocket: () => {
const mockWs = {
on: jest.fn(),
send: jest.fn(),
close: jest.fn()
};
return mockWs;
},
// Mock HTTP response for API tests
mockResponse: () => {
const res: any = {};
res.status = jest.fn().mockReturnValue(res);
res.json = jest.fn().mockReturnValue(res);
res.send = jest.fn().mockReturnValue(res);
res.end = jest.fn().mockReturnValue(res);
res.setHeader = jest.fn().mockReturnValue(res);
res.writeHead = jest.fn().mockReturnValue(res);
res.write = jest.fn().mockReturnValue(true);
return res;
},
// Mock HTTP request for API tests
mockRequest: (overrides = {}) => {
return {
headers: { 'content-type': 'application/json' },
body: {},
query: {},
params: {},
ip: TEST_CONFIG.TEST_CLIENT_IP,
method: 'GET',
path: '/api/test',
is: jest.fn(type => type === 'application/json'),
...overrides
};
},
// Create test client for SSE tests
createTestClient: (id: string = 'test-client') => ({
id,
ip: TEST_CONFIG.TEST_CLIENT_IP,
connectedAt: new Date(),
send: jest.fn(),
rateLimit: {
count: 0,
lastReset: Date.now()
},
connectionTime: Date.now()
}),
// Create test event for SSE tests
createTestEvent: (type: string = 'test_event', data: any = {}) => ({
event_type: type,
data,
origin: 'test',
time_fired: new Date().toISOString(),
context: { id: 'test' }
}),
// Create test entity for Home Assistant tests
createTestEntity: (entityId: string = 'test.entity', state: string = 'on') => ({
entity_id: entityId,
state,
attributes: {},
last_changed: new Date().toISOString(),
last_updated: new Date().toISOString()
}),
// Helper to wait for async operations
wait: (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
};
};
// Export test utilities
export const testUtils = setupTestEnvironment();
// Make test utilities available globally
(global as any).testUtils = testUtils;

View File

@@ -0,0 +1,135 @@
import { z } from 'zod';
// Test configuration schema
const testConfigSchema = z.object({
// Test Environment
TEST_PORT: z.number().default(3001),
TEST_HOST: z.string().default('http://localhost'),
TEST_WEBSOCKET_PORT: z.number().default(3002),
// Mock Authentication
TEST_JWT_SECRET: z.string().default('test_jwt_secret_key_that_is_at_least_32_chars'),
TEST_TOKEN: z.string().default('test_token_that_is_at_least_32_chars_long'),
TEST_INVALID_TOKEN: z.string().default('invalid_token'),
// Mock Client Settings
TEST_CLIENT_IP: z.string().default('127.0.0.1'),
TEST_MAX_CLIENTS: z.number().default(10),
TEST_PING_INTERVAL: z.number().default(100),
TEST_CLEANUP_INTERVAL: z.number().default(200),
TEST_MAX_CONNECTION_AGE: z.number().default(1000),
// Mock Rate Limiting
TEST_RATE_LIMIT_WINDOW: z.number().default(60000), // 1 minute
TEST_RATE_LIMIT_MAX_REQUESTS: z.number().default(100),
TEST_RATE_LIMIT_WEBSOCKET: z.number().default(1000),
// Mock Events
TEST_EVENT_TYPES: z.array(z.string()).default([
'state_changed',
'automation_triggered',
'script_executed',
'service_called'
]),
// Mock Entities
TEST_ENTITIES: z.array(z.object({
entity_id: z.string(),
state: z.string(),
attributes: z.record(z.any()),
last_changed: z.string(),
last_updated: z.string()
})).default([
{
entity_id: 'light.test_light',
state: 'on',
attributes: {
brightness: 255,
color_temp: 400
},
last_changed: new Date().toISOString(),
last_updated: new Date().toISOString()
},
{
entity_id: 'switch.test_switch',
state: 'off',
attributes: {},
last_changed: new Date().toISOString(),
last_updated: new Date().toISOString()
}
]),
// Mock Services
TEST_SERVICES: z.array(z.object({
domain: z.string(),
service: z.string(),
data: z.record(z.any())
})).default([
{
domain: 'light',
service: 'turn_on',
data: {
entity_id: 'light.test_light',
brightness: 255
}
},
{
domain: 'switch',
service: 'turn_off',
data: {
entity_id: 'switch.test_switch'
}
}
]),
// Mock Error Scenarios
TEST_ERROR_SCENARIOS: z.array(z.object({
type: z.string(),
message: z.string(),
code: z.number()
})).default([
{
type: 'authentication_error',
message: 'Invalid token',
code: 401
},
{
type: 'rate_limit_error',
message: 'Too many requests',
code: 429
},
{
type: 'validation_error',
message: 'Invalid request body',
code: 400
}
])
});
// Parse environment variables or use defaults
const parseTestConfig = () => {
const config = {
TEST_PORT: parseInt(process.env.TEST_PORT || '3001'),
TEST_HOST: process.env.TEST_HOST || 'http://localhost',
TEST_WEBSOCKET_PORT: parseInt(process.env.TEST_WEBSOCKET_PORT || '3002'),
TEST_JWT_SECRET: process.env.TEST_JWT_SECRET || 'test_jwt_secret_key_that_is_at_least_32_chars',
TEST_TOKEN: process.env.TEST_TOKEN || 'test_token_that_is_at_least_32_chars_long',
TEST_INVALID_TOKEN: process.env.TEST_INVALID_TOKEN || 'invalid_token',
TEST_CLIENT_IP: process.env.TEST_CLIENT_IP || '127.0.0.1',
TEST_MAX_CLIENTS: parseInt(process.env.TEST_MAX_CLIENTS || '10'),
TEST_PING_INTERVAL: parseInt(process.env.TEST_PING_INTERVAL || '100'),
TEST_CLEANUP_INTERVAL: parseInt(process.env.TEST_CLEANUP_INTERVAL || '200'),
TEST_MAX_CONNECTION_AGE: parseInt(process.env.TEST_MAX_CONNECTION_AGE || '1000'),
TEST_RATE_LIMIT_WINDOW: parseInt(process.env.TEST_RATE_LIMIT_WINDOW || '60000'),
TEST_RATE_LIMIT_MAX_REQUESTS: parseInt(process.env.TEST_RATE_LIMIT_MAX_REQUESTS || '100'),
TEST_RATE_LIMIT_WEBSOCKET: parseInt(process.env.TEST_RATE_LIMIT_WEBSOCKET || '1000'),
};
return testConfigSchema.parse(config);
};
// Export the validated test configuration
export const TEST_CONFIG = parseTestConfig();
// Export types
export type TestConfig = z.infer<typeof testConfigSchema>;

View File

@@ -0,0 +1,302 @@
import { Request, Response, NextFunction } from 'express';
import { middleware } from '../index';
import { TokenManager } from '../../security/index';
describe('Security Middleware', () => {
let mockRequest: Partial<Request>;
let mockResponse: Partial<Response>;
let nextFunction: NextFunction;
beforeEach(() => {
mockRequest = {
headers: {},
body: {},
ip: '127.0.0.1',
method: 'POST',
is: jest.fn(),
};
mockResponse = {
status: jest.fn().mockReturnThis(),
json: jest.fn().mockReturnThis(),
setHeader: jest.fn().mockReturnThis(),
};
nextFunction = jest.fn();
});
describe('authenticate', () => {
it('should pass valid authentication', () => {
const token = 'valid_token';
mockRequest.headers = { authorization: `Bearer ${token}` };
jest.spyOn(TokenManager, 'validateToken').mockReturnValue({ valid: true });
middleware.authenticate(
mockRequest as Request,
mockResponse as Response,
nextFunction
);
expect(nextFunction).toHaveBeenCalled();
expect(mockResponse.status).not.toHaveBeenCalled();
});
it('should reject invalid token', () => {
const token = 'invalid_token';
mockRequest.headers = { authorization: `Bearer ${token}` };
jest.spyOn(TokenManager, 'validateToken').mockReturnValue({
valid: false,
error: 'Invalid token'
});
middleware.authenticate(
mockRequest as Request,
mockResponse as Response,
nextFunction
);
expect(mockResponse.status).toHaveBeenCalledWith(401);
expect(mockResponse.json).toHaveBeenCalledWith(
expect.objectContaining({
success: false,
message: 'Unauthorized'
})
);
});
it('should handle missing authorization header', () => {
middleware.authenticate(
mockRequest as Request,
mockResponse as Response,
nextFunction
);
expect(mockResponse.status).toHaveBeenCalledWith(401);
});
});
describe('securityHeaders', () => {
it('should set all required security headers', () => {
middleware.securityHeaders(
mockRequest as Request,
mockResponse as Response,
nextFunction
);
expect(mockResponse.setHeader).toHaveBeenCalledWith(
'X-Content-Type-Options',
'nosniff'
);
expect(mockResponse.setHeader).toHaveBeenCalledWith(
'X-Frame-Options',
'DENY'
);
expect(mockResponse.setHeader).toHaveBeenCalledWith(
'Strict-Transport-Security',
expect.stringContaining('max-age=31536000')
);
expect(mockResponse.setHeader).toHaveBeenCalledWith(
'Content-Security-Policy',
expect.any(String)
);
expect(nextFunction).toHaveBeenCalled();
});
});
describe('validateRequest', () => {
it('should pass valid requests', () => {
mockRequest.is = jest.fn().mockReturnValue('application/json');
mockRequest.body = { test: 'data' };
Object.defineProperty(mockRequest, 'path', {
get: () => '/api/test',
configurable: true
});
middleware.validateRequest(
mockRequest as Request,
mockResponse as Response,
nextFunction
);
expect(nextFunction).toHaveBeenCalled();
});
it('should reject invalid content type', () => {
mockRequest.is = jest.fn().mockReturnValue(false);
Object.defineProperty(mockRequest, 'path', {
get: () => '/api/test',
configurable: true
});
middleware.validateRequest(
mockRequest as Request,
mockResponse as Response,
nextFunction
);
expect(mockResponse.status).toHaveBeenCalledWith(415);
});
it('should reject oversized requests', () => {
mockRequest.headers = { 'content-length': '2097152' }; // 2MB
mockRequest.is = jest.fn().mockReturnValue('application/json');
Object.defineProperty(mockRequest, 'path', {
get: () => '/api/test',
configurable: true
});
middleware.validateRequest(
mockRequest as Request,
mockResponse as Response,
nextFunction
);
expect(mockResponse.status).toHaveBeenCalledWith(413);
});
it('should skip validation for health check endpoints', () => {
Object.defineProperty(mockRequest, 'path', {
get: () => '/health',
configurable: true
});
middleware.validateRequest(
mockRequest as Request,
mockResponse as Response,
nextFunction
);
expect(nextFunction).toHaveBeenCalled();
expect(mockResponse.status).not.toHaveBeenCalled();
});
});
describe('sanitizeInput', () => {
it('should sanitize HTML in request body', () => {
mockRequest.body = {
text: '<script>alert("xss")</script>Hello',
nested: {
html: '<img src="x" onerror="alert(1)">World'
}
};
middleware.sanitizeInput(
mockRequest as Request,
mockResponse as Response,
nextFunction
);
expect(mockRequest.body.text).not.toContain('<script>');
expect(mockRequest.body.nested.html).not.toContain('<img');
expect(nextFunction).toHaveBeenCalled();
});
it('should handle non-object bodies', () => {
mockRequest.body = '<p>text</p>';
middleware.sanitizeInput(
mockRequest as Request,
mockResponse as Response,
nextFunction
);
expect(mockRequest.body).not.toContain('<p>');
expect(nextFunction).toHaveBeenCalled();
});
it('should preserve non-string values', () => {
const body = {
number: 42,
boolean: true,
null: null,
array: [1, 2, 3]
};
mockRequest.body = { ...body };
middleware.sanitizeInput(
mockRequest as Request,
mockResponse as Response,
nextFunction
);
expect(mockRequest.body).toEqual(body);
expect(nextFunction).toHaveBeenCalled();
});
});
describe('errorHandler', () => {
it('should handle ValidationError', () => {
const error = new Error('Validation failed');
error.name = 'ValidationError';
middleware.errorHandler(
error,
mockRequest as Request,
mockResponse as Response,
nextFunction
);
expect(mockResponse.status).toHaveBeenCalledWith(400);
expect(mockResponse.json).toHaveBeenCalledWith(
expect.objectContaining({
success: false,
message: 'Validation Error'
})
);
});
it('should handle UnauthorizedError', () => {
const error = new Error('Unauthorized access');
error.name = 'UnauthorizedError';
middleware.errorHandler(
error,
mockRequest as Request,
mockResponse as Response,
nextFunction
);
expect(mockResponse.status).toHaveBeenCalledWith(401);
});
it('should handle generic errors', () => {
const error = new Error('Something went wrong');
middleware.errorHandler(
error,
mockRequest as Request,
mockResponse as Response,
nextFunction
);
expect(mockResponse.status).toHaveBeenCalledWith(500);
expect(mockResponse.json).toHaveBeenCalledWith(
expect.objectContaining({
success: false,
message: 'Internal Server Error'
})
);
});
it('should hide error details in production', () => {
const originalEnv = process.env.NODE_ENV;
process.env.NODE_ENV = 'production';
const error = new Error('Sensitive error details');
middleware.errorHandler(
error,
mockRequest as Request,
mockResponse as Response,
nextFunction
);
expect(mockResponse.json).toHaveBeenCalledWith(
expect.objectContaining({
error: 'An unexpected error occurred'
})
);
process.env.NODE_ENV = originalEnv;
});
});
});

View File

@@ -0,0 +1,116 @@
import { TokenManager } from '../index';
import { SECURITY_CONFIG } from '../../config/security.config';
import jwt from 'jsonwebtoken';
describe('TokenManager', () => {
const validSecret = 'test_secret_key_that_is_at_least_32_chars_long';
const validToken = 'valid_token_that_is_at_least_32_chars_long';
const testIp = '127.0.0.1';
beforeEach(() => {
process.env.JWT_SECRET = validSecret;
// Reset rate limiting
jest.resetModules();
});
describe('Token Validation', () => {
it('should validate a properly formatted token', () => {
const payload = { userId: '123', role: 'user' };
const token = TokenManager.generateToken(payload);
const result = TokenManager.validateToken(token, testIp);
expect(result.valid).toBe(true);
});
it('should reject an invalid token', () => {
const result = TokenManager.validateToken('invalid_token', testIp);
expect(result.valid).toBe(false);
expect(result.error).toBeDefined();
});
it('should reject a token that is too short', () => {
const result = TokenManager.validateToken('short', testIp);
expect(result.valid).toBe(false);
expect(result.error).toContain('minimum requirement');
});
it('should reject an expired token', () => {
const payload = { userId: '123', role: 'user' };
const token = jwt.sign(payload, validSecret, { expiresIn: -1 });
const result = TokenManager.validateToken(token, testIp);
expect(result.valid).toBe(false);
expect(result.error).toContain('expired');
});
it('should implement rate limiting for failed attempts', () => {
// Simulate multiple failed attempts
for (let i = 0; i < SECURITY_CONFIG.MAX_FAILED_ATTEMPTS; i++) {
TokenManager.validateToken('invalid_token', testIp);
}
// Next attempt should be blocked
const result = TokenManager.validateToken('invalid_token', testIp);
expect(result.valid).toBe(false);
expect(result.error).toContain('Too many failed attempts');
});
});
describe('Token Generation', () => {
it('should generate a valid JWT token', () => {
const payload = { userId: '123', role: 'user' };
const token = TokenManager.generateToken(payload);
expect(token).toBeDefined();
expect(typeof token).toBe('string');
// Verify the token can be decoded
const decoded = jwt.verify(token, validSecret) as any;
expect(decoded.userId).toBe(payload.userId);
expect(decoded.role).toBe(payload.role);
});
it('should include required claims in generated tokens', () => {
const payload = { userId: '123' };
const token = TokenManager.generateToken(payload);
const decoded = jwt.verify(token, validSecret) as any;
expect(decoded.iat).toBeDefined();
expect(decoded.exp).toBeDefined();
expect(decoded.exp - decoded.iat).toBe(
Math.floor(SECURITY_CONFIG.JWT_EXPIRY / 1000)
);
});
it('should throw error when JWT secret is not configured', () => {
delete process.env.JWT_SECRET;
const payload = { userId: '123' };
expect(() => TokenManager.generateToken(payload)).toThrow('JWT secret not configured');
});
});
describe('Token Encryption', () => {
const encryptionKey = 'encryption_key_that_is_at_least_32_chars_long';
it('should encrypt and decrypt a token successfully', () => {
const originalToken = 'test_token_to_encrypt';
const encrypted = TokenManager.encryptToken(originalToken, encryptionKey);
const decrypted = TokenManager.decryptToken(encrypted, encryptionKey);
expect(decrypted).toBe(originalToken);
});
it('should throw error for invalid encryption inputs', () => {
expect(() => TokenManager.encryptToken('', encryptionKey)).toThrow('Invalid token');
expect(() => TokenManager.encryptToken(validToken, '')).toThrow('Invalid encryption key');
});
it('should throw error for invalid decryption inputs', () => {
expect(() => TokenManager.decryptToken('', encryptionKey)).toThrow('Invalid encrypted token');
expect(() => TokenManager.decryptToken('invalid:format', encryptionKey)).toThrow('Invalid encrypted token format');
});
it('should generate different ciphertexts for same plaintext', () => {
const token = 'test_token';
const encrypted1 = TokenManager.encryptToken(token, encryptionKey);
const encrypted2 = TokenManager.encryptToken(token, encryptionKey);
expect(encrypted1).not.toBe(encrypted2);
});
});
});

View File

@@ -0,0 +1,215 @@
import { SSEManager } from '../index';
import { TokenManager } from '../../security/index';
import { EventEmitter } from 'events';
describe('SSE Security Features', () => {
let sseManager: SSEManager;
const validToken = 'valid_token';
const testIp = '127.0.0.1';
const createTestClient = (id: string) => ({
id,
ip: testIp,
connectedAt: new Date(),
send: jest.fn(),
rateLimit: {
count: 0,
lastReset: Date.now()
},
connectionTime: Date.now()
});
beforeEach(() => {
sseManager = new SSEManager({
maxClients: 10,
pingInterval: 100,
cleanupInterval: 200,
maxConnectionAge: 1000
});
jest.spyOn(TokenManager, 'validateToken').mockReturnValue({ valid: true });
});
afterEach(() => {
jest.clearAllMocks();
});
describe('Client Authentication', () => {
it('should authenticate valid clients', () => {
const client = createTestClient('test-client-1');
const result = sseManager.addClient(client, validToken);
expect(result).toBeTruthy();
expect(result?.authenticated).toBe(true);
expect(TokenManager.validateToken).toHaveBeenCalledWith(validToken, testIp);
});
it('should reject invalid tokens', () => {
jest.spyOn(TokenManager, 'validateToken').mockReturnValue({
valid: false,
error: 'Invalid token'
});
const client = createTestClient('test-client-2');
const result = sseManager.addClient(client, 'invalid_token');
expect(result).toBeNull();
expect(TokenManager.validateToken).toHaveBeenCalledWith('invalid_token', testIp);
});
it('should enforce maximum client limit', () => {
const sseManager = new SSEManager({ maxClients: 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();
});
});
describe('Client Management', () => {
it('should track client connections', () => {
const client = createTestClient('test-client');
sseManager.addClient(client, validToken);
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();
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
await new Promise(resolve => setTimeout(resolve, 250));
const stats = sseManager.getStatistics();
expect(stats.totalClients).toBe(0);
});
});
describe('Rate Limiting', () => {
it('should enforce rate limits for message sending', () => {
const client = createTestClient('test-client');
const sseClient = sseManager.addClient(client, validToken);
expect(sseClient).toBeTruthy();
// Send messages up to rate limit
for (let i = 0; i < 1000; i++) {
sseManager['sendToClient'](sseClient!, { type: 'test', data: i });
}
// Next message should trigger rate limit
sseManager['sendToClient'](sseClient!, { type: 'test', data: 'overflow' });
const lastCall = client.send.mock.calls[client.send.mock.calls.length - 1][0];
expect(JSON.parse(lastCall)).toMatchObject({
type: 'error',
error: 'rate_limit_exceeded'
});
});
it('should reset rate limits after window expires', async () => {
const client = createTestClient('test-client');
const sseClient = sseManager.addClient(client, validToken);
expect(sseClient).toBeTruthy();
// Send messages up to rate limit
for (let i = 0; i < 1000; i++) {
sseManager['sendToClient'](sseClient!, { type: 'test', data: i });
}
// 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: 'new message' });
const lastCall = client.send.mock.calls[client.send.mock.calls.length - 1][0];
expect(JSON.parse(lastCall)).toMatchObject({
type: 'test',
data: 'new message'
});
});
});
describe('Event Broadcasting', () => {
it('should only send events to authenticated clients', () => {
const client1 = createTestClient('client1');
const client2 = createTestClient('client2');
const sseClient1 = sseManager.addClient(client1, validToken);
jest.spyOn(TokenManager, 'validateToken').mockReturnValue({ valid: false });
const sseClient2 = sseManager.addClient(client2, 'invalid_token');
expect(sseClient1).toBeTruthy();
expect(sseClient2).toBeNull();
sseManager.broadcastEvent({
event_type: 'test_event',
data: { test: true },
origin: 'test',
time_fired: new Date().toISOString(),
context: { id: 'test' }
});
expect(client1.send).toHaveBeenCalled();
expect(client2.send).not.toHaveBeenCalled();
});
it('should respect subscription filters', () => {
const client = createTestClient('test-client');
const sseClient = sseManager.addClient(client, validToken);
expect(sseClient).toBeTruthy();
sseManager.subscribeToEvent(client.id, 'test_event');
// Send matching event
sseManager.broadcastEvent({
event_type: 'test_event',
data: { test: true },
origin: 'test',
time_fired: new Date().toISOString(),
context: { id: 'test' }
});
// Send non-matching event
sseManager.broadcastEvent({
event_type: 'other_event',
data: { test: true },
origin: 'test',
time_fired: new Date().toISOString(),
context: { id: 'test' }
});
expect(client.send).toHaveBeenCalledTimes(1);
const sentMessage = JSON.parse(client.send.mock.calls[0][0]);
expect(sentMessage.type).toBe('test_event');
});
});
});