@@ -634,4 +634,187 @@ describe(AmazonS3Client.name, () => {
634634 ) ;
635635 } ) ;
636636 } ) ;
637+
638+ describe ( 'Streaming requests' , ( ) => {
639+ let realDate : typeof Date ;
640+ let realSetTimeout : typeof setTimeout ;
641+ beforeEach ( ( ) => {
642+ // mock date
643+ realDate = global . Date ;
644+ global . Date = MockedDate as typeof Date ;
645+
646+ // mock setTimeout
647+ realSetTimeout = global . setTimeout ;
648+ global . setTimeout = ( ( callback : ( ) => void , time : number ) => {
649+ return realSetTimeout ( callback , 1 ) ;
650+ } ) . bind ( global ) as typeof global . setTimeout ;
651+ } ) ;
652+
653+ afterEach ( ( ) => {
654+ jest . restoreAllMocks ( ) ;
655+ global . Date = realDate ;
656+ global . setTimeout = realSetTimeout . bind ( global ) ;
657+ } ) ;
658+
659+ describe ( 'Getting an object stream' , ( ) => {
660+ async function makeStreamGetRequestAsync (
661+ credentials : IAmazonS3Credentials | undefined ,
662+ options : IAmazonS3BuildCacheProviderOptionsAdvanced ,
663+ objectName : string ,
664+ status : number ,
665+ statusText ?: string
666+ ) : Promise < { result : NodeJS . ReadableStream | undefined ; spy : jest . SpyInstance } > {
667+ const { Readable } = await import ( 'node:stream' ) ;
668+ const mockStream = new Readable ( { read ( ) { } } ) ;
669+
670+ const spy : jest . SpyInstance = jest
671+ . spyOn ( WebClient . prototype , 'fetchStreamAsync' )
672+ . mockReturnValue (
673+ Promise . resolve ( {
674+ stream : mockStream ,
675+ headers : { } ,
676+ status,
677+ statusText,
678+ ok : status >= 200 && status < 300 ,
679+ redirected : false
680+ } )
681+ ) ;
682+
683+ const s3Client : AmazonS3Client = new AmazonS3Client ( credentials , options , webClient , terminal ) ;
684+ const result = await s3Client . getObjectStreamAsync ( objectName ) ;
685+ return { result, spy } ;
686+ }
687+
688+ it ( 'Can get an object stream' , async ( ) => {
689+ const { result, spy } = await makeStreamGetRequestAsync (
690+ {
691+ accessKeyId : 'accessKeyId' ,
692+ secretAccessKey : 'secretAccessKey' ,
693+ sessionToken : undefined
694+ } ,
695+ DUMMY_OPTIONS ,
696+ 'abc123' ,
697+ 200
698+ ) ;
699+ expect ( result ) . toBeDefined ( ) ;
700+ expect ( spy ) . toHaveBeenCalledTimes ( 1 ) ;
701+ expect ( spy . mock . calls [ 0 ] ) . toMatchSnapshot ( ) ;
702+ spy . mockRestore ( ) ;
703+ } ) ;
704+
705+ it ( 'Returns undefined for a 404 (missing) object stream' , async ( ) => {
706+ const { result, spy } = await makeStreamGetRequestAsync (
707+ {
708+ accessKeyId : 'accessKeyId' ,
709+ secretAccessKey : 'secretAccessKey' ,
710+ sessionToken : undefined
711+ } ,
712+ DUMMY_OPTIONS ,
713+ 'abc123' ,
714+ 404 ,
715+ 'Not Found'
716+ ) ;
717+ expect ( result ) . toBeUndefined ( ) ;
718+ expect ( spy ) . toHaveBeenCalledTimes ( 1 ) ;
719+ spy . mockRestore ( ) ;
720+ } ) ;
721+ } ) ;
722+
723+ describe ( 'Uploading an object stream' , ( ) => {
724+ it ( 'Throws an error if credentials are not provided' , async ( ) => {
725+ const { Readable } = await import ( 'node:stream' ) ;
726+ const s3Client : AmazonS3Client = new AmazonS3Client (
727+ undefined ,
728+ { s3Endpoint : 'http://foo.bar.baz' , ...DUMMY_OPTIONS_WITHOUT_ENDPOINT } ,
729+ webClient ,
730+ terminal
731+ ) ;
732+
733+ const mockStream = new Readable ( { read ( ) { } } ) ;
734+ try {
735+ await s3Client . uploadObjectStreamAsync ( 'temp' , mockStream ) ;
736+ fail ( 'Expected an exception to be thrown' ) ;
737+ } catch ( e ) {
738+ expect ( e ) . toMatchSnapshot ( ) ;
739+ }
740+ } ) ;
741+
742+ it ( 'Uploads a stream successfully' , async ( ) => {
743+ const { Readable } = await import ( 'node:stream' ) ;
744+ const mockStream = new Readable ( { read ( ) { } } ) ;
745+ const responseStream = new Readable ( { read ( ) { } } ) ;
746+
747+ const spy : jest . SpyInstance = jest
748+ . spyOn ( WebClient . prototype , 'fetchStreamAsync' )
749+ . mockReturnValue (
750+ Promise . resolve ( {
751+ stream : responseStream ,
752+ headers : { } ,
753+ status : 200 ,
754+ statusText : 'OK' ,
755+ ok : true ,
756+ redirected : false
757+ } )
758+ ) ;
759+
760+ const s3Client : AmazonS3Client = new AmazonS3Client (
761+ {
762+ accessKeyId : 'accessKeyId' ,
763+ secretAccessKey : 'secretAccessKey' ,
764+ sessionToken : undefined
765+ } ,
766+ DUMMY_OPTIONS ,
767+ webClient ,
768+ terminal
769+ ) ;
770+
771+ await s3Client . uploadObjectStreamAsync ( 'abc123' , mockStream ) ;
772+
773+ expect ( spy ) . toHaveBeenCalledTimes ( 1 ) ;
774+ expect ( spy . mock . calls [ 0 ] ) . toMatchSnapshot ( ) ;
775+ spy . mockRestore ( ) ;
776+ } ) ;
777+
778+ it ( 'Does not retry on failure (stream consumed)' , async ( ) => {
779+ const { Readable } = await import ( 'node:stream' ) ;
780+ const mockStream = new Readable ( { read ( ) { } } ) ;
781+ const responseStream = new Readable ( { read ( ) { } } ) ;
782+
783+ const spy : jest . SpyInstance = jest
784+ . spyOn ( WebClient . prototype , 'fetchStreamAsync' )
785+ . mockReturnValue (
786+ Promise . resolve ( {
787+ stream : responseStream ,
788+ headers : { } ,
789+ status : 500 ,
790+ statusText : 'InternalServerError' ,
791+ ok : false ,
792+ redirected : false
793+ } )
794+ ) ;
795+
796+ const s3Client : AmazonS3Client = new AmazonS3Client (
797+ {
798+ accessKeyId : 'accessKeyId' ,
799+ secretAccessKey : 'secretAccessKey' ,
800+ sessionToken : undefined
801+ } ,
802+ DUMMY_OPTIONS ,
803+ webClient ,
804+ terminal
805+ ) ;
806+
807+ try {
808+ await s3Client . uploadObjectStreamAsync ( 'abc123' , mockStream ) ;
809+ fail ( 'Expected an exception to be thrown' ) ;
810+ } catch ( e ) {
811+ expect ( ( e as Error ) . message ) . toContain ( '500' ) ;
812+ }
813+
814+ // Only 1 call - no retry for streams
815+ expect ( spy ) . toHaveBeenCalledTimes ( 1 ) ;
816+ spy . mockRestore ( ) ;
817+ } ) ;
818+ } ) ;
819+ } ) ;
637820} ) ;
0 commit comments