Add comprehensive security middleware and token management tests
- Introduced detailed test suites for security middleware components - Added robust test coverage for token encryption, decryption, and validation - Implemented comprehensive tests for input sanitization and error handling - Created test scenarios for rate limiting and security header configurations - Enhanced test infrastructure for security-related modules with edge case handling
This commit is contained in:
204
__tests__/ai/nlp/intent-classifier.test.ts
Normal file
204
__tests__/ai/nlp/intent-classifier.test.ts
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
import { IntentClassifier } from '../../../src/ai/nlp/intent-classifier.js';
|
||||||
|
|
||||||
|
describe('IntentClassifier', () => {
|
||||||
|
let classifier: IntentClassifier;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
classifier = new IntentClassifier();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Basic Intent Classification', () => {
|
||||||
|
it('should classify turn_on commands', async () => {
|
||||||
|
const testCases = [
|
||||||
|
{
|
||||||
|
input: 'turn on the living room light',
|
||||||
|
entities: { parameters: {}, primary_target: 'light.living_room' },
|
||||||
|
expectedAction: 'turn_on'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: 'switch on the kitchen lights',
|
||||||
|
entities: { parameters: {}, primary_target: 'light.kitchen' },
|
||||||
|
expectedAction: 'turn_on'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: 'enable the bedroom lamp',
|
||||||
|
entities: { parameters: {}, primary_target: 'light.bedroom' },
|
||||||
|
expectedAction: 'turn_on'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const test of testCases) {
|
||||||
|
const result = await classifier.classify(test.input, test.entities);
|
||||||
|
expect(result.action).toBe(test.expectedAction);
|
||||||
|
expect(result.target).toBe(test.entities.primary_target);
|
||||||
|
expect(result.confidence).toBeGreaterThan(0.5);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should classify turn_off commands', async () => {
|
||||||
|
const testCases = [
|
||||||
|
{
|
||||||
|
input: 'turn off the living room light',
|
||||||
|
entities: { parameters: {}, primary_target: 'light.living_room' },
|
||||||
|
expectedAction: 'turn_off'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: 'switch off the kitchen lights',
|
||||||
|
entities: { parameters: {}, primary_target: 'light.kitchen' },
|
||||||
|
expectedAction: 'turn_off'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: 'disable the bedroom lamp',
|
||||||
|
entities: { parameters: {}, primary_target: 'light.bedroom' },
|
||||||
|
expectedAction: 'turn_off'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const test of testCases) {
|
||||||
|
const result = await classifier.classify(test.input, test.entities);
|
||||||
|
expect(result.action).toBe(test.expectedAction);
|
||||||
|
expect(result.target).toBe(test.entities.primary_target);
|
||||||
|
expect(result.confidence).toBeGreaterThan(0.5);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should classify set commands with parameters', async () => {
|
||||||
|
const testCases = [
|
||||||
|
{
|
||||||
|
input: 'set the living room light brightness to 50',
|
||||||
|
entities: {
|
||||||
|
parameters: { brightness: 50 },
|
||||||
|
primary_target: 'light.living_room'
|
||||||
|
},
|
||||||
|
expectedAction: 'set'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: 'change the temperature to 72',
|
||||||
|
entities: {
|
||||||
|
parameters: { temperature: 72 },
|
||||||
|
primary_target: 'climate.living_room'
|
||||||
|
},
|
||||||
|
expectedAction: 'set'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: 'adjust the kitchen light color to red',
|
||||||
|
entities: {
|
||||||
|
parameters: { color: 'red' },
|
||||||
|
primary_target: 'light.kitchen'
|
||||||
|
},
|
||||||
|
expectedAction: 'set'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const test of testCases) {
|
||||||
|
const result = await classifier.classify(test.input, test.entities);
|
||||||
|
expect(result.action).toBe(test.expectedAction);
|
||||||
|
expect(result.target).toBe(test.entities.primary_target);
|
||||||
|
expect(result.parameters).toEqual(test.entities.parameters);
|
||||||
|
expect(result.confidence).toBeGreaterThan(0.5);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should classify query commands', async () => {
|
||||||
|
const testCases = [
|
||||||
|
{
|
||||||
|
input: 'what is the living room temperature',
|
||||||
|
entities: { parameters: {}, primary_target: 'sensor.living_room_temperature' },
|
||||||
|
expectedAction: 'query'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: 'get the kitchen light status',
|
||||||
|
entities: { parameters: {}, primary_target: 'light.kitchen' },
|
||||||
|
expectedAction: 'query'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: 'show me the front door camera',
|
||||||
|
entities: { parameters: {}, primary_target: 'camera.front_door' },
|
||||||
|
expectedAction: 'query'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const test of testCases) {
|
||||||
|
const result = await classifier.classify(test.input, test.entities);
|
||||||
|
expect(result.action).toBe(test.expectedAction);
|
||||||
|
expect(result.target).toBe(test.entities.primary_target);
|
||||||
|
expect(result.confidence).toBeGreaterThan(0.5);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Edge Cases and Error Handling', () => {
|
||||||
|
it('should handle empty input gracefully', async () => {
|
||||||
|
const result = await classifier.classify('', { parameters: {}, primary_target: '' });
|
||||||
|
expect(result.action).toBe('unknown');
|
||||||
|
expect(result.confidence).toBeLessThan(0.5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle unknown commands with low confidence', async () => {
|
||||||
|
const result = await classifier.classify(
|
||||||
|
'do something random',
|
||||||
|
{ parameters: {}, primary_target: 'light.living_room' }
|
||||||
|
);
|
||||||
|
expect(result.action).toBe('unknown');
|
||||||
|
expect(result.confidence).toBeLessThan(0.5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle missing entities gracefully', async () => {
|
||||||
|
const result = await classifier.classify(
|
||||||
|
'turn on the lights',
|
||||||
|
{ parameters: {}, primary_target: '' }
|
||||||
|
);
|
||||||
|
expect(result.action).toBe('turn_on');
|
||||||
|
expect(result.target).toBe('');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Confidence Calculation', () => {
|
||||||
|
it('should assign higher confidence to exact matches', async () => {
|
||||||
|
const exactMatch = await classifier.classify(
|
||||||
|
'turn on',
|
||||||
|
{ parameters: {}, primary_target: 'light.living_room' }
|
||||||
|
);
|
||||||
|
const partialMatch = await classifier.classify(
|
||||||
|
'please turn on the lights if possible',
|
||||||
|
{ parameters: {}, primary_target: 'light.living_room' }
|
||||||
|
);
|
||||||
|
expect(exactMatch.confidence).toBeGreaterThan(partialMatch.confidence);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should boost confidence for polite phrases', async () => {
|
||||||
|
const politeRequest = await classifier.classify(
|
||||||
|
'please turn on the lights',
|
||||||
|
{ parameters: {}, primary_target: 'light.living_room' }
|
||||||
|
);
|
||||||
|
const basicRequest = await classifier.classify(
|
||||||
|
'turn on the lights',
|
||||||
|
{ parameters: {}, primary_target: 'light.living_room' }
|
||||||
|
);
|
||||||
|
expect(politeRequest.confidence).toBeGreaterThan(basicRequest.confidence);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Context Inference', () => {
|
||||||
|
it('should infer set action when parameters are present', async () => {
|
||||||
|
const result = await classifier.classify(
|
||||||
|
'lights at 50%',
|
||||||
|
{
|
||||||
|
parameters: { brightness: 50 },
|
||||||
|
primary_target: 'light.living_room'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
expect(result.action).toBe('set');
|
||||||
|
expect(result.parameters).toHaveProperty('brightness', 50);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should infer query action for question-like inputs', async () => {
|
||||||
|
const result = await classifier.classify(
|
||||||
|
'how warm is it',
|
||||||
|
{ parameters: {}, primary_target: 'sensor.temperature' }
|
||||||
|
);
|
||||||
|
expect(result.action).toBe('query');
|
||||||
|
expect(result.confidence).toBeGreaterThan(0.5);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
248
__tests__/security/middleware.test.ts
Normal file
248
__tests__/security/middleware.test.ts
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
import {
|
||||||
|
validateRequest,
|
||||||
|
sanitizeInput,
|
||||||
|
errorHandler,
|
||||||
|
rateLimiter,
|
||||||
|
securityHeaders
|
||||||
|
} from '../../src/security/index.js';
|
||||||
|
|
||||||
|
interface MockRequest extends Partial<Request> {
|
||||||
|
headers: Record<string, string>;
|
||||||
|
is: jest.Mock;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Security Middleware', () => {
|
||||||
|
let mockRequest: MockRequest;
|
||||||
|
let mockResponse: Partial<Response>;
|
||||||
|
let mockNext: jest.Mock;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockRequest = {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'content-type': 'application/json',
|
||||||
|
'authorization': 'Bearer validToken'
|
||||||
|
},
|
||||||
|
is: jest.fn().mockReturnValue(true),
|
||||||
|
body: { test: 'data' }
|
||||||
|
};
|
||||||
|
mockResponse = {
|
||||||
|
status: jest.fn().mockReturnThis(),
|
||||||
|
json: jest.fn(),
|
||||||
|
setHeader: jest.fn(),
|
||||||
|
set: jest.fn()
|
||||||
|
};
|
||||||
|
mockNext = jest.fn();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Request Validation', () => {
|
||||||
|
it('should pass valid requests', () => {
|
||||||
|
validateRequest(
|
||||||
|
mockRequest as Request,
|
||||||
|
mockResponse as Response,
|
||||||
|
mockNext
|
||||||
|
);
|
||||||
|
expect(mockNext).toHaveBeenCalled();
|
||||||
|
expect(mockResponse.status).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject requests with invalid content type', () => {
|
||||||
|
mockRequest.is = jest.fn().mockReturnValue(false);
|
||||||
|
validateRequest(
|
||||||
|
mockRequest as Request,
|
||||||
|
mockResponse as Response,
|
||||||
|
mockNext
|
||||||
|
);
|
||||||
|
expect(mockResponse.status).toHaveBeenCalledWith(415);
|
||||||
|
expect(mockResponse.json).toHaveBeenCalledWith({
|
||||||
|
error: 'Unsupported Media Type - Content-Type must be application/json'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject requests without authorization', () => {
|
||||||
|
mockRequest.headers = {};
|
||||||
|
validateRequest(
|
||||||
|
mockRequest as Request,
|
||||||
|
mockResponse as Response,
|
||||||
|
mockNext
|
||||||
|
);
|
||||||
|
expect(mockResponse.status).toHaveBeenCalledWith(401);
|
||||||
|
expect(mockResponse.json).toHaveBeenCalledWith({
|
||||||
|
error: 'Invalid or expired token'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject requests with invalid token', () => {
|
||||||
|
mockRequest.headers.authorization = 'Bearer invalid.token.format';
|
||||||
|
validateRequest(
|
||||||
|
mockRequest as Request,
|
||||||
|
mockResponse as Response,
|
||||||
|
mockNext
|
||||||
|
);
|
||||||
|
expect(mockResponse.status).toHaveBeenCalledWith(401);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle GET requests without body validation', () => {
|
||||||
|
mockRequest.method = 'GET';
|
||||||
|
mockRequest.body = undefined;
|
||||||
|
validateRequest(
|
||||||
|
mockRequest as Request,
|
||||||
|
mockResponse as Response,
|
||||||
|
mockNext
|
||||||
|
);
|
||||||
|
expect(mockNext).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Input Sanitization', () => {
|
||||||
|
it('should remove HTML tags from request body', () => {
|
||||||
|
mockRequest.body = {
|
||||||
|
text: '<script>alert("xss")</script>',
|
||||||
|
nested: {
|
||||||
|
html: '<img src="x" onerror="alert(1)">'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
sanitizeInput(
|
||||||
|
mockRequest as Request,
|
||||||
|
mockResponse as Response,
|
||||||
|
mockNext
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mockRequest.body.text).not.toContain('<script>');
|
||||||
|
expect(mockRequest.body.nested.html).not.toContain('<img');
|
||||||
|
expect(mockNext).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle non-object body', () => {
|
||||||
|
mockRequest.body = 'plain text';
|
||||||
|
sanitizeInput(
|
||||||
|
mockRequest as Request,
|
||||||
|
mockResponse as Response,
|
||||||
|
mockNext
|
||||||
|
);
|
||||||
|
expect(mockRequest.body).toBe('plain text');
|
||||||
|
expect(mockNext).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle nested objects', () => {
|
||||||
|
mockRequest.body = {
|
||||||
|
level1: {
|
||||||
|
level2: {
|
||||||
|
text: '<p>test</p>'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
sanitizeInput(
|
||||||
|
mockRequest as Request,
|
||||||
|
mockResponse as Response,
|
||||||
|
mockNext
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mockRequest.body.level1.level2.text).not.toContain('<p>');
|
||||||
|
expect(mockNext).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should preserve safe content', () => {
|
||||||
|
mockRequest.body = {
|
||||||
|
text: 'Safe text without HTML',
|
||||||
|
number: 42,
|
||||||
|
boolean: true
|
||||||
|
};
|
||||||
|
|
||||||
|
const originalBody = { ...mockRequest.body };
|
||||||
|
sanitizeInput(
|
||||||
|
mockRequest as Request,
|
||||||
|
mockResponse as Response,
|
||||||
|
mockNext
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mockRequest.body).toEqual(originalBody);
|
||||||
|
expect(mockNext).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Error Handler', () => {
|
||||||
|
const originalEnv = process.env.NODE_ENV;
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
process.env.NODE_ENV = originalEnv;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle errors in production mode', () => {
|
||||||
|
process.env.NODE_ENV = 'production';
|
||||||
|
const error = new Error('Test error');
|
||||||
|
|
||||||
|
errorHandler(
|
||||||
|
error,
|
||||||
|
mockRequest as Request,
|
||||||
|
mockResponse as Response,
|
||||||
|
mockNext
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mockResponse.status).toHaveBeenCalledWith(500);
|
||||||
|
expect(mockResponse.json).toHaveBeenCalledWith({
|
||||||
|
error: 'Internal Server Error',
|
||||||
|
message: undefined
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include error details in development mode', () => {
|
||||||
|
process.env.NODE_ENV = 'development';
|
||||||
|
const error = new Error('Test error');
|
||||||
|
|
||||||
|
errorHandler(
|
||||||
|
error,
|
||||||
|
mockRequest as Request,
|
||||||
|
mockResponse as Response,
|
||||||
|
mockNext
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mockResponse.status).toHaveBeenCalledWith(500);
|
||||||
|
expect(mockResponse.json).toHaveBeenCalledWith({
|
||||||
|
error: 'Internal Server Error',
|
||||||
|
message: 'Test error'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle non-Error objects', () => {
|
||||||
|
const error = 'String error message';
|
||||||
|
|
||||||
|
errorHandler(
|
||||||
|
error as any,
|
||||||
|
mockRequest as Request,
|
||||||
|
mockResponse as Response,
|
||||||
|
mockNext
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mockResponse.status).toHaveBeenCalledWith(500);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Rate Limiter', () => {
|
||||||
|
it('should be configured with correct options', () => {
|
||||||
|
expect(rateLimiter).toBeDefined();
|
||||||
|
const middleware = rateLimiter as any;
|
||||||
|
expect(middleware.windowMs).toBeDefined();
|
||||||
|
expect(middleware.max).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Security Headers', () => {
|
||||||
|
it('should be configured with secure defaults', () => {
|
||||||
|
expect(securityHeaders).toBeDefined();
|
||||||
|
const middleware = securityHeaders as any;
|
||||||
|
expect(middleware.getDefaultDirectives).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set appropriate security headers', () => {
|
||||||
|
const mockRes = {
|
||||||
|
setHeader: jest.fn()
|
||||||
|
};
|
||||||
|
securityHeaders(mockRequest as Request, mockRes as any, mockNext);
|
||||||
|
expect(mockRes.setHeader).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
127
__tests__/security/token-manager.test.ts
Normal file
127
__tests__/security/token-manager.test.ts
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
import { TokenManager } from '../../src/security/index.js';
|
||||||
|
|
||||||
|
describe('TokenManager', () => {
|
||||||
|
const validToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiZXhwIjoxNzE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c';
|
||||||
|
const expiredToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiZXhwIjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c';
|
||||||
|
const encryptionKey = 'test_encryption_key_12345';
|
||||||
|
|
||||||
|
describe('Token Encryption/Decryption', () => {
|
||||||
|
it('should encrypt and decrypt tokens successfully', () => {
|
||||||
|
const encrypted = TokenManager.encryptToken(validToken, encryptionKey);
|
||||||
|
const decrypted = TokenManager.decryptToken(encrypted, encryptionKey);
|
||||||
|
expect(decrypted).toBe(validToken);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should generate different encrypted values for same token', () => {
|
||||||
|
const encrypted1 = TokenManager.encryptToken(validToken, encryptionKey);
|
||||||
|
const encrypted2 = TokenManager.encryptToken(validToken, encryptionKey);
|
||||||
|
expect(encrypted1).not.toBe(encrypted2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty tokens', () => {
|
||||||
|
expect(() => TokenManager.encryptToken('', encryptionKey)).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty encryption keys', () => {
|
||||||
|
expect(() => TokenManager.encryptToken(validToken, '')).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fail decryption with wrong key', () => {
|
||||||
|
const encrypted = TokenManager.encryptToken(validToken, encryptionKey);
|
||||||
|
expect(() => TokenManager.decryptToken(encrypted, 'wrong_key')).toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Token Validation', () => {
|
||||||
|
it('should validate correct tokens', () => {
|
||||||
|
expect(TokenManager.validateToken(validToken)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject expired tokens', () => {
|
||||||
|
expect(TokenManager.validateToken(expiredToken)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject malformed tokens', () => {
|
||||||
|
const malformedTokens = [
|
||||||
|
'not.a.token',
|
||||||
|
'invalid-token-format',
|
||||||
|
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9',
|
||||||
|
'',
|
||||||
|
'null',
|
||||||
|
'undefined'
|
||||||
|
];
|
||||||
|
|
||||||
|
malformedTokens.forEach(token => {
|
||||||
|
expect(TokenManager.validateToken(token)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject tokens with invalid signature', () => {
|
||||||
|
const tamperedToken = validToken.slice(0, -1) + 'X';
|
||||||
|
expect(TokenManager.validateToken(tamperedToken)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle tokens with missing expiration', () => {
|
||||||
|
const tokenWithoutExp = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c';
|
||||||
|
expect(TokenManager.validateToken(tokenWithoutExp)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Security Features', () => {
|
||||||
|
it('should use secure encryption algorithm', () => {
|
||||||
|
const encrypted = TokenManager.encryptToken(validToken, encryptionKey);
|
||||||
|
expect(encrypted).toMatch(/^[A-Za-z0-9+/=]+$/); // Base64 format
|
||||||
|
expect(encrypted.length).toBeGreaterThan(validToken.length); // Should include IV and tag
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should prevent token tampering', () => {
|
||||||
|
const encrypted = TokenManager.encryptToken(validToken, encryptionKey);
|
||||||
|
const tampered = encrypted.slice(0, -1) + 'X';
|
||||||
|
expect(() => TokenManager.decryptToken(tampered, encryptionKey)).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use unique IVs for each encryption', () => {
|
||||||
|
const encrypted1 = TokenManager.encryptToken(validToken, encryptionKey);
|
||||||
|
const encrypted2 = TokenManager.encryptToken(validToken, encryptionKey);
|
||||||
|
const encrypted3 = TokenManager.encryptToken(validToken, encryptionKey);
|
||||||
|
|
||||||
|
// Each encryption should be different due to unique IVs
|
||||||
|
expect(new Set([encrypted1, encrypted2, encrypted3]).size).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle large tokens', () => {
|
||||||
|
const largeToken = validToken.repeat(10); // Create a much larger token
|
||||||
|
const encrypted = TokenManager.encryptToken(largeToken, encryptionKey);
|
||||||
|
const decrypted = TokenManager.decryptToken(encrypted, encryptionKey);
|
||||||
|
expect(decrypted).toBe(largeToken);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Error Handling', () => {
|
||||||
|
it('should throw descriptive errors for invalid inputs', () => {
|
||||||
|
expect(() => TokenManager.encryptToken(null as any, encryptionKey))
|
||||||
|
.toThrow(/invalid/i);
|
||||||
|
expect(() => TokenManager.encryptToken(validToken, null as any))
|
||||||
|
.toThrow(/invalid/i);
|
||||||
|
expect(() => TokenManager.decryptToken('invalid-base64', encryptionKey))
|
||||||
|
.toThrow(/invalid/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle corrupted encrypted data', () => {
|
||||||
|
const encrypted = TokenManager.encryptToken(validToken, encryptionKey);
|
||||||
|
const corrupted = encrypted.substring(10); // Remove part of the encrypted data
|
||||||
|
expect(() => TokenManager.decryptToken(corrupted, encryptionKey))
|
||||||
|
.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle invalid base64 input', () => {
|
||||||
|
expect(() => TokenManager.decryptToken('not-base64!@#$', encryptionKey))
|
||||||
|
.toThrow(/invalid/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle undefined and null inputs', () => {
|
||||||
|
expect(() => TokenManager.validateToken(undefined as any)).toBe(false);
|
||||||
|
expect(() => TokenManager.validateToken(null as any)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
204
__tests__/tools/tool-registry.test.ts
Normal file
204
__tests__/tools/tool-registry.test.ts
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
import { ToolRegistry, ToolCategory, EnhancedTool } from '../../src/tools/index.js';
|
||||||
|
|
||||||
|
describe('ToolRegistry', () => {
|
||||||
|
let registry: ToolRegistry;
|
||||||
|
let mockTool: EnhancedTool;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
registry = new ToolRegistry();
|
||||||
|
mockTool = {
|
||||||
|
name: 'test_tool',
|
||||||
|
description: 'A test tool',
|
||||||
|
metadata: {
|
||||||
|
category: ToolCategory.DEVICE,
|
||||||
|
platform: 'test',
|
||||||
|
version: '1.0.0',
|
||||||
|
caching: {
|
||||||
|
enabled: true,
|
||||||
|
ttl: 1000
|
||||||
|
}
|
||||||
|
},
|
||||||
|
execute: jest.fn().mockResolvedValue({ success: true }),
|
||||||
|
validate: jest.fn().mockResolvedValue(true),
|
||||||
|
preExecute: jest.fn().mockResolvedValue(undefined),
|
||||||
|
postExecute: jest.fn().mockResolvedValue(undefined)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Tool Registration', () => {
|
||||||
|
it('should register a tool successfully', () => {
|
||||||
|
registry.registerTool(mockTool);
|
||||||
|
const retrievedTool = registry.getTool('test_tool');
|
||||||
|
expect(retrievedTool).toBe(mockTool);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should categorize tools correctly', () => {
|
||||||
|
registry.registerTool(mockTool);
|
||||||
|
const deviceTools = registry.getToolsByCategory(ToolCategory.DEVICE);
|
||||||
|
expect(deviceTools).toContain(mockTool);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle multiple tools in the same category', () => {
|
||||||
|
const mockTool2 = {
|
||||||
|
...mockTool,
|
||||||
|
name: 'test_tool_2'
|
||||||
|
};
|
||||||
|
registry.registerTool(mockTool);
|
||||||
|
registry.registerTool(mockTool2);
|
||||||
|
const deviceTools = registry.getToolsByCategory(ToolCategory.DEVICE);
|
||||||
|
expect(deviceTools).toHaveLength(2);
|
||||||
|
expect(deviceTools).toContain(mockTool);
|
||||||
|
expect(deviceTools).toContain(mockTool2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Tool Execution', () => {
|
||||||
|
it('should execute a tool with all hooks', async () => {
|
||||||
|
registry.registerTool(mockTool);
|
||||||
|
await registry.executeTool('test_tool', { param: 'value' });
|
||||||
|
|
||||||
|
expect(mockTool.validate).toHaveBeenCalledWith({ param: 'value' });
|
||||||
|
expect(mockTool.preExecute).toHaveBeenCalledWith({ param: 'value' });
|
||||||
|
expect(mockTool.execute).toHaveBeenCalledWith({ param: 'value' });
|
||||||
|
expect(mockTool.postExecute).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error for non-existent tool', async () => {
|
||||||
|
await expect(registry.executeTool('non_existent', {}))
|
||||||
|
.rejects.toThrow('Tool non_existent not found');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle validation failure', async () => {
|
||||||
|
mockTool.validate = jest.fn().mockResolvedValue(false);
|
||||||
|
registry.registerTool(mockTool);
|
||||||
|
|
||||||
|
await expect(registry.executeTool('test_tool', {}))
|
||||||
|
.rejects.toThrow('Invalid parameters');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should execute without optional hooks', async () => {
|
||||||
|
const simpleTool: EnhancedTool = {
|
||||||
|
name: 'simple_tool',
|
||||||
|
description: 'A simple tool',
|
||||||
|
metadata: {
|
||||||
|
category: ToolCategory.SYSTEM,
|
||||||
|
platform: 'test',
|
||||||
|
version: '1.0.0'
|
||||||
|
},
|
||||||
|
execute: jest.fn().mockResolvedValue({ success: true })
|
||||||
|
};
|
||||||
|
|
||||||
|
registry.registerTool(simpleTool);
|
||||||
|
const result = await registry.executeTool('simple_tool', {});
|
||||||
|
expect(result).toEqual({ success: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Caching', () => {
|
||||||
|
it('should cache tool results when enabled', async () => {
|
||||||
|
registry.registerTool(mockTool);
|
||||||
|
const params = { test: 'value' };
|
||||||
|
|
||||||
|
// First execution
|
||||||
|
await registry.executeTool('test_tool', params);
|
||||||
|
expect(mockTool.execute).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
// Second execution within TTL
|
||||||
|
await registry.executeTool('test_tool', params);
|
||||||
|
expect(mockTool.execute).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not cache results when disabled', async () => {
|
||||||
|
const uncachedTool: EnhancedTool = {
|
||||||
|
...mockTool,
|
||||||
|
metadata: {
|
||||||
|
...mockTool.metadata,
|
||||||
|
caching: {
|
||||||
|
enabled: false,
|
||||||
|
ttl: 1000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
registry.registerTool(uncachedTool);
|
||||||
|
const params = { test: 'value' };
|
||||||
|
|
||||||
|
// Multiple executions
|
||||||
|
await registry.executeTool('test_tool', params);
|
||||||
|
await registry.executeTool('test_tool', params);
|
||||||
|
|
||||||
|
expect(uncachedTool.execute).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should expire cache after TTL', async () => {
|
||||||
|
mockTool.metadata.caching!.ttl = 100; // Short TTL for testing
|
||||||
|
registry.registerTool(mockTool);
|
||||||
|
const params = { test: 'value' };
|
||||||
|
|
||||||
|
// First execution
|
||||||
|
await registry.executeTool('test_tool', params);
|
||||||
|
|
||||||
|
// Wait for cache to expire
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 150));
|
||||||
|
|
||||||
|
// Second execution after TTL
|
||||||
|
await registry.executeTool('test_tool', params);
|
||||||
|
|
||||||
|
expect(mockTool.execute).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should clean expired cache entries', async () => {
|
||||||
|
mockTool.metadata.caching!.ttl = 100;
|
||||||
|
registry.registerTool(mockTool);
|
||||||
|
const params = { test: 'value' };
|
||||||
|
|
||||||
|
// Execute and cache
|
||||||
|
await registry.executeTool('test_tool', params);
|
||||||
|
|
||||||
|
// Wait for cache to expire
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 150));
|
||||||
|
|
||||||
|
// Clean cache
|
||||||
|
registry.cleanCache();
|
||||||
|
|
||||||
|
// Verify cache is cleaned by forcing a new execution
|
||||||
|
await registry.executeTool('test_tool', params);
|
||||||
|
expect(mockTool.execute).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Category Management', () => {
|
||||||
|
it('should return empty array for unknown category', () => {
|
||||||
|
const tools = registry.getToolsByCategory('unknown' as ToolCategory);
|
||||||
|
expect(tools).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle tools across multiple categories', () => {
|
||||||
|
const systemTool: EnhancedTool = {
|
||||||
|
...mockTool,
|
||||||
|
name: 'system_tool',
|
||||||
|
metadata: {
|
||||||
|
...mockTool.metadata,
|
||||||
|
category: ToolCategory.SYSTEM
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const automationTool: EnhancedTool = {
|
||||||
|
...mockTool,
|
||||||
|
name: 'automation_tool',
|
||||||
|
metadata: {
|
||||||
|
...mockTool.metadata,
|
||||||
|
category: ToolCategory.AUTOMATION
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
registry.registerTool(mockTool); // DEVICE category
|
||||||
|
registry.registerTool(systemTool);
|
||||||
|
registry.registerTool(automationTool);
|
||||||
|
|
||||||
|
expect(registry.getToolsByCategory(ToolCategory.DEVICE)).toHaveLength(1);
|
||||||
|
expect(registry.getToolsByCategory(ToolCategory.SYSTEM)).toHaveLength(1);
|
||||||
|
expect(registry.getToolsByCategory(ToolCategory.AUTOMATION)).toHaveLength(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user