@@ -170,6 +170,31 @@ describe("streamed media upload fallback", () => {
170170 } ) ;
171171 } ) ;
172172
173+ it ( "accepts existing client hash formats for deduplication" , async ( ) => {
174+ const repo = new MediaRepository ( db ) ;
175+ const contentHash =
176+ "sha256:ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad" ;
177+ const existing = await repo . create ( {
178+ filename : "photo.png" ,
179+ mimeType : "image/png" ,
180+ size : 3 ,
181+ storageKey : "photo.png" ,
182+ contentHash,
183+ } ) ;
184+
185+ const response = await postUploadUrl (
186+ buildContext ( {
187+ db,
188+ request : uploadUrlRequestWithHash ( contentHash ) ,
189+ storage : unsupportedSignedUrlStorage ( ) ,
190+ } ) ,
191+ ) ;
192+
193+ expect ( response . status ) . toBe ( 200 ) ;
194+ const body = ( await response . json ( ) ) as { data : { existing ?: boolean ; mediaId : string } } ;
195+ expect ( body . data ) . toMatchObject ( { existing : true , mediaId : existing . id } ) ;
196+ } ) ;
197+
173198 it . each ( [
174199 { difference : "MIME type" , contentType : "video/mp4" , size : 3 } ,
175200 { difference : "size" , contentType : "audio/mp4" , size : 4 } ,
@@ -533,7 +558,7 @@ describe("streamed media upload fallback", () => {
533558 expect ( storage . objects . size ) . toBe ( 0 ) ;
534559 } ) ;
535560
536- it ( "keeps a completed object when the upload request is retried " , async ( ) => {
561+ it ( "preserves a completed object when a retry upload fails " , async ( ) => {
537562 const repo = new MediaRepository ( db ) ;
538563 const pending = await repo . createPending ( {
539564 filename : "photo.png" ,
@@ -571,11 +596,53 @@ describe("streamed media upload fallback", () => {
571596 } ) ,
572597 ) ;
573598
574- expect ( retryResponse . status ) . toBe ( 200 ) ;
575- expect ( storage . upload ) . toHaveBeenCalledOnce ( ) ;
599+ expect ( retryResponse . status ) . toBe ( 500 ) ;
600+ expect ( storage . upload ) . toHaveBeenCalledTimes ( 2 ) ;
601+ expect ( await repo . findById ( pending . id ) ) . toMatchObject ( { storageKey : completedKey } ) ;
576602 expect ( storage . objects . get ( completedKey ) ) . toEqual ( new Uint8Array ( [ 1 , 2 , 3 ] ) ) ;
577603 } ) ;
578604
605+ it ( "processes different same-size bytes when an upload target is retried" , async ( ) => {
606+ const repo = new MediaRepository ( db ) ;
607+ const pending = await repo . createPending ( {
608+ filename : "photo.png" ,
609+ mimeType : "image/png" ,
610+ size : 3 ,
611+ storageKey : "photo.png" ,
612+ authorId : "user-1" ,
613+ } ) ;
614+ const storage = streamingStorage ( ) ;
615+
616+ const firstResponse = await putUpload (
617+ buildContext ( {
618+ db,
619+ id : pending . id ,
620+ request : uploadRequest ( pending . id , new Uint8Array ( [ 1 , 2 , 3 ] ) ) ,
621+ storage,
622+ } ) ,
623+ ) ;
624+ expect ( firstResponse . status ) . toBe ( 200 ) ;
625+ const firstUpload = await repo . findById ( pending . id ) ;
626+ expect ( firstUpload ) . not . toBeNull ( ) ;
627+
628+ const secondResponse = await putUpload (
629+ buildContext ( {
630+ db,
631+ id : pending . id ,
632+ request : uploadRequest ( pending . id , new Uint8Array ( [ 4 , 5 , 6 ] ) ) ,
633+ storage,
634+ } ) ,
635+ ) ;
636+
637+ expect ( secondResponse . status ) . toBe ( 200 ) ;
638+ expect ( storage . upload ) . toHaveBeenCalledTimes ( 2 ) ;
639+ const retried = await repo . findById ( pending . id ) ;
640+ expect ( retried ?. storageKey ) . not . toBe ( firstUpload ! . storageKey ) ;
641+ expect ( storage . objects . get ( retried ! . storageKey ) ) . toEqual ( new Uint8Array ( [ 4 , 5 , 6 ] ) ) ;
642+ expect ( storage . objects . has ( firstUpload ! . storageKey ) ) . toBe ( false ) ;
643+ expect ( storage . delete ) . toHaveBeenCalledOnce ( ) ;
644+ } ) ;
645+
579646 it ( "publishes only one object when two uploads race" , async ( ) => {
580647 const repo = new MediaRepository ( db ) ;
581648 const pending = await repo . createPending ( {
0 commit comments