refactor: improve SSE types and testing utilities
- Enhanced SSE event type definitions with more precise typing - Added type guard and safe type assertion functions for mock objects - Updated security test suite to use new type utilities - Improved type safety for token validation and mock function handling - Refined event data type to support more flexible event structures
This commit is contained in:
@@ -1,8 +1,9 @@
|
|||||||
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 { describe, it, expect, beforeEach, afterEach, mock, Mock } from 'bun:test';
|
||||||
import type { SSEClient, SSEEvent, MockSend, MockSendFn } from '../types';
|
import type { SSEClient, SSEEvent, MockSend, MockSendFn, ValidateTokenFn } from '../types';
|
||||||
|
import { isMockFunction } from '../types';
|
||||||
|
|
||||||
describe('SSE Security Features', () => {
|
describe('SSE Security Features', () => {
|
||||||
let sseManager: SSEManager;
|
let sseManager: SSEManager;
|
||||||
@@ -28,19 +29,20 @@ describe('SSE Security Features', () => {
|
|||||||
cleanupInterval: 200,
|
cleanupInterval: 200,
|
||||||
maxConnectionAge: 1000
|
maxConnectionAge: 1000
|
||||||
});
|
});
|
||||||
TokenManager.validateToken = mock(() => ({ valid: true }));
|
TokenManager.validateToken = mock<ValidateTokenFn>(() => ({ valid: true }));
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
// Clear all mock function calls
|
// Clear all mock function calls
|
||||||
const mocks = Object.values(mock).filter(
|
const mocks = Object.values(mock).filter(isMockFunction);
|
||||||
(value): value is MockSend => typeof value === 'function' && 'mock' in value
|
|
||||||
);
|
|
||||||
mocks.forEach(mockFn => {
|
mocks.forEach(mockFn => {
|
||||||
mockFn.mock.calls = [];
|
if ('mock' in mockFn) {
|
||||||
mockFn.mock.results = [];
|
const m = mockFn as Mock<unknown>;
|
||||||
mockFn.mock.instances = [];
|
m.mock.calls = [];
|
||||||
mockFn.mock.lastCall = undefined;
|
m.mock.results = [];
|
||||||
|
m.mock.instances = [];
|
||||||
|
m.mock.lastCall = undefined;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -50,11 +52,13 @@ describe('SSE Security Features', () => {
|
|||||||
const result = sseManager.addClient(client, validToken);
|
const result = sseManager.addClient(client, validToken);
|
||||||
|
|
||||||
expect(result).toBeTruthy();
|
expect(result).toBeTruthy();
|
||||||
expect(TokenManager.validateToken).toHaveBeenCalledWith(validToken, testIp);
|
const validateToken = TokenManager.validateToken as Mock<ValidateTokenFn>;
|
||||||
|
const calls = validateToken.mock.calls;
|
||||||
|
expect(calls[0].args).toEqual([validToken, testIp]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reject invalid tokens', () => {
|
it('should reject invalid tokens', () => {
|
||||||
const validateTokenMock = mock(() => ({
|
const validateTokenMock = mock<ValidateTokenFn>(() => ({
|
||||||
valid: false,
|
valid: false,
|
||||||
error: 'Invalid token'
|
error: 'Invalid token'
|
||||||
}));
|
}));
|
||||||
@@ -64,7 +68,7 @@ describe('SSE Security Features', () => {
|
|||||||
const result = sseManager.addClient(client, 'invalid_token');
|
const result = sseManager.addClient(client, 'invalid_token');
|
||||||
|
|
||||||
expect(result).toBeNull();
|
expect(result).toBeNull();
|
||||||
expect(validateTokenMock).toHaveBeenCalledWith('invalid_token', testIp);
|
expect(validateTokenMock.mock.calls[0].args).toEqual(['invalid_token', testIp]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should enforce maximum client limit', () => {
|
it('should enforce maximum client limit', () => {
|
||||||
@@ -133,11 +137,11 @@ describe('SSE Security Features', () => {
|
|||||||
|
|
||||||
// Send messages up to rate limit
|
// Send messages up to rate limit
|
||||||
for (let i = 0; i < 10; i++) {
|
for (let i = 0; i < 10; i++) {
|
||||||
sseManager['sendToClient'](sseClient!, { type: 'test', data: i });
|
sseManager['sendToClient'](sseClient!, { type: 'test', data: { value: 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: { value: 'overflow' } });
|
||||||
|
|
||||||
const lastCall = client.send.mock.calls[client.send.mock.calls.length - 1];
|
const lastCall = client.send.mock.calls[client.send.mock.calls.length - 1];
|
||||||
const lastMessage = JSON.parse(lastCall.args[0] as string);
|
const lastMessage = JSON.parse(lastCall.args[0] as string);
|
||||||
@@ -154,19 +158,19 @@ describe('SSE Security Features', () => {
|
|||||||
|
|
||||||
// Send messages up to rate limit
|
// Send messages up to rate limit
|
||||||
for (let i = 0; i < 10; i++) {
|
for (let i = 0; i < 10; i++) {
|
||||||
sseManager['sendToClient'](sseClient!, { type: 'test', data: i });
|
sseManager['sendToClient'](sseClient!, { type: 'test', data: { value: i } });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for rate limit window to expire
|
// Wait for rate limit window to expire
|
||||||
await new Promise(resolve => setTimeout(resolve, 1100));
|
await new Promise(resolve => setTimeout(resolve, 1100));
|
||||||
|
|
||||||
// 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: { value: 'new message' } });
|
||||||
const lastCall = client.send.mock.calls[client.send.mock.calls.length - 1];
|
const lastCall = client.send.mock.calls[client.send.mock.calls.length - 1];
|
||||||
const lastMessage = JSON.parse(lastCall.args[0] as string);
|
const lastMessage = JSON.parse(lastCall.args[0] as string);
|
||||||
expect(lastMessage).toEqual({
|
expect(lastMessage).toEqual({
|
||||||
type: 'test',
|
type: 'test',
|
||||||
data: 'new message'
|
data: { value: 'new message' }
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -177,7 +181,7 @@ describe('SSE Security Features', () => {
|
|||||||
const client2 = createTestClient('client2');
|
const client2 = createTestClient('client2');
|
||||||
|
|
||||||
const sseClient1 = sseManager.addClient(client1, validToken);
|
const sseClient1 = sseManager.addClient(client1, validToken);
|
||||||
TokenManager.validateToken = mock(() => ({ valid: false }));
|
TokenManager.validateToken = mock<ValidateTokenFn>(() => ({ valid: false }));
|
||||||
const sseClient2 = sseManager.addClient(client2, 'invalid_token');
|
const sseClient2 = sseManager.addClient(client2, 'invalid_token');
|
||||||
|
|
||||||
expect(sseClient1).toBeTruthy();
|
expect(sseClient1).toBeTruthy();
|
||||||
|
|||||||
@@ -12,9 +12,13 @@ export interface SSEClient {
|
|||||||
connectionTime: number;
|
connectionTime: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface HassEventData {
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
export interface SSEEvent {
|
export interface SSEEvent {
|
||||||
event_type: string;
|
event_type: string;
|
||||||
data: unknown;
|
data: HassEventData;
|
||||||
origin: string;
|
origin: string;
|
||||||
time_fired: string;
|
time_fired: string;
|
||||||
context: {
|
context: {
|
||||||
@@ -39,4 +43,20 @@ export interface SSEManagerConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type MockSendFn = (data: string) => void;
|
export type MockSendFn = (data: string) => void;
|
||||||
export type MockSend = Mock<MockSendFn>;
|
export type MockSend = Mock<MockSendFn>;
|
||||||
|
|
||||||
|
export type ValidateTokenFn = (token: string, ip?: string) => { valid: boolean; error?: string };
|
||||||
|
export type MockValidateToken = Mock<ValidateTokenFn>;
|
||||||
|
|
||||||
|
// Type guard for mock functions
|
||||||
|
export function isMockFunction(value: unknown): value is Mock<unknown> {
|
||||||
|
return typeof value === 'function' && 'mock' in value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Safe type assertion for mock objects
|
||||||
|
export function asMockFunction<T extends (...args: any[]) => any>(value: unknown): Mock<T> {
|
||||||
|
if (!isMockFunction(value)) {
|
||||||
|
throw new Error('Value is not a mock function');
|
||||||
|
}
|
||||||
|
return value as Mock<T>;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user