diff --git a/__tests__/ai/nlp/intent-classifier.test.ts b/__tests__/ai/nlp/intent-classifier.test.ts new file mode 100644 index 0000000..91a068b --- /dev/null +++ b/__tests__/ai/nlp/intent-classifier.test.ts @@ -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); + }); + }); +}); \ No newline at end of file diff --git a/__tests__/security/middleware.test.ts b/__tests__/security/middleware.test.ts new file mode 100644 index 0000000..e05fc57 --- /dev/null +++ b/__tests__/security/middleware.test.ts @@ -0,0 +1,248 @@ +import { Request, Response } from 'express'; +import { + validateRequest, + sanitizeInput, + errorHandler, + rateLimiter, + securityHeaders +} from '../../src/security/index.js'; + +interface MockRequest extends Partial { + headers: Record; + is: jest.Mock; +} + +describe('Security Middleware', () => { + let mockRequest: MockRequest; + let mockResponse: Partial; + 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: '', + nested: { + html: '' + } + }; + + sanitizeInput( + mockRequest as Request, + mockResponse as Response, + mockNext + ); + + expect(mockRequest.body.text).not.toContain('