@@ -6,7 +6,7 @@ import { setTimeout as delay } from 'node:timers/promises'
66import { fileURLToPath } from 'node:url'
77
88import { Reader } from 'styled-map-package'
9- import { describe , it , expect } from 'vitest'
9+ import { describe , it , expect , vi } from 'vitest'
1010
1111import {
1212 DEMOTILES_Z2 ,
@@ -570,6 +570,94 @@ describe('Map Upload', () => {
570570 expect ( style ) . toEqual ( expectedStyle )
571571 } )
572572
573+ it ( 'should clean up temp file when write errors during upload' , async ( t ) => {
574+ const { localBaseUrl, customMapPath } = await startServer ( t , {
575+ customMapPath : nonExistentPath ,
576+ } )
577+ const tmpDir = path . dirname ( customMapPath )
578+ const customMapBasename = path . basename ( customMapPath )
579+
580+ // Mock fs.createWriteStream to return a writable that errors on write,
581+ // simulating a disk-full scenario
582+ const originalCreateWriteStream = fs . createWriteStream
583+ const spy = vi . spyOn ( fs , 'createWriteStream' ) . mockImplementation (
584+ ( ...args : any [ ] ) => {
585+ const stream = originalCreateWriteStream . apply ( fs , args as any )
586+ stream . _write = (
587+ _chunk : any ,
588+ _encoding : BufferEncoding ,
589+ callback : ( error ?: Error | null ) => void ,
590+ ) => {
591+ callback ( new Error ( 'ENOSPC: no space left on device' ) )
592+ }
593+ return stream
594+ } ,
595+ )
596+ t . onTestFinished ( ( ) => spy . mockRestore ( ) )
597+
598+ const fileBuffer = fs . readFileSync ( OSM_BRIGHT_Z6 )
599+ const response = await fetch ( `${ localBaseUrl } /maps/custom` , {
600+ method : 'PUT' ,
601+ body : fileBuffer ,
602+ headers : { 'Content-Type' : 'application/octet-stream' } ,
603+ } )
604+
605+ expect ( response . status ) . toBe ( 500 )
606+
607+ // Allow time for async cleanup to complete
608+ await delay ( 100 )
609+
610+ // Verify temp file was cleaned up
611+ const filesInDir = fs . readdirSync ( tmpDir )
612+ const tempFiles = filesInDir . filter (
613+ ( f ) => f . startsWith ( customMapBasename ) && f . includes ( '.download-' ) ,
614+ )
615+ expect ( tempFiles ) . toHaveLength ( 0 )
616+ } )
617+
618+ it ( 'should clean up temp file when close errors during upload' , async ( t ) => {
619+ const { localBaseUrl, customMapPath } = await startServer ( t , {
620+ customMapPath : nonExistentPath ,
621+ } )
622+ const tmpDir = path . dirname ( customMapPath )
623+ const customMapBasename = path . basename ( customMapPath )
624+
625+ // Mock fs.createWriteStream to return a writable that writes successfully
626+ // but errors on close (_final), simulating a flush/sync failure
627+ const originalCreateWriteStream = fs . createWriteStream
628+ const spy = vi . spyOn ( fs , 'createWriteStream' ) . mockImplementation (
629+ ( ...args : any [ ] ) => {
630+ const stream = originalCreateWriteStream . apply ( fs , args as any )
631+ stream . _final = (
632+ callback : ( error ?: Error | null ) => void ,
633+ ) => {
634+ callback ( new Error ( 'EIO: i/o error' ) )
635+ }
636+ return stream
637+ } ,
638+ )
639+ t . onTestFinished ( ( ) => spy . mockRestore ( ) )
640+
641+ const fileBuffer = fs . readFileSync ( OSM_BRIGHT_Z6 )
642+ const response = await fetch ( `${ localBaseUrl } /maps/custom` , {
643+ method : 'PUT' ,
644+ body : fileBuffer ,
645+ headers : { 'Content-Type' : 'application/octet-stream' } ,
646+ } )
647+
648+ expect ( response . status ) . toBe ( 500 )
649+
650+ // Allow time for async cleanup to complete
651+ await delay ( 100 )
652+
653+ // Verify temp file was cleaned up
654+ const filesInDir = fs . readdirSync ( tmpDir )
655+ const tempFiles = filesInDir . filter (
656+ ( f ) => f . startsWith ( customMapBasename ) && f . includes ( '.download-' ) ,
657+ )
658+ expect ( tempFiles ) . toHaveLength ( 0 )
659+ } )
660+
573661 it ( 'should not leave temp files after successful upload' , async ( t ) => {
574662 const { localBaseUrl, customMapPath } = await startServer ( t , {
575663 customMapPath : nonExistentPath ,
0 commit comments