@@ -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,146 @@ 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' , {
858+ name : 'project.tar.gz' ,
859+ } ) ;
860+
861+ expect ( mockClient . objects . create ) . toHaveBeenCalledWith (
862+ { name : 'project.tar.gz' , content_type : 'tgz' , metadata : null , ttl_ms : null } ,
863+ undefined ,
864+ ) ;
865+ expect ( mockTar . create ) . toHaveBeenCalled ( ) ;
866+ expect ( result ) . toBeInstanceOf ( StorageObject ) ;
867+ expect ( result . id ) . toBe ( 'dir-123' ) ;
868+ } ) ;
869+
870+ it ( 'should upload directory with TTL and metadata' , async ( ) => {
871+ // Mock directory exists
872+ mockFs . stat . mockResolvedValue ( { isDirectory : ( ) => true } ) ;
873+
874+ // Mock tar stream
875+ const mockTarballBuffer = Buffer . from ( 'compressed tarball' ) ;
876+ mockTar . create . mockReturnValue ( {
877+ [ Symbol . asyncIterator ] : async function * ( ) {
878+ yield mockTarballBuffer ;
879+ } ,
880+ } ) ;
881+
882+ const mockObjectData = { id : 'dir-456' , upload_url : 'https://upload.example.com/dir' } ;
883+ const mockCompletedData = { ...mockObjectData , state : 'READ_ONLY' } ;
884+
885+ mockClient . objects . create . mockResolvedValue ( mockObjectData ) ;
886+ mockClient . objects . complete . mockResolvedValue ( mockCompletedData ) ;
887+
888+ ( ( global as any ) . fetch as jest . Mock ) . mockResolvedValue ( {
889+ ok : true ,
890+ status : 200 ,
891+ statusText : 'OK' ,
892+ } ) ;
893+
894+ const result = await StorageObject . uploadFromDir ( mockClient , './my-project' , {
895+ name : 'project.tar.gz' ,
896+ ttl_ms : 3600000 ,
897+ metadata : { project : 'demo' } ,
898+ } ) ;
899+
900+ expect ( mockClient . objects . create ) . toHaveBeenCalledWith (
901+ { name : 'project.tar.gz' , content_type : 'tgz' , metadata : { project : 'demo' } , ttl_ms : 3600000 } ,
902+ { ttlMs : 3600000 , metadata : { project : 'demo' } } ,
903+ ) ;
904+ expect ( result . id ) . toBe ( 'dir-456' ) ;
905+ } ) ;
906+
907+ it ( 'should throw error if path is not a directory' , async ( ) => {
908+ mockFs . stat . mockResolvedValue ( { isDirectory : ( ) => false } ) ;
909+
910+ await expect (
911+ StorageObject . uploadFromDir ( mockClient , './file.txt' , { name : 'archive.tar.gz' } ) ,
912+ ) . rejects . toThrow ( 'Path is not a directory: ./file.txt' ) ;
913+ } ) ;
914+
915+ it ( 'should throw error if directory does not exist' , async ( ) => {
916+ mockFs . stat . mockRejectedValue ( new Error ( 'ENOENT: no such file or directory' ) ) ;
917+
918+ await expect (
919+ StorageObject . uploadFromDir ( mockClient , './nonexistent' , { name : 'archive.tar.gz' } ) ,
920+ ) . rejects . toThrow ( 'Failed to access directory ./nonexistent' ) ;
921+ } ) ;
922+
923+ it ( 'should throw error in browser environment' , async ( ) => {
924+ const originalProcess = global . process ;
925+ delete ( global as any ) . process ;
926+
927+ await expect (
928+ StorageObject . uploadFromDir ( mockClient , './project' , { name : 'project.tar.gz' } ) ,
929+ ) . rejects . toThrow ( 'File upload methods are only available in Node.js environment' ) ;
930+
931+ global . process = originalProcess ;
932+ } ) ;
933+
934+ it ( 'should handle upload failures gracefully' , async ( ) => {
935+ mockFs . stat . mockResolvedValue ( { isDirectory : ( ) => true } ) ;
936+
937+ const mockTarballBuffer = Buffer . from ( 'tarball' ) ;
938+ mockTar . create . mockReturnValue ( {
939+ [ Symbol . asyncIterator ] : async function * ( ) {
940+ yield mockTarballBuffer ;
941+ } ,
942+ } ) ;
943+
944+ const mockObjectData = { id : 'dir-999' , upload_url : 'https://upload.example.com/dir' } ;
945+ mockClient . objects . create . mockResolvedValue ( mockObjectData ) ;
946+
947+ ( ( global as any ) . fetch as jest . Mock ) . mockResolvedValue ( {
948+ ok : false ,
949+ status : 500 ,
950+ statusText : 'Internal Server Error' ,
951+ } ) ;
952+
953+ await expect (
954+ StorageObject . uploadFromDir ( mockClient , './project' , { name : 'project.tar.gz' } ) ,
955+ ) . rejects . toThrow ( 'Failed to upload tarball: Upload failed: 500 Internal Server Error' ) ;
956+ } ) ;
957+ } ) ;
958+
813959 describe ( 'error handling' , ( ) => {
814960 it ( 'should handle create errors' , async ( ) => {
815961 const error = new Error ( 'Create failed' ) ;
0 commit comments