@@ -17,10 +17,14 @@ const {
1717 mockIsUsingCloudStorage,
1818 mockGetStorageProvider,
1919 mockValidateFileType,
20+ mockValidateAttachmentFileType,
2021 mockGenerateCopilotUploadUrl,
2122 mockIsImageFileType,
2223 mockGetStorageProviderUploads,
2324 mockIsUsingCloudStorageUploads,
25+ mockGetUserEntityPermissions,
26+ mockGenerateWorkspaceFileKey,
27+ mockGenerateExecutionFileKey,
2428} = vi . hoisted ( ( ) => ( {
2529 mockVerifyFileAccess : vi . fn ( ) . mockResolvedValue ( true ) ,
2630 mockVerifyWorkspaceFileAccess : vi . fn ( ) . mockResolvedValue ( true ) ,
@@ -30,13 +34,22 @@ const {
3034 mockIsUsingCloudStorage : vi . fn ( ) ,
3135 mockGetStorageProvider : vi . fn ( ) ,
3236 mockValidateFileType : vi . fn ( ) . mockReturnValue ( null ) ,
37+ mockValidateAttachmentFileType : vi . fn ( ) . mockReturnValue ( null ) ,
3338 mockGenerateCopilotUploadUrl : vi . fn ( ) . mockResolvedValue ( {
3439 url : 'https://example.com/presigned-url' ,
3540 key : 'copilot/test-key.txt' ,
3641 } ) ,
3742 mockIsImageFileType : vi . fn ( ) . mockReturnValue ( true ) ,
3843 mockGetStorageProviderUploads : vi . fn ( ) ,
3944 mockIsUsingCloudStorageUploads : vi . fn ( ) ,
45+ mockGetUserEntityPermissions : vi . fn ( ) . mockResolvedValue ( 'admin' ) ,
46+ mockGenerateWorkspaceFileKey : vi . fn (
47+ ( workspaceId : string , fileName : string ) => `workspace/${ workspaceId } /${ fileName } `
48+ ) ,
49+ mockGenerateExecutionFileKey : vi . fn (
50+ ( ctx : { workspaceId : string ; workflowId : string ; executionId : string } , fileName : string ) =>
51+ `execution/${ ctx . workspaceId } /${ ctx . workflowId } /${ ctx . executionId } /${ fileName } `
52+ ) ,
4053} ) )
4154
4255vi . mock ( '@/app/api/files/authorization' , ( ) => ( {
@@ -61,6 +74,19 @@ vi.mock('@/lib/uploads/core/storage-service', () => storageServiceMock)
6174
6275vi . mock ( '@/lib/uploads/utils/validation' , ( ) => ( {
6376 validateFileType : mockValidateFileType ,
77+ validateAttachmentFileType : mockValidateAttachmentFileType ,
78+ } ) )
79+
80+ vi . mock ( '@/lib/workspaces/permissions/utils' , ( ) => ( {
81+ getUserEntityPermissions : mockGetUserEntityPermissions ,
82+ } ) )
83+
84+ vi . mock ( '@/lib/uploads/contexts/workspace/workspace-file-manager' , ( ) => ( {
85+ generateWorkspaceFileKey : mockGenerateWorkspaceFileKey ,
86+ } ) )
87+
88+ vi . mock ( '@/lib/uploads/contexts/execution/utils' , ( ) => ( {
89+ generateExecutionFileKey : mockGenerateExecutionFileKey ,
6490} ) )
6591
6692vi . mock ( '@/lib/uploads/utils/file-utils' , ( ) => ( {
@@ -139,6 +165,8 @@ function setupFileApiMocks(
139165 )
140166
141167 mockValidateFileType . mockReturnValue ( null )
168+ mockValidateAttachmentFileType . mockReturnValue ( null )
169+ mockGetUserEntityPermissions . mockResolvedValue ( 'admin' )
142170
143171 mockGetStorageProviderUploads . mockReturnValue (
144172 storageProvider === 'blob' ? 'Azure Blob' : storageProvider === 's3' ? 'S3' : 'Local'
@@ -518,6 +546,167 @@ describe('/api/files/presigned', () => {
518546 } )
519547 } )
520548
549+ describe ( 'mothership uploads' , ( ) => {
550+ it ( 'uses validateAttachmentFileType (not validateFileType) — accepts images' , async ( ) => {
551+ setupFileApiMocks ( { cloudEnabled : true , storageProvider : 's3' } )
552+
553+ const request = new NextRequest (
554+ 'http://localhost:3000/api/files/presigned?type=mothership&workspaceId=ws-1' ,
555+ {
556+ method : 'POST' ,
557+ body : JSON . stringify ( {
558+ fileName : 'screenshot.png' ,
559+ contentType : 'image/png' ,
560+ fileSize : 4096 ,
561+ } ) ,
562+ }
563+ )
564+
565+ const response = await POST ( request )
566+ expect ( response . status ) . toBe ( 200 )
567+ expect ( mockValidateAttachmentFileType ) . toHaveBeenCalledWith ( 'screenshot.png' )
568+ expect ( mockValidateFileType ) . not . toHaveBeenCalled ( )
569+ } )
570+
571+ it ( 'rejects unsupported types when validator returns an error' , async ( ) => {
572+ setupFileApiMocks ( { cloudEnabled : true , storageProvider : 's3' } )
573+ mockValidateAttachmentFileType . mockReturnValue ( {
574+ code : 'UNSUPPORTED_FILE_TYPE' ,
575+ message : 'Unsupported file type: exe.' ,
576+ supportedTypes : [ ] ,
577+ } )
578+
579+ const request = new NextRequest (
580+ 'http://localhost:3000/api/files/presigned?type=mothership&workspaceId=ws-1' ,
581+ {
582+ method : 'POST' ,
583+ body : JSON . stringify ( {
584+ fileName : 'virus.exe' ,
585+ contentType : 'application/octet-stream' ,
586+ fileSize : 4096 ,
587+ } ) ,
588+ }
589+ )
590+
591+ const response = await POST ( request )
592+ const data = await response . json ( )
593+ expect ( response . status ) . toBe ( 400 )
594+ expect ( data . code ) . toBe ( 'VALIDATION_ERROR' )
595+ expect ( data . error ) . toContain ( 'exe' )
596+ } )
597+
598+ it ( 'returns 403 when user lacks workspace write permission' , async ( ) => {
599+ setupFileApiMocks ( { cloudEnabled : true , storageProvider : 's3' } )
600+ mockGetUserEntityPermissions . mockResolvedValue ( 'read' )
601+
602+ const request = new NextRequest (
603+ 'http://localhost:3000/api/files/presigned?type=mothership&workspaceId=ws-1' ,
604+ {
605+ method : 'POST' ,
606+ body : JSON . stringify ( {
607+ fileName : 'doc.pdf' ,
608+ contentType : 'application/pdf' ,
609+ fileSize : 4096 ,
610+ } ) ,
611+ }
612+ )
613+
614+ const response = await POST ( request )
615+ expect ( response . status ) . toBe ( 403 )
616+ } )
617+ } )
618+
619+ describe ( 'execution uploads' , ( ) => {
620+ it ( 'uses validateAttachmentFileType — accepts video' , async ( ) => {
621+ setupFileApiMocks ( { cloudEnabled : true , storageProvider : 's3' } )
622+
623+ const request = new NextRequest (
624+ 'http://localhost:3000/api/files/presigned?type=execution&workspaceId=ws-1&workflowId=wf-1&executionId=exec-1' ,
625+ {
626+ method : 'POST' ,
627+ body : JSON . stringify ( {
628+ fileName : 'output.mp4' ,
629+ contentType : 'video/mp4' ,
630+ fileSize : 4096 ,
631+ } ) ,
632+ }
633+ )
634+
635+ const response = await POST ( request )
636+ expect ( response . status ) . toBe ( 200 )
637+ expect ( mockValidateAttachmentFileType ) . toHaveBeenCalledWith ( 'output.mp4' )
638+ expect ( mockValidateFileType ) . not . toHaveBeenCalled ( )
639+ } )
640+
641+ it ( 'rejects when validator returns an error' , async ( ) => {
642+ setupFileApiMocks ( { cloudEnabled : true , storageProvider : 's3' } )
643+ mockValidateAttachmentFileType . mockReturnValue ( {
644+ code : 'UNSUPPORTED_FILE_TYPE' ,
645+ message : 'Unsupported file type: bin.' ,
646+ supportedTypes : [ ] ,
647+ } )
648+
649+ const request = new NextRequest (
650+ 'http://localhost:3000/api/files/presigned?type=execution&workspaceId=ws-1&workflowId=wf-1&executionId=exec-1' ,
651+ {
652+ method : 'POST' ,
653+ body : JSON . stringify ( {
654+ fileName : 'blob.bin' ,
655+ contentType : 'application/octet-stream' ,
656+ fileSize : 4096 ,
657+ } ) ,
658+ }
659+ )
660+
661+ const response = await POST ( request )
662+ const data = await response . json ( )
663+ expect ( response . status ) . toBe ( 400 )
664+ expect ( data . code ) . toBe ( 'VALIDATION_ERROR' )
665+ } )
666+
667+ it ( 'returns 400 when missing workflowId/executionId' , async ( ) => {
668+ setupFileApiMocks ( { cloudEnabled : true , storageProvider : 's3' } )
669+
670+ const request = new NextRequest (
671+ 'http://localhost:3000/api/files/presigned?type=execution&workspaceId=ws-1' ,
672+ {
673+ method : 'POST' ,
674+ body : JSON . stringify ( {
675+ fileName : 'output.mp4' ,
676+ contentType : 'video/mp4' ,
677+ fileSize : 4096 ,
678+ } ) ,
679+ }
680+ )
681+
682+ const response = await POST ( request )
683+ expect ( response . status ) . toBe ( 400 )
684+ } )
685+ } )
686+
687+ describe ( 'knowledge-base uploads' , ( ) => {
688+ it ( 'uses validateFileType (docs-only), not validateAttachmentFileType' , async ( ) => {
689+ setupFileApiMocks ( { cloudEnabled : true , storageProvider : 's3' } )
690+
691+ const request = new NextRequest (
692+ 'http://localhost:3000/api/files/presigned?type=knowledge-base' ,
693+ {
694+ method : 'POST' ,
695+ body : JSON . stringify ( {
696+ fileName : 'doc.pdf' ,
697+ contentType : 'application/pdf' ,
698+ fileSize : 4096 ,
699+ } ) ,
700+ }
701+ )
702+
703+ const response = await POST ( request )
704+ expect ( response . status ) . toBe ( 200 )
705+ expect ( mockValidateFileType ) . toHaveBeenCalledWith ( 'doc.pdf' , 'application/pdf' )
706+ expect ( mockValidateAttachmentFileType ) . not . toHaveBeenCalled ( )
707+ } )
708+ } )
709+
521710 describe ( 'OPTIONS' , ( ) => {
522711 it ( 'should handle CORS preflight requests' , async ( ) => {
523712 const response = await OPTIONS ( )
0 commit comments