@@ -7,6 +7,7 @@ import { Amplify, StorageAccessLevel } from '@aws-amplify/core';
77import { getUrl } from '../../../../../src/providers/s3/apis/internal/getUrl' ;
88import {
99 getPresignedGetObjectUrl ,
10+ getPresignedPutObjectUrl ,
1011 headObject ,
1112} from '../../../../../src/providers/s3/utils/client/s3data' ;
1213import {
@@ -79,6 +80,7 @@ describe('getUrl test with key', () => {
7980 $metadata : { } as any ,
8081 } ) ;
8182 jest . mocked ( getPresignedGetObjectUrl ) . mockResolvedValue ( mockURL ) ;
83+ jest . mocked ( getPresignedPutObjectUrl ) . mockResolvedValue ( mockURL ) ;
8284 } ) ;
8385 afterEach ( ( ) => {
8486 jest . clearAllMocks ( ) ;
@@ -225,6 +227,87 @@ describe('getUrl test with key', () => {
225227 ) ;
226228 } ) ;
227229 } ) ;
230+
231+ describe ( 'method PUT for presigned upload URLs' , ( ) => {
232+ it ( 'should generate PUT presigned URL and skip validation' , async ( ) => {
233+ await getUrlWrapper ( {
234+ key : 'key' ,
235+ options : {
236+ method : 'PUT' ,
237+ validateObjectExistence : true ,
238+ } ,
239+ } ) ;
240+ expect ( getPresignedPutObjectUrl ) . toHaveBeenCalledTimes ( 1 ) ;
241+ expect ( headObject ) . not . toHaveBeenCalled ( ) ;
242+ await expect ( getPresignedPutObjectUrl ) . toBeLastCalledWithConfigAndInput (
243+ {
244+ credentials,
245+ region,
246+ expiration : expect . any ( Number ) ,
247+ } ,
248+ {
249+ Bucket : bucket ,
250+ Key : 'public/key' ,
251+ } ,
252+ ) ;
253+ } ) ;
254+
255+ it ( 'should include content type and disposition for PUT' , async ( ) => {
256+ const contentType = 'image/jpeg' ;
257+ const contentDisposition = 'attachment; filename="test.jpg"' ;
258+ const cacheControl = 'max-age=3600' ;
259+ await getUrlWrapper ( {
260+ key : 'key' ,
261+ options : {
262+ method : 'PUT' ,
263+ contentType,
264+ contentDisposition,
265+ cacheControl,
266+ } ,
267+ } ) ;
268+ expect ( getPresignedPutObjectUrl ) . toHaveBeenCalledTimes ( 1 ) ;
269+ await expect ( getPresignedPutObjectUrl ) . toBeLastCalledWithConfigAndInput (
270+ {
271+ credentials,
272+ region,
273+ expiration : expect . any ( Number ) ,
274+ } ,
275+ {
276+ Bucket : bucket ,
277+ Key : 'public/key' ,
278+ ContentType : contentType ,
279+ ContentDisposition : contentDisposition ,
280+ CacheControl : cacheControl ,
281+ } ,
282+ ) ;
283+ } ) ;
284+
285+ it ( 'should handle object content disposition for PUT' , async ( ) => {
286+ await getUrlWrapper ( {
287+ key : 'key' ,
288+ options : {
289+ method : 'PUT' ,
290+ contentDisposition : {
291+ type : 'attachment' ,
292+ filename : 'test.pdf' ,
293+ } ,
294+ } ,
295+ } ) ;
296+ expect ( getPresignedPutObjectUrl ) . toHaveBeenCalledTimes ( 1 ) ;
297+ await expect ( getPresignedPutObjectUrl ) . toBeLastCalledWithConfigAndInput (
298+ {
299+ credentials,
300+ region,
301+ expiration : expect . any ( Number ) ,
302+ } ,
303+ {
304+ Bucket : bucket ,
305+ Key : 'public/key' ,
306+ ContentDisposition : 'attachment; filename="test.pdf"' ,
307+ } ,
308+ ) ;
309+ } ) ;
310+ } ) ;
228311 } ) ;
229312 describe ( 'Error cases : With key' , ( ) => {
230313 afterAll ( ( ) => {
@@ -285,6 +368,7 @@ describe('getUrl test with path', () => {
285368 $metadata : { } as any ,
286369 } ) ;
287370 jest . mocked ( getPresignedGetObjectUrl ) . mockResolvedValue ( mockURL ) ;
371+ jest . mocked ( getPresignedPutObjectUrl ) . mockResolvedValue ( mockURL ) ;
288372 } ) ;
289373 afterEach ( ( ) => {
290374 jest . clearAllMocks ( ) ;
@@ -418,6 +502,32 @@ describe('getUrl test with path', () => {
418502 ) ;
419503 } ) ;
420504 } ) ;
505+
506+ describe ( 'method PUT for presigned upload URLs with path' , ( ) => {
507+ it ( 'should generate PUT presigned URL with path and skip validation' , async ( ) => {
508+ const inputPath = 'uploads/file.jpg' ;
509+ await getUrlWrapper ( {
510+ path : inputPath ,
511+ options : {
512+ method : 'PUT' ,
513+ validateObjectExistence : true ,
514+ } ,
515+ } ) ;
516+ expect ( getPresignedPutObjectUrl ) . toHaveBeenCalledTimes ( 1 ) ;
517+ expect ( headObject ) . not . toHaveBeenCalled ( ) ;
518+ await expect ( getPresignedPutObjectUrl ) . toBeLastCalledWithConfigAndInput (
519+ {
520+ credentials,
521+ region,
522+ expiration : expect . any ( Number ) ,
523+ } ,
524+ {
525+ Bucket : bucket ,
526+ Key : inputPath ,
527+ } ,
528+ ) ;
529+ } ) ;
530+ } ) ;
421531 } ) ;
422532 describe ( 'Happy cases: With path and Content Disposition, Content Type' , ( ) => {
423533 const config = {
@@ -435,6 +545,7 @@ describe('getUrl test with path', () => {
435545 $metadata : { } as any ,
436546 } ) ;
437547 jest . mocked ( getPresignedGetObjectUrl ) . mockResolvedValue ( mockURL ) ;
548+ jest . mocked ( getPresignedPutObjectUrl ) . mockResolvedValue ( mockURL ) ;
438549 } ) ;
439550 afterEach ( ( ) => {
440551 jest . clearAllMocks ( ) ;
@@ -500,6 +611,7 @@ describe('getUrl test with path', () => {
500611 $metadata : { } as any ,
501612 } ) ;
502613 jest . mocked ( getPresignedGetObjectUrl ) . mockResolvedValue ( mockURL ) ;
614+ jest . mocked ( getPresignedPutObjectUrl ) . mockResolvedValue ( mockURL ) ;
503615 } ) ;
504616
505617 afterEach ( ( ) => {
@@ -660,4 +772,143 @@ describe(`getURL with path and Expected Bucket Owner`, () => {
660772
661773 expect ( getPresignedGetObjectUrl ) . not . toHaveBeenCalled ( ) ;
662774 } ) ;
775+
776+ it ( 'should pass expectedBucketOwner to getPresignedPutObjectUrl for PUT method' , async ( ) => {
777+ const path = 'public/expectedbucketowner_test' ;
778+
779+ await getUrlWrapper ( {
780+ path,
781+ options : {
782+ method : 'PUT' ,
783+ expectedBucketOwner : validBucketOwner ,
784+ } ,
785+ } ) ;
786+
787+ expect ( getPresignedPutObjectUrl ) . toHaveBeenCalledTimes ( 1 ) ;
788+ await expect ( getPresignedPutObjectUrl ) . toBeLastCalledWithConfigAndInput (
789+ {
790+ credentials,
791+ region,
792+ expiration : expect . any ( Number ) ,
793+ } ,
794+ {
795+ Bucket : bucket ,
796+ ExpectedBucketOwner : validBucketOwner ,
797+ Key : path ,
798+ } ,
799+ ) ;
800+ } ) ;
801+ } ) ;
802+
803+ describe ( 'getUrl PUT method with expiresIn and credential expiration' , ( ) => {
804+ const getUrlWrapper = ( input : GetUrlWithPathInput ) => getUrl ( Amplify , input ) ;
805+ beforeAll ( ( ) => {
806+ mockGetConfig . mockReturnValue ( {
807+ Storage : {
808+ S3 : {
809+ bucket,
810+ region,
811+ buckets : { 'default-bucket' : { bucketName : bucket , region } } ,
812+ } ,
813+ } ,
814+ } ) ;
815+ } ) ;
816+
817+ beforeEach ( ( ) => {
818+ jest . mocked ( getPresignedPutObjectUrl ) . mockResolvedValue ( mockURL ) ;
819+ } ) ;
820+
821+ afterEach ( ( ) => {
822+ jest . clearAllMocks ( ) ;
823+ } ) ;
824+
825+ it ( 'should use custom expiresIn for PUT method' , async ( ) => {
826+ mockFetchAuthSession . mockResolvedValue ( {
827+ credentials,
828+ identityId : defaultIdentityId ,
829+ } ) ;
830+ const path = 'uploads/file.jpg' ;
831+
832+ await getUrlWrapper ( {
833+ path,
834+ options : {
835+ method : 'PUT' ,
836+ expiresIn : 3600 ,
837+ } ,
838+ } ) ;
839+
840+ expect ( getPresignedPutObjectUrl ) . toHaveBeenCalledTimes ( 1 ) ;
841+ await expect ( getPresignedPutObjectUrl ) . toBeLastCalledWithConfigAndInput (
842+ {
843+ credentials,
844+ region,
845+ expiration : 3600 ,
846+ } ,
847+ {
848+ Bucket : bucket ,
849+ Key : path ,
850+ } ,
851+ ) ;
852+ } ) ;
853+
854+ it ( 'should use credential expiration when it is shorter than expiresIn for PUT method' , async ( ) => {
855+ const credentialExpiration = new Date ( Date . now ( ) + 600 * 1000 ) ;
856+ mockFetchAuthSession . mockResolvedValue ( {
857+ credentials : { ...credentials , expiration : credentialExpiration } ,
858+ identityId : defaultIdentityId ,
859+ } ) ;
860+ const path = 'uploads/file.jpg' ;
861+
862+ await getUrlWrapper ( {
863+ path,
864+ options : {
865+ method : 'PUT' ,
866+ expiresIn : 3600 ,
867+ } ,
868+ } ) ;
869+
870+ expect ( getPresignedPutObjectUrl ) . toHaveBeenCalledTimes ( 1 ) ;
871+ await expect ( getPresignedPutObjectUrl ) . toBeLastCalledWithConfigAndInput (
872+ {
873+ credentials : { ...credentials , expiration : credentialExpiration } ,
874+ region,
875+ expiration : expect . any ( Number ) ,
876+ } ,
877+ {
878+ Bucket : bucket ,
879+ Key : path ,
880+ } ,
881+ ) ;
882+ const callExpiration = ( getPresignedPutObjectUrl as jest . Mock ) . mock
883+ . calls [ 0 ] [ 0 ] . expiration ;
884+ expect ( callExpiration ) . toBeLessThanOrEqual ( 600 ) ;
885+ } ) ;
886+
887+ it ( 'should use default expiresIn (900s) for PUT method when not specified' , async ( ) => {
888+ mockFetchAuthSession . mockResolvedValue ( {
889+ credentials,
890+ identityId : defaultIdentityId ,
891+ } ) ;
892+ const path = 'uploads/file.jpg' ;
893+
894+ await getUrlWrapper ( {
895+ path,
896+ options : {
897+ method : 'PUT' ,
898+ } ,
899+ } ) ;
900+
901+ expect ( getPresignedPutObjectUrl ) . toHaveBeenCalledTimes ( 1 ) ;
902+ await expect ( getPresignedPutObjectUrl ) . toBeLastCalledWithConfigAndInput (
903+ {
904+ credentials,
905+ region,
906+ expiration : 900 ,
907+ } ,
908+ {
909+ Bucket : bucket ,
910+ Key : path ,
911+ } ,
912+ ) ;
913+ } ) ;
663914} ) ;
0 commit comments