@@ -63,6 +63,7 @@ describe("Destructive Command Guard manager", () => {
6363 expect ( isTrustedDownloadUrl ( "https://cdn.objects.githubusercontent.com/release" ) ) . toBe ( true )
6464 expect ( isTrustedDownloadUrl ( "http://github.com/release" ) ) . toBe ( false )
6565 expect ( isTrustedDownloadUrl ( "https://evilgithub.com/release" ) ) . toBe ( false )
66+ expect ( isTrustedDownloadUrl ( "https://github.com.evil.com/release" ) ) . toBe ( false )
6667 expect ( isTrustedDownloadUrl ( "not a URL" ) ) . toBe ( false )
6768 } )
6869
@@ -72,8 +73,70 @@ describe("Destructive Command Guard manager", () => {
7273 )
7374 } )
7475
76+ it ( "rejects non-successful HTTP responses" , async ( ) => {
77+ const response = Object . assign ( new PassThrough ( ) , { statusCode : 503 , headers : { } , destroy : vi . fn ( ) } )
78+ const request = Object . assign ( new EventEmitter ( ) , { setTimeout : vi . fn ( ) , destroy : vi . fn ( ) } )
79+ mockGet . mockImplementation ( ( _url , optionsOrCallback , optionalCallback ) => {
80+ const callback = typeof optionsOrCallback === "function" ? optionsOrCallback : optionalCallback
81+ setImmediate ( ( ) => callback ?.( response as unknown as IncomingMessage ) )
82+ return request as unknown as ReturnType < typeof get >
83+ } )
84+
85+ await expect ( downloadFile ( "https://github.com/release" , path . join ( tempDir , "archive" ) ) ) . rejects . toThrow (
86+ "DCG download failed with HTTP 503" ,
87+ )
88+ } )
89+
90+ it ( "rejects request errors" , async ( ) => {
91+ const request = Object . assign ( new EventEmitter ( ) , { setTimeout : vi . fn ( ) , destroy : vi . fn ( ) } )
92+ mockGet . mockReturnValue ( request as unknown as ReturnType < typeof get > )
93+
94+ const download = downloadFile ( "https://github.com/release" , path . join ( tempDir , "archive" ) )
95+ request . emit ( "error" , new Error ( "socket failed" ) )
96+
97+ await expect ( download ) . rejects . toThrow ( "socket failed" )
98+ } )
99+
100+ it ( "times out stalled requests" , async ( ) => {
101+ const request = Object . assign ( new EventEmitter ( ) , {
102+ setTimeout : vi . fn ( ( _timeout : number , callback : ( ) => void ) => setImmediate ( callback ) ) ,
103+ destroy : vi . fn ( ( error : Error ) => request . emit ( "error" , error ) ) ,
104+ } )
105+ mockGet . mockReturnValue ( request as unknown as ReturnType < typeof get > )
106+
107+ await expect ( downloadFile ( "https://github.com/release" , path . join ( tempDir , "archive" ) ) ) . rejects . toThrow (
108+ "DCG download timed out" ,
109+ )
110+ expect ( request . setTimeout ) . toHaveBeenCalledWith ( 120_000 , expect . any ( Function ) )
111+ } )
112+
113+ it ( "rejects archives larger than 50 MiB" , async ( ) => {
114+ const response = Object . assign ( new PassThrough ( ) , {
115+ statusCode : 200 ,
116+ headers : { "content-length" : String ( 50 * 1024 * 1024 + 1 ) } ,
117+ destroy : vi . fn ( ) ,
118+ } )
119+ const request = Object . assign ( new EventEmitter ( ) , { setTimeout : vi . fn ( ) , destroy : vi . fn ( ) } )
120+ mockGet . mockImplementation ( ( _url , optionsOrCallback , optionalCallback ) => {
121+ const callback = typeof optionsOrCallback === "function" ? optionsOrCallback : optionalCallback
122+ setImmediate ( ( ) => callback ?.( response as unknown as IncomingMessage ) )
123+ return request as unknown as ReturnType < typeof get >
124+ } )
125+
126+ await expect ( downloadFile ( "https://github.com/release" , path . join ( tempDir , "archive" ) ) ) . rejects . toThrow (
127+ "DCG archive exceeds the download size limit" ,
128+ )
129+ } )
130+
75131 it ( "allows trusted relative redirects and rejects unsafe or exhausted redirects" , ( ) => {
76132 expect ( resolveTrustedRedirect ( "https://github.com/release" , "/asset" , 5 ) ) . toBe ( "https://github.com/asset" )
133+ expect (
134+ resolveTrustedRedirect (
135+ "https://github.com/release" ,
136+ "https://release-assets.githubusercontent.com/asset" ,
137+ 5 ,
138+ ) ,
139+ ) . toBe ( "https://release-assets.githubusercontent.com/asset" )
77140 expect ( ( ) => resolveTrustedRedirect ( "https://github.com/release" , "https://example.com/asset" , 5 ) ) . toThrow (
78141 "DCG download redirected to an untrusted host" ,
79142 )
@@ -92,9 +155,7 @@ describe("Destructive Command Guard manager", () => {
92155 const checksum = createHash ( "sha256" ) . update ( contents ) . digest ( "hex" )
93156
94157 await expect ( verifyChecksum ( filePath , checksum ) ) . resolves . toBeUndefined ( )
95- await expect ( verifyChecksum ( filePath , "0" . repeat ( 64 ) ) ) . rejects . toThrow (
96- "DCG archive checksum verification failed" ,
97- )
158+ await expect ( verifyChecksum ( filePath , "0" . repeat ( 64 ) ) ) . rejects . toThrow ( `got ${ checksum } ` )
98159 } )
99160
100161 it ( "uses the platform ZIP extractor" , async ( ) => {
@@ -180,6 +241,22 @@ describe("Destructive Command Guard manager", () => {
180241 }
181242 } )
182243
244+ it ( "warns when the current platform is unsupported" , async ( ) => {
245+ const platformKey = `${ process . platform } -${ process . arch } `
246+ const info = DCG_ARCHIVES [ platformKey ]
247+ if ( ! info ) return
248+ const warnSpy = vi . spyOn ( console , "warn" ) . mockImplementation ( ( ) => { } )
249+ Reflect . deleteProperty ( DCG_ARCHIVES , platformKey )
250+
251+ try {
252+ await expect ( ensureDcgInstalled ( tempDir ) ) . resolves . toBeUndefined ( )
253+ expect ( warnSpy ) . toHaveBeenCalledWith ( `[DCG] Unsupported platform: ${ platformKey } ` )
254+ } finally {
255+ Reflect . set ( DCG_ARCHIVES , platformKey , info )
256+ warnSpy . mockRestore ( )
257+ }
258+ } )
259+
183260 it ( "downloads, verifies, extracts, and deduplicates a new installation" , async ( ) => {
184261 const info = getDcgArchiveInfo ( )
185262 expect ( info ) . toBeDefined ( )
@@ -251,4 +328,60 @@ describe("Destructive Command Guard manager", () => {
251328 Object . defineProperty ( info , "sha256" , { value : originalChecksum , configurable : true } )
252329 }
253330 } )
331+
332+ it . skipIf ( ! getDcgArchiveInfo ( ) ?. archive . endsWith ( ".zip" ) ) (
333+ "downloads, verifies, extracts, and installs a ZIP archive" ,
334+ async ( ) => {
335+ const info = getDcgArchiveInfo ( )
336+ if ( ! info ) throw new Error ( "Expected a ZIP archive in this test" )
337+
338+ const archive = Buffer . from ( "test ZIP archive" )
339+ const originalChecksum = info . sha256
340+ Object . defineProperty ( info , "sha256" , {
341+ value : createHash ( "sha256" ) . update ( archive ) . digest ( "hex" ) ,
342+ configurable : true ,
343+ } )
344+ const response = Object . assign ( new PassThrough ( ) , {
345+ statusCode : 200 ,
346+ headers : { "content-length" : String ( archive . length ) } ,
347+ } )
348+ const request = Object . assign ( new EventEmitter ( ) , { setTimeout : vi . fn ( ) , destroy : vi . fn ( ) } )
349+ mockGet . mockImplementation ( ( _url , optionsOrCallback , optionalCallback ) => {
350+ const callback = typeof optionsOrCallback === "function" ? optionsOrCallback : optionalCallback
351+ setImmediate ( ( ) => {
352+ callback ?.( response as unknown as IncomingMessage )
353+ response . end ( archive )
354+ } )
355+ return request as unknown as ReturnType < typeof get >
356+ } )
357+ mockSpawn . mockImplementation ( ( _executable , args ) => {
358+ const child = Object . assign ( new EventEmitter ( ) , {
359+ stdout : new PassThrough ( ) ,
360+ stderr : new PassThrough ( ) ,
361+ kill : vi . fn ( ) ,
362+ } )
363+ setImmediate ( async ( ) => {
364+ const destinationIndex = args . indexOf (
365+ "$archivePath = $args[0]; $destination = $args[1]; Expand-Archive -LiteralPath $archivePath -DestinationPath $destination -Force" ,
366+ )
367+ const stagingDir = args [ destinationIndex + 2 ]
368+ await writeFile ( path . join ( stagingDir , info . binary ) , "ZIP executable" )
369+ child . emit ( "close" , 0 )
370+ } )
371+ return child as unknown as ReturnType < typeof spawn >
372+ } )
373+
374+ try {
375+ const binaryPath = await ensureDcgInstalled ( tempDir )
376+ if ( ! binaryPath ) throw new Error ( "Expected DCG to be supported in this test" )
377+ expect ( await readFile ( binaryPath , "utf8" ) ) . toBe ( "ZIP executable" )
378+ expect ( await readFile ( path . join ( tempDir , "destructive-command-guard" , ".dcg-version" ) , "utf8" ) ) . toBe (
379+ DCG_VERSION ,
380+ )
381+ await expect ( access ( path . join ( tempDir , `${ DCG_VERSION } -${ info . archive } ` ) ) ) . rejects . toThrow ( )
382+ } finally {
383+ Object . defineProperty ( info , "sha256" , { value : originalChecksum , configurable : true } )
384+ }
385+ } ,
386+ )
254387} )
0 commit comments