UNPKG

falkordb-mcpserver

Version:

Model Context Protocol server for FalkorDB graph databases - enables AI assistants to query and manage graph data using natural language

385 lines (384 loc) 15.5 kB
import { falkorDBService } from './falkordb.service'; import { AppError, CommonErrors } from '../errors/AppError.js'; // Mock the logger service jest.mock('./logger.service.js', () => ({ logger: { info: jest.fn().mockResolvedValue(undefined), warn: jest.fn().mockResolvedValue(undefined), error: jest.fn().mockResolvedValue(undefined), debug: jest.fn().mockResolvedValue(undefined), } })); // Mock the config jest.mock('../config/index.js', () => ({ config: { falkorDB: { host: 'localhost', port: 6379, username: 'testuser', password: 'testpass' } } })); // Mock the FalkorDB library jest.mock('falkordb', () => { const mockSelectGraph = jest.fn(); const mockQuery = jest.fn(); const mockList = jest.fn(); const mockClose = jest.fn(); const mockPing = jest.fn(); const mockDelete = jest.fn(); return { FalkorDB: { connect: jest.fn().mockResolvedValue({ connection: Promise.resolve({ ping: mockPing }), selectGraph: mockSelectGraph.mockReturnValue({ query: mockQuery, delete: mockDelete }), list: mockList, close: mockClose }) }, mockSelectGraph, mockQuery, mockList, mockClose, mockPing, mockDelete }; }); describe('FalkorDB Service', () => { let mockFalkorDB; beforeAll(async () => { // Access the mocks mockFalkorDB = await import('falkordb'); }); beforeEach(() => { jest.clearAllMocks(); // Reset service state falkorDBService.client = null; falkorDBService.retryCount = 0; falkorDBService.isInitializing = false; }); describe('initialize', () => { it('should successfully initialize and connect to FalkorDB', async () => { // Arrange mockFalkorDB.FalkorDB.connect.mockResolvedValue({ connection: Promise.resolve({ ping: mockFalkorDB.mockPing.mockResolvedValue('PONG') }), selectGraph: mockFalkorDB.mockSelectGraph, list: mockFalkorDB.mockList, close: mockFalkorDB.mockClose }); // Act await falkorDBService.initialize(); // Assert expect(mockFalkorDB.FalkorDB.connect).toHaveBeenCalledWith({ socket: { host: 'localhost', port: 6379, }, password: 'testpass', username: 'testuser', }); expect(mockFalkorDB.mockPing).toHaveBeenCalled(); expect(falkorDBService.client).not.toBeNull(); expect(falkorDBService.retryCount).toBe(0); expect(falkorDBService.isInitializing).toBe(false); }); it('should not initialize if already initializing', async () => { // Arrange falkorDBService.isInitializing = true; // Act await falkorDBService.initialize(); // Assert expect(mockFalkorDB.FalkorDB.connect).not.toHaveBeenCalled(); }); it('should retry connection on failure and eventually succeed', async () => { // Arrange const connectError = new Error('Connection failed'); mockFalkorDB.FalkorDB.connect .mockRejectedValueOnce(connectError) .mockRejectedValueOnce(connectError) .mockResolvedValueOnce({ connection: Promise.resolve({ ping: mockFalkorDB.mockPing.mockResolvedValue('PONG') }), selectGraph: mockFalkorDB.mockSelectGraph, list: mockFalkorDB.mockList, close: mockFalkorDB.mockClose }); // Mock setTimeout to avoid actual delays const setTimeoutSpy = jest.spyOn(global, 'setTimeout').mockImplementation((callback) => { setImmediate(callback); return {}; }); // Act await falkorDBService.initialize(); // Assert expect(mockFalkorDB.FalkorDB.connect).toHaveBeenCalledTimes(3); expect(falkorDBService.client).not.toBeNull(); expect(falkorDBService.retryCount).toBe(0); // Cleanup setTimeoutSpy.mockRestore(); }); it('should throw AppError after max retries exceeded', async () => { // Arrange const connectError = new Error('Connection failed'); mockFalkorDB.FalkorDB.connect.mockRejectedValue(connectError); // Mock setTimeout to avoid actual delays const setTimeoutSpy = jest.spyOn(global, 'setTimeout').mockImplementation((callback) => { setImmediate(callback); return {}; }); // Act & Assert try { await falkorDBService.initialize(); fail('Expected initialize to throw AppError'); } catch (error) { expect(error).toBeInstanceOf(AppError); expect(error.name).toBe(CommonErrors.CONNECTION_FAILED); } expect(mockFalkorDB.FalkorDB.connect).toHaveBeenCalledTimes(6); // 1 initial + 5 retries // Cleanup setTimeoutSpy.mockRestore(); }); it('should handle ping failure during connection test', async () => { // Arrange const pingError = new Error('Ping failed'); mockFalkorDB.FalkorDB.connect.mockResolvedValue({ connection: Promise.resolve({ ping: mockFalkorDB.mockPing.mockRejectedValue(pingError) }), selectGraph: mockFalkorDB.mockSelectGraph, list: mockFalkorDB.mockList, close: mockFalkorDB.mockClose }); // Mock setTimeout to avoid actual delays const setTimeoutSpy = jest.spyOn(global, 'setTimeout').mockImplementation((callback) => { setImmediate(callback); return {}; }); // Act & Assert await expect(falkorDBService.initialize()).rejects.toThrow(AppError); // Cleanup setTimeoutSpy.mockRestore(); }); }); describe('executeQuery', () => { it('should execute a query on the specified graph', async () => { // Arrange const graphName = 'testGraph'; const query = 'MATCH (n) RETURN n'; const params = { param1: 'value1' }; const expectedResult = { records: [{ id: 1 }] }; mockFalkorDB.mockQuery.mockResolvedValue(expectedResult); // Force client to be available falkorDBService.client = { selectGraph: mockFalkorDB.mockSelectGraph }; // Act const result = await falkorDBService.executeQuery(graphName, query, params); // Assert expect(mockFalkorDB.mockSelectGraph).toHaveBeenCalledWith(graphName); expect(mockFalkorDB.mockQuery).toHaveBeenCalledWith(query, params); expect(result).toEqual(expectedResult); }); it('should execute a query without params', async () => { // Arrange const graphName = 'testGraph'; const query = 'MATCH (n) RETURN n'; const expectedResult = { records: [{ id: 1 }] }; mockFalkorDB.mockQuery.mockResolvedValue(expectedResult); // Force client to be available falkorDBService.client = { selectGraph: mockFalkorDB.mockSelectGraph }; // Act const result = await falkorDBService.executeQuery(graphName, query); // Assert expect(mockFalkorDB.mockSelectGraph).toHaveBeenCalledWith(graphName); expect(mockFalkorDB.mockQuery).toHaveBeenCalledWith(query, undefined); expect(result).toEqual(expectedResult); }); it('should throw AppError if client is not initialized', async () => { // Arrange falkorDBService.client = null; // Act & Assert await expect(falkorDBService.executeQuery('graph', 'query')) .rejects .toThrow(AppError); await expect(falkorDBService.executeQuery('graph', 'query')) .rejects .toThrow('FalkorDB client not initialized'); }); it('should throw AppError when query execution fails', async () => { // Arrange const graphName = 'testGraph'; const query = 'INVALID QUERY'; const queryError = new Error('Query syntax error'); mockFalkorDB.mockQuery.mockRejectedValue(queryError); // Force client to be available falkorDBService.client = { selectGraph: mockFalkorDB.mockSelectGraph }; // Act & Assert try { await falkorDBService.executeQuery(graphName, query); fail('Expected executeQuery to throw AppError'); } catch (error) { expect(error).toBeInstanceOf(AppError); expect(error.name).toBe(CommonErrors.OPERATION_FAILED); } }); }); describe('listGraphs', () => { it('should return a list of graphs', async () => { // Arrange const expectedGraphs = ['graph1', 'graph2']; mockFalkorDB.mockList.mockResolvedValue(expectedGraphs); // Force client to be available falkorDBService.client = { list: mockFalkorDB.mockList }; // Act const result = await falkorDBService.listGraphs(); // Assert expect(mockFalkorDB.mockList).toHaveBeenCalled(); expect(result).toEqual(expectedGraphs); }); it('should return empty array when no graphs exist', async () => { // Arrange const expectedGraphs = []; mockFalkorDB.mockList.mockResolvedValue(expectedGraphs); // Force client to be available falkorDBService.client = { list: mockFalkorDB.mockList }; // Act const result = await falkorDBService.listGraphs(); // Assert expect(mockFalkorDB.mockList).toHaveBeenCalled(); expect(result).toEqual(expectedGraphs); }); it('should throw AppError if client is not initialized', async () => { // Arrange falkorDBService.client = null; // Act & Assert await expect(falkorDBService.listGraphs()) .rejects .toThrow(AppError); await expect(falkorDBService.listGraphs()) .rejects .toThrow('FalkorDB client not initialized'); }); it('should throw AppError when listing graphs fails', async () => { // Arrange const listError = new Error('Database connection lost'); mockFalkorDB.mockList.mockRejectedValue(listError); // Force client to be available falkorDBService.client = { list: mockFalkorDB.mockList }; // Act & Assert try { await falkorDBService.listGraphs(); fail('Expected listGraphs to throw AppError'); } catch (error) { expect(error).toBeInstanceOf(AppError); expect(error.name).toBe(CommonErrors.OPERATION_FAILED); } }); }); describe('deleteGraph', () => { it('should delete a graph successfully', async () => { // Arrange const graphName = 'testGraph'; mockFalkorDB.mockDelete.mockResolvedValue(undefined); // Force client to be available falkorDBService.client = { selectGraph: mockFalkorDB.mockSelectGraph }; // Act await falkorDBService.deleteGraph(graphName); // Assert expect(mockFalkorDB.mockSelectGraph).toHaveBeenCalledWith(graphName); expect(mockFalkorDB.mockDelete).toHaveBeenCalled(); }); it('should throw AppError if client is not initialized', async () => { // Arrange falkorDBService.client = null; // Act & Assert await expect(falkorDBService.deleteGraph('testGraph')) .rejects .toThrow(AppError); await expect(falkorDBService.deleteGraph('testGraph')) .rejects .toThrow('FalkorDB client not initialized'); }); it('should throw AppError when delete operation fails', async () => { // Arrange const graphName = 'testGraph'; const deleteError = new Error('Graph not found'); mockFalkorDB.mockDelete.mockRejectedValue(deleteError); // Force client to be available falkorDBService.client = { selectGraph: mockFalkorDB.mockSelectGraph }; // Act & Assert try { await falkorDBService.deleteGraph(graphName); fail('Expected deleteGraph to throw AppError'); } catch (error) { expect(error).toBeInstanceOf(AppError); expect(error.name).toBe(CommonErrors.OPERATION_FAILED); } }); }); describe('close', () => { it('should close the client connection successfully', async () => { // Arrange mockFalkorDB.mockClose.mockResolvedValue(undefined); falkorDBService.client = { close: mockFalkorDB.mockClose }; falkorDBService.retryCount = 3; // Act await falkorDBService.close(); // Assert expect(mockFalkorDB.mockClose).toHaveBeenCalled(); expect(falkorDBService.client).toBeNull(); expect(falkorDBService.retryCount).toBe(0); }); it('should handle close error gracefully', async () => { // Arrange const closeError = new Error('Close failed'); mockFalkorDB.mockClose.mockRejectedValue(closeError); falkorDBService.client = { close: mockFalkorDB.mockClose }; falkorDBService.retryCount = 2; // Act await falkorDBService.close(); // Assert expect(mockFalkorDB.mockClose).toHaveBeenCalled(); expect(falkorDBService.client).toBeNull(); expect(falkorDBService.retryCount).toBe(0); }); it('should not throw if client is already null', async () => { // Arrange falkorDBService.client = null; // Act & Assert await expect(falkorDBService.close()).resolves.not.toThrow(); expect(mockFalkorDB.mockClose).not.toHaveBeenCalled(); }); }); });