snippets.utils.test.ts1647 lines · main
1import fs from 'fs/promises'
2import path from 'path'
3import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
4
5import { generateDeterministicUuid } from './snippets.browser'
6import {
7 createFolder,
8 deleteFolder,
9 deleteSnippet,
10 getFilesystemEntries,
11 getFolders,
12 getSnippet,
13 getSnippets,
14 saveSnippet,
15 updateSnippet,
16 type Snippet,
17} from './snippets.utils'
18
19// Mock fs/promises — explicit factory required since Vitest 4 automocking
20// doesn't create mock functions for getter-based exports on Node built-ins.
21vi.mock('fs/promises', () => ({
22 default: {
23 access: vi.fn(),
24 mkdir: vi.fn(),
25 readdir: vi.fn(),
26 readFile: vi.fn(),
27 writeFile: vi.fn(),
28 unlink: vi.fn(),
29 rm: vi.fn(),
30 stat: vi.fn(),
31 },
32}))
33const mockedFS = vi.mocked(fs)
34
35// Mock SNIPPETS_DIR from constants
36vi.mock('./snippets.constants', () => ({
37 SNIPPETS_DIR: '/mock/snippets/dir',
38}))
39
40describe('snippets.utils', () => {
41 const MOCK_SNIPPETS_DIR = '/mock/snippets/dir'
42
43 beforeEach(() => {
44 vi.clearAllMocks()
45 })
46
47 afterEach(() => {
48 vi.resetAllMocks()
49 })
50
51 describe('getFilesystemEntries', () => {
52 it('should create the snippets directory if it does not exist', async () => {
53 const accessError = new Error('Directory not found')
54 mockedFS.access.mockRejectedValue(accessError)
55 mockedFS.mkdir.mockResolvedValue(undefined)
56 mockedFS.readdir.mockResolvedValue([])
57
58 await getFilesystemEntries()
59
60 expect(mockedFS.access).toHaveBeenCalledWith(MOCK_SNIPPETS_DIR)
61 expect(mockedFS.mkdir).toHaveBeenCalledWith(MOCK_SNIPPETS_DIR, { recursive: true })
62 })
63
64 it('should read and parse SQL files from the snippets directory', async () => {
65 const files = [
66 {
67 name: 'snippet1.sql',
68 isDirectory: () => false,
69 isFile: () => true,
70 },
71 {
72 name: 'snippet2.sql',
73 isDirectory: () => false,
74 isFile: () => true,
75 },
76 {
77 name: 'not-sql.txt',
78 isDirectory: () => false,
79 isFile: () => true,
80 },
81 ]
82
83 mockedFS.access.mockResolvedValue(undefined)
84 mockedFS.readdir.mockResolvedValue(files as any)
85 mockedFS.readFile
86 .mockResolvedValueOnce('SELECT * FROM users;')
87 .mockResolvedValueOnce('SELECT * FROM posts;')
88 mockedFS.stat.mockResolvedValue({ birthtime: new Date('2023-01-01') } as any)
89
90 const entries = await getFilesystemEntries()
91
92 expect(entries).toHaveLength(2)
93 expect(entries[0].name).toBe('snippet1')
94 expect(entries[0].content).toBe('SELECT * FROM users;')
95 expect(entries[0].type).toBe('file')
96 expect(entries[1].name).toBe('snippet2')
97 expect(entries[1].content).toBe('SELECT * FROM posts;')
98 expect(entries[1].type).toBe('file')
99 expect(mockedFS.readFile).toHaveBeenCalledTimes(2)
100 })
101
102 it('should return empty array when no SQL files exist', async () => {
103 mockedFS.access.mockResolvedValue(undefined)
104 mockedFS.readdir.mockResolvedValue([
105 { name: 'not-sql.txt', isDirectory: () => false, isFile: () => true },
106 ] as any)
107
108 const entries = await getFilesystemEntries()
109
110 expect(entries).toEqual([])
111 })
112
113 it('should read SQL files from subdirectories (folders)', async () => {
114 // Mock fs.Dirent objects for different file types
115 const createMockDirent = (name: string, isDirectory: boolean) => ({
116 name,
117 isDirectory: () => isDirectory,
118 isFile: () => !isDirectory,
119 })
120
121 // Mock the main directory structure
122 const mainDirItems = [
123 createMockDirent('root-snippet.sql', false),
124 createMockDirent('folder1', true),
125 createMockDirent('not-sql.txt', false),
126 ]
127
128 // Mock folder1 contents
129 const folder1Items = [createMockDirent('folder1-snippet.sql', false)]
130
131 mockedFS.access.mockResolvedValue(undefined)
132
133 // Mock readdir to return different results based on the path
134 mockedFS.readdir.mockImplementation((dirPath: any) => {
135 if (dirPath === MOCK_SNIPPETS_DIR) {
136 return Promise.resolve(mainDirItems as any)
137 } else if (dirPath === path.join(MOCK_SNIPPETS_DIR, 'folder1')) {
138 return Promise.resolve(folder1Items as any)
139 }
140 return Promise.resolve([])
141 })
142
143 // Mock readFile to return different content based on the file
144 mockedFS.readFile.mockImplementation((filePath: any) => {
145 const fileName = path.basename(filePath as string)
146 switch (fileName) {
147 case 'root-snippet.sql':
148 return Promise.resolve('SELECT * FROM root;')
149 case 'folder1-snippet.sql':
150 return Promise.resolve('SELECT * FROM folder1_table;')
151 default:
152 return Promise.reject(new Error('File not found'))
153 }
154 })
155 mockedFS.stat.mockResolvedValue({ birthtime: new Date('2023-01-01') } as any)
156
157 const entries = await getFilesystemEntries()
158
159 // Should find 1 folder and 2 SQL files
160 expect(entries).toHaveLength(3)
161
162 const folders = entries.filter((e) => e.type === 'folder')
163 const files = entries.filter((e) => e.type === 'file')
164
165 expect(folders).toHaveLength(1)
166 expect(folders[0].name).toBe('folder1')
167 expect(folders[0].folderId).toBe(null) // Folders are always at root level
168
169 expect(files).toHaveLength(2)
170 const rootFile = files.find((f) => f.name === 'root-snippet')
171 const folderFile = files.find((f) => f.name === 'folder1-snippet')
172
173 expect(rootFile?.folderId).toBe(null)
174 expect(folderFile?.folderId).toBe(generateDeterministicUuid(['folder1']))
175 })
176
177 it('should skip subdirectories inside folders (not supported)', async () => {
178 const createMockDirent = (name: string, isDirectory: boolean) => ({
179 name,
180 isDirectory: () => isDirectory,
181 isFile: () => !isDirectory,
182 })
183
184 const mainDirItems = [createMockDirent('folder1', true)]
185 const folder1Items = [
186 createMockDirent('snippet.sql', false),
187 createMockDirent('subfolder', true), // This should be skipped
188 ]
189
190 mockedFS.access.mockResolvedValue(undefined)
191 mockedFS.readdir.mockImplementation((dirPath: any) => {
192 if (dirPath === MOCK_SNIPPETS_DIR) {
193 return Promise.resolve(mainDirItems as any)
194 } else if (dirPath === path.join(MOCK_SNIPPETS_DIR, 'folder1')) {
195 return Promise.resolve(folder1Items as any)
196 }
197 return Promise.resolve([])
198 })
199
200 mockedFS.readFile.mockResolvedValue('SELECT * FROM table;')
201 mockedFS.stat.mockResolvedValue({ birthtime: new Date('2023-01-01') } as any)
202
203 const entries = await getFilesystemEntries()
204
205 // Should only find 1 folder and 1 file (subfolder should be skipped)
206 expect(entries).toHaveLength(2)
207 expect(entries.filter((e) => e.type === 'folder')).toHaveLength(1)
208 expect(entries.filter((e) => e.type === 'file')).toHaveLength(1)
209 })
210
211 it('should handle empty SQL files', async () => {
212 mockedFS.access.mockResolvedValue(undefined)
213 mockedFS.readdir.mockResolvedValue([
214 { name: 'empty.sql', isDirectory: () => false, isFile: () => true },
215 ] as any)
216 mockedFS.readFile.mockResolvedValue('')
217 mockedFS.stat.mockResolvedValue({ birthtime: new Date('2023-01-01') } as any)
218
219 const entries = await getFilesystemEntries()
220
221 expect(entries).toHaveLength(1)
222 expect(entries[0].content).toBe('')
223 })
224
225 it('should handle file read errors gracefully', async () => {
226 mockedFS.access.mockResolvedValue(undefined)
227 mockedFS.readdir.mockResolvedValue([
228 { name: 'error.sql', isDirectory: () => false, isFile: () => true },
229 ] as any)
230 mockedFS.readFile.mockRejectedValue(new Error('Permission denied'))
231
232 await expect(getFilesystemEntries()).rejects.toThrow('Permission denied')
233 })
234
235 it('should handle mkdir failure', async () => {
236 mockedFS.access.mockRejectedValue(new Error('Directory not found'))
237 mockedFS.mkdir.mockRejectedValue(new Error('Permission denied'))
238
239 await expect(getFilesystemEntries()).rejects.toThrow('Permission denied')
240 })
241 })
242
243 describe('getSnippet', () => {
244 it('should get a specific snippet by id', async () => {
245 const snippetName = 'test-snippet'
246 const snippetId = generateDeterministicUuid([`${snippetName}.sql`])
247
248 mockedFS.access.mockResolvedValue(undefined)
249 mockedFS.readdir.mockResolvedValue([
250 { name: 'test-snippet.sql', isDirectory: () => false, isFile: () => true },
251 ] as any)
252 mockedFS.readFile.mockResolvedValue('SELECT * FROM test;')
253 mockedFS.stat.mockResolvedValue({ birthtime: new Date('2023-01-01') } as any)
254
255 const snippet = await getSnippet(snippetId)
256
257 // The snippet ID is generated from the full filename in buildSnippet
258 expect(snippet.id).toBe(generateDeterministicUuid([`${snippetName}.sql`]))
259 expect(snippet.name).toBe(snippetName)
260 expect(snippet.content.sql).toBe('SELECT * FROM test;')
261 expect(snippet.folder_id).toBe(null)
262 })
263
264 it('should get a snippet from a folder', async () => {
265 const folderName = 'my-folder'
266 const folderId = generateDeterministicUuid([folderName])
267 const snippetName = 'folder-snippet'
268 const snippetId = generateDeterministicUuid([folderId, `${snippetName}.sql`])
269
270 const createMockDirent = (name: string, isDirectory: boolean) => ({
271 name,
272 isDirectory: () => isDirectory,
273 isFile: () => !isDirectory,
274 })
275
276 mockedFS.access.mockResolvedValue(undefined)
277 mockedFS.readdir.mockImplementation((dirPath: any) => {
278 if (dirPath === MOCK_SNIPPETS_DIR) {
279 return Promise.resolve([createMockDirent(folderName, true)] as any)
280 } else if (dirPath === path.join(MOCK_SNIPPETS_DIR, folderName)) {
281 return Promise.resolve([createMockDirent(`${snippetName}.sql`, false)] as any)
282 }
283 return Promise.resolve([])
284 })
285 mockedFS.readFile.mockResolvedValue('SELECT * FROM folder_table;')
286 mockedFS.stat.mockResolvedValue({ birthtime: new Date('2023-01-01') } as any)
287
288 const snippet = await getSnippet(snippetId)
289
290 expect(snippet.id).toBe(generateDeterministicUuid([folderId, `${snippetName}.sql`]))
291 expect(snippet.name).toBe(snippetName)
292 expect(snippet.content.sql).toBe('SELECT * FROM folder_table;')
293 expect(snippet.folder_id).toBe(generateDeterministicUuid(['my-folder']))
294 })
295
296 it('should throw error when snippet not found', async () => {
297 mockedFS.access.mockResolvedValue(undefined)
298 mockedFS.readdir.mockResolvedValue([])
299
300 await expect(getSnippet('non-existent-id')).rejects.toThrow(
301 'Snippet with id non-existent-id not found'
302 )
303 })
304 })
305
306 describe('getSnippets', () => {
307 it('should get all snippets from root folder', async () => {
308 mockedFS.access.mockResolvedValue(undefined)
309 mockedFS.readdir.mockResolvedValue([
310 { name: 'snippet1.sql', isDirectory: () => false, isFile: () => true },
311 { name: 'snippet2.sql', isDirectory: () => false, isFile: () => true },
312 { name: 'folder1', isDirectory: () => true, isFile: () => false },
313 ] as any)
314 mockedFS.readFile
315 .mockResolvedValueOnce('SELECT * FROM table1;')
316 .mockResolvedValueOnce('SELECT * FROM table2;')
317 mockedFS.stat.mockResolvedValue({ birthtime: new Date('2023-01-01') } as any)
318
319 const result = await getSnippets({ folderId: null, sort: 'name', sortOrder: 'asc' })
320
321 expect(result.snippets).toHaveLength(2)
322 expect(result.snippets[0].name).toBe('snippet1')
323 expect(result.snippets[1].name).toBe('snippet2')
324 expect(result.snippets[0].folder_id).toBe(null)
325 expect(result.snippets[1].folder_id).toBe(null)
326 expect(result.cursor).toBeUndefined()
327 })
328
329 it('should get snippets from a specific folder', async () => {
330 const folderId = generateDeterministicUuid(['my-folder'])
331
332 const createMockDirent = (name: string, isDirectory: boolean) => ({
333 name,
334 isDirectory: () => isDirectory,
335 isFile: () => !isDirectory,
336 })
337
338 mockedFS.access.mockResolvedValue(undefined)
339 mockedFS.readdir.mockImplementation((dirPath: any) => {
340 if (dirPath === MOCK_SNIPPETS_DIR) {
341 return Promise.resolve([createMockDirent('my-folder', true)] as any)
342 } else if (dirPath === path.join(MOCK_SNIPPETS_DIR, 'my-folder')) {
343 return Promise.resolve([createMockDirent('folder-snippet.sql', false)] as any)
344 }
345 return Promise.resolve([])
346 })
347 mockedFS.readFile.mockResolvedValue('SELECT * FROM folder_table;')
348 mockedFS.stat.mockResolvedValue({ birthtime: new Date('2023-01-01') } as any)
349
350 const result = await getSnippets({ folderId })
351
352 expect(result.snippets).toHaveLength(1)
353 expect(result.snippets[0].name).toBe('folder-snippet')
354 expect(result.snippets[0].folder_id).toBe(folderId)
355 })
356
357 it('should return empty result when no snippets in folder', async () => {
358 const folderId = generateDeterministicUuid(['empty-folder'])
359
360 mockedFS.access.mockResolvedValue(undefined)
361 mockedFS.readdir.mockResolvedValue([])
362
363 const result = await getSnippets({ folderId })
364
365 expect(result.snippets).toEqual([])
366 expect(result.cursor).toBeUndefined()
367 })
368
369 it('should filter snippets by search term across all folders', async () => {
370 const createMockDirent = (name: string, isDirectory: boolean) => ({
371 name,
372 isDirectory: () => isDirectory,
373 isFile: () => !isDirectory,
374 })
375
376 mockedFS.access.mockResolvedValue(undefined)
377 mockedFS.readdir.mockImplementation((dirPath: any) => {
378 if (dirPath === MOCK_SNIPPETS_DIR) {
379 return Promise.resolve([
380 createMockDirent('user-query.sql', false),
381 createMockDirent('admin-report.sql', false),
382 createMockDirent('test-user-data.sql', false),
383 createMockDirent('my-folder', true),
384 ] as any)
385 } else if (dirPath === path.join(MOCK_SNIPPETS_DIR, 'my-folder')) {
386 return Promise.resolve([createMockDirent('user-permissions.sql', false)] as any)
387 }
388 return Promise.resolve([])
389 })
390 mockedFS.readFile.mockImplementation((filePath: any) => {
391 const fileName = path.basename(filePath as string)
392 return Promise.resolve(`SELECT * FROM ${fileName.replace('.sql', '')};`)
393 })
394 mockedFS.stat.mockResolvedValue({ birthtime: new Date('2023-01-01') } as any)
395
396 const result = await getSnippets({ searchTerm: 'user', sort: 'name', sortOrder: 'asc' })
397
398 expect(result.snippets).toHaveLength(3)
399 expect(result.snippets.map((s) => s.name)).toEqual([
400 'test-user-data',
401 'user-permissions',
402 'user-query',
403 ])
404 })
405
406 it('should sort snippets by name in ascending order', async () => {
407 mockedFS.access.mockResolvedValue(undefined)
408 mockedFS.readdir.mockResolvedValue([
409 { name: 'zebra.sql', isDirectory: () => false, isFile: () => true },
410 { name: 'apple.sql', isDirectory: () => false, isFile: () => true },
411 { name: 'banana.sql', isDirectory: () => false, isFile: () => true },
412 ] as any)
413 mockedFS.readFile.mockResolvedValue('SELECT 1;')
414 mockedFS.stat.mockResolvedValue({ birthtime: new Date('2023-01-01') } as any)
415
416 const result = await getSnippets({ sort: 'name', sortOrder: 'asc' })
417
418 expect(result.snippets.map((s) => s.name)).toEqual(['apple', 'banana', 'zebra'])
419 })
420
421 it('should sort snippets by name in descending order', async () => {
422 mockedFS.access.mockResolvedValue(undefined)
423 mockedFS.readdir.mockResolvedValue([
424 { name: 'zebra.sql', isDirectory: () => false, isFile: () => true },
425 { name: 'apple.sql', isDirectory: () => false, isFile: () => true },
426 { name: 'banana.sql', isDirectory: () => false, isFile: () => true },
427 ] as any)
428 mockedFS.readFile.mockResolvedValue('SELECT 1;')
429 mockedFS.stat.mockResolvedValue({ birthtime: new Date('2023-01-01') } as any)
430
431 const result = await getSnippets({ sort: 'name', sortOrder: 'desc' })
432
433 expect(result.snippets.map((s) => s.name)).toEqual(['zebra', 'banana', 'apple'])
434 })
435
436 it('should sort snippets by creation date', async () => {
437 mockedFS.access.mockResolvedValue(undefined)
438 mockedFS.readdir.mockResolvedValue([
439 { name: 'newest.sql', isDirectory: () => false, isFile: () => true },
440 { name: 'oldest.sql', isDirectory: () => false, isFile: () => true },
441 { name: 'middle.sql', isDirectory: () => false, isFile: () => true },
442 ] as any)
443 mockedFS.readFile.mockResolvedValue('SELECT 1;')
444 mockedFS.stat.mockImplementation((filePath: any) => {
445 const fileName = path.basename(filePath as string)
446 switch (fileName) {
447 case 'newest.sql':
448 return Promise.resolve({ birthtime: new Date('2023-03-01') } as any)
449 case 'oldest.sql':
450 return Promise.resolve({ birthtime: new Date('2023-01-01') } as any)
451 case 'middle.sql':
452 return Promise.resolve({ birthtime: new Date('2023-02-01') } as any)
453 default:
454 return Promise.resolve({ birthtime: new Date('2023-01-01') } as any)
455 }
456 })
457
458 // Test descending order (newest first - default behavior)
459 const resultDesc = await getSnippets({ sort: 'inserted_at', sortOrder: 'desc' })
460 expect(resultDesc.snippets.map((s) => s.name)).toEqual(['newest', 'middle', 'oldest'])
461
462 // Test ascending order (oldest first)
463 const resultAsc = await getSnippets({ sort: 'inserted_at', sortOrder: 'asc' })
464 expect(resultAsc.snippets.map((s) => s.name)).toEqual(['oldest', 'middle', 'newest'])
465 })
466
467 it('should paginate results with limit', async () => {
468 mockedFS.access.mockResolvedValue(undefined)
469 mockedFS.readdir.mockResolvedValue([
470 { name: 'snippet1.sql', isDirectory: () => false, isFile: () => true },
471 { name: 'snippet2.sql', isDirectory: () => false, isFile: () => true },
472 { name: 'snippet3.sql', isDirectory: () => false, isFile: () => true },
473 { name: 'snippet4.sql', isDirectory: () => false, isFile: () => true },
474 ] as any)
475 mockedFS.readFile.mockResolvedValue('SELECT 1;')
476 mockedFS.stat.mockResolvedValue({ birthtime: new Date('2023-01-01') } as any)
477
478 const result = await getSnippets({ limit: 2, sort: 'name', sortOrder: 'asc' })
479
480 expect(result.snippets).toHaveLength(2)
481 expect(result.snippets.map((s) => s.name)).toEqual(['snippet1', 'snippet2'])
482 expect(result.cursor).toBeDefined() // Should have a cursor for next page
483 })
484
485 it('should use cursor for pagination', async () => {
486 mockedFS.access.mockResolvedValue(undefined)
487 mockedFS.readdir.mockResolvedValue([
488 { name: 'snippet1.sql', isDirectory: () => false, isFile: () => true },
489 { name: 'snippet2.sql', isDirectory: () => false, isFile: () => true },
490 { name: 'snippet3.sql', isDirectory: () => false, isFile: () => true },
491 ] as any)
492 mockedFS.readFile.mockResolvedValue('SELECT 1;')
493 mockedFS.stat.mockResolvedValue({ birthtime: new Date('2023-01-01') } as any)
494
495 // Get first page
496 const firstPage = await getSnippets({ limit: 1, sort: 'name', sortOrder: 'asc' })
497 expect(firstPage.snippets).toHaveLength(1)
498 expect(firstPage.snippets[0].name).toBe('snippet1')
499 expect(firstPage.cursor).toBeDefined()
500
501 // Get second page using cursor
502 const secondPage = await getSnippets({
503 cursor: firstPage.cursor,
504 limit: 1,
505 sort: 'name',
506 sortOrder: 'asc',
507 })
508 expect(secondPage.snippets).toHaveLength(1)
509 expect(secondPage.snippets[0].name).toBe('snippet2')
510 })
511
512 it('should handle invalid cursor gracefully', async () => {
513 mockedFS.access.mockResolvedValue(undefined)
514 mockedFS.readdir.mockResolvedValue([
515 { name: 'snippet1.sql', isDirectory: () => false, isFile: () => true },
516 ] as any)
517 mockedFS.readFile.mockResolvedValue('SELECT 1;')
518 mockedFS.stat.mockResolvedValue({ birthtime: new Date('2023-01-01') } as any)
519
520 const result = await getSnippets({ cursor: 'invalid-cursor-id' })
521
522 expect(result.snippets).toHaveLength(1) // Should return all snippets
523 expect(result.snippets[0].name).toBe('snippet1')
524 })
525
526 it('should use default parameters when none provided', async () => {
527 mockedFS.access.mockResolvedValue(undefined)
528 mockedFS.readdir.mockResolvedValue([
529 { name: 'snippet1.sql', isDirectory: () => false, isFile: () => true },
530 ] as any)
531 mockedFS.readFile.mockResolvedValue('SELECT 1;')
532 mockedFS.stat.mockResolvedValue({ birthtime: new Date('2023-01-01') } as any)
533
534 const result = await getSnippets({})
535
536 expect(result.snippets).toHaveLength(1)
537 expect(result.cursor).toBeUndefined()
538 // Default sort should be by inserted_at desc
539 })
540
541 it('should handle empty search term', async () => {
542 const folderId = generateDeterministicUuid(['my-folder'])
543
544 const createMockDirent = (name: string, isDirectory: boolean) => ({
545 name,
546 isDirectory: () => isDirectory,
547 isFile: () => !isDirectory,
548 })
549
550 mockedFS.access.mockResolvedValue(undefined)
551 mockedFS.readdir.mockImplementation((dirPath: any) => {
552 if (dirPath === MOCK_SNIPPETS_DIR) {
553 return Promise.resolve([
554 createMockDirent('root-snippet.sql', false),
555 createMockDirent('my-folder', true),
556 ] as any)
557 } else if (dirPath === path.join(MOCK_SNIPPETS_DIR, 'my-folder')) {
558 return Promise.resolve([createMockDirent('folder-snippet.sql', false)] as any)
559 }
560 return Promise.resolve([])
561 })
562 mockedFS.readFile.mockResolvedValue('SELECT 1;')
563 mockedFS.stat.mockResolvedValue({ birthtime: new Date('2023-01-01') } as any)
564
565 // Empty search term should filter by folderId
566 const result = await getSnippets({ searchTerm: ' ', folderId })
567
568 expect(result.snippets).toHaveLength(1)
569 expect(result.snippets[0].name).toBe('folder-snippet')
570 })
571
572 it('should handle filesystem errors', async () => {
573 mockedFS.access.mockRejectedValue(new Error('Directory not accessible'))
574 mockedFS.mkdir.mockRejectedValue(new Error('Directory not accessible'))
575
576 await expect(getSnippets({})).rejects.toThrow('Directory not accessible')
577 })
578
579 it('should return correct snippet structure', async () => {
580 mockedFS.access.mockResolvedValue(undefined)
581 mockedFS.readdir.mockResolvedValue([
582 { name: 'test-snippet.sql', isDirectory: () => false, isFile: () => true },
583 ] as any)
584 mockedFS.readFile.mockResolvedValue('SELECT * FROM users;')
585 mockedFS.stat.mockResolvedValue({ birthtime: new Date('2023-01-01T10:00:00Z') } as any)
586
587 const result = await getSnippets({})
588
589 expect(result.snippets).toHaveLength(1)
590 const snippet = result.snippets[0]
591
592 expect(snippet).toMatchObject({
593 name: 'test-snippet',
594 type: 'sql',
595 content: {
596 sql: 'SELECT * FROM users;',
597 schema_version: '1.0',
598 },
599 visibility: 'user',
600 project_id: 1,
601 folder_id: null,
602 owner_id: 1,
603 owner: { id: 1, username: 'johndoe' },
604 updated_by: { id: 1, username: 'johndoe' },
605 })
606
607 expect(snippet.id).toBeDefined()
608 expect(snippet.inserted_at).toBe('2023-01-01T10:00:00.000Z')
609 expect(snippet.updated_at).toBe('2023-01-01T10:00:00.000Z')
610 })
611
612 it('should perform case-insensitive search', async () => {
613 mockedFS.access.mockResolvedValue(undefined)
614 mockedFS.readdir.mockResolvedValue([
615 { name: 'USER-query.sql', isDirectory: () => false, isFile: () => true },
616 { name: 'admin-report.sql', isDirectory: () => false, isFile: () => true },
617 { name: 'Test-User-Data.sql', isDirectory: () => false, isFile: () => true },
618 ] as any)
619 mockedFS.readFile.mockResolvedValue('SELECT 1;')
620 mockedFS.stat.mockResolvedValue({ birthtime: new Date('2023-01-01') } as any)
621
622 const result = await getSnippets({ searchTerm: 'user', sort: 'name', sortOrder: 'asc' })
623
624 expect(result.snippets).toHaveLength(2)
625 expect(result.snippets.map((s) => s.name)).toEqual(['Test-User-Data', 'USER-query'])
626 })
627
628 it('should perform case-insensitive name sorting', async () => {
629 mockedFS.access.mockResolvedValue(undefined)
630 mockedFS.readdir.mockResolvedValue([
631 { name: 'zebra.sql', isDirectory: () => false, isFile: () => true },
632 { name: 'Apple.sql', isDirectory: () => false, isFile: () => true },
633 { name: 'banana.sql', isDirectory: () => false, isFile: () => true },
634 ] as any)
635 mockedFS.readFile.mockResolvedValue('SELECT 1;')
636 mockedFS.stat.mockResolvedValue({ birthtime: new Date('2023-01-01') } as any)
637
638 const result = await getSnippets({ sort: 'name', sortOrder: 'asc' })
639
640 expect(result.snippets.map((s) => s.name)).toEqual(['Apple', 'banana', 'zebra'])
641 })
642
643 it('should validate limit parameter', async () => {
644 await expect(getSnippets({ limit: 0 })).rejects.toThrow('Limit must be a positive number')
645 await expect(getSnippets({ limit: -5 })).rejects.toThrow('Limit must be a positive number')
646 await expect(getSnippets({ limit: 1001 })).rejects.toThrow('Limit cannot exceed 1000')
647 })
648
649 it('should handle large valid limit', async () => {
650 mockedFS.access.mockResolvedValue(undefined)
651 mockedFS.readdir.mockResolvedValue([
652 { name: 'snippet1.sql', isDirectory: () => false, isFile: () => true },
653 ] as any)
654 mockedFS.readFile.mockResolvedValue('SELECT 1;')
655 mockedFS.stat.mockResolvedValue({ birthtime: new Date('2023-01-01') } as any)
656
657 const result = await getSnippets({ limit: 1000 })
658
659 expect(result.snippets).toHaveLength(1)
660 })
661 })
662
663 describe('saveSnippet', () => {
664 it('should save a snippet to the filesystem', async () => {
665 const mockSnippet: Snippet = {
666 id: 'test-id',
667 inserted_at: '2023-01-01T00:00:00.000Z',
668 updated_at: '2023-01-01T00:00:00.000Z',
669 type: 'sql',
670 name: 'test-snippet',
671 description: 'Test snippet',
672 favorite: false,
673 content: {
674 sql: 'SELECT * FROM test;',
675 content_id: 'content-id',
676 schema_version: '1.0',
677 },
678 visibility: 'user',
679 project_id: 1,
680 folder_id: null,
681 owner_id: 1,
682 owner: { id: 1, username: 'testuser' },
683 updated_by: { id: 1, username: 'testuser' },
684 }
685
686 // Mock that no existing snippet exists
687 mockedFS.access.mockResolvedValue(undefined)
688 mockedFS.readdir.mockResolvedValue([])
689 mockedFS.writeFile.mockResolvedValue(undefined)
690 mockedFS.stat.mockResolvedValue({ birthtime: new Date('2023-01-01') } as any)
691
692 const result = await saveSnippet(mockSnippet)
693
694 expect(mockedFS.writeFile).toHaveBeenCalledWith(
695 path.join(MOCK_SNIPPETS_DIR, 'test-snippet.sql'),
696 'SELECT * FROM test;',
697 'utf-8'
698 )
699 expect(result.name).toBe('test-snippet')
700 expect(result.content.sql).toBe('SELECT * FROM test;')
701 })
702
703 it('should handle empty content', async () => {
704 const mockSnippet: Snippet = {
705 id: 'test-id',
706 inserted_at: '2023-01-01T00:00:00.000Z',
707 updated_at: '2023-01-01T00:00:00.000Z',
708 type: 'sql',
709 name: 'empty-snippet',
710 description: '',
711 favorite: false,
712 content: {
713 sql: '',
714 content_id: 'content-id',
715 schema_version: '1.0',
716 },
717 visibility: 'user',
718 project_id: 1,
719 folder_id: null,
720 owner_id: 1,
721 owner: { id: 1, username: 'testuser' },
722 updated_by: { id: 1, username: 'testuser' },
723 }
724
725 mockedFS.access.mockResolvedValue(undefined)
726 mockedFS.readdir.mockResolvedValue([])
727 mockedFS.writeFile.mockResolvedValue(undefined)
728 mockedFS.stat.mockResolvedValue({ birthtime: new Date('2023-01-01') } as any)
729
730 const result = await saveSnippet(mockSnippet)
731
732 expect(mockedFS.writeFile).toHaveBeenCalledWith(
733 path.join(MOCK_SNIPPETS_DIR, 'empty-snippet.sql'),
734 '',
735 'utf-8'
736 )
737 expect(result.content.sql).toBe('')
738 })
739
740 it('should throw error when snippet already exists', async () => {
741 const mockSnippet: Snippet = {
742 id: generateDeterministicUuid(['existing-snippet.sql']),
743 inserted_at: '2023-01-01T00:00:00.000Z',
744 updated_at: '2023-01-01T00:00:00.000Z',
745 type: 'sql',
746 name: 'existing-snippet',
747 description: '',
748 favorite: false,
749 content: {
750 sql: 'SELECT * FROM test;',
751 content_id: 'content-id',
752 schema_version: '1.0',
753 },
754 visibility: 'user',
755 project_id: 1,
756 folder_id: null,
757 owner_id: 1,
758 owner: { id: 1, username: 'testuser' },
759 updated_by: { id: 1, username: 'testuser' },
760 }
761
762 // Mock that snippet already exists
763 mockedFS.access.mockResolvedValue(undefined)
764 mockedFS.readdir.mockResolvedValue([
765 { name: 'existing-snippet.sql', isDirectory: () => false, isFile: () => true },
766 ] as any)
767 mockedFS.readFile.mockResolvedValue('SELECT * FROM existing;')
768 mockedFS.stat.mockResolvedValue({ birthtime: new Date('2023-01-01') } as any)
769
770 await expect(saveSnippet(mockSnippet)).rejects.toThrow(
771 `Snippet with id ${mockSnippet.id} already exists`
772 )
773 })
774
775 it('should save snippet to a folder when folder_id is provided', async () => {
776 const mockSnippet: Snippet = {
777 id: 'test-id',
778 inserted_at: '2023-01-01T00:00:00.000Z',
779 updated_at: '2023-01-01T00:00:00.000Z',
780 type: 'sql',
781 name: 'folder-snippet',
782 description: '',
783 favorite: false,
784 content: {
785 sql: 'SELECT * FROM folder_table;',
786 content_id: 'content-id',
787 schema_version: '1.0',
788 },
789 visibility: 'user',
790 project_id: 1,
791 folder_id: generateDeterministicUuid(['test-folder']),
792 owner_id: 1,
793 owner: { id: 1, username: 'testuser' },
794 updated_by: { id: 1, username: 'testuser' },
795 }
796
797 mockedFS.access.mockResolvedValue(undefined)
798 mockedFS.readdir.mockResolvedValue([
799 { name: 'test-folder', isDirectory: () => true, isFile: () => false },
800 ] as any)
801 mockedFS.writeFile.mockResolvedValue(undefined)
802 mockedFS.stat.mockResolvedValue({ birthtime: new Date('2023-01-01') } as any)
803
804 const result = await saveSnippet(mockSnippet)
805
806 expect(mockedFS.writeFile).toHaveBeenCalledWith(
807 path.join(MOCK_SNIPPETS_DIR, 'test-folder', 'folder-snippet.sql'),
808 'SELECT * FROM folder_table;',
809 'utf-8'
810 )
811 expect(result.folder_id).toBe(generateDeterministicUuid(['test-folder']))
812 })
813
814 it('should throw error when folder_id is provided but folder does not exist', async () => {
815 const mockSnippet: Snippet = {
816 id: 'test-id',
817 inserted_at: '2023-01-01T00:00:00.000Z',
818 updated_at: '2023-01-01T00:00:00.000Z',
819 type: 'sql',
820 name: 'orphan-snippet',
821 description: '',
822 favorite: false,
823 content: {
824 sql: 'SELECT * FROM table;',
825 content_id: 'content-id',
826 schema_version: '1.0',
827 },
828 visibility: 'user',
829 project_id: 1,
830 folder_id: 'non-existent-folder-id',
831 owner_id: 1,
832 owner: { id: 1, username: 'testuser' },
833 updated_by: { id: 1, username: 'testuser' },
834 }
835
836 mockedFS.access.mockResolvedValue(undefined)
837 mockedFS.readdir.mockResolvedValue([])
838
839 await expect(saveSnippet(mockSnippet)).rejects.toThrow(
840 'Folder with id non-existent-folder-id not found'
841 )
842 })
843
844 it('should reject path traversal attempts with ../', async () => {
845 const mockSnippet: Snippet = {
846 id: 'test-id',
847 inserted_at: '2023-01-01T00:00:00.000Z',
848 updated_at: '2023-01-01T00:00:00.000Z',
849 type: 'sql',
850 name: '../malicious-snippet',
851 description: '',
852 favorite: false,
853 content: {
854 sql: 'SELECT * FROM test;',
855 content_id: 'content-id',
856 schema_version: '1.0',
857 },
858 visibility: 'user',
859 project_id: 1,
860 folder_id: null,
861 owner_id: 1,
862 owner: { id: 1, username: 'testuser' },
863 updated_by: { id: 1, username: 'testuser' },
864 }
865
866 mockedFS.access.mockResolvedValue(undefined)
867 mockedFS.readdir.mockResolvedValue([])
868
869 await expect(saveSnippet(mockSnippet)).rejects.toThrow(
870 'Invalid name: path traversal or null bytes detected'
871 )
872 })
873
874 it('should reject path traversal attempts with nested directories', async () => {
875 const mockSnippet: Snippet = {
876 id: 'test-id',
877 inserted_at: '2023-01-01T00:00:00.000Z',
878 updated_at: '2023-01-01T00:00:00.000Z',
879 type: 'sql',
880 name: 'foo/bar/snippet',
881 description: '',
882 favorite: false,
883 content: {
884 sql: 'SELECT * FROM test;',
885 content_id: 'content-id',
886 schema_version: '1.0',
887 },
888 visibility: 'user',
889 project_id: 1,
890 folder_id: null,
891 owner_id: 1,
892 owner: { id: 1, username: 'testuser' },
893 updated_by: { id: 1, username: 'testuser' },
894 }
895
896 mockedFS.access.mockResolvedValue(undefined)
897 mockedFS.readdir.mockResolvedValue([])
898
899 await expect(saveSnippet(mockSnippet)).rejects.toThrow(
900 'Invalid name: path traversal or null bytes detected'
901 )
902 })
903
904 it('should reject names with null bytes', async () => {
905 const mockSnippet: Snippet = {
906 id: 'test-id',
907 inserted_at: '2023-01-01T00:00:00.000Z',
908 updated_at: '2023-01-01T00:00:00.000Z',
909 type: 'sql',
910 name: 'malicious\0snippet',
911 description: '',
912 favorite: false,
913 content: {
914 sql: 'SELECT * FROM test;',
915 content_id: 'content-id',
916 schema_version: '1.0',
917 },
918 visibility: 'user',
919 project_id: 1,
920 folder_id: null,
921 owner_id: 1,
922 owner: { id: 1, username: 'testuser' },
923 updated_by: { id: 1, username: 'testuser' },
924 }
925
926 mockedFS.access.mockResolvedValue(undefined)
927 mockedFS.readdir.mockResolvedValue([])
928
929 await expect(saveSnippet(mockSnippet)).rejects.toThrow(
930 'Invalid name: path traversal or null bytes detected'
931 )
932 })
933
934 it('should reject absolute path attempts', async () => {
935 const mockSnippet: Snippet = {
936 id: 'test-id',
937 inserted_at: '2023-01-01T00:00:00.000Z',
938 updated_at: '2023-01-01T00:00:00.000Z',
939 type: 'sql',
940 name: '/etc/passwd',
941 description: '',
942 favorite: false,
943 content: {
944 sql: 'SELECT * FROM test;',
945 content_id: 'content-id',
946 schema_version: '1.0',
947 },
948 visibility: 'user',
949 project_id: 1,
950 folder_id: null,
951 owner_id: 1,
952 owner: { id: 1, username: 'testuser' },
953 updated_by: { id: 1, username: 'testuser' },
954 }
955
956 mockedFS.access.mockResolvedValue(undefined)
957 mockedFS.readdir.mockResolvedValue([])
958
959 await expect(saveSnippet(mockSnippet)).rejects.toThrow(
960 'Invalid name: path traversal or null bytes detected'
961 )
962 })
963
964 it('should allow valid names with special characters', async () => {
965 const mockSnippet: Snippet = {
966 id: 'test-id',
967 inserted_at: '2023-01-01T00:00:00.000Z',
968 updated_at: '2023-01-01T00:00:00.000Z',
969 type: 'sql',
970 name: 'my-snippet_v2 (copy)',
971 description: '',
972 favorite: false,
973 content: {
974 sql: 'SELECT * FROM test;',
975 content_id: 'content-id',
976 schema_version: '1.0',
977 },
978 visibility: 'user',
979 project_id: 1,
980 folder_id: null,
981 owner_id: 1,
982 owner: { id: 1, username: 'testuser' },
983 updated_by: { id: 1, username: 'testuser' },
984 }
985
986 mockedFS.access.mockResolvedValue(undefined)
987 mockedFS.readdir.mockResolvedValue([])
988 mockedFS.writeFile.mockResolvedValue(undefined)
989 mockedFS.stat.mockResolvedValue({ birthtime: new Date('2023-01-01') } as any)
990
991 const result = await saveSnippet(mockSnippet)
992
993 expect(result.name).toBe('my-snippet_v2 (copy)')
994 expect(mockedFS.writeFile).toHaveBeenCalledWith(
995 path.join(MOCK_SNIPPETS_DIR, 'my-snippet_v2 (copy).sql'),
996 'SELECT * FROM test;',
997 'utf-8'
998 )
999 })
1000 })
1001
1002 describe('deleteSnippet', () => {
1003 it('should delete a snippet file', async () => {
1004 const snippetId = generateDeterministicUuid(['test-snippet.sql'])
1005
1006 mockedFS.access.mockResolvedValue(undefined)
1007 mockedFS.readdir.mockResolvedValue([
1008 { name: 'test-snippet.sql', isDirectory: () => false, isFile: () => true },
1009 ] as any)
1010 mockedFS.readFile.mockResolvedValue('SELECT * FROM test;')
1011 mockedFS.unlink.mockResolvedValue(undefined)
1012 mockedFS.stat.mockResolvedValue({ birthtime: new Date('2023-01-01') } as any)
1013
1014 await deleteSnippet(snippetId)
1015
1016 expect(mockedFS.unlink).toHaveBeenCalledWith(path.join(MOCK_SNIPPETS_DIR, `test-snippet.sql`))
1017 })
1018
1019 it('should not throw error when file does not exist', async () => {
1020 const snippetId = generateDeterministicUuid(['test-snippet.sql'])
1021
1022 mockedFS.access.mockResolvedValue(undefined)
1023 mockedFS.readdir.mockResolvedValue([
1024 { name: 'test-snippet.sql', isDirectory: () => false, isFile: () => true },
1025 ] as any)
1026 mockedFS.readFile.mockResolvedValue('SELECT * FROM test;')
1027
1028 const error = new Error('File not found') as NodeJS.ErrnoException
1029 error.code = 'ENOENT'
1030 mockedFS.unlink.mockRejectedValue(error)
1031 mockedFS.stat.mockResolvedValue({ birthtime: new Date('2023-01-01') } as any)
1032
1033 await expect(deleteSnippet(snippetId)).resolves.not.toThrow()
1034 })
1035
1036 it('should throw error for other filesystem errors', async () => {
1037 const snippetId = generateDeterministicUuid(['test-snippet.sql'])
1038
1039 mockedFS.access.mockResolvedValue(undefined)
1040 mockedFS.readdir.mockResolvedValue([
1041 { name: 'test-snippet.sql', isDirectory: () => false, isFile: () => true },
1042 ] as any)
1043 mockedFS.readFile.mockResolvedValue('SELECT * FROM test;')
1044
1045 const error = new Error('Permission denied') as NodeJS.ErrnoException
1046 error.code = 'EACCES'
1047 mockedFS.unlink.mockRejectedValue(error)
1048 mockedFS.stat.mockResolvedValue({ birthtime: new Date('2023-01-01') } as any)
1049
1050 await expect(deleteSnippet(snippetId)).rejects.toThrow('Permission denied')
1051 })
1052
1053 it('should throw error when snippet not found', async () => {
1054 mockedFS.access.mockResolvedValue(undefined)
1055 mockedFS.readdir.mockResolvedValue([])
1056
1057 await expect(deleteSnippet('non-existent-id')).rejects.toThrow(
1058 'Snippet with id non-existent-id not found'
1059 )
1060 })
1061
1062 it('should delete a snippet file from a folder', async () => {
1063 const folderName = 'my-folder'
1064 const folderId = generateDeterministicUuid([folderName])
1065 const snippetName = 'folder-snippet'
1066 const snippetId = generateDeterministicUuid([folderId, `${snippetName}.sql`])
1067
1068 const createMockDirent = (name: string, isDirectory: boolean) => ({
1069 name,
1070 isDirectory: () => isDirectory,
1071 isFile: () => !isDirectory,
1072 })
1073
1074 mockedFS.access.mockResolvedValue(undefined)
1075 mockedFS.readdir.mockImplementation((dirPath: any) => {
1076 if (dirPath === MOCK_SNIPPETS_DIR) {
1077 return Promise.resolve([createMockDirent(folderName, true)] as any)
1078 } else if (dirPath === path.join(MOCK_SNIPPETS_DIR, folderName)) {
1079 return Promise.resolve([createMockDirent('folder-snippet.sql', false)] as any)
1080 }
1081 return Promise.resolve([])
1082 })
1083 mockedFS.readFile.mockResolvedValue('SELECT * FROM folder_table;')
1084 mockedFS.unlink.mockResolvedValue(undefined)
1085 mockedFS.stat.mockResolvedValue({ birthtime: new Date('2023-01-01') } as any)
1086
1087 await deleteSnippet(snippetId)
1088
1089 expect(mockedFS.unlink).toHaveBeenCalledWith(
1090 path.join(MOCK_SNIPPETS_DIR, 'my-folder', 'folder-snippet.sql')
1091 )
1092 })
1093 })
1094
1095 describe('updateSnippet', () => {
1096 it('should update an existing snippet', async () => {
1097 const id = generateDeterministicUuid(['existing-snippet.sql'])
1098
1099 mockedFS.access.mockResolvedValue(undefined)
1100
1101 // First call to getFilesystemEntries - find existing snippet
1102 mockedFS.readdir.mockResolvedValue([
1103 { name: 'existing-snippet.sql', isDirectory: () => false, isFile: () => true },
1104 ] as any)
1105
1106 // Read the existing snippet content
1107 mockedFS.readFile.mockResolvedValue('SELECT * FROM old;')
1108 mockedFS.stat.mockResolvedValue({ birthtime: new Date('2023-01-01') } as any)
1109
1110 const updates = { name: 'updated-snippet', content: { sql: 'SELECT * FROM new;' } }
1111
1112 const result = await updateSnippet(id, updates)
1113
1114 expect(mockedFS.unlink).toHaveBeenCalledWith(
1115 path.join(MOCK_SNIPPETS_DIR, 'existing-snippet.sql')
1116 )
1117 expect(mockedFS.writeFile).toHaveBeenCalledWith(
1118 path.join(MOCK_SNIPPETS_DIR, 'updated-snippet.sql'),
1119 'SELECT * FROM new;',
1120 'utf-8'
1121 )
1122 expect(result.name).toBe('updated-snippet')
1123 expect(result.content.sql).toBe('SELECT * FROM new;')
1124 })
1125
1126 it('should throw error when snippet not found', async () => {
1127 mockedFS.access.mockResolvedValue(undefined)
1128 mockedFS.readdir.mockResolvedValue([])
1129
1130 await expect(updateSnippet('non-existent-id', {})).rejects.toThrow(
1131 'Snippet with id non-existent-id not found'
1132 )
1133 })
1134
1135 it('should update only provided fields', async () => {
1136 const id = generateDeterministicUuid(['existing-snippet.sql'])
1137
1138 mockedFS.access.mockResolvedValue(undefined)
1139 // Mock readdir for both getFilesystemEntries calls (initial and final)
1140 mockedFS.readdir.mockResolvedValue([
1141 { name: 'existing-snippet.sql', isDirectory: () => false, isFile: () => true },
1142 ] as any)
1143 // Mock readFile for both calls (initial and final)
1144 mockedFS.readFile.mockResolvedValue('SELECT * FROM old;')
1145 mockedFS.stat.mockResolvedValue({ birthtime: new Date('2023-01-01') } as any)
1146 mockedFS.writeFile.mockResolvedValue(undefined)
1147
1148 const updates = {
1149 name: 'new-name',
1150 }
1151
1152 const result = await updateSnippet(id, updates)
1153
1154 expect(result.name).toBe('new-name')
1155 expect(result.content.sql).toBe('SELECT * FROM old;')
1156 })
1157
1158 it('should move snippet to a folder when folder_id is updated', async () => {
1159 const id = generateDeterministicUuid(['existing-snippet.sql'])
1160 const targetFolderId = generateDeterministicUuid(['target-folder'])
1161
1162 const createMockDirent = (name: string, isDirectory: boolean) => ({
1163 name,
1164 isDirectory: () => isDirectory,
1165 isFile: () => !isDirectory,
1166 })
1167
1168 // Mock existing snippet in root directory
1169 mockedFS.access.mockResolvedValue(undefined)
1170
1171 // Mock multiple readdir calls - the function calls getFilesystemEntries twice
1172 mockedFS.readdir.mockImplementation((dirPath: any) => {
1173 if (dirPath === MOCK_SNIPPETS_DIR) {
1174 return Promise.resolve([
1175 createMockDirent('existing-snippet.sql', false),
1176 createMockDirent('target-folder', true),
1177 ] as any)
1178 } else if (dirPath === path.join(MOCK_SNIPPETS_DIR, 'target-folder')) {
1179 return Promise.resolve([])
1180 }
1181 return Promise.resolve([])
1182 })
1183
1184 mockedFS.readFile.mockResolvedValue('SELECT * FROM old;')
1185 mockedFS.writeFile.mockResolvedValue(undefined)
1186 mockedFS.unlink.mockResolvedValue(undefined)
1187 mockedFS.stat.mockResolvedValue({ birthtime: new Date('2023-01-01') } as any)
1188
1189 const updates = { folder_id: targetFolderId }
1190
1191 const result = await updateSnippet(id, updates)
1192
1193 // Should write to the folder path
1194 expect(mockedFS.writeFile).toHaveBeenCalledWith(
1195 path.join(MOCK_SNIPPETS_DIR, 'target-folder', 'existing-snippet.sql'),
1196 'SELECT * FROM old;',
1197 'utf-8'
1198 )
1199 // Should delete the old file from root
1200 expect(mockedFS.unlink).toHaveBeenCalledWith(
1201 path.join(MOCK_SNIPPETS_DIR, 'existing-snippet.sql')
1202 )
1203 expect(result.folder_id).toBe(targetFolderId)
1204 })
1205
1206 it('should move snippet from folder to root when folder_id is set to null', async () => {
1207 const folderName = 'source-folder'
1208 const folderId = generateDeterministicUuid([folderName])
1209 const id = generateDeterministicUuid([folderId, 'existing-snippet.sql'])
1210
1211 const createMockDirent = (name: string, isDirectory: boolean) => ({
1212 name,
1213 isDirectory: () => isDirectory,
1214 isFile: () => !isDirectory,
1215 })
1216
1217 mockedFS.access.mockResolvedValue(undefined)
1218
1219 // Mock existing snippet in a folder - the function expects the snippet to have folder_id set
1220 mockedFS.readdir.mockImplementation((dirPath: any) => {
1221 if (dirPath === MOCK_SNIPPETS_DIR) {
1222 return Promise.resolve([createMockDirent(folderName, true)] as any)
1223 } else if (dirPath === path.join(MOCK_SNIPPETS_DIR, folderName)) {
1224 return Promise.resolve([createMockDirent('existing-snippet.sql', false)] as any)
1225 }
1226 return Promise.resolve([])
1227 })
1228
1229 mockedFS.readFile.mockResolvedValue('SELECT * FROM folder;')
1230 mockedFS.writeFile.mockResolvedValue(undefined)
1231 mockedFS.unlink.mockResolvedValue(undefined)
1232 mockedFS.stat.mockResolvedValue({ birthtime: new Date('2023-01-01') } as any)
1233
1234 const updates = { folder_id: null }
1235
1236 const result = await updateSnippet(id, updates)
1237
1238 // Should write to the root path
1239 expect(mockedFS.writeFile).toHaveBeenCalledWith(
1240 path.join(MOCK_SNIPPETS_DIR, 'existing-snippet.sql'),
1241 'SELECT * FROM folder;',
1242 'utf-8'
1243 )
1244 // Should delete the old file from folder
1245 expect(mockedFS.unlink).toHaveBeenCalledWith(
1246 path.join(MOCK_SNIPPETS_DIR, 'source-folder', 'existing-snippet.sql')
1247 )
1248 expect(result.folder_id).toBe(null)
1249 })
1250
1251 it('should throw error when trying to move to non-existent folder', async () => {
1252 const id = generateDeterministicUuid(['existing-snippet.sql'])
1253 const nonExistentFolderId = 'non-existent-folder-id'
1254
1255 mockedFS.access.mockResolvedValue(undefined)
1256 mockedFS.readdir.mockResolvedValue([
1257 { name: 'existing-snippet.sql', isDirectory: () => false, isFile: () => true },
1258 ] as any)
1259 mockedFS.readFile.mockResolvedValue('SELECT * FROM table;')
1260 mockedFS.stat.mockResolvedValue({ birthtime: new Date('2023-01-01') } as any)
1261
1262 const updates = { folder_id: nonExistentFolderId }
1263
1264 await expect(updateSnippet(id, updates)).rejects.toThrow(
1265 `Folder with id ${nonExistentFolderId} not found`
1266 )
1267 })
1268
1269 it('should handle renaming snippet while moving to folder', async () => {
1270 const id = generateDeterministicUuid(['existing-snippet.sql'])
1271 const targetFolderId = generateDeterministicUuid(['target-folder'])
1272
1273 const createMockDirent = (name: string, isDirectory: boolean) => ({
1274 name,
1275 isDirectory: () => isDirectory,
1276 isFile: () => !isDirectory,
1277 })
1278
1279 mockedFS.access.mockResolvedValue(undefined)
1280 mockedFS.readdir.mockImplementation((dirPath: any) => {
1281 if (dirPath === MOCK_SNIPPETS_DIR) {
1282 return Promise.resolve([
1283 createMockDirent('existing-snippet.sql', false),
1284 createMockDirent('target-folder', true),
1285 ] as any)
1286 } else if (dirPath === path.join(MOCK_SNIPPETS_DIR, 'target-folder')) {
1287 return Promise.resolve([])
1288 }
1289 return Promise.resolve([])
1290 })
1291
1292 mockedFS.readFile.mockResolvedValue('SELECT * FROM old;')
1293 mockedFS.writeFile.mockResolvedValue(undefined)
1294 mockedFS.unlink.mockResolvedValue(undefined)
1295 mockedFS.stat.mockResolvedValue({ birthtime: new Date('2023-01-01') } as any)
1296
1297 const updates = {
1298 name: 'renamed-snippet',
1299 folder_id: targetFolderId,
1300 content: { sql: 'SELECT * FROM new;' },
1301 }
1302
1303 const result = await updateSnippet(id, updates as any)
1304
1305 // Should write to the folder path with new name
1306 expect(mockedFS.writeFile).toHaveBeenCalledWith(
1307 path.join(MOCK_SNIPPETS_DIR, 'target-folder', 'renamed-snippet.sql'),
1308 'SELECT * FROM new;',
1309 'utf-8'
1310 )
1311 // Should delete the old file from root with old name
1312 expect(mockedFS.unlink).toHaveBeenCalledWith(
1313 path.join(MOCK_SNIPPETS_DIR, 'existing-snippet.sql')
1314 )
1315 expect(result.name).toBe('renamed-snippet')
1316 expect(result.folder_id).toBe(targetFolderId)
1317 expect(result.content.sql).toBe('SELECT * FROM new;')
1318 })
1319
1320 it('should update content to empty string', async () => {
1321 const id = generateDeterministicUuid(['existing-snippet.sql'])
1322
1323 mockedFS.access.mockResolvedValue(undefined)
1324 mockedFS.readdir.mockResolvedValue([
1325 { name: 'existing-snippet.sql', isDirectory: () => false, isFile: () => true },
1326 ] as any)
1327 mockedFS.readFile.mockResolvedValue('SELECT * FROM old;')
1328 mockedFS.writeFile.mockResolvedValue(undefined)
1329 mockedFS.unlink.mockResolvedValue(undefined)
1330 mockedFS.stat.mockResolvedValue({ birthtime: new Date('2023-01-01') } as any)
1331
1332 const updates = { content: { sql: '' } }
1333
1334 const result = await updateSnippet(id, updates as any)
1335
1336 expect(result.content.sql).toBe('')
1337 expect(mockedFS.writeFile).toHaveBeenCalledWith(
1338 path.join(MOCK_SNIPPETS_DIR, 'existing-snippet.sql'),
1339 '',
1340 'utf-8'
1341 )
1342 })
1343
1344 it('should handle old file deletion errors other than ENOENT', async () => {
1345 const id = generateDeterministicUuid(['existing-snippet.sql'])
1346
1347 mockedFS.access.mockResolvedValue(undefined)
1348 mockedFS.readdir.mockResolvedValue([
1349 { name: 'existing-snippet.sql', isDirectory: () => false, isFile: () => true },
1350 ] as any)
1351 mockedFS.readFile.mockResolvedValue('SELECT * FROM old;')
1352 mockedFS.stat.mockResolvedValue({ birthtime: new Date('2023-01-01') } as any)
1353
1354 const error = new Error('Permission denied') as NodeJS.ErrnoException
1355 error.code = 'EACCES'
1356 mockedFS.unlink.mockRejectedValue(error)
1357
1358 const updates = { name: 'new-name' }
1359
1360 await expect(updateSnippet(id, updates)).rejects.toThrow('Permission denied')
1361 })
1362
1363 it('should continue when old file deletion fails with ENOENT', async () => {
1364 const id = generateDeterministicUuid(['existing-snippet.sql'])
1365
1366 mockedFS.access.mockResolvedValue(undefined)
1367 mockedFS.readdir.mockResolvedValue([
1368 { name: 'existing-snippet.sql', isDirectory: () => false, isFile: () => true },
1369 ] as any)
1370 mockedFS.readFile.mockResolvedValue('SELECT * FROM old;')
1371 mockedFS.writeFile.mockResolvedValue(undefined)
1372 mockedFS.stat.mockResolvedValue({ birthtime: new Date('2023-01-01') } as any)
1373
1374 const error = new Error('File not found') as NodeJS.ErrnoException
1375 error.code = 'ENOENT'
1376 mockedFS.unlink.mockRejectedValue(error)
1377
1378 const updates = { name: 'new-name' }
1379
1380 const result = await updateSnippet(id, updates)
1381
1382 expect(result.name).toBe('new-name')
1383 expect(result.content.sql).toBe('SELECT * FROM old;')
1384 })
1385
1386 it('should throw error when moving snippet to folder that already contains snippet with same name', async () => {
1387 const snippetName = 'existing-snippet'
1388 const existingSnippetId = generateDeterministicUuid([`${snippetName}.sql`])
1389 const targetFolderId = generateDeterministicUuid(['target-folder'])
1390
1391 const createMockDirent = (name: string, isDirectory: boolean) => ({
1392 name,
1393 isDirectory: () => isDirectory,
1394 isFile: () => !isDirectory,
1395 })
1396
1397 mockedFS.access.mockResolvedValue(undefined)
1398
1399 // Mock filesystem structure with:
1400 // - existing-snippet.sql in root
1401 // - target-folder/ with existing-snippet.sql inside
1402 mockedFS.readdir.mockImplementation((dirPath: any) => {
1403 if (dirPath === MOCK_SNIPPETS_DIR) {
1404 return Promise.resolve([
1405 createMockDirent('existing-snippet.sql', false),
1406 createMockDirent('target-folder', true),
1407 ] as any)
1408 } else if (dirPath === path.join(MOCK_SNIPPETS_DIR, 'target-folder')) {
1409 return Promise.resolve([createMockDirent(`${snippetName}.sql`, false)] as any)
1410 }
1411 return Promise.resolve([])
1412 })
1413
1414 mockedFS.readFile.mockResolvedValue('SELECT * FROM table;')
1415 mockedFS.unlink.mockResolvedValue(undefined)
1416 mockedFS.stat.mockResolvedValue({ birthtime: new Date('2023-01-01') } as any)
1417
1418 const updates = { folder_id: targetFolderId }
1419
1420 await expect(updateSnippet(existingSnippetId, updates)).rejects.toThrow(
1421 `Snippet named "${snippetName}" already exists in the specified folder`
1422 )
1423 })
1424 })
1425
1426 describe('getFolders', () => {
1427 it('should read directories from the snippets folder', async () => {
1428 const mockDirents = [
1429 { name: 'folder1', isDirectory: () => true, isFile: () => false },
1430 { name: 'folder2', isDirectory: () => true, isFile: () => false },
1431 { name: 'snippet.sql', isDirectory: () => false, isFile: () => true },
1432 ]
1433
1434 mockedFS.access.mockResolvedValue(undefined)
1435 mockedFS.readdir
1436 .mockResolvedValueOnce(mockDirents as any) // First call for getFilesystemEntries
1437 .mockResolvedValueOnce([]) // folder1 contents (empty)
1438 .mockResolvedValueOnce([]) // folder2 contents (empty)
1439 mockedFS.stat.mockResolvedValue({ birthtime: new Date('2023-01-01') } as any)
1440
1441 const folders = await getFolders()
1442
1443 expect(folders).toHaveLength(2)
1444 expect(folders[0].name).toBe('folder1')
1445 expect(folders[1].name).toBe('folder2')
1446 expect(folders[0].id).toBe(generateDeterministicUuid(['folder1']))
1447 expect(folders[1].id).toBe(generateDeterministicUuid(['folder2']))
1448 })
1449
1450 it('should return empty array when no directories exist', async () => {
1451 const mockDirents = [{ name: 'snippet.sql', isDirectory: () => false, isFile: () => true }]
1452
1453 mockedFS.access.mockResolvedValue(undefined)
1454 mockedFS.readdir.mockResolvedValue(mockDirents as any)
1455 mockedFS.stat.mockResolvedValue({ birthtime: new Date('2023-01-01') } as any)
1456
1457 const folders = await getFolders()
1458
1459 expect(folders).toEqual([])
1460 })
1461
1462 it('should handle directory creation when it does not exist', async () => {
1463 mockedFS.access.mockRejectedValue(new Error('Directory does not exist'))
1464 mockedFS.readdir.mockResolvedValue([])
1465
1466 const folders = await getFolders()
1467
1468 expect(folders).toEqual([])
1469 })
1470 })
1471
1472 describe('createFolder', () => {
1473 it('should create a new folder as an actual directory', async () => {
1474 mockedFS.access.mockResolvedValue(undefined)
1475 mockedFS.readdir.mockResolvedValue([]) // No existing folders
1476 mockedFS.mkdir.mockResolvedValue(undefined)
1477
1478 const result = await createFolder('New Folder')
1479
1480 expect(result.id).toBe(generateDeterministicUuid(['New Folder']))
1481 expect(result.name).toBe('New Folder')
1482 expect(result.owner_id).toBe(1)
1483 expect(mockedFS.mkdir).toHaveBeenCalledWith(path.join(MOCK_SNIPPETS_DIR, 'New Folder'), {
1484 recursive: true,
1485 })
1486 })
1487
1488 it('should handle folder creation with special characters in name', async () => {
1489 mockedFS.access.mockResolvedValue(undefined)
1490 mockedFS.readdir.mockResolvedValue([])
1491 mockedFS.mkdir.mockResolvedValue(undefined)
1492
1493 const result = await createFolder('Folder with spaces & symbols!')
1494
1495 expect(result.id).toBe(generateDeterministicUuid(['Folder with spaces & symbols!']))
1496 expect(result.name).toBe('Folder with spaces & symbols!')
1497 expect(mockedFS.mkdir).toHaveBeenCalledWith(
1498 path.join(MOCK_SNIPPETS_DIR, 'Folder with spaces & symbols!'),
1499 { recursive: true }
1500 )
1501 })
1502
1503 it('should throw error when folder already exists', async () => {
1504 mockedFS.access.mockResolvedValue(undefined)
1505 mockedFS.readdir.mockResolvedValue([
1506 { name: 'Existing Folder', isDirectory: () => true, isFile: () => false },
1507 ] as any)
1508 mockedFS.stat.mockResolvedValue({ birthtime: new Date('2023-01-01') } as any)
1509
1510 await expect(createFolder('Existing Folder')).rejects.toThrow(
1511 'Folder with name Existing Folder already exists'
1512 )
1513 })
1514
1515 it('should handle mkdir errors', async () => {
1516 mockedFS.access.mockResolvedValue(undefined)
1517 mockedFS.readdir.mockResolvedValue([])
1518 mockedFS.mkdir.mockRejectedValue(new Error('Permission denied'))
1519
1520 await expect(createFolder('New Folder')).rejects.toThrow('Permission denied')
1521 })
1522
1523 it('should create directory when snippets directory does not exist', async () => {
1524 mockedFS.access.mockRejectedValue(new Error('Directory does not exist'))
1525 mockedFS.readdir.mockResolvedValue([])
1526 mockedFS.mkdir.mockResolvedValue(undefined)
1527
1528 const result = await createFolder('New Folder')
1529
1530 expect(result.name).toBe('New Folder')
1531 expect(mockedFS.mkdir).toHaveBeenCalledWith(path.join(MOCK_SNIPPETS_DIR, 'New Folder'), {
1532 recursive: true,
1533 })
1534 })
1535
1536 it('should reject path traversal attempts with ../', async () => {
1537 mockedFS.access.mockResolvedValue(undefined)
1538 mockedFS.readdir.mockResolvedValue([])
1539
1540 await expect(createFolder('../malicious-folder')).rejects.toThrow(
1541 'Invalid name: path traversal or null bytes detected'
1542 )
1543 })
1544
1545 it('should reject path traversal attempts with nested directories', async () => {
1546 mockedFS.access.mockResolvedValue(undefined)
1547 mockedFS.readdir.mockResolvedValue([])
1548
1549 await expect(createFolder('foo/bar/folder')).rejects.toThrow(
1550 'Invalid name: path traversal or null bytes detected'
1551 )
1552 })
1553
1554 it('should reject names with null bytes', async () => {
1555 mockedFS.access.mockResolvedValue(undefined)
1556 mockedFS.readdir.mockResolvedValue([])
1557
1558 await expect(createFolder('malicious\0folder')).rejects.toThrow(
1559 'Invalid name: path traversal or null bytes detected'
1560 )
1561 })
1562
1563 it('should reject absolute path attempts', async () => {
1564 mockedFS.access.mockResolvedValue(undefined)
1565 mockedFS.readdir.mockResolvedValue([])
1566
1567 await expect(createFolder('/etc/malicious')).rejects.toThrow(
1568 'Invalid name: path traversal or null bytes detected'
1569 )
1570 })
1571 })
1572
1573 describe('deleteFolder', () => {
1574 it('should delete an existing folder directory', async () => {
1575 const folderId = generateDeterministicUuid(['Delete Me'])
1576
1577 mockedFS.access.mockResolvedValue(undefined)
1578 mockedFS.readdir.mockResolvedValue([
1579 { name: 'Delete Me', isDirectory: () => true, isFile: () => false, folderId: null },
1580 { name: 'Keep Me', isDirectory: () => true, isFile: () => false, folderId: null },
1581 ] as any)
1582 mockedFS.rm.mockResolvedValue(undefined)
1583 mockedFS.stat.mockResolvedValue({ birthtime: new Date('2023-01-01') } as any)
1584
1585 await deleteFolder(folderId)
1586
1587 expect(mockedFS.rm).toHaveBeenCalledWith(path.join(MOCK_SNIPPETS_DIR, 'Delete Me'), {
1588 recursive: true,
1589 force: true,
1590 })
1591 })
1592
1593 it('should throw error when folder not found', async () => {
1594 const nonExistentId = 'non-existent-folder-id'
1595
1596 mockedFS.access.mockResolvedValue(undefined)
1597 mockedFS.readdir.mockResolvedValue([])
1598
1599 await expect(deleteFolder(nonExistentId)).rejects.toThrow(
1600 `Folder with id ${nonExistentId} not found`
1601 )
1602 })
1603
1604 it('should handle directory deletion errors gracefully', async () => {
1605 const folderId = generateDeterministicUuid(['Test Folder'])
1606
1607 mockedFS.access.mockResolvedValue(undefined)
1608 mockedFS.readdir.mockResolvedValue([
1609 { name: 'Test Folder', isDirectory: () => true, isFile: () => false, folderId: null },
1610 ] as any)
1611
1612 const error = new Error('Permission denied') as NodeJS.ErrnoException
1613 error.code = 'EACCES'
1614 mockedFS.rm.mockRejectedValue(error)
1615 mockedFS.stat.mockResolvedValue({ birthtime: new Date('2023-01-01') } as any)
1616
1617 await expect(deleteFolder(folderId)).rejects.toThrow('Permission denied')
1618 })
1619
1620 it('should throw original error when directory does not exist during deletion', async () => {
1621 const folderId = generateDeterministicUuid(['Test Folder'])
1622
1623 mockedFS.access.mockResolvedValue(undefined)
1624 mockedFS.readdir.mockResolvedValue([
1625 { name: 'Test Folder', isDirectory: () => true, isFile: () => false, folderId: null },
1626 ] as any)
1627
1628 const error = new Error('Directory not found') as NodeJS.ErrnoException
1629 error.code = 'ENOENT'
1630 mockedFS.rm.mockRejectedValue(error)
1631 mockedFS.stat.mockResolvedValue({ birthtime: new Date('2023-01-01') } as any)
1632
1633 await expect(deleteFolder(folderId)).rejects.toThrow(`Folder with id ${folderId} not found`)
1634 })
1635
1636 it('should handle case when snippets directory needs to be created first', async () => {
1637 const nonExistentId = 'non-existent-folder-id'
1638
1639 mockedFS.access.mockRejectedValue(new Error('Directory does not exist'))
1640 mockedFS.readdir.mockResolvedValue([])
1641
1642 await expect(deleteFolder(nonExistentId)).rejects.toThrow(
1643 `Folder with id ${nonExistentId} not found`
1644 )
1645 })
1646 })
1647})