Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: DownloadData refactor #13073

Merged
merged 20 commits into from
Mar 11, 2024
Merged
Show file tree
Hide file tree
Changes from 19 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 @@ -466,7 +466,7 @@
"name": "[Storage] downloadData (S3)",
"path": "./dist/esm/storage/index.mjs",
"import": "{ downloadData }",
"limit": "14.00 kB"
"limit": "14.05 kB"
},
{
"name": "[Storage] getProperties (S3)",
Expand Down
173 changes: 155 additions & 18 deletions packages/storage/__tests__/providers/s3/apis/downloadData.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,18 @@ import { AWSCredentials } from '@aws-amplify/core/internals/utils';
import { Amplify } from '@aws-amplify/core';
import { getObject } from '../../../../src/providers/s3/utils/client';
import { downloadData } from '../../../../src/providers/s3';
import { createDownloadTask } from '../../../../src/providers/s3/utils';
import { DownloadDataOptions } from '../../../../src/providers/s3/types';
import {
createDownloadTask,
validateStorageOperationInput,
} from '../../../../src/providers/s3/utils';
import {
DownloadDataOptionsKey,
DownloadDataOptionsPath,
} from '../../../../src/providers/s3/types';
import {
STORAGE_INPUT_KEY,
STORAGE_INPUT_PATH,
} from '../../../../src/providers/s3/utils/constants';

jest.mock('../../../../src/providers/s3/utils/client');
jest.mock('../../../../src/providers/s3/utils');
Expand Down Expand Up @@ -34,9 +44,10 @@ const defaultIdentityId = 'defaultIdentityId';

const mockFetchAuthSession = Amplify.Auth.fetchAuthSession as jest.Mock;
const mockCreateDownloadTask = createDownloadTask as jest.Mock;
const mockValidateStorageInput = validateStorageOperationInput as jest.Mock;
const mockGetConfig = Amplify.getConfig as jest.Mock;

describe('downloadData', () => {
describe('downloadData with key', () => {
beforeAll(() => {
mockFetchAuthSession.mockResolvedValue({
credentials,
Expand All @@ -52,16 +63,20 @@ describe('downloadData', () => {
});
});
mockCreateDownloadTask.mockReturnValue('downloadTask');
mockValidateStorageInput.mockReturnValue({
inputType: STORAGE_INPUT_KEY,
objectKey: key,
});

beforeEach(() => {
jest.clearAllMocks();
});

it('should return a download task', async () => {
it('should return a download task with key', async () => {
expect(downloadData({ key: 'key' })).toBe('downloadTask');
});

[
test.each([
{
expectedKey: `public/${key}`,
},
Expand All @@ -81,14 +96,9 @@ describe('downloadData', () => {
options: { accessLevel: 'protected', targetIdentityId },
expectedKey: `protected/${targetIdentityId}/${key}`,
},
].forEach(({ options, expectedKey }) => {
const accessLevelMsg = options?.accessLevel ?? 'default';
const targetIdentityIdMsg = options?.targetIdentityId
? `and targetIdentityId`
: '';

it(`should supply the correct parameters to getObject API handler with ${accessLevelMsg} accessLevel ${targetIdentityIdMsg}`, async () => {
expect.assertions(2);
])(
'should supply the correct parameters to getObject API handler with $expectedKey accessLevel',
async ({ options, expectedKey }) => {
(getObject as jest.Mock).mockResolvedValueOnce({ Body: 'body' });
const onProgress = jest.fn();
downloadData({
Expand All @@ -97,7 +107,7 @@ describe('downloadData', () => {
...options,
useAccelerateEndpoint: true,
onProgress,
} as DownloadDataOptions,
} as DownloadDataOptionsKey,
});
const job = mockCreateDownloadTask.mock.calls[0][0].job;
await job();
Expand All @@ -116,11 +126,10 @@ describe('downloadData', () => {
Key: expectedKey,
},
);
});
});
},
);

it('should assign the getObject API handler response to the result', async () => {
expect.assertions(2);
it('should assign the getObject API handler response to the result with key', async () => {
const lastModified = 'lastModified';
const contentLength = 'contentLength';
const eTag = 'eTag';
Expand Down Expand Up @@ -177,3 +186,131 @@ describe('downloadData', () => {
);
});
});

describe('downloadData with path', () => {
beforeAll(() => {
mockFetchAuthSession.mockResolvedValue({
credentials,
identityId: defaultIdentityId,
});
mockGetConfig.mockReturnValue({
Storage: {
S3: {
bucket,
region,
},
},
});
mockCreateDownloadTask.mockReturnValue('downloadTask');
mockValidateStorageInput.mockReturnValue({
inputType: STORAGE_INPUT_PATH,
objectKey: 'path',
});
});

beforeEach(() => {
jest.clearAllMocks();
});

it('should return a download task with path', async () => {
expect(downloadData({ path: 'path' })).toBe('downloadTask');
});

test.each([
{
path: 'path',
expectedKey: 'path',
},
{
path: () => 'path',
expectedKey: 'path',
},
])(
'should call getObject API with $expectedKey when path provided is $path',
async ({ path, expectedKey }) => {
(getObject as jest.Mock).mockResolvedValueOnce({ Body: 'body' });
const onProgress = jest.fn();
downloadData({
path: path,
options: {
useAccelerateEndpoint: true,
onProgress,
} as DownloadDataOptionsPath,
});
const job = mockCreateDownloadTask.mock.calls[0][0].job;
await job();
expect(getObject).toHaveBeenCalledTimes(1);
expect(getObject).toHaveBeenCalledWith(
{
credentials,
region,
useAccelerateEndpoint: true,
onDownloadProgress: onProgress,
abortSignal: expect.any(AbortSignal),
userAgentValue: expect.any(String),
},
{
Bucket: bucket,
Key: expectedKey,
},
);
},
);

it('should assign the getObject API handler response to the result with path', async () => {
const lastModified = 'lastModified';
const contentLength = 'contentLength';
const eTag = 'eTag';
const metadata = 'metadata';
const versionId = 'versionId';
const contentType = 'contentType';
const body = 'body';
const path = 'path';
(getObject as jest.Mock).mockResolvedValueOnce({
Body: body,
LastModified: lastModified,
ContentLength: contentLength,
ETag: eTag,
Metadata: metadata,
VersionId: versionId,
ContentType: contentType,
});
downloadData({ path });
const job = mockCreateDownloadTask.mock.calls[0][0].job;
const result = await job();
expect(getObject).toHaveBeenCalledTimes(1);
expect(result).toEqual({
path,
body,
lastModified,
size: contentLength,
eTag,
metadata,
versionId,
contentType,
});
});

it('should forward the bytes range option to the getObject API', async () => {
const start = 1;
const end = 100;
(getObject as jest.Mock).mockResolvedValueOnce({ Body: 'body' });

downloadData({
path: 'mockPath',
options: {
bytesRange: { start, end },
},
});

const job = mockCreateDownloadTask.mock.calls[0][0].job;
await job();

expect(getObject).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
Range: `bytes=${start}-${end}`,
}),
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import {
StorageValidationErrorCode,
validationErrorMap,
} from '../../../../../src/errors/types/validation';
import { validateStorageOperationInput } from '../../../../../src/providers/s3/utils';
import {
STORAGE_INPUT_KEY,
STORAGE_INPUT_PATH,
} from '../../../../../src/providers/s3/utils/constants';

describe('validateStorageOperationInput', () => {
it('should return inputType as STORAGE_INPUT_PATH and objectKey as testPath when input is path as string', () => {
const input = { path: 'testPath' };
const result = validateStorageOperationInput(input);
expect(result).toEqual({
inputType: STORAGE_INPUT_PATH,
objectKey: 'testPath',
});
});

it('should return inputType as STORAGE_INPUT_PATH and objectKey as result of path function when input is path as function', () => {
const input = {
path: ({ identityId }: { identityId?: string }) =>
`testPath/${identityId}`,
};
const result = validateStorageOperationInput(input, '123');
expect(result).toEqual({
inputType: STORAGE_INPUT_PATH,
objectKey: 'testPath/123',
});
});

it('should return inputType as STORAGE_INPUT_KEY and objectKey as testKey when input is key', () => {
const input = { key: 'testKey' };
const result = validateStorageOperationInput(input);
expect(result).toEqual({
inputType: STORAGE_INPUT_KEY,
objectKey: 'testKey',
});
});

it('should throw an error when input is invalid', () => {
const input = { invalid: 'test' } as any;
expect(() => validateStorageOperationInput(input)).toThrow(
validationErrorMap[
StorageValidationErrorCode.InvalidStorageOperationInput
].message,
);
});
});
4 changes: 4 additions & 0 deletions packages/storage/src/errors/types/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export enum StorageValidationErrorCode {
UrlExpirationMaxLimitExceed = 'UrlExpirationMaxLimitExceed',
ObjectIsTooLarge = 'ObjectIsTooLarge',
InvalidUploadSource = 'InvalidUploadSource',
InvalidStorageOperationInput = 'InvalidStorageOperationInput',
}

export const validationErrorMap: AmplifyErrorMap<StorageValidationErrorCode> = {
Expand Down Expand Up @@ -49,4 +50,7 @@ export const validationErrorMap: AmplifyErrorMap<StorageValidationErrorCode> = {
message:
'Upload source type can only be a `Blob`, `File`, `ArrayBuffer`, or `string`.',
},
[StorageValidationErrorCode.InvalidStorageOperationInput]: {
message: 'Missing path or key parameter in Input',
ashika112 marked this conversation as resolved.
Show resolved Hide resolved
},
};
Loading
Loading