test: comprehensive coverage for NoteTools
Add extensive test suite for note-tools.ts covering all major operations and error paths. Improves statement coverage from 13.94% to 96.01%. Tests added: - readNote: success, errors, frontmatter parsing - createNote: success, conflict strategies (error/overwrite/rename), parent folder creation - updateNote: success, errors, waypoint protection - deleteNote: soft/permanent delete, dry run, version checking - renameFile: success, errors, version checking, parent folder creation - readExcalidraw: success, non-Excalidraw files, errors - updateFrontmatter: success, field removal, version checking - updateSections: success, invalid ranges, version checking - Path validation for all methods - Empty path validation for all methods Mock enhancements: - Added modify, delete, trash methods to VaultAdapter mock - Added parseYaml function to Obsidian mock for frontmatter testing Coverage improvements for note-tools.ts: - Statements: 13.94% -> 96.01% (+82.07%) - Branches: 5.1% -> 88.44% (+83.34%) - Functions: 27.27% -> 90.9% (+63.63%) - Lines: 13.94% -> 96.4% (+82.46%)
This commit is contained in:
@@ -14,6 +14,9 @@ export function createMockVaultAdapter(overrides?: Partial<IVaultAdapter>): IVau
|
||||
process: jest.fn(),
|
||||
createFolder: jest.fn(),
|
||||
create: jest.fn(),
|
||||
modify: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
trash: jest.fn(),
|
||||
...overrides
|
||||
};
|
||||
}
|
||||
|
||||
@@ -71,3 +71,29 @@ export class Plugin {}
|
||||
export class Notice {}
|
||||
export class PluginSettingTab {}
|
||||
export class Setting {}
|
||||
|
||||
// Mock parseYaml function
|
||||
export function parseYaml(yaml: string): any {
|
||||
// Simple YAML parser mock for testing
|
||||
const result: any = {};
|
||||
const lines = yaml.split('\n');
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.trim() && !line.startsWith('#')) {
|
||||
const colonIndex = line.indexOf(':');
|
||||
if (colonIndex > 0) {
|
||||
const key = line.substring(0, colonIndex).trim();
|
||||
let value = line.substring(colonIndex + 1).trim();
|
||||
|
||||
// Handle arrays
|
||||
if (value.startsWith('[') && value.endsWith(']')) {
|
||||
value = value.slice(1, -1).split(',').map(v => v.trim());
|
||||
}
|
||||
|
||||
result[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
893
tests/note-tools.test.ts
Normal file
893
tests/note-tools.test.ts
Normal file
@@ -0,0 +1,893 @@
|
||||
import { NoteTools } from '../src/tools/note-tools';
|
||||
import { createMockVaultAdapter, createMockFileManagerAdapter, createMockTFile, createMockTFolder } from './__mocks__/adapters';
|
||||
import { App, Vault, TFile, TFolder } from 'obsidian';
|
||||
|
||||
// Mock PathUtils since NoteTools uses it extensively
|
||||
jest.mock('../src/utils/path-utils', () => ({
|
||||
PathUtils: {
|
||||
normalizePath: jest.fn((path: string) => path),
|
||||
isValidVaultPath: jest.fn(() => true),
|
||||
resolveFile: jest.fn(),
|
||||
fileExists: jest.fn(),
|
||||
folderExists: jest.fn(),
|
||||
pathExists: jest.fn(),
|
||||
getParentPath: jest.fn((path: string) => {
|
||||
const lastSlash = path.lastIndexOf('/');
|
||||
return lastSlash > 0 ? path.substring(0, lastSlash) : '';
|
||||
})
|
||||
}
|
||||
}));
|
||||
|
||||
// Import the mocked PathUtils
|
||||
import { PathUtils } from '../src/utils/path-utils';
|
||||
|
||||
describe('NoteTools', () => {
|
||||
let noteTools: NoteTools;
|
||||
let mockVault: ReturnType<typeof createMockVaultAdapter>;
|
||||
let mockFileManager: ReturnType<typeof createMockFileManagerAdapter>;
|
||||
let mockApp: App;
|
||||
|
||||
beforeEach(() => {
|
||||
mockVault = createMockVaultAdapter();
|
||||
mockFileManager = createMockFileManagerAdapter();
|
||||
mockApp = new App();
|
||||
noteTools = new NoteTools(mockVault, mockFileManager, mockApp);
|
||||
|
||||
// Reset all mocks
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('readNote', () => {
|
||||
it('should read note content successfully', async () => {
|
||||
const mockFile = createMockTFile('test.md');
|
||||
const content = '# Test Note\n\nThis is test content.';
|
||||
|
||||
(PathUtils.resolveFile as jest.Mock).mockReturnValue(mockFile);
|
||||
mockVault.read = jest.fn().mockResolvedValue(content);
|
||||
|
||||
const result = await noteTools.readNote('test.md');
|
||||
|
||||
expect(result.isError).toBeUndefined();
|
||||
expect(result.content[0].text).toBe(content);
|
||||
expect(mockVault.read).toHaveBeenCalledWith(mockFile);
|
||||
});
|
||||
|
||||
it('should return error if file not found', async () => {
|
||||
(PathUtils.resolveFile as jest.Mock).mockReturnValue(null);
|
||||
(PathUtils.folderExists as jest.Mock).mockReturnValue(false);
|
||||
|
||||
const result = await noteTools.readNote('nonexistent.md');
|
||||
|
||||
expect(result.isError).toBe(true);
|
||||
expect(result.content[0].text).toContain('not found');
|
||||
});
|
||||
|
||||
it('should return error if path is a folder', async () => {
|
||||
(PathUtils.resolveFile as jest.Mock).mockReturnValue(null);
|
||||
(PathUtils.folderExists as jest.Mock).mockReturnValue(true);
|
||||
|
||||
const result = await noteTools.readNote('folder');
|
||||
|
||||
expect(result.isError).toBe(true);
|
||||
expect(result.content[0].text).toContain('not a file');
|
||||
});
|
||||
|
||||
it('should handle read errors', async () => {
|
||||
const mockFile = createMockTFile('test.md');
|
||||
(PathUtils.resolveFile as jest.Mock).mockReturnValue(mockFile);
|
||||
mockVault.read = jest.fn().mockRejectedValue(new Error('Read permission denied'));
|
||||
|
||||
const result = await noteTools.readNote('test.md');
|
||||
|
||||
expect(result.isError).toBe(true);
|
||||
expect(result.content[0].text).toContain('Read permission denied');
|
||||
});
|
||||
|
||||
it('should parse frontmatter when requested', async () => {
|
||||
const mockFile = createMockTFile('test.md');
|
||||
const content = '---\ntitle: Test\ntags: [test, example]\n---\n\nContent here';
|
||||
|
||||
(PathUtils.resolveFile as jest.Mock).mockReturnValue(mockFile);
|
||||
mockVault.read = jest.fn().mockResolvedValue(content);
|
||||
|
||||
const result = await noteTools.readNote('test.md', { parseFrontmatter: true });
|
||||
|
||||
expect(result.isError).toBeUndefined();
|
||||
const parsed = JSON.parse(result.content[0].text);
|
||||
expect(parsed.hasFrontmatter).toBe(true);
|
||||
expect(parsed.path).toBe('test.md');
|
||||
// frontmatter field is the raw YAML string
|
||||
expect(parsed.frontmatter).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('createNote', () => {
|
||||
it('should create note successfully', async () => {
|
||||
const mockFile = createMockTFile('test.md', {
|
||||
ctime: 1000,
|
||||
mtime: 2000,
|
||||
size: 100
|
||||
});
|
||||
|
||||
(PathUtils.fileExists as jest.Mock).mockReturnValue(false);
|
||||
(PathUtils.folderExists as jest.Mock).mockReturnValue(false);
|
||||
(PathUtils.getParentPath as jest.Mock).mockReturnValue('');
|
||||
mockVault.create = jest.fn().mockResolvedValue(mockFile);
|
||||
|
||||
const result = await noteTools.createNote('test.md', 'content');
|
||||
|
||||
expect(result.isError).toBeUndefined();
|
||||
expect(mockVault.create).toHaveBeenCalledWith('test.md', 'content');
|
||||
const parsed = JSON.parse(result.content[0].text);
|
||||
expect(parsed.success).toBe(true);
|
||||
expect(parsed.path).toBe('test.md');
|
||||
});
|
||||
|
||||
it('should return error if file exists and strategy is error', async () => {
|
||||
(PathUtils.fileExists as jest.Mock).mockReturnValue(true);
|
||||
|
||||
const result = await noteTools.createNote('test.md', 'content', false, 'error');
|
||||
|
||||
expect(result.isError).toBe(true);
|
||||
expect(result.content[0].text).toContain('already exists');
|
||||
});
|
||||
|
||||
it('should overwrite if strategy is overwrite', async () => {
|
||||
const mockFile = createMockTFile('test.md');
|
||||
|
||||
(PathUtils.fileExists as jest.Mock).mockReturnValue(true);
|
||||
(PathUtils.resolveFile as jest.Mock).mockReturnValue(mockFile);
|
||||
mockVault.delete = jest.fn().mockResolvedValue(undefined);
|
||||
mockVault.create = jest.fn().mockResolvedValue(mockFile);
|
||||
(PathUtils.folderExists as jest.Mock).mockReturnValue(false);
|
||||
(PathUtils.getParentPath as jest.Mock).mockReturnValue('');
|
||||
|
||||
const result = await noteTools.createNote('test.md', 'content', false, 'overwrite');
|
||||
|
||||
expect(result.isError).toBeUndefined();
|
||||
expect(mockVault.delete).toHaveBeenCalledWith(mockFile);
|
||||
expect(mockVault.create).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should rename if strategy is rename', async () => {
|
||||
const mockFile = createMockTFile('test 1.md');
|
||||
|
||||
(PathUtils.fileExists as jest.Mock)
|
||||
.mockReturnValueOnce(true) // Original exists
|
||||
.mockReturnValueOnce(false); // test 1.md doesn't exist
|
||||
(PathUtils.folderExists as jest.Mock).mockReturnValue(false);
|
||||
(PathUtils.getParentPath as jest.Mock).mockReturnValue('');
|
||||
mockVault.create = jest.fn().mockResolvedValue(mockFile);
|
||||
|
||||
const result = await noteTools.createNote('test.md', 'content', false, 'rename');
|
||||
|
||||
expect(result.isError).toBeUndefined();
|
||||
expect(mockVault.create).toHaveBeenCalledWith('test 1.md', 'content');
|
||||
const parsed = JSON.parse(result.content[0].text);
|
||||
expect(parsed.renamed).toBe(true);
|
||||
expect(parsed.originalPath).toBe('test.md');
|
||||
});
|
||||
|
||||
it('should return error if parent folder does not exist and createParents is false', async () => {
|
||||
(PathUtils.fileExists as jest.Mock).mockReturnValue(false);
|
||||
(PathUtils.folderExists as jest.Mock).mockReturnValue(false);
|
||||
(PathUtils.getParentPath as jest.Mock).mockReturnValue('folder');
|
||||
(PathUtils.pathExists as jest.Mock).mockReturnValue(false);
|
||||
|
||||
const result = await noteTools.createNote('folder/file.md', 'content', false);
|
||||
|
||||
expect(result.isError).toBe(true);
|
||||
expect(result.content[0].text).toContain('Parent folder');
|
||||
});
|
||||
|
||||
it('should create parent folders when createParents is true', async () => {
|
||||
const mockFile = createMockTFile('folder/file.md');
|
||||
|
||||
(PathUtils.fileExists as jest.Mock).mockReturnValue(false);
|
||||
(PathUtils.folderExists as jest.Mock).mockReturnValue(false);
|
||||
(PathUtils.getParentPath as jest.Mock)
|
||||
.mockReturnValueOnce('folder') // getParentPath('folder/file.md') in createNote
|
||||
.mockReturnValueOnce(''); // getParentPath('folder') in createParentFolders - stops recursion
|
||||
(PathUtils.pathExists as jest.Mock)
|
||||
.mockReturnValueOnce(false) // Check in createNote: parentPath exists?
|
||||
.mockReturnValueOnce(false); // Check in createParentFolders: folder exists?
|
||||
mockVault.createFolder = jest.fn().mockResolvedValue(undefined);
|
||||
mockVault.create = jest.fn().mockResolvedValue(mockFile);
|
||||
|
||||
const result = await noteTools.createNote('folder/file.md', 'content', true);
|
||||
|
||||
expect(result.isError).toBeUndefined();
|
||||
expect(mockVault.createFolder).toHaveBeenCalledWith('folder');
|
||||
expect(mockVault.create).toHaveBeenCalledWith('folder/file.md', 'content');
|
||||
});
|
||||
|
||||
it('should handle create errors', async () => {
|
||||
(PathUtils.fileExists as jest.Mock).mockReturnValue(false);
|
||||
(PathUtils.folderExists as jest.Mock).mockReturnValue(false);
|
||||
(PathUtils.getParentPath as jest.Mock).mockReturnValue('');
|
||||
mockVault.create = jest.fn().mockRejectedValue(new Error('Disk full'));
|
||||
|
||||
const result = await noteTools.createNote('test.md', 'content');
|
||||
|
||||
expect(result.isError).toBe(true);
|
||||
expect(result.content[0].text).toContain('Disk full');
|
||||
});
|
||||
|
||||
it('should return error if parent path is a file', async () => {
|
||||
(PathUtils.fileExists as jest.Mock)
|
||||
.mockReturnValueOnce(false) // test.md doesn't exist
|
||||
.mockReturnValueOnce(true); // parent is a file
|
||||
(PathUtils.folderExists as jest.Mock).mockReturnValue(false);
|
||||
(PathUtils.getParentPath as jest.Mock).mockReturnValue('parent');
|
||||
|
||||
const result = await noteTools.createNote('parent/test.md', 'content');
|
||||
|
||||
expect(result.isError).toBe(true);
|
||||
expect(result.content[0].text).toContain('not a folder');
|
||||
});
|
||||
|
||||
it('should return error if path is a folder', async () => {
|
||||
(PathUtils.fileExists as jest.Mock).mockReturnValue(false);
|
||||
(PathUtils.folderExists as jest.Mock).mockReturnValue(true);
|
||||
|
||||
const result = await noteTools.createNote('folder', 'content');
|
||||
|
||||
expect(result.isError).toBe(true);
|
||||
expect(result.content[0].text).toContain('not a file');
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateNote', () => {
|
||||
it('should update note successfully', async () => {
|
||||
const mockFile = createMockTFile('test.md');
|
||||
const currentContent = 'old content';
|
||||
const newContent = 'new content';
|
||||
|
||||
(PathUtils.resolveFile as jest.Mock).mockReturnValue(mockFile);
|
||||
mockVault.read = jest.fn().mockResolvedValue(currentContent);
|
||||
mockVault.modify = jest.fn().mockResolvedValue(undefined);
|
||||
|
||||
const result = await noteTools.updateNote('test.md', newContent);
|
||||
|
||||
expect(result.isError).toBeUndefined();
|
||||
expect(mockVault.modify).toHaveBeenCalledWith(mockFile, newContent);
|
||||
expect(result.content[0].text).toContain('updated successfully');
|
||||
});
|
||||
|
||||
it('should return error if file not found', async () => {
|
||||
(PathUtils.resolveFile as jest.Mock).mockReturnValue(null);
|
||||
(PathUtils.folderExists as jest.Mock).mockReturnValue(false);
|
||||
|
||||
const result = await noteTools.updateNote('nonexistent.md', 'content');
|
||||
|
||||
expect(result.isError).toBe(true);
|
||||
expect(result.content[0].text).toContain('not found');
|
||||
});
|
||||
|
||||
it('should return error if path is a folder', async () => {
|
||||
(PathUtils.resolveFile as jest.Mock).mockReturnValue(null);
|
||||
(PathUtils.folderExists as jest.Mock).mockReturnValue(true);
|
||||
|
||||
const result = await noteTools.updateNote('folder', 'content');
|
||||
|
||||
expect(result.isError).toBe(true);
|
||||
expect(result.content[0].text).toContain('not a file');
|
||||
});
|
||||
|
||||
it('should handle update errors', async () => {
|
||||
const mockFile = createMockTFile('test.md');
|
||||
|
||||
(PathUtils.resolveFile as jest.Mock).mockReturnValue(mockFile);
|
||||
mockVault.read = jest.fn().mockResolvedValue('old content');
|
||||
mockVault.modify = jest.fn().mockRejectedValue(new Error('File locked'));
|
||||
|
||||
const result = await noteTools.updateNote('test.md', 'new content');
|
||||
|
||||
expect(result.isError).toBe(true);
|
||||
expect(result.content[0].text).toContain('File locked');
|
||||
});
|
||||
|
||||
it('should prevent waypoint modification', async () => {
|
||||
const mockFile = createMockTFile('test.md');
|
||||
const waypointContent = 'before\n%% Begin Waypoint %%\nwaypoint content\n%% End Waypoint %%\nafter';
|
||||
const newContent = 'before\nmodified waypoint\nafter';
|
||||
|
||||
(PathUtils.resolveFile as jest.Mock).mockReturnValue(mockFile);
|
||||
mockVault.read = jest.fn().mockResolvedValue(waypointContent);
|
||||
|
||||
const result = await noteTools.updateNote('test.md', newContent);
|
||||
|
||||
expect(result.isError).toBe(true);
|
||||
expect(result.content[0].text).toContain('Waypoint');
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteNote', () => {
|
||||
it('should soft delete note successfully', async () => {
|
||||
const mockFile = createMockTFile('test.md');
|
||||
|
||||
(PathUtils.resolveFile as jest.Mock).mockReturnValue(mockFile);
|
||||
mockVault.trash = jest.fn().mockResolvedValue(undefined);
|
||||
|
||||
const result = await noteTools.deleteNote('test.md', true, false);
|
||||
|
||||
expect(result.isError).toBeUndefined();
|
||||
expect(mockVault.trash).toHaveBeenCalledWith(mockFile, true);
|
||||
const parsed = JSON.parse(result.content[0].text);
|
||||
expect(parsed.deleted).toBe(true);
|
||||
expect(parsed.soft).toBe(true);
|
||||
});
|
||||
|
||||
it('should permanently delete note', async () => {
|
||||
const mockFile = createMockTFile('test.md');
|
||||
|
||||
(PathUtils.resolveFile as jest.Mock).mockReturnValue(mockFile);
|
||||
mockVault.delete = jest.fn().mockResolvedValue(undefined);
|
||||
|
||||
const result = await noteTools.deleteNote('test.md', false, false);
|
||||
|
||||
expect(result.isError).toBeUndefined();
|
||||
expect(mockVault.delete).toHaveBeenCalledWith(mockFile);
|
||||
const parsed = JSON.parse(result.content[0].text);
|
||||
expect(parsed.deleted).toBe(true);
|
||||
expect(parsed.soft).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle dry run', async () => {
|
||||
const mockFile = createMockTFile('test.md');
|
||||
|
||||
(PathUtils.resolveFile as jest.Mock).mockReturnValue(mockFile);
|
||||
|
||||
const result = await noteTools.deleteNote('test.md', true, true);
|
||||
|
||||
expect(result.isError).toBeUndefined();
|
||||
const parsed = JSON.parse(result.content[0].text);
|
||||
expect(parsed.deleted).toBe(false);
|
||||
expect(parsed.dryRun).toBe(true);
|
||||
expect(mockVault.trash).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return error if file not found', async () => {
|
||||
(PathUtils.resolveFile as jest.Mock).mockReturnValue(null);
|
||||
(PathUtils.folderExists as jest.Mock).mockReturnValue(false);
|
||||
|
||||
const result = await noteTools.deleteNote('nonexistent.md');
|
||||
|
||||
expect(result.isError).toBe(true);
|
||||
expect(result.content[0].text).toContain('not found');
|
||||
});
|
||||
|
||||
it('should return error if path is a folder', async () => {
|
||||
(PathUtils.resolveFile as jest.Mock).mockReturnValue(null);
|
||||
(PathUtils.folderExists as jest.Mock).mockReturnValue(true);
|
||||
|
||||
const result = await noteTools.deleteNote('folder');
|
||||
|
||||
expect(result.isError).toBe(true);
|
||||
expect(result.content[0].text).toContain('Path is a folder');
|
||||
});
|
||||
|
||||
it('should handle delete errors', async () => {
|
||||
const mockFile = createMockTFile('test.md');
|
||||
|
||||
(PathUtils.resolveFile as jest.Mock).mockReturnValue(mockFile);
|
||||
mockVault.trash = jest.fn().mockRejectedValue(new Error('Cannot delete'));
|
||||
|
||||
const result = await noteTools.deleteNote('test.md');
|
||||
|
||||
expect(result.isError).toBe(true);
|
||||
expect(result.content[0].text).toContain('Cannot delete');
|
||||
});
|
||||
|
||||
it('should check version if ifMatch provided', async () => {
|
||||
const mockFile = createMockTFile('test.md', {
|
||||
ctime: 1000,
|
||||
mtime: 2000,
|
||||
size: 100
|
||||
});
|
||||
|
||||
(PathUtils.resolveFile as jest.Mock).mockReturnValue(mockFile);
|
||||
|
||||
// Wrong version
|
||||
const result = await noteTools.deleteNote('test.md', true, false, '1000-50');
|
||||
|
||||
expect(result.isError).toBe(true);
|
||||
expect(result.content[0].text).toContain('Version mismatch');
|
||||
expect(mockVault.trash).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('renameFile', () => {
|
||||
it('should rename file successfully', async () => {
|
||||
const mockFile = createMockTFile('old.md');
|
||||
const renamedFile = createMockTFile('new.md');
|
||||
|
||||
(PathUtils.resolveFile as jest.Mock)
|
||||
.mockReturnValueOnce(mockFile) // Source file
|
||||
.mockReturnValueOnce(renamedFile); // After rename
|
||||
(PathUtils.fileExists as jest.Mock).mockReturnValue(false);
|
||||
(PathUtils.folderExists as jest.Mock).mockReturnValue(false);
|
||||
(PathUtils.pathExists as jest.Mock).mockReturnValue(false);
|
||||
(PathUtils.getParentPath as jest.Mock).mockReturnValue('');
|
||||
mockFileManager.renameFile = jest.fn().mockResolvedValue(undefined);
|
||||
|
||||
const result = await noteTools.renameFile('old.md', 'new.md');
|
||||
|
||||
expect(result.isError).toBeUndefined();
|
||||
expect(mockFileManager.renameFile).toHaveBeenCalledWith(mockFile, 'new.md');
|
||||
const parsed = JSON.parse(result.content[0].text);
|
||||
expect(parsed.success).toBe(true);
|
||||
expect(parsed.oldPath).toBe('old.md');
|
||||
expect(parsed.newPath).toBe('new.md');
|
||||
});
|
||||
|
||||
it('should return error if source file not found', async () => {
|
||||
(PathUtils.resolveFile as jest.Mock).mockReturnValue(null);
|
||||
(PathUtils.folderExists as jest.Mock).mockReturnValue(false);
|
||||
|
||||
const result = await noteTools.renameFile('nonexistent.md', 'new.md');
|
||||
|
||||
expect(result.isError).toBe(true);
|
||||
expect(result.content[0].text).toContain('not found');
|
||||
});
|
||||
|
||||
it('should return error if destination exists', async () => {
|
||||
const mockFile = createMockTFile('old.md');
|
||||
|
||||
(PathUtils.resolveFile as jest.Mock).mockReturnValue(mockFile);
|
||||
(PathUtils.fileExists as jest.Mock).mockReturnValue(true);
|
||||
|
||||
const result = await noteTools.renameFile('old.md', 'existing.md');
|
||||
|
||||
expect(result.isError).toBe(true);
|
||||
expect(result.content[0].text).toContain('already exists');
|
||||
});
|
||||
|
||||
it('should handle rename errors', async () => {
|
||||
const mockFile = createMockTFile('old.md');
|
||||
|
||||
(PathUtils.resolveFile as jest.Mock).mockReturnValue(mockFile);
|
||||
(PathUtils.fileExists as jest.Mock).mockReturnValue(false);
|
||||
(PathUtils.folderExists as jest.Mock).mockReturnValue(false);
|
||||
(PathUtils.pathExists as jest.Mock).mockReturnValue(false);
|
||||
(PathUtils.getParentPath as jest.Mock).mockReturnValue('');
|
||||
mockFileManager.renameFile = jest.fn().mockRejectedValue(new Error('Name conflict'));
|
||||
|
||||
const result = await noteTools.renameFile('old.md', 'new.md');
|
||||
|
||||
expect(result.isError).toBe(true);
|
||||
expect(result.content[0].text).toContain('Name conflict');
|
||||
});
|
||||
|
||||
it('should check version if ifMatch provided', async () => {
|
||||
const mockFile = createMockTFile('old.md', {
|
||||
ctime: 1000,
|
||||
mtime: 2000,
|
||||
size: 100
|
||||
});
|
||||
|
||||
(PathUtils.resolveFile as jest.Mock).mockReturnValue(mockFile);
|
||||
(PathUtils.fileExists as jest.Mock).mockReturnValue(false);
|
||||
(PathUtils.folderExists as jest.Mock).mockReturnValue(false);
|
||||
|
||||
// Wrong version
|
||||
const result = await noteTools.renameFile('old.md', 'new.md', true, '1000-50');
|
||||
|
||||
expect(result.isError).toBe(true);
|
||||
expect(result.content[0].text).toContain('Version mismatch');
|
||||
expect(mockFileManager.renameFile).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should create parent folders if needed', async () => {
|
||||
const mockFile = createMockTFile('old.md');
|
||||
const renamedFile = createMockTFile('folder/new.md');
|
||||
|
||||
(PathUtils.resolveFile as jest.Mock)
|
||||
.mockReturnValueOnce(mockFile)
|
||||
.mockReturnValueOnce(renamedFile);
|
||||
(PathUtils.fileExists as jest.Mock).mockReturnValue(false);
|
||||
(PathUtils.folderExists as jest.Mock).mockReturnValue(false);
|
||||
(PathUtils.getParentPath as jest.Mock)
|
||||
.mockReturnValueOnce('folder') // getParentPath('folder/new.md') in renameFile
|
||||
.mockReturnValueOnce(''); // getParentPath('folder') in createParentFolders
|
||||
(PathUtils.pathExists as jest.Mock)
|
||||
.mockReturnValueOnce(false) // Check in renameFile: parentPath exists?
|
||||
.mockReturnValueOnce(false); // Check in createParentFolders: folder exists?
|
||||
mockVault.createFolder = jest.fn().mockResolvedValue(undefined);
|
||||
mockFileManager.renameFile = jest.fn().mockResolvedValue(undefined);
|
||||
|
||||
const result = await noteTools.renameFile('old.md', 'folder/new.md');
|
||||
|
||||
expect(result.isError).toBeUndefined();
|
||||
expect(mockVault.createFolder).toHaveBeenCalledWith('folder');
|
||||
});
|
||||
});
|
||||
|
||||
describe('readExcalidraw', () => {
|
||||
it('should read Excalidraw file successfully', async () => {
|
||||
const mockFile = createMockTFile('drawing.md');
|
||||
// Excalidraw files must have the Drawing section with json code block
|
||||
const excalidrawContent = `# Text Elements
|
||||
Some text
|
||||
|
||||
## Drawing
|
||||
\`\`\`json
|
||||
{"type":"excalidraw","version":2,"source":"https://excalidraw.com","elements":[{"id":"1","type":"rectangle"}],"appState":{"viewBackgroundColor":"#ffffff"},"files":{}}
|
||||
\`\`\``;
|
||||
|
||||
(PathUtils.resolveFile as jest.Mock).mockReturnValue(mockFile);
|
||||
mockVault.read = jest.fn().mockResolvedValue(excalidrawContent);
|
||||
|
||||
const result = await noteTools.readExcalidraw('drawing.md');
|
||||
|
||||
expect(result.isError).toBeUndefined();
|
||||
const parsed = JSON.parse(result.content[0].text);
|
||||
expect(parsed.isExcalidraw).toBe(true);
|
||||
});
|
||||
|
||||
it('should return error for non-Excalidraw files', async () => {
|
||||
const mockFile = createMockTFile('regular.md');
|
||||
const content = '# Regular Note\n\nNot an Excalidraw file';
|
||||
|
||||
(PathUtils.resolveFile as jest.Mock).mockReturnValue(mockFile);
|
||||
mockVault.read = jest.fn().mockResolvedValue(content);
|
||||
|
||||
const result = await noteTools.readExcalidraw('regular.md');
|
||||
|
||||
expect(result.isError).toBeUndefined();
|
||||
const parsed = JSON.parse(result.content[0].text);
|
||||
expect(parsed.isExcalidraw).toBe(false);
|
||||
});
|
||||
|
||||
it('should return error if file not found', async () => {
|
||||
(PathUtils.resolveFile as jest.Mock).mockReturnValue(null);
|
||||
(PathUtils.folderExists as jest.Mock).mockReturnValue(false);
|
||||
|
||||
const result = await noteTools.readExcalidraw('nonexistent.md');
|
||||
|
||||
expect(result.isError).toBe(true);
|
||||
expect(result.content[0].text).toContain('not found');
|
||||
});
|
||||
|
||||
it('should handle read errors', async () => {
|
||||
const mockFile = createMockTFile('drawing.md');
|
||||
|
||||
(PathUtils.resolveFile as jest.Mock).mockReturnValue(mockFile);
|
||||
mockVault.read = jest.fn().mockRejectedValue(new Error('Read error'));
|
||||
|
||||
const result = await noteTools.readExcalidraw('drawing.md');
|
||||
|
||||
expect(result.isError).toBe(true);
|
||||
expect(result.content[0].text).toContain('Read error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateFrontmatter', () => {
|
||||
it('should update frontmatter successfully', async () => {
|
||||
const mockFile = createMockTFile('test.md', {
|
||||
ctime: 1000,
|
||||
mtime: 2000,
|
||||
size: 100
|
||||
});
|
||||
const content = '---\ntitle: Old\n---\n\nContent';
|
||||
|
||||
(PathUtils.resolveFile as jest.Mock).mockReturnValue(mockFile);
|
||||
mockVault.read = jest.fn().mockResolvedValue(content);
|
||||
mockVault.modify = jest.fn().mockResolvedValue(undefined);
|
||||
|
||||
const result = await noteTools.updateFrontmatter('test.md', { title: 'New', author: 'Test' });
|
||||
|
||||
expect(result.isError).toBeUndefined();
|
||||
expect(mockVault.modify).toHaveBeenCalled();
|
||||
const parsed = JSON.parse(result.content[0].text);
|
||||
expect(parsed.success).toBe(true);
|
||||
expect(parsed.updatedFields).toContain('title');
|
||||
expect(parsed.updatedFields).toContain('author');
|
||||
});
|
||||
|
||||
it('should remove frontmatter fields', async () => {
|
||||
const mockFile = createMockTFile('test.md', {
|
||||
ctime: 1000,
|
||||
mtime: 2000,
|
||||
size: 100
|
||||
});
|
||||
const content = '---\ntitle: Test\nauthor: Me\n---\n\nContent';
|
||||
|
||||
(PathUtils.resolveFile as jest.Mock).mockReturnValue(mockFile);
|
||||
mockVault.read = jest.fn().mockResolvedValue(content);
|
||||
mockVault.modify = jest.fn().mockResolvedValue(undefined);
|
||||
|
||||
const result = await noteTools.updateFrontmatter('test.md', undefined, ['author']);
|
||||
|
||||
expect(result.isError).toBeUndefined();
|
||||
const parsed = JSON.parse(result.content[0].text);
|
||||
expect(parsed.removedFields).toContain('author');
|
||||
});
|
||||
|
||||
it('should return error if no operations provided', async () => {
|
||||
const result = await noteTools.updateFrontmatter('test.md');
|
||||
|
||||
expect(result.isError).toBe(true);
|
||||
expect(result.content[0].text).toContain('No operations provided');
|
||||
});
|
||||
|
||||
it('should return error if file not found', async () => {
|
||||
(PathUtils.resolveFile as jest.Mock).mockReturnValue(null);
|
||||
(PathUtils.folderExists as jest.Mock).mockReturnValue(false);
|
||||
|
||||
const result = await noteTools.updateFrontmatter('nonexistent.md', { title: 'Test' });
|
||||
|
||||
expect(result.isError).toBe(true);
|
||||
expect(result.content[0].text).toContain('not found');
|
||||
});
|
||||
|
||||
it('should check version if ifMatch provided', async () => {
|
||||
const mockFile = createMockTFile('test.md', {
|
||||
ctime: 1000,
|
||||
mtime: 2000,
|
||||
size: 100
|
||||
});
|
||||
|
||||
(PathUtils.resolveFile as jest.Mock).mockReturnValue(mockFile);
|
||||
|
||||
// Wrong version
|
||||
const result = await noteTools.updateFrontmatter('test.md', { title: 'Test' }, [], '1000-50');
|
||||
|
||||
expect(result.isError).toBe(true);
|
||||
expect(result.content[0].text).toContain('Version mismatch');
|
||||
expect(mockVault.modify).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle update errors', async () => {
|
||||
const mockFile = createMockTFile('test.md');
|
||||
const content = '---\ntitle: Test\n---\n\nContent';
|
||||
|
||||
(PathUtils.resolveFile as jest.Mock).mockReturnValue(mockFile);
|
||||
mockVault.read = jest.fn().mockResolvedValue(content);
|
||||
mockVault.modify = jest.fn().mockRejectedValue(new Error('Write error'));
|
||||
|
||||
const result = await noteTools.updateFrontmatter('test.md', { title: 'New' });
|
||||
|
||||
expect(result.isError).toBe(true);
|
||||
expect(result.content[0].text).toContain('Write error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateSections', () => {
|
||||
it('should update sections successfully', async () => {
|
||||
const mockFile = createMockTFile('test.md', {
|
||||
ctime: 1000,
|
||||
mtime: 2000,
|
||||
size: 100
|
||||
});
|
||||
const content = 'Line 1\nLine 2\nLine 3\nLine 4';
|
||||
|
||||
(PathUtils.resolveFile as jest.Mock).mockReturnValue(mockFile);
|
||||
mockVault.read = jest.fn().mockResolvedValue(content);
|
||||
mockVault.modify = jest.fn().mockResolvedValue(undefined);
|
||||
|
||||
const result = await noteTools.updateSections('test.md', [
|
||||
{ startLine: 2, endLine: 3, content: 'New Line 2\nNew Line 3' }
|
||||
]);
|
||||
|
||||
expect(result.isError).toBeUndefined();
|
||||
expect(mockVault.modify).toHaveBeenCalled();
|
||||
const parsed = JSON.parse(result.content[0].text);
|
||||
expect(parsed.success).toBe(true);
|
||||
expect(parsed.sectionsUpdated).toBe(1);
|
||||
});
|
||||
|
||||
it('should return error if no edits provided', async () => {
|
||||
const result = await noteTools.updateSections('test.md', []);
|
||||
|
||||
expect(result.isError).toBe(true);
|
||||
expect(result.content[0].text).toContain('No edits provided');
|
||||
});
|
||||
|
||||
it('should return error for invalid line range', async () => {
|
||||
const mockFile = createMockTFile('test.md');
|
||||
const content = 'Line 1\nLine 2\nLine 3';
|
||||
|
||||
(PathUtils.resolveFile as jest.Mock).mockReturnValue(mockFile);
|
||||
mockVault.read = jest.fn().mockResolvedValue(content);
|
||||
|
||||
const result = await noteTools.updateSections('test.md', [
|
||||
{ startLine: 1, endLine: 10, content: 'New' }
|
||||
]);
|
||||
|
||||
expect(result.isError).toBe(true);
|
||||
expect(result.content[0].text).toContain('Invalid line range');
|
||||
});
|
||||
|
||||
it('should check version if ifMatch provided', async () => {
|
||||
const mockFile = createMockTFile('test.md', {
|
||||
ctime: 1000,
|
||||
mtime: 2000,
|
||||
size: 100
|
||||
});
|
||||
|
||||
(PathUtils.resolveFile as jest.Mock).mockReturnValue(mockFile);
|
||||
|
||||
// Wrong version
|
||||
const result = await noteTools.updateSections('test.md', [
|
||||
{ startLine: 1, endLine: 1, content: 'New' }
|
||||
], '1000-50');
|
||||
|
||||
expect(result.isError).toBe(true);
|
||||
expect(result.content[0].text).toContain('Version mismatch');
|
||||
expect(mockVault.modify).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle update errors', async () => {
|
||||
const mockFile = createMockTFile('test.md');
|
||||
const content = 'Line 1\nLine 2';
|
||||
|
||||
(PathUtils.resolveFile as jest.Mock).mockReturnValue(mockFile);
|
||||
mockVault.read = jest.fn().mockResolvedValue(content);
|
||||
mockVault.modify = jest.fn().mockRejectedValue(new Error('Update error'));
|
||||
|
||||
const result = await noteTools.updateSections('test.md', [
|
||||
{ startLine: 1, endLine: 1, content: 'New' }
|
||||
]);
|
||||
|
||||
expect(result.isError).toBe(true);
|
||||
expect(result.content[0].text).toContain('Update error');
|
||||
});
|
||||
|
||||
it('should return error if file not found', async () => {
|
||||
(PathUtils.resolveFile as jest.Mock).mockReturnValue(null);
|
||||
(PathUtils.folderExists as jest.Mock).mockReturnValue(false);
|
||||
|
||||
const result = await noteTools.updateSections('nonexistent.md', [
|
||||
{ startLine: 1, endLine: 1, content: 'New' }
|
||||
]);
|
||||
|
||||
expect(result.isError).toBe(true);
|
||||
expect(result.content[0].text).toContain('not found');
|
||||
});
|
||||
});
|
||||
|
||||
describe('path validation', () => {
|
||||
beforeEach(() => {
|
||||
(PathUtils.isValidVaultPath as jest.Mock).mockReturnValue(false);
|
||||
});
|
||||
|
||||
it('should validate path in readNote', async () => {
|
||||
const result = await noteTools.readNote('../../../etc/passwd');
|
||||
|
||||
expect(result.isError).toBe(true);
|
||||
expect(result.content[0].text).toContain('Invalid path');
|
||||
});
|
||||
|
||||
it('should validate path in createNote', async () => {
|
||||
const result = await noteTools.createNote('../bad.md', 'content');
|
||||
|
||||
expect(result.isError).toBe(true);
|
||||
expect(result.content[0].text).toContain('Invalid path');
|
||||
});
|
||||
|
||||
it('should validate path in updateNote', async () => {
|
||||
const result = await noteTools.updateNote('/absolute/path.md', 'content');
|
||||
|
||||
expect(result.isError).toBe(true);
|
||||
expect(result.content[0].text).toContain('Invalid path');
|
||||
});
|
||||
|
||||
it('should validate path in deleteNote', async () => {
|
||||
const result = await noteTools.deleteNote('bad//path.md');
|
||||
|
||||
expect(result.isError).toBe(true);
|
||||
expect(result.content[0].text).toContain('Invalid path');
|
||||
});
|
||||
|
||||
it('should validate source path in renameFile', async () => {
|
||||
const result = await noteTools.renameFile('../bad.md', 'good.md');
|
||||
|
||||
expect(result.isError).toBe(true);
|
||||
expect(result.content[0].text).toContain('Invalid path');
|
||||
});
|
||||
|
||||
it('should validate destination path in renameFile', async () => {
|
||||
(PathUtils.isValidVaultPath as jest.Mock)
|
||||
.mockReturnValueOnce(true) // source is valid
|
||||
.mockReturnValueOnce(false); // destination is invalid
|
||||
|
||||
const result = await noteTools.renameFile('good.md', '../bad.md');
|
||||
|
||||
expect(result.isError).toBe(true);
|
||||
expect(result.content[0].text).toContain('Invalid path');
|
||||
});
|
||||
|
||||
it('should validate path in readExcalidraw', async () => {
|
||||
const result = await noteTools.readExcalidraw('../../bad.md');
|
||||
|
||||
expect(result.isError).toBe(true);
|
||||
expect(result.content[0].text).toContain('Invalid path');
|
||||
});
|
||||
|
||||
it('should validate path in updateFrontmatter', async () => {
|
||||
const result = await noteTools.updateFrontmatter('../bad.md', { title: 'Test' });
|
||||
|
||||
expect(result.isError).toBe(true);
|
||||
expect(result.content[0].text).toContain('Invalid path');
|
||||
});
|
||||
|
||||
it('should validate path in updateSections', async () => {
|
||||
const result = await noteTools.updateSections('../bad.md', [
|
||||
{ startLine: 1, endLine: 1, content: 'New' }
|
||||
]);
|
||||
|
||||
expect(result.isError).toBe(true);
|
||||
expect(result.content[0].text).toContain('Invalid path');
|
||||
});
|
||||
});
|
||||
|
||||
describe('empty path validation', () => {
|
||||
it('should reject empty path in readNote', async () => {
|
||||
const result = await noteTools.readNote('');
|
||||
|
||||
expect(result.isError).toBe(true);
|
||||
expect(result.content[0].text).toContain('empty');
|
||||
});
|
||||
|
||||
it('should reject empty path in createNote', async () => {
|
||||
const result = await noteTools.createNote('', 'content');
|
||||
|
||||
expect(result.isError).toBe(true);
|
||||
expect(result.content[0].text).toContain('empty');
|
||||
});
|
||||
|
||||
it('should reject empty path in updateNote', async () => {
|
||||
const result = await noteTools.updateNote('', 'content');
|
||||
|
||||
expect(result.isError).toBe(true);
|
||||
expect(result.content[0].text).toContain('empty');
|
||||
});
|
||||
|
||||
it('should reject empty path in deleteNote', async () => {
|
||||
const result = await noteTools.deleteNote('');
|
||||
|
||||
expect(result.isError).toBe(true);
|
||||
expect(result.content[0].text).toContain('empty');
|
||||
});
|
||||
|
||||
it('should reject empty source path in renameFile', async () => {
|
||||
const result = await noteTools.renameFile('', 'new.md');
|
||||
|
||||
expect(result.isError).toBe(true);
|
||||
expect(result.content[0].text).toContain('empty');
|
||||
});
|
||||
|
||||
it('should reject empty destination path in renameFile', async () => {
|
||||
(PathUtils.resolveFile as jest.Mock).mockReturnValue(createMockTFile('old.md'));
|
||||
|
||||
const result = await noteTools.renameFile('old.md', '');
|
||||
|
||||
expect(result.isError).toBe(true);
|
||||
expect(result.content[0].text).toContain('empty');
|
||||
});
|
||||
|
||||
it('should reject empty path in readExcalidraw', async () => {
|
||||
const result = await noteTools.readExcalidraw('');
|
||||
|
||||
expect(result.isError).toBe(true);
|
||||
expect(result.content[0].text).toContain('empty');
|
||||
});
|
||||
|
||||
it('should reject empty path in updateFrontmatter', async () => {
|
||||
const result = await noteTools.updateFrontmatter('', { title: 'Test' });
|
||||
|
||||
expect(result.isError).toBe(true);
|
||||
expect(result.content[0].text).toContain('empty');
|
||||
});
|
||||
|
||||
it('should reject empty path in updateSections', async () => {
|
||||
const result = await noteTools.updateSections('', [
|
||||
{ startLine: 1, endLine: 1, content: 'New' }
|
||||
]);
|
||||
|
||||
expect(result.isError).toBe(true);
|
||||
expect(result.content[0].text).toContain('empty');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user