UNPKG

osc-mcp-server

Version:

Model Context Protocol server for OSC (Open Sound Control) endpoint management

379 lines 16.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const endpoint_1 = require("./endpoint"); const index_1 = require("../types/index"); const dgram_1 = require("dgram"); jest.mock('dgram'); const mockCreateSocket = dgram_1.createSocket; describe('OSCEndpoint', () => { let mockSocket; let endpoint; const testConfig = { port: 8000, bufferSize: 100, addressFilters: ['/test/*'], }; beforeEach(() => { mockSocket = { bind: jest.fn(), close: jest.fn(), on: jest.fn(), removeAllListeners: jest.fn(), }; mockCreateSocket.mockReturnValue(mockSocket); endpoint = (0, endpoint_1.createOSCEndpoint)('test-endpoint', testConfig); }); afterEach(() => { jest.clearAllMocks(); }); describe('constructor', () => { it('should create endpoint with valid configuration', () => { const status = endpoint.getStatus(); expect(status.id).toBe('test-endpoint'); expect(status.port).toBe(8000); expect(status.bufferSize).toBe(100); expect(status.addressFilters).toEqual(['/test/*']); expect(status.status).toBe('stopped'); expect(status.messageCount).toBe(0); expect(status.createdAt).toBeInstanceOf(Date); }); it('should use default buffer size when not specified', () => { const configWithoutBuffer = { port: 8001 }; const endpointWithDefaults = (0, endpoint_1.createOSCEndpoint)('test-defaults', configWithoutBuffer); expect(endpointWithDefaults.getStatus().bufferSize).toBe(1000); }); it('should use empty address filters when not specified', () => { const configWithoutFilters = { port: 8002 }; const endpointWithDefaults = (0, endpoint_1.createOSCEndpoint)('test-no-filters', configWithoutFilters); expect(endpointWithDefaults.getStatus().addressFilters).toEqual([]); }); it('should throw error for invalid port numbers', () => { expect(() => { (0, endpoint_1.createOSCEndpoint)('invalid-port-low', { port: 1023 }); }).toThrow('Invalid port number: 1023'); expect(() => { (0, endpoint_1.createOSCEndpoint)('invalid-port-high', { port: 65536 }); }).toThrow('Invalid port number: 65536'); expect(() => { (0, endpoint_1.createOSCEndpoint)('invalid-port-float', { port: 8000.5 }); }).toThrow('Invalid port number: 8000.5'); }); }); describe('startListening', () => { it('should start listening successfully', async () => { mockSocket.bind.mockImplementation((_port, callback) => { setTimeout(callback, 0); }); const statusChangeHandler = jest.fn(); endpoint.on('statusChange', statusChangeHandler); await endpoint.startListening(); expect(mockCreateSocket).toHaveBeenCalledWith('udp4'); expect(mockSocket.bind).toHaveBeenCalledWith(8000, expect.any(Function)); expect(endpoint.getStatus().status).toBe('active'); expect(statusChangeHandler).toHaveBeenCalledWith('active'); }); it('should set up error and message handlers', async () => { mockSocket.bind.mockImplementation((_port, callback) => { setTimeout(callback, 0); }); await endpoint.startListening(); expect(mockSocket.on).toHaveBeenCalledWith('error', expect.any(Function)); expect(mockSocket.on).toHaveBeenCalledWith('message', expect.any(Function)); }); it('should reject if already listening', async () => { mockSocket.bind.mockImplementation((_port, callback) => { setTimeout(callback, 0); }); await endpoint.startListening(); await expect(endpoint.startListening()).rejects.toThrow("Endpoint 'test-endpoint' is already active and listening"); }); it('should handle socket errors during startup', async () => { const testError = new Error('EADDRINUSE: Address already in use'); const errorHandler = jest.fn(); endpoint.on('error', errorHandler); mockSocket.bind.mockImplementation(() => { setTimeout(() => { const errorCallback = mockSocket.on.mock.calls.find((call) => call[0] === 'error')?.[1]; if (errorCallback) { errorCallback(testError); } }, 0); }); await expect(endpoint.startListening()).rejects.toThrow(testError); expect(endpoint.getStatus().status).toBe('error'); }); }); describe('stopListening', () => { beforeEach(async () => { mockSocket.bind.mockImplementation((_port, callback) => { setTimeout(callback, 0); }); await endpoint.startListening(); }); it('should stop listening successfully', async () => { mockSocket.close.mockImplementation((callback) => { setTimeout(callback, 0); }); const statusChangeHandler = jest.fn(); endpoint.on('statusChange', statusChangeHandler); await endpoint.stopListening(); expect(mockSocket.close).toHaveBeenCalled(); expect(endpoint.getStatus().status).toBe('stopped'); expect(statusChangeHandler).toHaveBeenCalledWith('stopped'); }); it('should handle multiple stop calls gracefully', async () => { mockSocket.close.mockImplementation((callback) => { setTimeout(callback, 0); }); await endpoint.stopListening(); await endpoint.stopListening(); expect(endpoint.getStatus().status).toBe('stopped'); }); it('should handle stop when not started', async () => { const stoppedEndpoint = (0, endpoint_1.createOSCEndpoint)('stopped-test', { port: 8003 }); await stoppedEndpoint.stopListening(); expect(stoppedEndpoint.getStatus().status).toBe('stopped'); }); }); describe('message handling', () => { let messageHandler; beforeEach(async () => { mockSocket.bind.mockImplementation((_port, callback) => { setTimeout(callback, 0); }); await endpoint.startListening(); messageHandler = mockSocket.on.mock.calls.find((call) => call[0] === 'message')[1]; }); it('should parse and store valid OSC messages', () => { const messageEventHandler = jest.fn(); endpoint.on('message', messageEventHandler); const oscMessage = Buffer.from([ 0x2f, 0x74, 0x65, 0x73, 0x74, 0x00, 0x00, 0x00, 0x2c, 0x66, 0x00, 0x00, 0x41, 0x88, 0x00, 0x00, ]); const rinfo = { address: '192.168.1.100', port: 57120, family: 'IPv4', size: oscMessage.length, }; messageHandler(oscMessage, rinfo); expect(messageEventHandler).toHaveBeenCalledWith(expect.objectContaining({ address: '/test', typeTags: 'f', arguments: [17.0], sourceIp: '192.168.1.100', sourcePort: 57120, })); expect(endpoint.getStatus().messageCount).toBe(1); }); it('should emit error for malformed OSC messages', () => { const errorHandler = jest.fn(); endpoint.on('error', errorHandler); const invalidMessage = Buffer.from([0x2f, 0x74]); const rinfo = { address: '192.168.1.100', port: 57120, family: 'IPv4', size: 2 }; messageHandler(invalidMessage, rinfo); expect(errorHandler).toHaveBeenCalledWith(expect.objectContaining({ code: index_1.ErrorCode.INVALID_OSC_MESSAGE, message: expect.stringContaining('OSC message too short'), })); expect(endpoint.getStatus().messageCount).toBe(0); }); it('should continue listening after parse errors', () => { const errorHandler = jest.fn(); const messageEventHandler = jest.fn(); endpoint.on('error', errorHandler); endpoint.on('message', messageEventHandler); const invalidMessage = Buffer.from([0x2f, 0x74]); const rinfo1 = { address: '192.168.1.100', port: 57120, family: 'IPv4', size: 2 }; messageHandler(invalidMessage, rinfo1); const validMessage = Buffer.from([ 0x2f, 0x74, 0x65, 0x73, 0x74, 0x00, 0x00, 0x00, 0x2c, 0x66, 0x00, 0x00, 0x41, 0x88, 0x00, 0x00, ]); const rinfo2 = { address: '192.168.1.100', port: 57120, family: 'IPv4', size: 16 }; messageHandler(validMessage, rinfo2); expect(errorHandler).toHaveBeenCalledTimes(1); expect(messageEventHandler).toHaveBeenCalledTimes(1); expect(endpoint.getStatus().messageCount).toBe(1); }); }); describe('error handling', () => { it('should handle port in use error', async () => { const errorEventHandler = jest.fn(); const statusChangeHandler = jest.fn(); endpoint.on('error', errorEventHandler); endpoint.on('statusChange', statusChangeHandler); mockSocket.bind.mockImplementation((_port, callback) => { setTimeout(callback, 0); }); await endpoint.startListening(); const portError = new Error('EADDRINUSE: Address already in use'); const errorCallback = mockSocket.on.mock.calls.find((call) => call[0] === 'error')?.[1]; if (errorCallback) { errorCallback(portError); } expect(endpoint.getStatus().status).toBe('error'); expect(statusChangeHandler).toHaveBeenCalledWith('error'); expect(errorEventHandler).toHaveBeenCalledWith(expect.objectContaining({ code: index_1.ErrorCode.PORT_IN_USE, message: 'Port 8000 is already in use. Please try a different port.', details: expect.objectContaining({ port: 8000, suggestedPorts: [8001, 8002, 8003], }), })); }); it('should handle permission denied error', async () => { const errorEventHandler = jest.fn(); endpoint.on('error', errorEventHandler); mockSocket.bind.mockImplementation((_port, callback) => { setTimeout(callback, 0); }); await endpoint.startListening(); const permissionError = new Error('EACCES: Permission denied'); const errorCallback = mockSocket.on.mock.calls.find((call) => call[0] === 'error')?.[1]; if (errorCallback) { errorCallback(permissionError); } expect(errorEventHandler).toHaveBeenCalledWith(expect.objectContaining({ code: index_1.ErrorCode.PERMISSION_DENIED, message: 'Permission denied to bind to port 8000. Try using a port number above 1024 or run with appropriate privileges.', })); }); it('should handle generic network errors', async () => { const errorEventHandler = jest.fn(); endpoint.on('error', errorEventHandler); mockSocket.bind.mockImplementation((_port, callback) => { setTimeout(callback, 0); }); await endpoint.startListening(); const networkError = new Error('Some network error'); const errorCallback = mockSocket.on.mock.calls.find((call) => call[0] === 'error')?.[1]; if (errorCallback) { errorCallback(networkError); } expect(errorEventHandler).toHaveBeenCalledWith(expect.objectContaining({ code: index_1.ErrorCode.NETWORK_ERROR, message: 'Network error: Network error on endpoint test-endpoint: Some network error', })); }); }); describe('utility methods', () => { it('should return correct ID', () => { expect(endpoint.getId()).toBe('test-endpoint'); }); it('should return correct port', () => { expect(endpoint.getPort()).toBe(8000); }); it('should return correct active status', async () => { expect(endpoint.isActive()).toBe(false); mockSocket.bind.mockImplementation((_port, callback) => { setTimeout(callback, 0); }); await endpoint.startListening(); expect(endpoint.isActive()).toBe(true); mockSocket.close.mockImplementation((callback) => { setTimeout(callback, 0); }); await endpoint.stopListening(); expect(endpoint.isActive()).toBe(false); }); it('should provide access to message buffer', () => { const buffer = endpoint.getMessageBuffer(); expect(buffer).toBeDefined(); expect(typeof buffer.addMessage).toBe('function'); expect(typeof buffer.getMessages).toBe('function'); }); }); describe('integration with message buffer', () => { beforeEach(async () => { mockSocket.bind.mockImplementation((_port, callback) => { setTimeout(callback, 0); }); await endpoint.startListening(); }); it('should store messages in buffer with address filtering', () => { const filteredEndpoint = (0, endpoint_1.createOSCEndpoint)('filtered', { port: 8004, addressFilters: ['/synth/*'], }); const filteredMockSocket = { bind: jest.fn((_port, callback) => setTimeout(callback, 0)), close: jest.fn(), on: jest.fn(), removeAllListeners: jest.fn(), }; mockCreateSocket.mockReturnValue(filteredMockSocket); return filteredEndpoint.startListening().then(() => { const filteredMessageHandler = filteredMockSocket.on.mock.calls.find((call) => call[0] === 'message')[1]; const matchingMessage = Buffer.from([ 0x2f, 0x73, 0x79, 0x6e, 0x74, 0x68, 0x2f, 0x66, 0x72, 0x65, 0x71, 0x00, 0x2c, 0x66, 0x00, 0x00, 0x41, 0x88, 0x00, 0x00, ]); const nonMatchingMessage = Buffer.from([ 0x2f, 0x64, 0x72, 0x75, 0x6d, 0x2f, 0x6b, 0x69, 0x63, 0x6b, 0x00, 0x00, 0x2c, 0x66, 0x00, 0x00, 0x42, 0x20, 0x00, 0x00, ]); const rinfo = { address: '192.168.1.100', port: 57120, family: 'IPv4', size: 16 }; filteredMessageHandler(matchingMessage, rinfo); filteredMessageHandler(nonMatchingMessage, rinfo); const buffer = filteredEndpoint.getMessageBuffer(); const messages = buffer.getMessages(); expect(messages).toHaveLength(1); expect(messages[0]?.address).toBe('/synth/freq'); }); }); }); }); //# sourceMappingURL=endpoint.test.js.map