@@ -19,6 +19,12 @@ jest.mock('node:path', () => ({
1919 const ext = path . split ( '.' ) . pop ( ) ;
2020 return ext ? `.${ ext } ` : '' ;
2121 } ) ,
22+ join : jest . fn ( ( ...paths ) => paths . join ( '/' ) ) ,
23+ } ) ) ;
24+
25+ // Mock tar module
26+ jest . mock ( 'tar' , ( ) => ( {
27+ create : jest . fn ( ) ,
2228} ) ) ;
2329
2430describe ( 'StorageObject (New API)' , ( ) => {
@@ -810,6 +816,143 @@ describe('StorageObject (New API)', () => {
810816 } ) ;
811817 } ) ;
812818
819+ describe ( 'uploadFromDir' , ( ) => {
820+ let mockTar : any ;
821+
822+ beforeEach ( ( ) => {
823+ // Clear all mocks
824+ jest . clearAllMocks ( ) ;
825+ // Reset global fetch mock
826+ ( ( global as any ) . fetch as jest . Mock ) . mockClear ( ) ;
827+ // Get tar mock
828+ mockTar = require ( 'tar' ) ;
829+ } ) ;
830+
831+ it ( 'should upload a directory as gzipped tarball' , async ( ) => {
832+ // Mock directory exists
833+ mockFs . stat . mockResolvedValue ( { isDirectory : ( ) => true } ) ;
834+
835+ // Mock tar stream
836+ const mockTarballBuffer = Buffer . from ( 'compressed tarball content' ) ;
837+ mockTar . create . mockReturnValue ( {
838+ [ Symbol . asyncIterator ] : async function * ( ) {
839+ yield mockTarballBuffer ;
840+ } ,
841+ } ) ;
842+
843+ const mockObjectData = { id : 'dir-123' , upload_url : 'https://upload.example.com/dir' } ;
844+ const mockObjectInfo = { ...mockObjectData , name : 'project.tar.gz' , state : 'UPLOADING' } ;
845+ const mockCompletedData = { ...mockObjectInfo , state : 'READ_ONLY' } ;
846+
847+ mockClient . objects . create . mockResolvedValue ( mockObjectData ) ;
848+ mockClient . objects . retrieve . mockResolvedValue ( mockObjectInfo ) ;
849+ mockClient . objects . complete . mockResolvedValue ( mockCompletedData ) ;
850+
851+ ( ( global as any ) . fetch as jest . Mock ) . mockResolvedValue ( {
852+ ok : true ,
853+ status : 200 ,
854+ statusText : 'OK' ,
855+ } ) ;
856+
857+ const result = await StorageObject . uploadFromDir ( mockClient , './my-project' , 'project.tar.gz' ) ;
858+
859+ expect ( mockClient . objects . create ) . toHaveBeenCalledWith (
860+ { name : 'project.tar.gz' , content_type : 'tgz' , metadata : null , ttl_ms : null } ,
861+ undefined ,
862+ ) ;
863+ expect ( mockTar . create ) . toHaveBeenCalled ( ) ;
864+ expect ( result ) . toBeInstanceOf ( StorageObject ) ;
865+ expect ( result . id ) . toBe ( 'dir-123' ) ;
866+ } ) ;
867+
868+ it ( 'should upload directory with TTL and metadata' , async ( ) => {
869+ // Mock directory exists
870+ mockFs . stat . mockResolvedValue ( { isDirectory : ( ) => true } ) ;
871+
872+ // Mock tar stream
873+ const mockTarballBuffer = Buffer . from ( 'compressed tarball' ) ;
874+ mockTar . create . mockReturnValue ( {
875+ [ Symbol . asyncIterator ] : async function * ( ) {
876+ yield mockTarballBuffer ;
877+ } ,
878+ } ) ;
879+
880+ const mockObjectData = { id : 'dir-456' , upload_url : 'https://upload.example.com/dir' } ;
881+ const mockCompletedData = { ...mockObjectData , state : 'READ_ONLY' } ;
882+
883+ mockClient . objects . create . mockResolvedValue ( mockObjectData ) ;
884+ mockClient . objects . complete . mockResolvedValue ( mockCompletedData ) ;
885+
886+ ( ( global as any ) . fetch as jest . Mock ) . mockResolvedValue ( {
887+ ok : true ,
888+ status : 200 ,
889+ statusText : 'OK' ,
890+ } ) ;
891+
892+ const result = await StorageObject . uploadFromDir ( mockClient , './my-project' , 'project.tar.gz' , {
893+ ttlMs : 3600000 ,
894+ metadata : { project : 'demo' } ,
895+ } ) ;
896+
897+ expect ( mockClient . objects . create ) . toHaveBeenCalledWith (
898+ { name : 'project.tar.gz' , content_type : 'tgz' , metadata : { project : 'demo' } , ttl_ms : 3600000 } ,
899+ { ttlMs : 3600000 , metadata : { project : 'demo' } } ,
900+ ) ;
901+ expect ( result . id ) . toBe ( 'dir-456' ) ;
902+ } ) ;
903+
904+ it ( 'should throw error if path is not a directory' , async ( ) => {
905+ mockFs . stat . mockResolvedValue ( { isDirectory : ( ) => false } ) ;
906+
907+ await expect ( StorageObject . uploadFromDir ( mockClient , './file.txt' , 'archive.tar.gz' ) ) . rejects . toThrow (
908+ 'Path is not a directory: ./file.txt' ,
909+ ) ;
910+ } ) ;
911+
912+ it ( 'should throw error if directory does not exist' , async ( ) => {
913+ mockFs . stat . mockRejectedValue ( new Error ( 'ENOENT: no such file or directory' ) ) ;
914+
915+ await expect (
916+ StorageObject . uploadFromDir ( mockClient , './nonexistent' , 'archive.tar.gz' ) ,
917+ ) . rejects . toThrow ( 'Failed to access directory ./nonexistent' ) ;
918+ } ) ;
919+
920+ it ( 'should throw error in browser environment' , async ( ) => {
921+ const originalProcess = global . process ;
922+ delete ( global as any ) . process ;
923+
924+ await expect ( StorageObject . uploadFromDir ( mockClient , './project' , 'project.tar.gz' ) ) . rejects . toThrow (
925+ 'File upload methods are only available in Node.js environment' ,
926+ ) ;
927+
928+ global . process = originalProcess ;
929+ } ) ;
930+
931+ it ( 'should handle upload failures gracefully' , async ( ) => {
932+ mockFs . stat . mockResolvedValue ( { isDirectory : ( ) => true } ) ;
933+
934+ const mockTarballBuffer = Buffer . from ( 'tarball' ) ;
935+ mockTar . create . mockReturnValue ( {
936+ [ Symbol . asyncIterator ] : async function * ( ) {
937+ yield mockTarballBuffer ;
938+ } ,
939+ } ) ;
940+
941+ const mockObjectData = { id : 'dir-999' , upload_url : 'https://upload.example.com/dir' } ;
942+ mockClient . objects . create . mockResolvedValue ( mockObjectData ) ;
943+
944+ ( ( global as any ) . fetch as jest . Mock ) . mockResolvedValue ( {
945+ ok : false ,
946+ status : 500 ,
947+ statusText : 'Internal Server Error' ,
948+ } ) ;
949+
950+ await expect ( StorageObject . uploadFromDir ( mockClient , './project' , 'project.tar.gz' ) ) . rejects . toThrow (
951+ 'Failed to upload tarball: Upload failed: 500 Internal Server Error' ,
952+ ) ;
953+ } ) ;
954+ } ) ;
955+
813956 describe ( 'error handling' , ( ) => {
814957 it ( 'should handle create errors' , async ( ) => {
815958 const error = new Error ( 'Create failed' ) ;
0 commit comments