snippets.utils.test.ts 61 KB

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