Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.

utils.spec.ts 19KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614
  1. import mime from 'mime'
  2. import { upload } from '@/service/base'
  3. import {
  4. downloadFile,
  5. fileIsUploaded,
  6. fileUpload,
  7. getFileAppearanceType,
  8. getFileExtension,
  9. getFileNameFromUrl,
  10. getFilesInLogs,
  11. getProcessedFiles,
  12. getProcessedFilesFromResponse,
  13. getSupportFileExtensionList,
  14. getSupportFileType,
  15. isAllowedFileExtension,
  16. } from './utils'
  17. import { FileAppearanceTypeEnum } from './types'
  18. import { SupportUploadFileTypes } from '@/app/components/workflow/types'
  19. import { TransferMethod } from '@/types/app'
  20. import { FILE_EXTS } from '../prompt-editor/constants'
  21. jest.mock('mime', () => ({
  22. __esModule: true,
  23. default: {
  24. getExtension: jest.fn(),
  25. },
  26. }))
  27. jest.mock('@/service/base', () => ({
  28. upload: jest.fn(),
  29. }))
  30. describe('file-uploader utils', () => {
  31. beforeEach(() => {
  32. jest.clearAllMocks()
  33. })
  34. describe('fileUpload', () => {
  35. it('should handle successful file upload', async () => {
  36. const mockFile = new File(['test'], 'test.txt')
  37. const mockCallbacks = {
  38. onProgressCallback: jest.fn(),
  39. onSuccessCallback: jest.fn(),
  40. onErrorCallback: jest.fn(),
  41. }
  42. jest.mocked(upload).mockResolvedValue({ id: '123' })
  43. await fileUpload({
  44. file: mockFile,
  45. ...mockCallbacks,
  46. })
  47. expect(upload).toHaveBeenCalled()
  48. expect(mockCallbacks.onSuccessCallback).toHaveBeenCalledWith({ id: '123' })
  49. })
  50. })
  51. describe('getFileExtension', () => {
  52. it('should get extension from mimetype', () => {
  53. jest.mocked(mime.getExtension).mockReturnValue('pdf')
  54. expect(getFileExtension('file', 'application/pdf')).toBe('pdf')
  55. })
  56. it('should get extension from filename if mimetype fails', () => {
  57. jest.mocked(mime.getExtension).mockReturnValue(null)
  58. expect(getFileExtension('file.txt', '')).toBe('txt')
  59. expect(getFileExtension('file.txt.docx', '')).toBe('docx')
  60. expect(getFileExtension('file', '')).toBe('')
  61. })
  62. it('should return empty string for remote files', () => {
  63. expect(getFileExtension('file.txt', '', true)).toBe('')
  64. })
  65. })
  66. describe('getFileAppearanceType', () => {
  67. it('should identify gif files', () => {
  68. jest.mocked(mime.getExtension).mockReturnValue('gif')
  69. expect(getFileAppearanceType('image.gif', 'image/gif'))
  70. .toBe(FileAppearanceTypeEnum.gif)
  71. })
  72. it('should identify image files', () => {
  73. jest.mocked(mime.getExtension).mockReturnValue('jpg')
  74. expect(getFileAppearanceType('image.jpg', 'image/jpeg'))
  75. .toBe(FileAppearanceTypeEnum.image)
  76. jest.mocked(mime.getExtension).mockReturnValue('jpeg')
  77. expect(getFileAppearanceType('image.jpeg', 'image/jpeg'))
  78. .toBe(FileAppearanceTypeEnum.image)
  79. jest.mocked(mime.getExtension).mockReturnValue('png')
  80. expect(getFileAppearanceType('image.png', 'image/png'))
  81. .toBe(FileAppearanceTypeEnum.image)
  82. jest.mocked(mime.getExtension).mockReturnValue('webp')
  83. expect(getFileAppearanceType('image.webp', 'image/webp'))
  84. .toBe(FileAppearanceTypeEnum.image)
  85. jest.mocked(mime.getExtension).mockReturnValue('svg')
  86. expect(getFileAppearanceType('image.svg', 'image/svgxml'))
  87. .toBe(FileAppearanceTypeEnum.image)
  88. })
  89. it('should identify video files', () => {
  90. jest.mocked(mime.getExtension).mockReturnValue('mp4')
  91. expect(getFileAppearanceType('video.mp4', 'video/mp4'))
  92. .toBe(FileAppearanceTypeEnum.video)
  93. jest.mocked(mime.getExtension).mockReturnValue('mov')
  94. expect(getFileAppearanceType('video.mov', 'video/quicktime'))
  95. .toBe(FileAppearanceTypeEnum.video)
  96. jest.mocked(mime.getExtension).mockReturnValue('mpeg')
  97. expect(getFileAppearanceType('video.mpeg', 'video/mpeg'))
  98. .toBe(FileAppearanceTypeEnum.video)
  99. jest.mocked(mime.getExtension).mockReturnValue('webm')
  100. expect(getFileAppearanceType('video.web', 'video/webm'))
  101. .toBe(FileAppearanceTypeEnum.video)
  102. })
  103. it('should identify audio files', () => {
  104. jest.mocked(mime.getExtension).mockReturnValue('mp3')
  105. expect(getFileAppearanceType('audio.mp3', 'audio/mpeg'))
  106. .toBe(FileAppearanceTypeEnum.audio)
  107. jest.mocked(mime.getExtension).mockReturnValue('m4a')
  108. expect(getFileAppearanceType('audio.m4a', 'audio/mp4'))
  109. .toBe(FileAppearanceTypeEnum.audio)
  110. jest.mocked(mime.getExtension).mockReturnValue('wav')
  111. expect(getFileAppearanceType('audio.wav', 'audio/vnd.wav'))
  112. .toBe(FileAppearanceTypeEnum.audio)
  113. jest.mocked(mime.getExtension).mockReturnValue('amr')
  114. expect(getFileAppearanceType('audio.amr', 'audio/AMR'))
  115. .toBe(FileAppearanceTypeEnum.audio)
  116. jest.mocked(mime.getExtension).mockReturnValue('mpga')
  117. expect(getFileAppearanceType('audio.mpga', 'audio/mpeg'))
  118. .toBe(FileAppearanceTypeEnum.audio)
  119. })
  120. it('should identify code files', () => {
  121. jest.mocked(mime.getExtension).mockReturnValue('html')
  122. expect(getFileAppearanceType('index.html', 'text/html'))
  123. .toBe(FileAppearanceTypeEnum.code)
  124. })
  125. it('should identify PDF files', () => {
  126. jest.mocked(mime.getExtension).mockReturnValue('pdf')
  127. expect(getFileAppearanceType('doc.pdf', 'application/pdf'))
  128. .toBe(FileAppearanceTypeEnum.pdf)
  129. })
  130. it('should identify markdown files', () => {
  131. jest.mocked(mime.getExtension).mockReturnValue('md')
  132. expect(getFileAppearanceType('file.md', 'text/markdown'))
  133. .toBe(FileAppearanceTypeEnum.markdown)
  134. jest.mocked(mime.getExtension).mockReturnValue('markdown')
  135. expect(getFileAppearanceType('file.markdown', 'text/markdown'))
  136. .toBe(FileAppearanceTypeEnum.markdown)
  137. jest.mocked(mime.getExtension).mockReturnValue('mdx')
  138. expect(getFileAppearanceType('file.mdx', 'text/mdx'))
  139. .toBe(FileAppearanceTypeEnum.markdown)
  140. })
  141. it('should identify excel files', () => {
  142. jest.mocked(mime.getExtension).mockReturnValue('xlsx')
  143. expect(getFileAppearanceType('doc.xlsx', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'))
  144. .toBe(FileAppearanceTypeEnum.excel)
  145. jest.mocked(mime.getExtension).mockReturnValue('xls')
  146. expect(getFileAppearanceType('doc.xls', 'application/vnd.ms-excel'))
  147. .toBe(FileAppearanceTypeEnum.excel)
  148. })
  149. it('should identify word files', () => {
  150. jest.mocked(mime.getExtension).mockReturnValue('doc')
  151. expect(getFileAppearanceType('doc.doc', 'application/msword'))
  152. .toBe(FileAppearanceTypeEnum.word)
  153. jest.mocked(mime.getExtension).mockReturnValue('docx')
  154. expect(getFileAppearanceType('doc.docx', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'))
  155. .toBe(FileAppearanceTypeEnum.word)
  156. })
  157. it('should identify word files', () => {
  158. jest.mocked(mime.getExtension).mockReturnValue('ppt')
  159. expect(getFileAppearanceType('doc.ppt', 'application/vnd.ms-powerpoint'))
  160. .toBe(FileAppearanceTypeEnum.ppt)
  161. jest.mocked(mime.getExtension).mockReturnValue('pptx')
  162. expect(getFileAppearanceType('doc.pptx', 'application/vnd.openxmlformats-officedocument.presentationml.presentation'))
  163. .toBe(FileAppearanceTypeEnum.ppt)
  164. })
  165. it('should identify document files', () => {
  166. jest.mocked(mime.getExtension).mockReturnValue('txt')
  167. expect(getFileAppearanceType('file.txt', 'text/plain'))
  168. .toBe(FileAppearanceTypeEnum.document)
  169. jest.mocked(mime.getExtension).mockReturnValue('csv')
  170. expect(getFileAppearanceType('file.csv', 'text/csv'))
  171. .toBe(FileAppearanceTypeEnum.document)
  172. jest.mocked(mime.getExtension).mockReturnValue('msg')
  173. expect(getFileAppearanceType('file.msg', 'application/vnd.ms-outlook'))
  174. .toBe(FileAppearanceTypeEnum.document)
  175. jest.mocked(mime.getExtension).mockReturnValue('eml')
  176. expect(getFileAppearanceType('file.eml', 'message/rfc822'))
  177. .toBe(FileAppearanceTypeEnum.document)
  178. jest.mocked(mime.getExtension).mockReturnValue('xml')
  179. expect(getFileAppearanceType('file.xml', 'application/rssxml'))
  180. .toBe(FileAppearanceTypeEnum.document)
  181. jest.mocked(mime.getExtension).mockReturnValue('epub')
  182. expect(getFileAppearanceType('file.epub', 'application/epubzip'))
  183. .toBe(FileAppearanceTypeEnum.document)
  184. })
  185. it('should handle null mime extension', () => {
  186. jest.mocked(mime.getExtension).mockReturnValue(null)
  187. expect(getFileAppearanceType('file.txt', 'text/plain'))
  188. .toBe(FileAppearanceTypeEnum.document)
  189. })
  190. })
  191. describe('getSupportFileType', () => {
  192. it('should return custom type when isCustom is true', () => {
  193. expect(getSupportFileType('file.txt', '', true))
  194. .toBe(SupportUploadFileTypes.custom)
  195. })
  196. it('should return file type when isCustom is false', () => {
  197. expect(getSupportFileType('file.txt', 'text/plain'))
  198. .toBe(SupportUploadFileTypes.document)
  199. })
  200. })
  201. describe('getProcessedFiles', () => {
  202. it('should process files correctly', () => {
  203. const files = [{
  204. id: '123',
  205. name: 'test.txt',
  206. size: 1024,
  207. type: 'text/plain',
  208. progress: 100,
  209. supportFileType: 'document',
  210. transferMethod: TransferMethod.remote_url,
  211. url: 'http://example.com',
  212. uploadedId: '123',
  213. }]
  214. const result = getProcessedFiles(files)
  215. expect(result[0]).toEqual({
  216. type: 'document',
  217. transfer_method: TransferMethod.remote_url,
  218. url: 'http://example.com',
  219. upload_file_id: '123',
  220. })
  221. })
  222. })
  223. describe('getProcessedFilesFromResponse', () => {
  224. it('should process files correctly', () => {
  225. const files = [{
  226. related_id: '2a38e2ca-1295-415d-a51d-65d4ff9912d9',
  227. extension: '.jpeg',
  228. filename: 'test.jpeg',
  229. size: 2881761,
  230. mime_type: 'image/jpeg',
  231. transfer_method: TransferMethod.local_file,
  232. type: 'image',
  233. url: 'https://upload.dify.dev/files/xxx/file-preview',
  234. }]
  235. const result = getProcessedFilesFromResponse(files)
  236. expect(result[0]).toEqual({
  237. id: '2a38e2ca-1295-415d-a51d-65d4ff9912d9',
  238. name: 'test.jpeg',
  239. size: 2881761,
  240. type: 'image/jpeg',
  241. progress: 100,
  242. transferMethod: TransferMethod.local_file,
  243. supportFileType: 'image',
  244. uploadedId: '2a38e2ca-1295-415d-a51d-65d4ff9912d9',
  245. url: 'https://upload.dify.dev/files/xxx/file-preview',
  246. })
  247. })
  248. })
  249. describe('getFileNameFromUrl', () => {
  250. it('should extract filename from URL', () => {
  251. expect(getFileNameFromUrl('http://example.com/path/file.txt'))
  252. .toBe('file.txt')
  253. })
  254. })
  255. describe('getSupportFileExtensionList', () => {
  256. it('should handle custom file types', () => {
  257. const result = getSupportFileExtensionList(
  258. [SupportUploadFileTypes.custom],
  259. ['.pdf', '.txt', '.doc'],
  260. )
  261. expect(result).toEqual(['PDF', 'TXT', 'DOC'])
  262. })
  263. it('should handle standard file types', () => {
  264. const mockFileExts = {
  265. image: ['JPG', 'PNG'],
  266. document: ['PDF', 'TXT'],
  267. video: ['MP4', 'MOV'],
  268. }
  269. // Temporarily mock FILE_EXTS
  270. const originalFileExts = { ...FILE_EXTS }
  271. Object.assign(FILE_EXTS, mockFileExts)
  272. const result = getSupportFileExtensionList(
  273. ['image', 'document'],
  274. [],
  275. )
  276. expect(result).toEqual(['JPG', 'PNG', 'PDF', 'TXT'])
  277. // Restore original FILE_EXTS
  278. Object.assign(FILE_EXTS, originalFileExts)
  279. })
  280. it('should return empty array for empty inputs', () => {
  281. const result = getSupportFileExtensionList([], [])
  282. expect(result).toEqual([])
  283. })
  284. it('should prioritize custom types over standard types', () => {
  285. const mockFileExts = {
  286. image: ['JPG', 'PNG'],
  287. }
  288. // Temporarily mock FILE_EXTS
  289. const originalFileExts = { ...FILE_EXTS }
  290. Object.assign(FILE_EXTS, mockFileExts)
  291. const result = getSupportFileExtensionList(
  292. [SupportUploadFileTypes.custom, 'image'],
  293. ['.csv', '.xml'],
  294. )
  295. expect(result).toEqual(['CSV', 'XML'])
  296. // Restore original FILE_EXTS
  297. Object.assign(FILE_EXTS, originalFileExts)
  298. })
  299. })
  300. describe('isAllowedFileExtension', () => {
  301. it('should validate allowed file extensions', () => {
  302. jest.mocked(mime.getExtension).mockReturnValue('pdf')
  303. expect(isAllowedFileExtension(
  304. 'test.pdf',
  305. 'application/pdf',
  306. ['document'],
  307. ['.pdf'],
  308. )).toBe(true)
  309. })
  310. })
  311. describe('getFilesInLogs', () => {
  312. const mockFileData = {
  313. dify_model_identity: '__dify__file__',
  314. related_id: '123',
  315. filename: 'test.pdf',
  316. size: 1024,
  317. mime_type: 'application/pdf',
  318. transfer_method: 'local_file',
  319. type: 'document',
  320. url: 'http://example.com/test.pdf',
  321. }
  322. it('should handle empty or null input', () => {
  323. expect(getFilesInLogs(null)).toEqual([])
  324. expect(getFilesInLogs({})).toEqual([])
  325. expect(getFilesInLogs(undefined)).toEqual([])
  326. })
  327. it('should process single file object', () => {
  328. const input = {
  329. file1: mockFileData,
  330. }
  331. const expected = [{
  332. varName: 'file1',
  333. list: [{
  334. id: '123',
  335. name: 'test.pdf',
  336. size: 1024,
  337. type: 'application/pdf',
  338. progress: 100,
  339. transferMethod: 'local_file',
  340. supportFileType: 'document',
  341. uploadedId: '123',
  342. url: 'http://example.com/test.pdf',
  343. }],
  344. }]
  345. expect(getFilesInLogs(input)).toEqual(expected)
  346. })
  347. it('should process array of files', () => {
  348. const input = {
  349. files: [mockFileData, mockFileData],
  350. }
  351. const expected = [{
  352. varName: 'files',
  353. list: [
  354. {
  355. id: '123',
  356. name: 'test.pdf',
  357. size: 1024,
  358. type: 'application/pdf',
  359. progress: 100,
  360. transferMethod: 'local_file',
  361. supportFileType: 'document',
  362. uploadedId: '123',
  363. url: 'http://example.com/test.pdf',
  364. },
  365. {
  366. id: '123',
  367. name: 'test.pdf',
  368. size: 1024,
  369. type: 'application/pdf',
  370. progress: 100,
  371. transferMethod: 'local_file',
  372. supportFileType: 'document',
  373. uploadedId: '123',
  374. url: 'http://example.com/test.pdf',
  375. },
  376. ],
  377. }]
  378. expect(getFilesInLogs(input)).toEqual(expected)
  379. })
  380. it('should ignore non-file objects and arrays', () => {
  381. const input = {
  382. regularString: 'not a file',
  383. regularNumber: 123,
  384. regularArray: [1, 2, 3],
  385. regularObject: { key: 'value' },
  386. file: mockFileData,
  387. }
  388. const expected = [{
  389. varName: 'file',
  390. list: [{
  391. id: '123',
  392. name: 'test.pdf',
  393. size: 1024,
  394. type: 'application/pdf',
  395. progress: 100,
  396. transferMethod: 'local_file',
  397. supportFileType: 'document',
  398. uploadedId: '123',
  399. url: 'http://example.com/test.pdf',
  400. }],
  401. }]
  402. expect(getFilesInLogs(input)).toEqual(expected)
  403. })
  404. it('should handle mixed file types in array', () => {
  405. const input = {
  406. mixedFiles: [
  407. mockFileData,
  408. { notAFile: true },
  409. mockFileData,
  410. ],
  411. }
  412. const expected = [{
  413. varName: 'mixedFiles',
  414. list: [
  415. {
  416. id: '123',
  417. name: 'test.pdf',
  418. size: 1024,
  419. type: 'application/pdf',
  420. progress: 100,
  421. transferMethod: 'local_file',
  422. supportFileType: 'document',
  423. uploadedId: '123',
  424. url: 'http://example.com/test.pdf',
  425. },
  426. {
  427. id: undefined,
  428. name: undefined,
  429. progress: 100,
  430. size: 0,
  431. supportFileType: undefined,
  432. transferMethod: undefined,
  433. type: undefined,
  434. uploadedId: undefined,
  435. url: undefined,
  436. },
  437. {
  438. id: '123',
  439. name: 'test.pdf',
  440. size: 1024,
  441. type: 'application/pdf',
  442. progress: 100,
  443. transferMethod: 'local_file',
  444. supportFileType: 'document',
  445. uploadedId: '123',
  446. url: 'http://example.com/test.pdf',
  447. },
  448. ],
  449. }]
  450. expect(getFilesInLogs(input)).toEqual(expected)
  451. })
  452. })
  453. describe('fileIsUploaded', () => {
  454. it('should identify uploaded files', () => {
  455. expect(fileIsUploaded({
  456. uploadedId: '123',
  457. progress: 100,
  458. } as any)).toBe(true)
  459. })
  460. it('should identify remote files as uploaded', () => {
  461. expect(fileIsUploaded({
  462. transferMethod: TransferMethod.remote_url,
  463. progress: 100,
  464. } as any)).toBe(true)
  465. })
  466. })
  467. describe('downloadFile', () => {
  468. let mockAnchor: HTMLAnchorElement
  469. let createElementMock: jest.SpyInstance
  470. let appendChildMock: jest.SpyInstance
  471. let removeChildMock: jest.SpyInstance
  472. beforeEach(() => {
  473. // Mock createElement and appendChild
  474. mockAnchor = {
  475. href: '',
  476. download: '',
  477. style: { display: '' },
  478. target: '',
  479. title: '',
  480. click: jest.fn(),
  481. } as unknown as HTMLAnchorElement
  482. createElementMock = jest.spyOn(document, 'createElement').mockReturnValue(mockAnchor as any)
  483. appendChildMock = jest.spyOn(document.body, 'appendChild').mockImplementation((node: Node) => {
  484. return node
  485. })
  486. removeChildMock = jest.spyOn(document.body, 'removeChild').mockImplementation((node: Node) => {
  487. return node
  488. })
  489. })
  490. afterEach(() => {
  491. jest.resetAllMocks()
  492. })
  493. it('should create and trigger download with correct attributes', () => {
  494. const url = 'https://example.com/test.pdf'
  495. const filename = 'test.pdf'
  496. downloadFile(url, filename)
  497. // Verify anchor element was created with correct properties
  498. expect(createElementMock).toHaveBeenCalledWith('a')
  499. expect(mockAnchor.href).toBe(url)
  500. expect(mockAnchor.download).toBe(filename)
  501. expect(mockAnchor.style.display).toBe('none')
  502. expect(mockAnchor.target).toBe('_blank')
  503. expect(mockAnchor.title).toBe(filename)
  504. // Verify DOM operations
  505. expect(appendChildMock).toHaveBeenCalledWith(mockAnchor)
  506. expect(mockAnchor.click).toHaveBeenCalled()
  507. expect(removeChildMock).toHaveBeenCalledWith(mockAnchor)
  508. })
  509. it('should handle empty filename', () => {
  510. const url = 'https://example.com/test.pdf'
  511. const filename = ''
  512. downloadFile(url, filename)
  513. expect(mockAnchor.download).toBe('')
  514. expect(mockAnchor.title).toBe('')
  515. })
  516. it('should handle empty url', () => {
  517. const url = ''
  518. const filename = 'test.pdf'
  519. downloadFile(url, filename)
  520. expect(mockAnchor.href).toBe('')
  521. })
  522. })
  523. })