Skip to main content
Glama
by RadonX
create-note.test.js16.6 kB
import { jest } from '@jest/globals'; import { createNote } from '../src/tools/create-note.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('createNote', () => { let mockTriliumClient; beforeEach(() => { // Create a fresh mock for each test mockTriliumClient = { post: jest.fn() }; jest.clearAllMocks(); }); describe('successful note creation', () => { test('should create basic text note successfully', async () => { const mockResponse = { note: { noteId: 'abc123', title: 'My Test Note', type: 'text', dateCreated: '2024-01-15T14:30:00.000Z', dateModified: '2024-01-15T14:30:00.000Z' } }; mockTriliumClient.post.mockResolvedValue(mockResponse); const result = await createNote(mockTriliumClient, { title: 'My Test Note', content: 'This is the note content', type: 'text' }); expect(mockTriliumClient.post).toHaveBeenCalledWith('create-note', { title: 'My Test Note', content: 'This is the note content', type: 'text', parentNoteId: 'root' }); expect(result.content).toHaveLength(2); expect(result.content[0].type).toBe('text'); expect(result.content[0].text).toBe('Note created: "My Test Note" (ID: abc123)'); expect(result.content[1].type).toBe('application/json'); const jsonData = JSON.parse(result.content[1].text); expect(jsonData.operation).toBe('create_note'); expect(jsonData.request.title).toBe('My Test Note'); expect(jsonData.request.type).toBe('text'); expect(jsonData.request.contentLength).toBe(24); expect(jsonData.result.noteId).toBe('abc123'); expect(jsonData.result.triliumUrl).toBe('trilium://note/abc123'); }); test('should create note with parent successfully', async () => { const mockResponse = { note: { noteId: 'child123', title: 'Child Note', type: 'text', parentNoteId: 'parent456' } }; mockTriliumClient.post.mockResolvedValue(mockResponse); const result = await createNote(mockTriliumClient, { title: 'Child Note', content: 'Child content', type: 'text', parentNoteId: 'parent456' }); expect(mockTriliumClient.post).toHaveBeenCalledWith('create-note', { title: 'Child Note', content: 'Child content', type: 'text', parentNoteId: 'parent456' }); const jsonData = JSON.parse(result.content[1].text); expect(jsonData.request.parentNoteId).toBe('parent456'); }); test('should create different note types', async () => { const mockResponse = { note: { noteId: 'code123', title: 'JavaScript Code', type: 'code' } }; mockTriliumClient.post.mockResolvedValue(mockResponse); await createNote(mockTriliumClient, { title: 'JavaScript Code', content: 'console.log("Hello World");', type: 'code' }); expect(mockTriliumClient.post).toHaveBeenCalledWith('create-note', { title: 'JavaScript Code', content: 'console.log("Hello World");', type: 'code', parentNoteId: 'root' }); }); test('should default to root parentNoteId when not provided', async () => { const mockResponse = { note: { noteId: 'root123', title: 'Root Note', type: 'text' } }; mockTriliumClient.post.mockResolvedValue(mockResponse); const result = await createNote(mockTriliumClient, { title: 'Root Note', content: 'Root content', type: 'text' }); expect(mockTriliumClient.post).toHaveBeenCalledWith('create-note', { title: 'Root Note', content: 'Root content', type: 'text', parentNoteId: 'root' }); const jsonData = JSON.parse(result.content[1].text); expect(jsonData.request.parentNoteId).toBe('root'); }); test('should preserve additional API response fields', async () => { const mockResponse = { note: { noteId: 'abc123', title: 'My Note', type: 'text', isProtected: false, mime: 'text/html', attributes: ['#tag1', '#tag2'] } }; mockTriliumClient.post.mockResolvedValue(mockResponse); const result = await createNote(mockTriliumClient, { title: 'My Note', content: 'Content', type: 'text' }); const jsonData = JSON.parse(result.content[1].text); expect(jsonData.result.isProtected).toBe(false); expect(jsonData.result.mime).toBe('text/html'); expect(jsonData.result.attributes).toEqual(['#tag1', '#tag2']); }); }); describe('input validation', () => { test('should reject empty title', async () => { const result = await createNote(mockTriliumClient, { title: '', content: 'Content', type: 'text' }); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Validation error:'); expect(mockTriliumClient.post).not.toHaveBeenCalled(); }); test('should reject whitespace-only title', async () => { const result = await createNote(mockTriliumClient, { title: ' \t\n ', content: 'Content', type: 'text' }); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Validation error:'); expect(mockTriliumClient.post).not.toHaveBeenCalled(); }); test('should reject title that is too long', async () => { const longTitle = 'A'.repeat(201); // Exceeds 200 character limit const result = await createNote(mockTriliumClient, { title: longTitle, content: 'Content', type: 'text' }); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Validation error:'); expect(mockTriliumClient.post).not.toHaveBeenCalled(); }); test('should reject non-string title', async () => { const result = await createNote(mockTriliumClient, { title: 123, content: 'Content', type: 'text' }); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Validation error:'); expect(mockTriliumClient.post).not.toHaveBeenCalled(); }); test('should reject null/undefined title', async () => { const result = await createNote(mockTriliumClient, { title: null, content: 'Content', type: 'text' }); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Validation error:'); expect(mockTriliumClient.post).not.toHaveBeenCalled(); }); test('should reject non-string content', async () => { const result = await createNote(mockTriliumClient, { title: 'Valid Title', content: 123, type: 'text' }); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Validation error:'); expect(mockTriliumClient.post).not.toHaveBeenCalled(); }); test('should reject invalid note type', async () => { const result = await createNote(mockTriliumClient, { title: 'Valid Title', content: 'Valid content', type: 'invalid-type' }); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Validation error:'); expect(mockTriliumClient.post).not.toHaveBeenCalled(); }); test('should accept valid note types', async () => { const validTypes = ['text', 'code', 'file', 'image', 'search', 'book']; const mockResponse = { note: { noteId: 'test123', title: 'Test Note', type: 'text' } }; for (const type of validTypes) { mockTriliumClient.post.mockResolvedValue(mockResponse); const result = await createNote(mockTriliumClient, { title: 'Test Note', content: 'Test content', type: type }); expect(result.isError).not.toBe(true); mockTriliumClient.post.mockClear(); } }); test('should reject invalid parentNoteId format', async () => { const result = await createNote(mockTriliumClient, { title: 'Valid Title', content: 'Valid content', type: 'text', parentNoteId: ' ' // whitespace-only should trigger validation error }); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Validation error:'); expect(mockTriliumClient.post).not.toHaveBeenCalled(); }); test('should trim whitespace from title', async () => { const mockResponse = { note: { noteId: 'abc123', title: 'Trimmed Title', type: 'text' } }; mockTriliumClient.post.mockResolvedValue(mockResponse); await createNote(mockTriliumClient, { title: ' Trimmed Title ', content: 'Content', type: 'text' }); expect(mockTriliumClient.post).toHaveBeenCalledWith('create-note', { title: 'Trimmed Title', content: 'Content', type: 'text', parentNoteId: 'root' }); }); }); describe('API error handling', () => { test('should handle TriliumNext API errors', async () => { const apiError = new TriliumAPIError('Server unavailable', 503); mockTriliumClient.post.mockRejectedValue(apiError); const result = await createNote(mockTriliumClient, { title: 'Test Note', content: 'Test content', type: 'text' }); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('TriliumNext API error: Server unavailable'); const jsonData = JSON.parse(result.content[1].text); expect(jsonData.error.type).toBe('TriliumAPIError'); expect(jsonData.error.status).toBe(503); expect(jsonData.error.message).toBe('Server unavailable'); }); test('should handle authentication errors', async () => { const authError = new TriliumAPIError('Authentication failed', 401); mockTriliumClient.post.mockRejectedValue(authError); const result = await createNote(mockTriliumClient, { title: 'Test Note', content: 'Test content', type: 'text' }); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('TriliumNext API error: Authentication failed'); const jsonData = JSON.parse(result.content[1].text); expect(jsonData.error.status).toBe(401); }); test('should handle invalid API response format', async () => { // Return response without note or noteId mockTriliumClient.post.mockResolvedValue({ invalid: 'response' }); const result = await createNote(mockTriliumClient, { title: 'Test Note', content: 'Test content', type: 'text' }); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('TriliumNext API error: Invalid response from TriliumNext API - missing note ID'); }); test('should handle network errors', async () => { const networkError = new Error('Network timeout'); mockTriliumClient.post.mockRejectedValue(networkError); const result = await createNote(mockTriliumClient, { title: 'Test Note', content: 'Test content', type: 'text' }); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Note creation failed: Network timeout'); const jsonData = JSON.parse(result.content[1].text); expect(jsonData.error.type).toBe('Error'); expect(jsonData.error.message).toBe('Network timeout'); }); test('should include error context in structured response', async () => { const apiError = new TriliumAPIError('Server error', 500, { details: 'Database connection failed' }); mockTriliumClient.post.mockRejectedValue(apiError); const result = await createNote(mockTriliumClient, { title: 'Test Note', content: 'Test content', type: 'text' }); const jsonData = JSON.parse(result.content[1].text); expect(jsonData.operation).toBe('create_note'); expect(jsonData.request.title).toBe('Test Note'); expect(jsonData.request.contentLength).toBe(12); expect(jsonData.error.details).toEqual({ details: 'Database connection failed' }); }); }); describe('edge cases', () => { test('should handle very long content', async () => { const longContent = 'A'.repeat(10000); const mockResponse = { note: { noteId: 'long123', title: 'Long Content Note', type: 'text' } }; mockTriliumClient.post.mockResolvedValue(mockResponse); const result = await createNote(mockTriliumClient, { title: 'Long Content Note', content: longContent, type: 'text' }); expect(result.content[0].text).toBe('Note created: "Long Content Note" (ID: long123)'); const jsonData = JSON.parse(result.content[1].text); expect(jsonData.request.contentLength).toBe(10000); }); test('should handle special characters in title and content', async () => { const mockResponse = { note: { noteId: 'special123', title: 'Special & "Chars" <Test>', type: 'text' } }; mockTriliumClient.post.mockResolvedValue(mockResponse); const result = await createNote(mockTriliumClient, { title: 'Special & "Chars" <Test>', content: 'Content with special chars: !@#$%^&*(){}[]|\\:";\'<>?,./', type: 'text' }); expect(mockTriliumClient.post).toHaveBeenCalledWith('create-note', { title: 'Special & "Chars" <Test>', content: 'Content with special chars: !@#$%^&*(){}[]|\\:";\'<>?,./', type: 'text', parentNoteId: 'root' }); expect(result.content[0].text).toBe('Note created: "Special & "Chars" <Test>" (ID: special123)'); }); test('should handle Unicode characters', async () => { const mockResponse = { note: { noteId: 'unicode123', title: '日本語のノート 🌸', type: 'text' } }; mockTriliumClient.post.mockResolvedValue(mockResponse); const result = await createNote(mockTriliumClient, { title: '日本語のノート 🌸', content: 'Unicode content: 你好世界 🌍 Здравствуй мир', type: 'text' }); expect(result.content[0].text).toBe('Note created: "日本語のノート 🌸" (ID: unicode123)'); const jsonData = JSON.parse(result.content[1].text); expect(jsonData.request.title).toBe('日本語のノート 🌸'); }); test('should handle empty content', async () => { const mockResponse = { note: { noteId: 'empty123', title: 'Empty Content Note', type: 'text' } }; mockTriliumClient.post.mockResolvedValue(mockResponse); const result = await createNote(mockTriliumClient, { title: 'Empty Content Note', content: '', type: 'text' }); const jsonData = JSON.parse(result.content[1].text); expect(jsonData.request.contentLength).toBe(0); }); test('should handle missing optional response fields gracefully', async () => { const mockResponse = { note: { noteId: 'minimal123' // Missing title, type, and other optional fields } }; mockTriliumClient.post.mockResolvedValue(mockResponse); const result = await createNote(mockTriliumClient, { title: 'Test Note', content: 'Test content', type: 'text' }); expect(result.content[0].text).toBe('Note created: "Test Note" (ID: minimal123)'); const jsonData = JSON.parse(result.content[1].text); expect(jsonData.result.noteId).toBe('minimal123'); }); }); });

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