@@ -15,13 +15,15 @@ import { promises as fs } from 'node:fs'
1515import http from 'node:http'
1616import type https from 'node:https'
1717import path from 'node:path'
18+ import { Writable } from 'node:stream'
1819
1920import {
2021 httpDownload ,
2122 httpGetJson ,
2223 httpGetText ,
2324 httpRequest ,
2425} from '@socketsecurity/lib/http-request'
26+ import { Logger } from '@socketsecurity/lib/logger'
2527import { afterAll , beforeAll , describe , expect , it } from 'vitest'
2628import { runWithTempDir } from './utils/temp-file-helper.mjs'
2729
@@ -691,6 +693,144 @@ describe('http-request', () => {
691693 expect ( result . size ) . toBeGreaterThan ( 0 )
692694 } , 'httpDownload-default-timeout-' )
693695 } )
696+
697+ it ( 'should log progress with logger option' , async ( ) => {
698+ await runWithTempDir ( async tmpDir => {
699+ const destPath = path . join ( tmpDir , 'logger.txt' )
700+ const logMessages : string [ ] = [ ]
701+
702+ const stdout = new Writable ( {
703+ write ( chunk , _encoding , callback ) {
704+ logMessages . push ( chunk . toString ( ) )
705+ callback ( )
706+ } ,
707+ } )
708+
709+ const logger = new Logger ( { stdout } )
710+
711+ await httpDownload ( `${ httpBaseUrl } /large-download` , destPath , {
712+ logger,
713+ progressInterval : 25 , // Log every 25%
714+ } )
715+
716+ // Should have logged progress at 25%, 50%, 75%, 100%
717+ expect ( logMessages . length ) . toBeGreaterThan ( 0 )
718+ expect ( logMessages . some ( msg => msg . includes ( 'Progress:' ) ) ) . toBe ( true )
719+ expect ( logMessages . some ( msg => msg . includes ( 'MB' ) ) ) . toBe ( true )
720+
721+ const content = await fs . readFile ( destPath , 'utf8' )
722+ expect ( content ) . toBe ( 'X' . repeat ( 1000 ) )
723+ } , 'httpDownload-logger-' )
724+ } )
725+
726+ it ( 'should use default progressInterval of 10%' , async ( ) => {
727+ await runWithTempDir ( async tmpDir => {
728+ const destPath = path . join ( tmpDir , 'logger-default.txt' )
729+ const logMessages : string [ ] = [ ]
730+
731+ const stdout = new Writable ( {
732+ write ( chunk , _encoding , callback ) {
733+ logMessages . push ( chunk . toString ( ) )
734+ callback ( )
735+ } ,
736+ } )
737+
738+ const logger = new Logger ( { stdout } )
739+
740+ await httpDownload ( `${ httpBaseUrl } /large-download` , destPath , {
741+ logger,
742+ // No progressInterval specified - should default to 10%
743+ } )
744+
745+ expect ( logMessages . length ) . toBeGreaterThan ( 0 )
746+ expect ( logMessages . some ( msg => msg . includes ( 'Progress:' ) ) ) . toBe ( true )
747+ } , 'httpDownload-logger-default-' )
748+ } )
749+
750+ it ( 'should prefer onProgress callback over logger' , async ( ) => {
751+ await runWithTempDir ( async tmpDir => {
752+ const destPath = path . join ( tmpDir , 'logger-precedence.txt' )
753+ const logMessages : string [ ] = [ ]
754+ let onProgressCalled = false
755+
756+ const stdout = new Writable ( {
757+ write ( chunk , _encoding , callback ) {
758+ logMessages . push ( chunk . toString ( ) )
759+ callback ( )
760+ } ,
761+ } )
762+
763+ const logger = new Logger ( { stdout } )
764+
765+ await httpDownload ( `${ httpBaseUrl } /large-download` , destPath , {
766+ logger,
767+ onProgress : ( ) => {
768+ onProgressCalled = true
769+ } ,
770+ progressInterval : 25 ,
771+ } )
772+
773+ // onProgress should have been called
774+ expect ( onProgressCalled ) . toBe ( true )
775+ // Logger should NOT have been used
776+ expect ( logMessages . length ) . toBe ( 0 )
777+ } , 'httpDownload-logger-precedence-' )
778+ } )
779+
780+ it ( 'should format progress with MB units correctly' , async ( ) => {
781+ await runWithTempDir ( async tmpDir => {
782+ const destPath = path . join ( tmpDir , 'logger-format.txt' )
783+ const logMessages : string [ ] = [ ]
784+
785+ const stdout = new Writable ( {
786+ write ( chunk , _encoding , callback ) {
787+ logMessages . push ( chunk . toString ( ) )
788+ callback ( )
789+ } ,
790+ } )
791+
792+ const logger = new Logger ( { stdout } )
793+
794+ await httpDownload ( `${ httpBaseUrl } /large-download` , destPath , {
795+ logger,
796+ progressInterval : 50 ,
797+ } )
798+
799+ // Check format: " Progress: XX% (Y.Y MB / Z.Z MB)"
800+ expect ( logMessages . length ) . toBeGreaterThan ( 0 )
801+ const progressMsg = logMessages . find ( msg => msg . includes ( 'Progress:' ) )
802+ expect ( progressMsg ) . toBeDefined ( )
803+ expect ( progressMsg ) . toMatch (
804+ / P r o g r e s s : \d + % \( \d + \. \d + M B \/ \d + \. \d + M B \) / ,
805+ )
806+ } , 'httpDownload-logger-format-' )
807+ } )
808+
809+ it ( 'should not log progress with logger when no content-length' , async ( ) => {
810+ await runWithTempDir ( async tmpDir => {
811+ const destPath = path . join ( tmpDir , 'logger-no-length.txt' )
812+ const logMessages : string [ ] = [ ]
813+
814+ const stdout = new Writable ( {
815+ write ( chunk , _encoding , callback ) {
816+ logMessages . push ( chunk . toString ( ) )
817+ callback ( )
818+ } ,
819+ } )
820+
821+ const logger = new Logger ( { stdout } )
822+
823+ await httpDownload ( `${ httpBaseUrl } /download-no-length` , destPath , {
824+ logger,
825+ } )
826+
827+ // Should not have logged any progress (no content-length header)
828+ expect ( logMessages . length ) . toBe ( 0 )
829+
830+ const content = await fs . readFile ( destPath , 'utf8' )
831+ expect ( content ) . toBe ( 'No content length' )
832+ } , 'httpDownload-logger-no-length-' )
833+ } )
694834 } )
695835
696836 describe ( 'httpGetJson' , ( ) => {
0 commit comments