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
JavaScript
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();
});
});
});