Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/aws-amplify/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -511,7 +511,7 @@
"name": "[Storage] getUrl (S3)",
"path": "./dist/esm/storage/index.mjs",
"import": "{ getUrl }",
"limit": "18.12 kB"
"limit": "18.5 kB"
},
{
"name": "[Storage] list (S3)",
Expand Down
2 changes: 1 addition & 1 deletion packages/predictions/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
"name": "Predictions",
"path": "./dist/esm/index.mjs",
"import": "{ Predictions }",
"limit": "77 kB"
"limit": "77.5 kB"
}
]
}
162 changes: 162 additions & 0 deletions packages/storage/__tests__/providers/s3/apis/internal/getUrl.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Amplify, StorageAccessLevel } from '@aws-amplify/core';
import { getUrl } from '../../../../../src/providers/s3/apis/internal/getUrl';
import {
getPresignedGetObjectUrl,
getPresignedPutObjectUrl,
headObject,
} from '../../../../../src/providers/s3/utils/client/s3data';
import {
Expand Down Expand Up @@ -79,6 +80,7 @@ describe('getUrl test with key', () => {
$metadata: {} as any,
});
jest.mocked(getPresignedGetObjectUrl).mockResolvedValue(mockURL);
jest.mocked(getPresignedPutObjectUrl).mockResolvedValue(mockURL);
});
afterEach(() => {
jest.clearAllMocks();
Expand Down Expand Up @@ -225,6 +227,87 @@ describe('getUrl test with key', () => {
);
});
});

