@@ -12,6 +12,7 @@ const {
1212 mockPutObjectCommand,
1313 mockGetObjectCommand,
1414 mockDeleteObjectCommand,
15+ mockCompleteMultipartUploadCommand,
1516 mockGetSignedUrl,
1617 mockEnv,
1718 mockS3Config,
@@ -51,6 +52,7 @@ const {
5152 mockPutObjectCommand : vi . fn ( ) . mockImplementation ( class { } ) ,
5253 mockGetObjectCommand : vi . fn ( ) . mockImplementation ( class { } ) ,
5354 mockDeleteObjectCommand : vi . fn ( ) . mockImplementation ( class { } ) ,
55+ mockCompleteMultipartUploadCommand : vi . fn ( ) . mockImplementation ( class { } ) ,
5456 mockGetSignedUrl : vi . fn ( ) ,
5557 mockEnv,
5658 }
@@ -61,6 +63,7 @@ vi.mock('@aws-sdk/client-s3', () => ({
6163 PutObjectCommand : mockPutObjectCommand ,
6264 GetObjectCommand : mockGetObjectCommand ,
6365 DeleteObjectCommand : mockDeleteObjectCommand ,
66+ CompleteMultipartUploadCommand : mockCompleteMultipartUploadCommand ,
6467} ) )
6568
6669vi . mock ( '@aws-sdk/s3-request-presigner' , ( ) => ( {
@@ -92,6 +95,7 @@ vi.mock('@/lib/uploads/config', () => ({
9295} ) )
9396
9497import {
98+ completeS3MultipartUpload ,
9599 deleteFromS3 ,
96100 downloadFromS3 ,
97101 getPresignedUrl ,
@@ -398,4 +402,70 @@ describe('S3 Client', () => {
398402 } )
399403 } )
400404 } )
405+
406+ describe ( 'completeS3MultipartUpload fallback location' , ( ) => {
407+ const parts = [ { ETag : 'etag-1' , PartNumber : 1 } ]
408+
409+ it ( 'uses the SDK-provided Location when present' , async ( ) => {
410+ mockSend . mockResolvedValueOnce ( { Location : 'https://provided.example.com/object' } )
411+
412+ const result = await completeS3MultipartUpload ( 'kb/uuid-file.txt' , 'upload-1' , parts )
413+
414+ expect ( result . location ) . toBe ( 'https://provided.example.com/object' )
415+ expect ( result . key ) . toBe ( 'kb/uuid-file.txt' )
416+ expect ( result . path ) . toBe ( '/api/files/serve/kb%2Fuuid-file.txt' )
417+ } )
418+
419+ it ( 'falls back to an AWS virtual-hosted URL when Location is absent' , async ( ) => {
420+ mockSend . mockResolvedValueOnce ( { } )
421+
422+ const result = await completeS3MultipartUpload ( 'kb/uuid-file.txt' , 'upload-1' , parts )
423+
424+ expect ( result . location ) . toBe (
425+ 'https://test-kb-bucket.s3.test-region.amazonaws.com/kb/uuid-file.txt'
426+ )
427+ } )
428+
429+ it ( 'builds a path-style fallback URL for a custom endpoint with forcePathStyle' , async ( ) => {
430+ mockS3Config . endpoint = 'https://minio.example.com'
431+ mockS3Config . forcePathStyle = true
432+ mockSend . mockResolvedValueOnce ( { } )
433+
434+ const result = await completeS3MultipartUpload ( 'kb/uuid-file.txt' , 'upload-1' , parts )
435+
436+ expect ( result . location ) . toBe ( 'https://minio.example.com/test-kb-bucket/kb/uuid-file.txt' )
437+ } )
438+
439+ it ( 'builds a virtual-hosted fallback URL for a custom endpoint without forcePathStyle' , async ( ) => {
440+ mockS3Config . endpoint = 'https://account.r2.cloudflarestorage.com'
441+ mockS3Config . forcePathStyle = false
442+ mockSend . mockResolvedValueOnce ( { } )
443+
444+ const result = await completeS3MultipartUpload ( 'kb/uuid-file.txt' , 'upload-1' , parts )
445+
446+ expect ( result . location ) . toBe (
447+ 'https://test-kb-bucket.account.r2.cloudflarestorage.com/kb/uuid-file.txt'
448+ )
449+ } )
450+
451+ it ( 'strips a trailing slash from the custom endpoint before appending the key' , async ( ) => {
452+ mockS3Config . endpoint = 'https://minio.example.com/'
453+ mockS3Config . forcePathStyle = true
454+ mockSend . mockResolvedValueOnce ( { } )
455+
456+ const result = await completeS3MultipartUpload ( 'kb/uuid-file.txt' , 'upload-1' , parts )
457+
458+ expect ( result . location ) . toBe ( 'https://minio.example.com/test-kb-bucket/kb/uuid-file.txt' )
459+ } )
460+
461+ it ( 'percent-encodes special characters per path segment, preserving slashes' , async ( ) => {
462+ mockSend . mockResolvedValueOnce ( { } )
463+
464+ const result = await completeS3MultipartUpload ( 'kb/uuid-my file.txt' , 'upload-1' , parts )
465+
466+ expect ( result . location ) . toBe (
467+ 'https://test-kb-bucket.s3.test-region.amazonaws.com/kb/uuid-my%20file.txt'
468+ )
469+ } )
470+ } )
401471} )
0 commit comments