Skip to main content
Glama
by RadonX
search-notes.test.js18.1 kB
import { jest } from '@jest/globals'; import { searchNotes } from '../src/tools/search-notes.js'; import { ValidationError } from '../src/utils/validation.js'; import { TriliumAPIError } from '../src/utils/trilium-client.js'; // Mock the logger to avoid console output during tests jest.mock('../src/utils/logger.js', () => ({ logger: { debug: jest.fn(), info: jest.fn(), error: jest.fn(), warn: jest.fn() } })); describe('searchNotes', () => { let mockTriliumClient; beforeEach(() => { // Create a fresh mock for each test mockTriliumClient = { get: jest.fn() }; jest.clearAllMocks(); }); describe('successful searches', () => { test('should return formatted results for basic fulltext search', async () => { const mockResponse = { results: [ { noteId: 'note123', title: 'JavaScript Programming', type: 'text', dateModified: '2024-01-15T14:30:00.000Z', parentNoteIds: ['parent456'] }, { noteId: 'note789', title: 'Advanced JavaScript', type: 'code', dateModified: '2024-01-14T10:15:00.000Z', parentNoteIds: ['parent456'] } ] }; mockTriliumClient.get.mockResolvedValue(mockResponse); const result = await searchNotes(mockTriliumClient, { query: 'javascript programming', limit: 10 }); expect(mockTriliumClient.get).toHaveBeenCalledWith('notes?search=javascript+programming&limit=10'); expect(result.content).toHaveLength(2); expect(result.content[0].type).toBe('text'); expect(result.content[0].text).toBe('Found 2 notes for "javascript programming"'); expect(result.content[1].type).toBe('application/json'); const searchData = JSON.parse(result.content[1].text); expect(searchData.query).toBe('javascript programming'); expect(searchData.limit).toBe(10); expect(searchData.totalResults).toBe(2); expect(searchData.notes).toHaveLength(2); expect(searchData.notes[0].noteId).toBe('note123'); expect(searchData.notes[0].title).toBe('JavaScript Programming'); expect(searchData.notes[1].noteId).toBe('note789'); expect(searchData.notes[1].title).toBe('Advanced JavaScript'); }); test('should handle exact match search with quotes', async () => { const mockResponse = { results: [ { noteId: 'note456', title: 'The Two Towers', type: 'text', dateModified: '2024-01-15T14:30:00.000Z' } ] }; mockTriliumClient.get.mockResolvedValue(mockResponse); const result = await searchNotes(mockTriliumClient, { query: '"Two Towers"', limit: 5 }); expect(mockTriliumClient.get).toHaveBeenCalledWith('notes?search=%22Two+Towers%22&limit=5'); expect(result.content[0].text).toBe('Found 1 note for "\"Two Towers\""'); const jsonData = JSON.parse(result.content[1].text); expect(jsonData.query).toBe('"Two Towers"'); expect(jsonData.notes[0].title).toBe('The Two Towers'); }); test('should handle label-based search', async () => { const mockResponse = { results: [ { noteId: 'note789', title: 'JavaScript Book', type: 'book', dateModified: '2024-01-15T14:30:00.000Z' } ] }; mockTriliumClient.get.mockResolvedValue(mockResponse); const result = await searchNotes(mockTriliumClient, { query: '#book javascript', limit: 10 }); expect(mockTriliumClient.get).toHaveBeenCalledWith('notes?search=%23book+javascript&limit=10'); expect(result.content[0].text).toBe('Found 1 note for "#book javascript"'); const jsonData = JSON.parse(result.content[1].text); expect(jsonData.query).toBe('#book javascript'); }); test('should use default limit when not specified', async () => { const mockResponse = { results: [] }; mockTriliumClient.get.mockResolvedValue(mockResponse); await searchNotes(mockTriliumClient, { query: 'test query' }); expect(mockTriliumClient.get).toHaveBeenCalledWith('notes?search=test+query&limit=10'); }); test('should handle notes with missing optional fields', async () => { const mockResponse = { results: [ { noteId: 'note123', // Missing title, type, dateModified, parentNoteId } ] }; mockTriliumClient.get.mockResolvedValue(mockResponse); const result = await searchNotes(mockTriliumClient, { query: 'test', limit: 10 }); expect(result.content[0].text).toBe('Found 1 note for "test"'); const jsonData = JSON.parse(result.content[1].text); expect(jsonData.notes[0].title).toBe('Untitled'); expect(jsonData.notes[0].type).toBe('text'); expect(jsonData.notes[0].dateModified).toBeUndefined(); expect(jsonData.notes[0].parentNoteIds).toEqual([]); }); test('should include all optional fields when present', async () => { const mockResponse = { results: [ { noteId: 'note123', title: 'Complete Note', type: 'code', dateModified: '2024-01-15T14:30:00.000Z', parentNoteIds: ['parent456'] } ] }; mockTriliumClient.get.mockResolvedValue(mockResponse); const result = await searchNotes(mockTriliumClient, { query: 'test', limit: 10 }); expect(result.content[0].text).toBe('Found 1 note for "test"'); const jsonData = JSON.parse(result.content[1].text); expect(jsonData.notes[0].title).toBe('Complete Note'); expect(jsonData.notes[0].type).toBe('code'); expect(jsonData.notes[0].dateModified).toBe('2024-01-15T14:30:00.000Z'); expect(jsonData.notes[0].parentNoteIds).toEqual(['parent456']); }); }); describe('empty results', () => { test('should return helpful message when no notes found', async () => { mockTriliumClient.get.mockResolvedValue({ results: [] }); const result = await searchNotes(mockTriliumClient, { query: 'nonexistent query', limit: 10 }); expect(result.content).toHaveLength(2); expect(result.content[0].text).toBe('No notes found for "nonexistent query"'); const jsonData = JSON.parse(result.content[1].text); expect(jsonData.query).toBe('nonexistent query'); expect(jsonData.totalResults).toBe(0); expect(jsonData.notes).toEqual([]); }); }); describe('input validation', () => { test('should reject empty query', async () => { const result = await searchNotes(mockTriliumClient, { query: '', limit: 10 }); expect(result.isError).toBe(true); expect(result.content[0].text).toBe('Validation error: Search query must be a non-empty string'); const jsonData = JSON.parse(result.content[1].text); expect(jsonData.error.type).toBe('ValidationError'); expect(jsonData.error.message).toBe('Search query must be a non-empty string'); expect(mockTriliumClient.get).not.toHaveBeenCalled(); }); test('should reject whitespace-only query', async () => { const result = await searchNotes(mockTriliumClient, { query: ' \t\n ', limit: 10 }); expect(result.isError).toBe(true); expect(result.content[0].text).toBe('Validation error: Search query cannot be empty'); const jsonData = JSON.parse(result.content[1].text); expect(jsonData.error.message).toBe('Search query cannot be empty'); expect(mockTriliumClient.get).not.toHaveBeenCalled(); }); test('should reject query that is too long', async () => { const longQuery = 'a'.repeat(501); // Exceeds 500 character limit const result = await searchNotes(mockTriliumClient, { query: longQuery, limit: 10 }); expect(result.isError).toBe(true); expect(result.content[0].text).toBe('Validation error: Search query cannot exceed 500 characters'); const jsonData = JSON.parse(result.content[1].text); expect(jsonData.error.message).toBe('Search query cannot exceed 500 characters'); expect(mockTriliumClient.get).not.toHaveBeenCalled(); }); test('should reject non-string query', async () => { const result = await searchNotes(mockTriliumClient, { query: 123, limit: 10 }); expect(result.isError).toBe(true); expect(result.content[0].text).toBe('Validation error: Search query must be a non-empty string'); const jsonData = JSON.parse(result.content[1].text); expect(jsonData.error.message).toBe('Search query must be a non-empty string'); expect(mockTriliumClient.get).not.toHaveBeenCalled(); }); test('should reject null/undefined query', async () => { const result = await searchNotes(mockTriliumClient, { query: null, limit: 10 }); expect(result.isError).toBe(true); expect(result.content[0].text).toBe('Validation error: Search query must be a non-empty string'); const jsonData = JSON.parse(result.content[1].text); expect(jsonData.error.message).toBe('Search query must be a non-empty string'); expect(mockTriliumClient.get).not.toHaveBeenCalled(); }); test('should reject limit less than 1', async () => { const result = await searchNotes(mockTriliumClient, { query: 'test', limit: 0 }); expect(result.isError).toBe(true); expect(result.content[0].text).toBe('Validation error: Limit must be a positive integer'); expect(mockTriliumClient.get).not.toHaveBeenCalled(); }); test('should reject limit greater than 100', async () => { const result = await searchNotes(mockTriliumClient, { query: 'test', limit: 101 }); expect(result.isError).toBe(true); expect(result.content[0].text).toBe('Validation error: Limit cannot exceed 100'); expect(mockTriliumClient.get).not.toHaveBeenCalled(); }); test('should reject non-numeric limit', async () => { const result = await searchNotes(mockTriliumClient, { query: 'test', limit: 'invalid' }); expect(result.isError).toBe(true); expect(result.content[0].text).toBe('Validation error: Limit must be a positive integer'); expect(mockTriliumClient.get).not.toHaveBeenCalled(); }); test('should accept valid limit boundaries', async () => { mockTriliumClient.get.mockResolvedValue({ results: [] }); // Test minimum valid limit await searchNotes(mockTriliumClient, { query: 'test', limit: 1 }); expect(mockTriliumClient.get).toHaveBeenCalledWith('notes?search=test&limit=1'); mockTriliumClient.get.mockClear(); // Test maximum valid limit await searchNotes(mockTriliumClient, { query: 'test', limit: 100 }); expect(mockTriliumClient.get).toHaveBeenCalledWith('notes?search=test&limit=100'); }); test('should trim whitespace from query', async () => { mockTriliumClient.get.mockResolvedValue({ results: [] }); await searchNotes(mockTriliumClient, { query: ' test query ', limit: 10 }); expect(mockTriliumClient.get).toHaveBeenCalledWith('notes?search=test+query&limit=10'); }); }); describe('API error handling', () => { test('should handle TriliumNext API errors', async () => { const apiError = new TriliumAPIError('Server unavailable', 503); mockTriliumClient.get.mockRejectedValue(apiError); const result = await searchNotes(mockTriliumClient, { query: 'test', limit: 10 }); expect(result.isError).toBe(true); expect(result.content[0].text).toBe('TriliumNext API error: Server unavailable'); const jsonData = JSON.parse(result.content[1].text); expect(jsonData.error.type).toBe('TriliumAPIError'); expect(jsonData.error.message).toBe('Server unavailable'); expect(jsonData.error.status).toBe(503); }); test('should handle authentication errors', async () => { const authError = new TriliumAPIError('Authentication failed', 401); mockTriliumClient.get.mockRejectedValue(authError); const result = await searchNotes(mockTriliumClient, { query: 'test', limit: 10 }); expect(result.isError).toBe(true); expect(result.content[0].text).toBe('TriliumNext API error: Authentication failed'); const jsonData = JSON.parse(result.content[1].text); expect(jsonData.error.message).toBe('Authentication failed'); expect(jsonData.error.status).toBe(401); }); test('should handle invalid API response format', async () => { // Return response without results array mockTriliumClient.get.mockResolvedValue({ invalid: 'response' }); const result = await searchNotes(mockTriliumClient, { query: 'test', limit: 10 }); expect(result.isError).toBe(true); expect(result.content[0].text).toBe('TriliumNext API error: Invalid response from TriliumNext API - expected object with results array'); const jsonData = JSON.parse(result.content[1].text); expect(jsonData.error.message).toBe('Invalid response from TriliumNext API - expected object with results array'); }); test('should handle network errors', async () => { const networkError = new Error('Network timeout'); mockTriliumClient.get.mockRejectedValue(networkError); const result = await searchNotes(mockTriliumClient, { query: 'test', limit: 10 }); expect(result.isError).toBe(true); expect(result.content[0].text).toBe('Search failed: Network timeout'); const jsonData = JSON.parse(result.content[1].text); expect(jsonData.error.type).toBe('Error'); expect(jsonData.error.message).toBe('Network timeout'); }); }); describe('URL encoding', () => { test('should properly encode special characters in search query', async () => { mockTriliumClient.get.mockResolvedValue({ results: [] }); await searchNotes(mockTriliumClient, { query: 'test & encode + special % chars', limit: 10 }); expect(mockTriliumClient.get).toHaveBeenCalledWith( 'notes?search=test+%26+encode+%2B+special+%25+chars&limit=10' ); }); test('should properly encode quotes in search query', async () => { mockTriliumClient.get.mockResolvedValue({ results: [] }); await searchNotes(mockTriliumClient, { query: '"exact phrase" with quotes', limit: 10 }); expect(mockTriliumClient.get).toHaveBeenCalledWith( 'notes?search=%22exact+phrase%22+with+quotes&limit=10' ); }); test('should properly encode hash symbols for label search', async () => { mockTriliumClient.get.mockResolvedValue({ results: [] }); await searchNotes(mockTriliumClient, { query: '#label #tag search', limit: 10 }); expect(mockTriliumClient.get).toHaveBeenCalledWith( 'notes?search=%23label+%23tag+search&limit=10' ); }); }); describe('edge cases', () => { test('should handle very large result sets', async () => { // Create mock response with many notes const mockResponse = { results: Array.from({ length: 50 }, (_, i) => ({ noteId: `note${i}`, title: `Note ${i}`, type: 'text', dateModified: '2024-01-15T14:30:00.000Z' })) }; mockTriliumClient.get.mockResolvedValue(mockResponse); const result = await searchNotes(mockTriliumClient, { query: 'test', limit: 50 }); expect(result.content[0].text).toBe('Found 50 notes for "test" (showing first 50)'); const jsonData = JSON.parse(result.content[1].text); expect(jsonData.totalResults).toBe(50); expect(jsonData.hasMore).toBe(true); expect(jsonData.notes).toHaveLength(50); // Check first and last notes expect(jsonData.notes[0].title).toBe('Note 0'); expect(jsonData.notes[49].title).toBe('Note 49'); }); test('should handle notes with very long titles', async () => { const longTitle = 'A'.repeat(200); const mockResponse = { results: [ { noteId: 'note123', title: longTitle, type: 'text' } ] }; mockTriliumClient.get.mockResolvedValue(mockResponse); const result = await searchNotes(mockTriliumClient, { query: 'test', limit: 10 }); expect(result.content[0].text).toBe('Found 1 note for "test"'); const jsonData = JSON.parse(result.content[1].text); expect(jsonData.notes[0].title).toBe(longTitle); }); test('should handle Unicode characters in search query and results', async () => { const mockResponse = { results: [ { noteId: 'note123', title: '日本語のノート 🌸', type: 'text' } ] }; mockTriliumClient.get.mockResolvedValue(mockResponse); const result = await searchNotes(mockTriliumClient, { query: '日本語 emoji 🌸', limit: 10 }); expect(mockTriliumClient.get).toHaveBeenCalledWith( 'notes?search=%E6%97%A5%E6%9C%AC%E8%AA%9E+emoji+%F0%9F%8C%B8&limit=10' ); expect(result.content[0].text).toBe('Found 1 note for "日本語 emoji 🌸"'); const jsonData = JSON.parse(result.content[1].text); expect(jsonData.notes[0].title).toBe('日本語のノート 🌸'); }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/RadonX/mcp-trilium'

If you have feedback or need assistance with the MCP directory API, please join our Discord server