describe('method PUT for presigned upload URLs', () => {
it('should generate PUT presigned URL and skip validation', async () => {
await getUrlWrapper({
key: 'key',
options: {
method: 'PUT',
validateObjectExistence: true,
},
});
expect(getPresignedPutObjectUrl).toHaveBeenCalledTimes(1);
expect(headObject).not.toHaveBeenCalled();
await expect(getPresignedPutObjectUrl).toBeLastCalledWithConfigAndInput(
{
credentials,
region,
expiration: expect.any(Number),
},
{
Bucket: bucket,
Key: 'public/key',
},
);
});

it('should include content type and disposition for PUT', async () => {
const contentType = 'image/jpeg';
const contentDisposition = 'attachment; filename="test.jpg"';
const cacheControl = 'max-age=3600';
await getUrlWrapper({
key: 'key',
options: {
method: 'PUT',
contentType,
contentDisposition,
cacheControl,
},
});
expect(getPresignedPutObjectUrl).toHaveBeenCalledTimes(1);
await expect(getPresignedPutObjectUrl).toBeLastCalledWithConfigAndInput(
{
credentials,
region,
expiration: expect.any(Number),
},
{
Bucket: bucket,
Key: 'public/key',
ContentType: contentType,
ContentDisposition: contentDisposition,
CacheControl: cacheControl,
},
);
});

it('should handle object content disposition for PUT', async () => {
await getUrlWrapper({
key: 'key',
options: {
method: 'PUT',
contentDisposition: {
type: 'attachment',
filename: 'test.pdf',
},
},
});
expect(getPresignedPutObjectUrl).toHaveBeenCalledTimes(1);
await expect(getPresignedPutObjectUrl).toBeLastCalledWithConfigAndInput(
{
credentials,
region,
expiration: expect.any(Number),
},
{
Bucket: bucket,
Key: 'public/key',
ContentDisposition: 'attachment; filename="test.pdf"',
},
);
});
});
});
describe('Error cases : With key', () => {
afterAll(() => {
Expand Down Expand Up @@ -285,6 +368,7 @@ describe('getUrl test with path', () => {
$metadata: {} as any,
});
jest.mocked(getPresignedGetObjectUrl).mockResolvedValue(mockURL);
jest.mocked(getPresignedPutObjectUrl).mockResolvedValue(mockURL);
});
afterEach(() => {
jest.clearAllMocks();
Expand Down Expand Up @@ -418,6 +502,56 @@ describe('getUrl test with path', () => {
);
});
});

describe('method PUT for presigned upload URLs with path', () => {
it('should generate PUT presigned URL with path and skip validation', async () => {
const inputPath = 'uploads/file.jpg';
await getUrlWrapper({
path: inputPath,
options: {
method: 'PUT',
validateObjectExistence: true,
},
});
expect(getPresignedPutObjectUrl).toHaveBeenCalledTimes(1);
expect(headObject).not.toHaveBeenCalled();
await expect(getPresignedPutObjectUrl).toBeLastCalledWithConfigAndInput(
{
credentials,
region,
expiration: expect.any(Number),
},
{
Bucket: bucket,
Key: inputPath,
},
);
});

it('should include expectedBucketOwner for PUT with path', async () => {
const inputPath = 'uploads/file.jpg';
await getUrlWrapper({
path: inputPath,
options: {
method: 'PUT',
expectedBucketOwner: validBucketOwner,
},
});
expect(getPresignedPutObjectUrl).toHaveBeenCalledTimes(1);
await expect(getPresignedPutObjectUrl).toBeLastCalledWithConfigAndInput(
{
credentials,
region,
expiration: expect.any(Number),
},
{
Bucket: bucket,
Key: inputPath,
ExpectedBucketOwner: validBucketOwner,
},
);
});
});
});
describe('Happy cases: With path and Content Disposition, Content Type', () => {
const config = {
Expand All @@ -435,6 +569,7 @@ describe('getUrl test with path', () => {
$metadata: {} as any,
});
jest.mocked(getPresignedGetObjectUrl).mockResolvedValue(mockURL);
jest.mocked(getPresignedPutObjectUrl).mockResolvedValue(mockURL);
});
afterEach(() => {
jest.clearAllMocks();
Expand Down Expand Up @@ -500,6 +635,7 @@ describe('getUrl test with path', () => {
$metadata: {} as any,
});
jest.mocked(getPresignedGetObjectUrl).mockResolvedValue(mockURL);
jest.mocked(getPresignedPutObjectUrl).mockResolvedValue(mockURL);
});

afterEach(() => {
Expand Down Expand Up @@ -660,4 +796,30 @@ describe(`getURL with path and Expected Bucket Owner`, () => {

expect(getPresignedGetObjectUrl).not.toHaveBeenCalled();
});

it('should pass expectedBucketOwner to getPresignedPutObjectUrl for PUT method', async () => {
const path = 'public/expectedbucketowner_test';

await getUrlWrapper({
path,
options: {
method: 'PUT',
expectedBucketOwner: validBucketOwner,
},
});

expect(getPresignedPutObjectUrl).toHaveBeenCalledTimes(1);
await expect(getPresignedPutObjectUrl).toBeLastCalledWithConfigAndInput(
{
credentials,
region,
expiration: expect.any(Number),
},
{
Bucket: bucket,
ExpectedBucketOwner: validBucketOwner,
Key: path,
},
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import {
UNSIGNED_PAYLOAD,
presignUrl,
} from '@aws-amplify/core/internals/aws-client-utils';

import { getPresignedPutObjectUrl } from '../../../../../../src/providers/s3/utils/client/s3data';

import { defaultConfigWithStaticCredentials } from './cases/shared';

jest.mock('@aws-amplify/core/internals/aws-client-utils', () => {
const original = jest.requireActual(
'@aws-amplify/core/internals/aws-client-utils',
);
const { presignUrl: getPresignedUrl } = original;

return {
...original,
presignUrl: jest.fn((...args) => getPresignedUrl(...args)),
};
});

const mockPresignUrl = presignUrl as jest.Mock;

describe('getPresignedPutObjectUrl', () => {
it('should return put object API request', async () => {
const actual = await getPresignedPutObjectUrl(
{
...defaultConfigWithStaticCredentials,
signingRegion: defaultConfigWithStaticCredentials.region,
signingService: 's3',
expiration: 900,
userAgentValue: 'UA',
},
{
Bucket: 'bucket',
Key: 'key',
},
);
const actualUrl = actual;
expect(actualUrl.hostname).toEqual(
`bucket.s3.${defaultConfigWithStaticCredentials.region}.amazonaws.com`,
);
expect(actualUrl.pathname).toEqual('/key');
expect(actualUrl.searchParams.get('X-Amz-Expires')).toEqual('900');
expect(actualUrl.searchParams.get('x-amz-user-agent')).toEqual('UA');
});

it('should call presignUrl with uriEscapePath param set to false', async () => {
await getPresignedPutObjectUrl(
{
...defaultConfigWithStaticCredentials,
signingRegion: defaultConfigWithStaticCredentials.region,
signingService: 's3',
expiration: 900,
userAgentValue: 'UA',
},
{
Bucket: 'bucket',
Key: 'key',
},
);

expect(mockPresignUrl).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
uriEscapePath: false,
}),
);
});

it('should return put object API request with content type and disposition', async () => {
const actual = await getPresignedPutObjectUrl(
{
...defaultConfigWithStaticCredentials,
signingRegion: defaultConfigWithStaticCredentials.region,
signingService: 's3',
expiration: 900,
userAgentValue: 'UA',
},
{
Bucket: 'bucket',
Key: 'key',
ContentType: 'image/jpeg',
ContentDisposition: 'attachment; filename="photo.jpg"',
},
);

expect(actual).toEqual(
expect.objectContaining({
hostname: `bucket.s3.${defaultConfigWithStaticCredentials.region}.amazonaws.com`,
pathname: '/key',
searchParams: expect.objectContaining({
get: expect.any(Function),
}),
}),
);

expect(actual.searchParams.get('X-Amz-Expires')).toBe('900');
expect(actual.searchParams.get('content-type')).toBe('image/jpeg');
expect(actual.searchParams.get('content-disposition')).toBe(
'attachment; filename="photo.jpg"',
);
expect(actual.searchParams.get('x-amz-user-agent')).toBe('UA');
});

it('should use UNSIGNED-PAYLOAD for presigned URLs', async () => {
mockPresignUrl.mockClear();

const result = await getPresignedPutObjectUrl(
{
...defaultConfigWithStaticCredentials,
signingRegion: defaultConfigWithStaticCredentials.region,
signingService: 's3',
expiration: 900,
},
{
Bucket: 'bucket',
Key: 'key',
},
);

expect(mockPresignUrl).toHaveBeenCalledWith(
expect.objectContaining({
body: UNSIGNED_PAYLOAD,
}),
expect.anything(),
);

expect(result.searchParams.get('x-amz-content-sha256')).toBeNull();
});
});
1 change: 1 addition & 0 deletions packages/storage/src/internals/apis/getUrl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export const getUrl = (input: GetUrlInput) =>
// Advanced options
locationCredentialsProvider: input?.options?.locationCredentialsProvider,
customEndpoint: input?.options?.customEndpoint,
method: input?.options?.method,
},
// Type casting is necessary because `getPropertiesInternal` supports both Gen1 and Gen2 signatures, but here
// given in input can only be Gen2 signature, the return can only ben Gen2 signature.
Expand Down
2 changes: 1 addition & 1 deletion packages/storage/src/providers/s3/apis/getUrl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
import { getUrl as getUrlInternal } from './internal/getUrl';

/**
* Get a temporary presigned URL to download the specified S3 object.
* Get a temporary presigned URL to download or upload the specified S3 object.
* The presigned URL expires when the associated role used to sign the request expires or
* the option `expiresIn` is reached. The `expiresAt` property in the output object indicates when the URL MAY expire.
*
Expand Down
Loading
Loading