diff --git a/packages/analytics/src/index.ts b/packages/analytics/src/index.ts index 0a3ebe04134..215d6c33eab 100644 --- a/packages/analytics/src/index.ts +++ b/packages/analytics/src/index.ts @@ -1,5 +1,10 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -export * from './providers/pinpoint'; +export { + record, + identifyUser, + RecordInput, + IdentifyUserInput, +} from './providers/pinpoint'; export { AnalyticsError } from './errors'; diff --git a/packages/analytics/src/providers/pinpoint/apis/identifyUser.ts b/packages/analytics/src/providers/pinpoint/apis/identifyUser.ts index c8a303478e2..b86f24a465d 100644 --- a/packages/analytics/src/providers/pinpoint/apis/identifyUser.ts +++ b/packages/analytics/src/providers/pinpoint/apis/identifyUser.ts @@ -5,21 +5,56 @@ import { AnalyticsAction } from '@aws-amplify/core/internals/utils'; import { updateEndpoint } from '@aws-amplify/core/internals/providers/pinpoint'; import { AnalyticsValidationErrorCode } from '../../../errors'; import { getAnalyticsUserAgentString } from '../../../utils/userAgent'; -import { IdentifyUserParameters, UpdateEndpointException } from '../types'; +import { IdentifyUserInput, UpdateEndpointException } from '../types'; import { resolveConfig, resolveCredentials } from '../utils'; /** - * Identifies the current user with Pinpoint. + * Sends information about a user to Pinpoint. Sending user information allows you to associate a user to their user + * profile and activities or actions in your application. Activity can be tracked across devices & platforms by using + * the same `userId`. * - * @param {IdentifyUserParameters} params parameters used to construct requests sent to Pinpoint's UpdateEndpoint API. + * @param {IdentifyUserParameters} params The input object used to construct requests sent to Pinpoint's UpdateEndpoint + * API. * - * @throws An {@link UpdateEndpointException} when the underlying Pinpoint service returns an error. - * @throws An {@link AnalyticsValidationErrorCode} when API call parameters are invalid. + * @throws service: {@link UpdateEndpointException} - Thrown when the underlying Pinpoint service returns an error. + * @throws validation: {@link AnalyticsValidationErrorCode} - Thrown when the provided parameters or library + * configuration is incorrect. + * + * @returns A promise that will resolve when the operation is complete. + * + * @example + * ```ts + * // Identify a user with Pinpoint + * await identifyUser({ + * userId, + * userProfile: { + * attributes: { + * email: [userEmail], + * }, + * } + * }); + * ``` + * + * @example + * ```ts + * // Identify a user with Pinpoint with some additional demographics + * await identifyUser({ + * userId, + * userProfile: { + * attributes: { + * email: [userEmail], + * }, + * demographic: { + * platform: 'ios', + * timezone: 'America/Los_Angeles' + * } + * } + * }); */ export const identifyUser = async ({ userId, userProfile, -}: IdentifyUserParameters): Promise => { +}: IdentifyUserInput): Promise => { const { credentials, identityId } = await resolveCredentials(); const { appId, region } = resolveConfig(); updateEndpoint({ diff --git a/packages/analytics/src/providers/pinpoint/apis/record.ts b/packages/analytics/src/providers/pinpoint/apis/record.ts index a7bd16d9696..1df99786efb 100644 --- a/packages/analytics/src/providers/pinpoint/apis/record.ts +++ b/packages/analytics/src/providers/pinpoint/apis/record.ts @@ -11,21 +11,46 @@ import { assertValidationError, } from '../../../errors'; import { getAnalyticsUserAgentString } from '../../../utils/userAgent'; -import { RecordParameters } from '../types/parameters'; +import { RecordInput } from '../types'; import { resolveConfig, resolveCredentials } from '../utils'; const logger = new Logger('Analytics'); /** - * Sends an event to Pinpoint. + * Records an Analytic event to Pinpoint. Events will be buffered and periodically sent to Pinpoint. * - * @param {RecordParameters} params Parameters used to construct the request. + * @param {RecordInput} params The input object used to construct the request. * - * @throws An {@link AnalyticsValidationErrorCode} when there is an error in the parameters or configuration. + * @throws validation: {@link AnalyticsValidationErrorCode} - Thrown when the provided parameters or library + * configuration is incorrect. * - * @returns A promise that will resolve when the request is complete. + * @example + * ```ts + * // Send an event to Pinpoint + * record({ + * event: { + * name: eventName, + * } + * }) + * ``` + * + * @example + * ```ts + * // Send an event to Pinpoint with metrics & custom attributes + * record({ + * event: { + * name: eventName, + * attributes: { + * 'my-attribute': attributeValue + * }, + * metrics: { + * 'my-metric': metricValue + * } + * } + * }) + * ``` */ -export const record = ({ event }: RecordParameters): void => { +export const record = ({ event }: RecordInput): void => { const { appId, region } = resolveConfig(); assertValidationError(!!event, AnalyticsValidationErrorCode.NoEvent); diff --git a/packages/analytics/src/providers/pinpoint/index.ts b/packages/analytics/src/providers/pinpoint/index.ts index 6206929b659..c7ae7f368b1 100644 --- a/packages/analytics/src/providers/pinpoint/index.ts +++ b/packages/analytics/src/providers/pinpoint/index.ts @@ -1,4 +1,5 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -export * from './apis'; +export { record, identifyUser } from './apis'; +export { RecordInput, IdentifyUserInput } from './types/inputs'; diff --git a/packages/analytics/src/providers/pinpoint/types/index.ts b/packages/analytics/src/providers/pinpoint/types/index.ts index d938f2d9631..f64d7479665 100644 --- a/packages/analytics/src/providers/pinpoint/types/index.ts +++ b/packages/analytics/src/providers/pinpoint/types/index.ts @@ -2,4 +2,4 @@ // SPDX-License-Identifier: Apache-2.0 export { UpdateEndpointException } from './errors'; -export { IdentifyUserParameters } from './parameters'; +export { RecordInput, IdentifyUserInput } from './inputs'; diff --git a/packages/analytics/src/providers/pinpoint/types/parameters.ts b/packages/analytics/src/providers/pinpoint/types/inputs.ts similarity index 87% rename from packages/analytics/src/providers/pinpoint/types/parameters.ts rename to packages/analytics/src/providers/pinpoint/types/inputs.ts index 35d46c34c29..feae0a39fbd 100644 --- a/packages/analytics/src/providers/pinpoint/types/parameters.ts +++ b/packages/analytics/src/providers/pinpoint/types/inputs.ts @@ -4,14 +4,14 @@ import { UserProfile } from '@aws-amplify/core'; import { PinpointAnalyticsEvent } from '@aws-amplify/core/internals/providers/pinpoint'; -export type RecordParameters = { +export type RecordInput = { /** * An event to send to the default Analytics provider. */ event: PinpointAnalyticsEvent; }; -export type IdentifyUserParameters = { +export type IdentifyUserInput = { /** * A User ID associated to the current device. */ diff --git a/packages/core/src/providers/pinpoint/apis/record.ts b/packages/core/src/providers/pinpoint/apis/record.ts index 6b92b75d4de..3453bb022c1 100644 --- a/packages/core/src/providers/pinpoint/apis/record.ts +++ b/packages/core/src/providers/pinpoint/apis/record.ts @@ -2,9 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 import { v4 as uuid } from 'uuid'; -import { PinpointRecordParameters, PinpointSession } from '../types'; +import { PinpointRecordInput, PinpointSession } from '../types'; import { getEndpointId } from '../utils'; -import { +import { BUFFER_SIZE, FLUSH_INTERVAL, FLUSH_SIZE, @@ -28,11 +28,11 @@ export const record = async ({ identityId, region, userAgentValue, -}: PinpointRecordParameters): Promise => { +}: PinpointRecordInput): Promise => { const timestampISOString = new Date().toISOString(); const eventId = uuid(); let endpointId = await getEndpointId(appId, category); - + // Prepare event buffer if required const buffer = getEventBuffer({ appId, @@ -43,7 +43,7 @@ export const record = async ({ identityId, region, resendLimit: RESEND_LIMIT, - userAgentValue + userAgentValue, }); // Prepare a Pinpoint endpoint via updateEndpoint if one does not already exist, which will generate and cache an @@ -64,7 +64,7 @@ export const record = async ({ if (!endpointId) { throw new AmplifyError({ name: 'ENDPOINT_NOT_CREATED', - message: 'Endpoint was not created.' + message: 'Endpoint was not created.', }); } @@ -77,7 +77,7 @@ export const record = async ({ StartTimestamp: timestampISOString, }; } - + // Push event to buffer buffer.push({ eventId, @@ -85,6 +85,6 @@ export const record = async ({ event, session, timestamp: timestampISOString, - resendLimit: RESEND_LIMIT + resendLimit: RESEND_LIMIT, }); }; diff --git a/packages/core/src/providers/pinpoint/apis/updateEndpoint.ts b/packages/core/src/providers/pinpoint/apis/updateEndpoint.ts index 849c29aa627..fec31197293 100644 --- a/packages/core/src/providers/pinpoint/apis/updateEndpoint.ts +++ b/packages/core/src/providers/pinpoint/apis/updateEndpoint.ts @@ -7,7 +7,7 @@ import { updateEndpoint as clientUpdateEndpoint, UpdateEndpointInput, } from '../../../AwsClients/Pinpoint'; -import { PinpointUpdateEndpointParameters } from '../types'; +import { PinpointUpdateEndpointInput } from '../types'; import { cacheEndpointId, getEndpointId } from '../utils'; /** @@ -25,7 +25,7 @@ export const updateEndpoint = async ({ userId, userProfile, userAgentValue, -}: PinpointUpdateEndpointParameters): Promise => { +}: PinpointUpdateEndpointInput): Promise => { const endpointId = await getEndpointId(appId, category); // only generate a new endpoint id if one was not found in cache const createdEndpointId = !endpointId ? uuidv4() : undefined; diff --git a/packages/core/src/providers/pinpoint/types/pinpoint.ts b/packages/core/src/providers/pinpoint/types/pinpoint.ts index 545e75d6cbb..301f6341a2e 100644 --- a/packages/core/src/providers/pinpoint/types/pinpoint.ts +++ b/packages/core/src/providers/pinpoint/types/pinpoint.ts @@ -44,13 +44,13 @@ type PinpointCommonParameters = { userAgentValue?: string; }; -export type PinpointUpdateEndpointParameters = PinpointCommonParameters & +export type PinpointUpdateEndpointInput = PinpointCommonParameters & PinpointServiceOptions & { channelType?: SupportedChannelType; userId?: string; userProfile?: UserProfile; }; -export type PinpointRecordParameters = PinpointCommonParameters & { +export type PinpointRecordInput = PinpointCommonParameters & { event: PinpointAnalyticsEvent; }; diff --git a/packages/storage/__tests__/providers/s3/apis/copy.test.ts b/packages/storage/__tests__/providers/s3/apis/copy.test.ts index 83d8dbd2176..41f40ac660a 100644 --- a/packages/storage/__tests__/providers/s3/apis/copy.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/copy.test.ts @@ -5,6 +5,10 @@ import { Credentials } from '@aws-sdk/types'; import { Amplify, StorageAccessLevel } from '@aws-amplify/core'; import { copyObject } from '../../../../src/providers/s3/utils/client'; import { copy } from '../../../../src/providers/s3/apis'; +import { + StorageCopySourceOptions, + StorageCopyDestinationOptions, +} from '../../../../src/types'; jest.mock('../../../../src/providers/s3/utils/client'); jest.mock('@aws-amplify/core', () => ({ @@ -24,6 +28,7 @@ const destinationKey = 'destinationKey'; const bucket = 'bucket'; const region = 'region'; const targetIdentityId = 'targetIdentityId'; +const defaultIdentityId = 'defaultIdentityId'; const copyResult = { key: destinationKey }; const credentials: Credentials = { accessKeyId: 'accessKeyId', @@ -39,72 +44,17 @@ const copyObjectClientBaseParams = { MetadataDirective: 'COPY', }; -/** - * bucket is appended at start if it's a sourceKey - * guest: public/${key}` - * private: private/${targetIdentityId}/${key}` - * protected: protected/${targetIdentityId}/${key}` - */ -const buildClientRequestKey = ( - key: string, - KeyType: 'source' | 'destination', - accessLevel: StorageAccessLevel -) => { - const targetIdentityId = 'targetIdentityId'; - const bucket = 'bucket'; - const finalAccessLevel = accessLevel == 'guest' ? 'public' : accessLevel; - let finalKey = KeyType == 'source' ? `${bucket}/` : ''; - finalKey += `${finalAccessLevel}/`; - finalKey += finalAccessLevel != 'public' ? `${targetIdentityId}/` : ''; - finalKey += `${key}`; - return finalKey; -}; - -const interAccessLevelTest = async ( - sourceAccessLevel, - destinationAccessLevel -) => { - expect.assertions(3); - const source = { - key: sourceKey, - accessLevel: sourceAccessLevel, - }; - sourceAccessLevel == 'protected' - ? (source['targetIdentityId'] = targetIdentityId) - : null; - - expect( - await copy({ - source, - destination: { - key: destinationKey, - accessLevel: destinationAccessLevel, - }, - }) - ).toEqual(copyResult); - expect(copyObject).toBeCalledTimes(1); - expect(copyObject).toHaveBeenCalledWith(copyObjectClientConfig, { - ...copyObjectClientBaseParams, - CopySource: buildClientRequestKey(sourceKey, 'source', sourceAccessLevel), - Key: buildClientRequestKey( - destinationKey, - 'destination', - destinationAccessLevel - ), - }); -}; - describe('copy API', () => { beforeAll(() => { mockFetchAuthSession.mockResolvedValue({ credentials, - identityId: targetIdentityId, + identityId: defaultIdentityId, }); mockGetConfig.mockReturnValue({ Storage: { S3: { - bucket: 'bucket', - region: 'region', + bucket, + region, }, }, }); @@ -120,40 +70,114 @@ describe('copy API', () => { afterEach(() => { jest.clearAllMocks(); }); - - describe('Copy from guest to all access levels', () => { - it('Should copy guest -> guest', async () => - await interAccessLevelTest('guest', 'guest')); - it('Should copy guest -> private', async () => - await interAccessLevelTest('guest', 'private')); - it('Should copy guest -> protected', async () => - await interAccessLevelTest('guest', 'protected')); - }); - - describe('Copy from private to all access levels', () => { - it('Should copy private -> guest', async () => - await interAccessLevelTest('private', 'guest')); - it('Should copy private -> private', async () => - await interAccessLevelTest('private', 'private')); - it('Should copy private -> protected', async () => - await interAccessLevelTest('private', 'protected')); - }); - - describe('Copy from protected to all access levels', () => { - it('Should copy protected -> guest', async () => - await interAccessLevelTest('protected', 'guest')); - it('Should copy protected -> private', async () => - await interAccessLevelTest('protected', 'private')); - it('Should copy protected -> protected', async () => - await interAccessLevelTest('protected', 'protected')); - }); + [ + { + source: { accessLevel: 'guest' }, + destination: { accessLevel: 'guest' }, + expectedSourceKey: `${bucket}/public/${sourceKey}`, + expectedDestinationKey: `public/${destinationKey}`, + }, + { + source: { accessLevel: 'guest' }, + destination: { accessLevel: 'private' }, + expectedSourceKey: `${bucket}/public/${sourceKey}`, + expectedDestinationKey: `private/${defaultIdentityId}/${destinationKey}`, + }, + { + source: { accessLevel: 'guest' }, + destination: { accessLevel: 'protected' }, + expectedSourceKey: `${bucket}/public/${sourceKey}`, + expectedDestinationKey: `protected/${defaultIdentityId}/${destinationKey}`, + }, + { + source: { accessLevel: 'private' }, + destination: { accessLevel: 'guest' }, + expectedSourceKey: `${bucket}/private/${defaultIdentityId}/${sourceKey}`, + expectedDestinationKey: `public/${destinationKey}`, + }, + { + source: { accessLevel: 'private' }, + destination: { accessLevel: 'private' }, + expectedSourceKey: `${bucket}/private/${defaultIdentityId}/${sourceKey}`, + expectedDestinationKey: `private/${defaultIdentityId}/${destinationKey}`, + }, + { + source: { accessLevel: 'private' }, + destination: { accessLevel: 'protected' }, + expectedSourceKey: `${bucket}/private/${defaultIdentityId}/${sourceKey}`, + expectedDestinationKey: `protected/${defaultIdentityId}/${destinationKey}`, + }, + { + source: { accessLevel: 'protected' }, + destination: { accessLevel: 'guest' }, + expectedSourceKey: `${bucket}/protected/${defaultIdentityId}/${sourceKey}`, + expectedDestinationKey: `public/${destinationKey}`, + }, + { + source: { accessLevel: 'protected' }, + destination: { accessLevel: 'private' }, + expectedSourceKey: `${bucket}/protected/${defaultIdentityId}/${sourceKey}`, + expectedDestinationKey: `private/${defaultIdentityId}/${destinationKey}`, + }, + { + source: { accessLevel: 'protected' }, + destination: { accessLevel: 'protected' }, + expectedSourceKey: `${bucket}/protected/${defaultIdentityId}/${sourceKey}`, + expectedDestinationKey: `protected/${defaultIdentityId}/${destinationKey}`, + }, + { + source: { accessLevel: 'protected', targetIdentityId }, + destination: { accessLevel: 'guest' }, + expectedSourceKey: `${bucket}/protected/${targetIdentityId}/${sourceKey}`, + expectedDestinationKey: `public/${destinationKey}`, + }, + { + source: { accessLevel: 'protected', targetIdentityId }, + destination: { accessLevel: 'private' }, + expectedSourceKey: `${bucket}/protected/${targetIdentityId}/${sourceKey}`, + expectedDestinationKey: `private/${defaultIdentityId}/${destinationKey}`, + }, + { + source: { accessLevel: 'protected', targetIdentityId }, + destination: { accessLevel: 'protected' }, + expectedSourceKey: `${bucket}/protected/${targetIdentityId}/${sourceKey}`, + expectedDestinationKey: `protected/${defaultIdentityId}/${destinationKey}`, + }, + ].forEach( + ({ source, destination, expectedSourceKey, expectedDestinationKey }) => { + const targetIdentityIdMsg = source?.targetIdentityId + ? `with targetIdentityId` + : ''; + it(`should copy ${source.accessLevel} ${targetIdentityIdMsg} -> ${destination.accessLevel}`, async () => { + expect.assertions(3); + expect( + await copy({ + source: { + ...(source as StorageCopySourceOptions), + key: sourceKey, + }, + destination: { + ...(destination as StorageCopyDestinationOptions), + key: destinationKey, + }, + }) + ).toEqual(copyResult); + expect(copyObject).toBeCalledTimes(1); + expect(copyObject).toHaveBeenCalledWith(copyObjectClientConfig, { + ...copyObjectClientBaseParams, + CopySource: expectedSourceKey, + Key: expectedDestinationKey, + }); + }); + } + ); }); describe('Error Path Cases:', () => { afterEach(() => { jest.clearAllMocks(); }); - it('Should return a not found error', async () => { + it('should return a not found error', async () => { mockCopyObject.mockRejectedValueOnce( Object.assign(new Error(), { $metadata: { httpStatusCode: 404 }, diff --git a/packages/storage/__tests__/providers/s3/apis/downloadData.test.ts b/packages/storage/__tests__/providers/s3/apis/downloadData.test.ts index 7dec964df35..a4b095a6d90 100644 --- a/packages/storage/__tests__/providers/s3/apis/downloadData.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/downloadData.test.ts @@ -6,6 +6,7 @@ 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 { StorageOptions } from '../../../../src/types'; jest.mock('../../../../src/providers/s3/utils/client'); jest.mock('../../../../src/providers/s3/utils'); @@ -16,14 +17,17 @@ jest.mock('@aws-amplify/core', () => ({ fetchAuthSession: jest.fn(), }, }, - fetchAuthSession: jest.fn(), })); const credentials: Credentials = { accessKeyId: 'accessKeyId', sessionToken: 'sessionToken', secretAccessKey: 'secretAccessKey', }; -const identityId = 'identityId'; +const key = 'key'; +const bucket = 'bucket'; +const region = 'region'; +const targetIdentityId = 'targetIdentityId'; +const defaultIdentityId = 'defaultIdentityId'; const mockFetchAuthSession = Amplify.Auth.fetchAuthSession as jest.Mock; const mockCreateDownloadTask = createDownloadTask as jest.Mock; @@ -35,13 +39,13 @@ describe('downloadData', () => { beforeAll(() => { mockFetchAuthSession.mockResolvedValue({ credentials, - identityId: identityId, + identityId: defaultIdentityId, }); mockGetConfig.mockReturnValue({ Storage: { S3: { - bucket: 'bucket', - region: 'region', + bucket, + region, }, }, }); @@ -56,38 +60,61 @@ describe('downloadData', () => { expect(downloadData({ key: 'key' })).toBe('downloadTask'); }); - it('should supply the correct parameters to getObject API handler', async () => { - expect.assertions(2); - (getObject as jest.Mock).mockResolvedValueOnce({ Body: 'body' }); - const onProgress = jest.fn(); - const targetIdentityId = 'targetIdentityId'; - const accessLevel = 'protected'; - const key = 'key'; - downloadData({ - key, - options: { - targetIdentityId, - accessLevel, - useAccelerateEndpoint: true, - onProgress, - }, + [ + { + expectedKey: `public/${key}`, + }, + { + options: { accessLevel: 'guest' }, + expectedKey: `public/${key}`, + }, + { + options: { accessLevel: 'private' }, + expectedKey: `private/${defaultIdentityId}/${key}`, + }, + { + options: { accessLevel: 'protected' }, + expectedKey: `protected/${defaultIdentityId}/${key}`, + }, + { + 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); + (getObject as jest.Mock).mockResolvedValueOnce({ Body: 'body' }); + const onProgress = jest.fn(); + downloadData({ + key, + options: { + ...(options as StorageOptions), + useAccelerateEndpoint: true, + onProgress, + }, + }); + const job = mockCreateDownloadTask.mock.calls[0][0].job; + await job(); + expect(getObject).toBeCalledTimes(1); + expect(getObject).toHaveBeenCalledWith( + { + credentials, + region, + useAccelerateEndpoint: true, + onDownloadProgress: onProgress, + abortSignal: expect.any(AbortSignal), + }, + { + Bucket: bucket, + Key: expectedKey, + } + ); }); - const job = mockCreateDownloadTask.mock.calls[0][0].job; - await job(); - expect(getObject).toBeCalledTimes(1); - expect(getObject).toHaveBeenCalledWith( - { - credentials, - region: 'region', - useAccelerateEndpoint: true, - onDownloadProgress: onProgress, - abortSignal: expect.any(AbortSignal), - }, - { - Bucket: 'bucket', - Key: `${accessLevel}/${targetIdentityId}/${key}`, - } - ); }); it('should assign the getObject API handler response to the result', async () => { diff --git a/packages/storage/__tests__/providers/s3/apis/getProperties.test.ts b/packages/storage/__tests__/providers/s3/apis/getProperties.test.ts index 0485b8c5869..60765c2a95f 100644 --- a/packages/storage/__tests__/providers/s3/apis/getProperties.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/getProperties.test.ts @@ -28,13 +28,13 @@ const credentials: Credentials = { secretAccessKey: 'secretAccessKey', }; const targetIdentityId = 'targetIdentityId'; -const identityId = 'identityId'; +const defaultIdentityId = 'defaultIdentityId'; describe('getProperties api', () => { beforeAll(() => { mockFetchAuthSession.mockResolvedValue({ credentials, - identityId, + identityId: defaultIdentityId, }); mockGetConfig.mockReturnValue({ Storage: { @@ -73,26 +73,32 @@ describe('getProperties api', () => { afterEach(() => { jest.clearAllMocks(); }); - it.each([ + [ + { + expectedKey: `public/${key}`, + }, { options: { accessLevel: 'guest' }, - expectedKey: 'public/key', + expectedKey: `public/${key}`, }, { - options: { accessLevel: 'protected', targetIdentityId }, - expectedKey: 'protected/targetIdentityId/key', + options: { accessLevel: 'private' }, + expectedKey: `private/${defaultIdentityId}/${key}`, }, { options: { accessLevel: 'protected' }, - expectedKey: 'protected/identityId/key', + expectedKey: `protected/${defaultIdentityId}/${key}`, }, { - options: { accessLevel: 'private' }, - expectedKey: 'private/identityId/key', + options: { accessLevel: 'protected', targetIdentityId }, + expectedKey: `protected/${targetIdentityId}/${key}`, }, - ])( - 'getProperties api with $options.accessLevel', - async ({ options, expectedKey }) => { + ].forEach(({ options, expectedKey }) => { + const accessLevelMsg = options?.accessLevel ?? 'default'; + const targetIdentityIdMsg = options?.targetIdentityId + ? `and targetIdentityId` + : ''; + it(`should getProperties with ${accessLevelMsg} accessLevel ${targetIdentityIdMsg}`, async () => { const headObjectOptions = { Bucket: 'bucket', Key: expectedKey, @@ -106,8 +112,8 @@ describe('getProperties api', () => { ).toEqual(expected); expect(headObject).toBeCalledTimes(1); expect(headObject).toHaveBeenCalledWith(config, headObjectOptions); - } - ); + }); + }); }); describe('getProperties error path', () => { diff --git a/packages/storage/__tests__/providers/s3/apis/getUrl.test.ts b/packages/storage/__tests__/providers/s3/apis/getUrl.test.ts index 4cd7d7586bf..74b992f18c2 100644 --- a/packages/storage/__tests__/providers/s3/apis/getUrl.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/getUrl.test.ts @@ -30,13 +30,13 @@ const credentials: Credentials = { secretAccessKey: 'secretAccessKey', }; const targetIdentityId = 'targetIdentityId'; -const identityId = 'identityId'; +const defaultIdentityId = 'defaultIdentityId'; describe('getUrl test', () => { beforeAll(() => { mockFetchAuthSession.mockResolvedValue({ credentials, - identityId, + identityId: defaultIdentityId, }); mockGetConfig.mockReturnValue({ Storage: { @@ -47,10 +47,11 @@ describe('getUrl test', () => { }, }); }); + describe('getUrl happy path', () => { const config = { credentials, - region: 'region', + region, }; const key = 'key'; beforeEach(() => { @@ -71,40 +72,50 @@ describe('getUrl test', () => { afterEach(() => { jest.clearAllMocks(); }); - it.each([ + [ + { + expectedKey: `public/${key}`, + }, { options: { accessLevel: 'guest' }, - expectedKey: 'public/key', + expectedKey: `public/${key}`, }, { - options: { accessLevel: 'protected', targetIdentityId }, - expectedKey: 'protected/targetIdentityId/key', + options: { accessLevel: 'private' }, + expectedKey: `private/${defaultIdentityId}/${key}`, }, { options: { accessLevel: 'protected' }, - expectedKey: 'protected/identityId/key', + expectedKey: `protected/${defaultIdentityId}/${key}`, }, { - options: { accessLevel: 'private' }, - expectedKey: 'private/identityId/key', + options: { accessLevel: 'protected', targetIdentityId }, + expectedKey: `protected/${targetIdentityId}/${key}`, }, - ])('getUrl with $options.accessLevel', async ({ options, expectedKey }) => { - const headObjectOptions = { - Bucket: 'bucket', - Key: expectedKey, - }; - const optionsVal = { ...options, validateObjectExistence: true }; - - expect.assertions(4); - const result = await getUrl({ - key, - options: optionsVal as StorageOptions, - }); - expect(getPresignedGetObjectUrl).toBeCalledTimes(1); - expect(headObject).toBeCalledTimes(1); - expect(headObject).toHaveBeenCalledWith(config, headObjectOptions); - expect(result.url).toEqual({ - url: new URL('https://google.com'), + ].forEach(({ options, expectedKey }) => { + const accessLevelMsg = options?.accessLevel ?? 'default'; + const targetIdentityIdMsg = options?.targetIdentityId + ? `and targetIdentityId` + : ''; + it(`should getUrl with ${accessLevelMsg} accessLevel ${targetIdentityIdMsg}`, async () => { + const headObjectOptions = { + Bucket: bucket, + Key: expectedKey, + }; + expect.assertions(4); + const result = await getUrl({ + key, + options: { + ...(options as StorageOptions), + validateObjectExistence: true, + }, + }); + expect(getPresignedGetObjectUrl).toBeCalledTimes(1); + expect(headObject).toBeCalledTimes(1); + expect(headObject).toHaveBeenCalledWith(config, headObjectOptions); + expect(result.url).toEqual({ + url: new URL('https://google.com'), + }); }); }); }); @@ -112,7 +123,7 @@ describe('getUrl test', () => { afterAll(() => { jest.clearAllMocks(); }); - it('Should return not found error when the object is not found', async () => { + it('should return not found error when the object is not found', async () => { (headObject as jest.Mock).mockImplementation(() => Object.assign(new Error(), { $metadata: { httpStatusCode: 404 }, diff --git a/packages/storage/__tests__/providers/s3/apis/list.test.ts b/packages/storage/__tests__/providers/s3/apis/list.test.ts index b57d282f096..fecad15e696 100644 --- a/packages/storage/__tests__/providers/s3/apis/list.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/list.test.ts @@ -5,6 +5,7 @@ import { Credentials } from '@aws-sdk/types'; import { Amplify } from '@aws-amplify/core'; import { listObjectsV2 } from '../../../../src/providers/s3/utils/client'; import { list } from '../../../../src/providers/s3/apis'; +import { StorageOptions } from '../../../../src/types'; jest.mock('../../../../src/providers/s3/utils/client'); jest.mock('@aws-amplify/core', () => ({ @@ -19,10 +20,12 @@ const mockFetchAuthSession = Amplify.Auth.fetchAuthSession as jest.Mock; const mockGetConfig = Amplify.getConfig as jest.Mock; const mockListObject = listObjectsV2 as jest.Mock; const key = 'path/itemsKey'; +const path = key; const bucket = 'bucket'; const region = 'region'; const nextToken = 'nextToken'; const targetIdentityId = 'targetIdentityId'; +const defaultIdentityId = 'defaultIdentityId'; const eTag = 'eTag'; const lastModified = 'lastModified'; const size = 'size'; @@ -40,17 +43,11 @@ const listObjectClientBaseResultItem = { LastModified: lastModified, Size: size, }; -const copyResultItem = { - key, +const listResultItem = { eTag, lastModified, size, }; - -const listResultObj = { - ...listObjectClientBaseResultItem, - Key: `public/${key}`, -}; const mockListObjectsV2ApiWithPages = pages => { let methodCalls = 0; mockListObject.mockClear(); @@ -63,22 +60,18 @@ const mockListObjectsV2ApiWithPages = pages => { if (input.ContinuationToken === undefined || methodCalls < pages) { token = nextToken; } - if (input.Prefix === 'public/listALLResultsPath') { - return { - Contents: [listResultObj], - NextContinuationToken: token, - }; - } + return { + Contents: [{ ...listObjectClientBaseResultItem, Key: input.Prefix }], + NextContinuationToken: token, + }; }); }; -// TODO(ashwinkumar6) this currently only tests for guest -// Update to test across all accessLevels describe('list API', () => { beforeAll(() => { mockFetchAuthSession.mockResolvedValue({ credentials, - identityId: targetIdentityId, + identityId: defaultIdentityId, }); mockGetConfig.mockReturnValue({ Storage: { @@ -94,108 +87,176 @@ describe('list API', () => { jest.clearAllMocks(); }); - it('Should list objects with default params', async () => { - mockListObject.mockImplementationOnce(() => { - return { - Contents: [listResultObj], - NextContinuationToken: nextToken, - }; - }); + const accessLevelTests = [ + { + expectedPath: `public/`, + }, + { + path, + expectedPath: `public/${path}`, + }, + { + path, + options: { accessLevel: 'guest' }, + expectedPath: `public/${path}`, + }, + { + path, + options: { accessLevel: 'private' }, + expectedPath: `private/${defaultIdentityId}/${path}`, + }, + { + path, + options: { accessLevel: 'protected' }, + expectedPath: `protected/${defaultIdentityId}/${path}`, + }, + { + path, + options: { accessLevel: 'protected', targetIdentityId }, + expectedPath: `protected/${targetIdentityId}/${path}`, + }, + ]; - expect.assertions(4); - let response = await list(); - expect(response.items).toEqual([copyResultItem]); - expect(response.nextToken).toEqual(nextToken); - expect(listObjectsV2).toBeCalledTimes(1); - expect(listObjectsV2).toHaveBeenCalledWith(listObjectClientConfig, { - Bucket: bucket, - MaxKeys: 1000, - Prefix: 'public/', + accessLevelTests.forEach(({ path, options, expectedPath }) => { + const pathMsg = path ? 'custom' : 'default'; + const accessLevelMsg = options?.accessLevel ?? 'default'; + const targetIdentityIdMsg = options?.targetIdentityId + ? `with targetIdentityId` + : ''; + it(`should list objects with pagination, default pageSize, ${pathMsg} path, ${accessLevelMsg} accessLevel ${targetIdentityIdMsg}`, async () => { + mockListObject.mockImplementationOnce(() => { + return { + Contents: [ + { ...listObjectClientBaseResultItem, Key: expectedPath }, + ], + NextContinuationToken: nextToken, + }; + }); + expect.assertions(4); + let response = await list({ + prefix: path, + options: options as StorageOptions, + }); + expect(response.items).toEqual([ + { ...listResultItem, key: path ?? '' }, + ]); + expect(response.nextToken).toEqual(nextToken); + expect(listObjectsV2).toBeCalledTimes(1); + expect(listObjectsV2).toHaveBeenCalledWith(listObjectClientConfig, { + Bucket: bucket, + MaxKeys: 1000, + Prefix: expectedPath, + }); }); }); - it('Should list object with pagination using pageSize and nextToken', async () => { - mockListObject.mockImplementationOnce(() => { - return { - Contents: [listResultObj], - NextContinuationToken: nextToken, - }; - }); - - expect.assertions(4); - const customPageSize = 5; - const response = await list({ - prefix: 'listWithTokenResultsPath', - options: { - accessLevel: 'guest', - pageSize: customPageSize, - nextToken: nextToken, - }, - }); - expect(response.items).toEqual([copyResultItem]); - expect(response.nextToken).toEqual(nextToken); - expect(listObjectsV2).toBeCalledTimes(1); - expect(listObjectsV2).toHaveBeenCalledWith(listObjectClientConfig, { - Bucket: bucket, - Prefix: 'public/listWithTokenResultsPath', - ContinuationToken: nextToken, - MaxKeys: customPageSize, + accessLevelTests.forEach(({ path, options, expectedPath }) => { + const pathMsg = path ? 'custom' : 'default'; + const accessLevelMsg = options?.accessLevel ?? 'default'; + const targetIdentityIdMsg = options?.targetIdentityId + ? `with targetIdentityId` + : ''; + it(`should list objects with pagination using pageSize, nextToken, ${pathMsg} path, ${accessLevelMsg} accessLevel ${targetIdentityIdMsg}`, async () => { + mockListObject.mockImplementationOnce(() => { + return { + Contents: [ + { ...listObjectClientBaseResultItem, Key: expectedPath }, + ], + NextContinuationToken: nextToken, + }; + }); + expect.assertions(4); + const customPageSize = 5; + const response = await list({ + prefix: path, + options: { + ...(options as StorageOptions), + pageSize: customPageSize, + nextToken: nextToken, + }, + }); + expect(response.items).toEqual([ + { ...listResultItem, key: path ?? '' }, + ]); + expect(response.nextToken).toEqual(nextToken); + expect(listObjectsV2).toBeCalledTimes(1); + expect(listObjectsV2).toHaveBeenCalledWith(listObjectClientConfig, { + Bucket: bucket, + Prefix: expectedPath, + ContinuationToken: nextToken, + MaxKeys: customPageSize, + }); }); }); - it('Should list all objects successfully having three pages', async () => { - expect.assertions(5); - mockListObjectsV2ApiWithPages(3); - - const result = await list({ - prefix: 'listALLResultsPath', - options: { accessLevel: 'guest', listAll: true }, + accessLevelTests.forEach(({ path, options, expectedPath }) => { + const pathMsg = path ? 'custom' : 'default'; + const accessLevelMsg = options?.accessLevel ?? 'default'; + const targetIdentityIdMsg = options?.targetIdentityId + ? `with targetIdentityId` + : ''; + it(`should list objects with zero results with ${pathMsg} path, ${accessLevelMsg} accessLevel ${targetIdentityIdMsg}`, async () => { + mockListObject.mockImplementationOnce(() => { + return {}; + }); + expect.assertions(3); + let response = await list({ + prefix: path, + options: options as StorageOptions, + }); + expect(response.items).toEqual([]); + expect(response.nextToken).toEqual(undefined); + expect(listObjectsV2).toHaveBeenCalledWith(listObjectClientConfig, { + Bucket: bucket, + MaxKeys: 1000, + Prefix: expectedPath, + }); }); + }); - expect(result.items).toEqual([ - copyResultItem, - copyResultItem, - copyResultItem, - ]); - expect(result).not.toHaveProperty(nextToken); + accessLevelTests.forEach(({ path, options, expectedPath }) => { + const pathMsg = path ? 'custom' : 'default'; + const accessLevelMsg = options?.accessLevel ?? 'default'; + const targetIdentityIdMsg = options?.targetIdentityId + ? `with targetIdentityId` + : ''; + it(`should list all objects having three pages with ${pathMsg} path, ${accessLevelMsg} accessLevel ${targetIdentityIdMsg}`, async () => { + expect.assertions(5); + mockListObjectsV2ApiWithPages(3); + const result = await list({ + prefix: path, + options: { ...(options as StorageOptions), listAll: true }, + }); - // listing three times for three pages - expect(listObjectsV2).toHaveBeenCalledTimes(3); + const listResult = { ...listResultItem, key: path ?? '' }; + expect(result.items).toEqual([listResult, listResult, listResult]); + expect(result).not.toHaveProperty(nextToken); - // first input recieves undefined as the Continuation Token - expect(listObjectsV2).toHaveBeenNthCalledWith(1, listObjectClientConfig, { - Bucket: bucket, - Prefix: 'public/listALLResultsPath', - MaxKeys: 1000, - ContinuationToken: undefined, - }); - // last input recieves TEST_TOKEN as the Continuation Token - expect(listObjectsV2).toHaveBeenNthCalledWith(3, listObjectClientConfig, { - Bucket: bucket, - Prefix: 'public/listALLResultsPath', - MaxKeys: 1000, - ContinuationToken: nextToken, - }); - }); + // listing three times for three pages + expect(listObjectsV2).toHaveBeenCalledTimes(3); - it('Should list objects with zero results', async () => { - mockListObject.mockImplementationOnce(() => { - return {}; - }); - - expect.assertions(3); - let response = await list({ - prefix: 'emptyListResultsPath', - options: { - accessLevel: 'guest', - }, - }); - expect(response.items).toEqual([]); - expect(response.nextToken).toEqual(undefined); - expect(listObjectsV2).toHaveBeenCalledWith(listObjectClientConfig, { - Bucket: bucket, - MaxKeys: 1000, - Prefix: 'public/emptyListResultsPath', + // first input recieves undefined as the Continuation Token + expect(listObjectsV2).toHaveBeenNthCalledWith( + 1, + listObjectClientConfig, + { + Bucket: bucket, + Prefix: expectedPath, + MaxKeys: 1000, + ContinuationToken: undefined, + } + ); + // last input recieves TEST_TOKEN as the Continuation Token + expect(listObjectsV2).toHaveBeenNthCalledWith( + 3, + listObjectClientConfig, + { + Bucket: bucket, + Prefix: expectedPath, + MaxKeys: 1000, + ContinuationToken: nextToken, + } + ); }); }); }); @@ -204,7 +265,7 @@ describe('list API', () => { afterEach(() => { jest.clearAllMocks(); }); - it('Should return a not found error', async () => { + it('should return a not found error', async () => { mockListObject.mockRejectedValueOnce( Object.assign(new Error(), { $metadata: { httpStatusCode: 404 }, diff --git a/packages/storage/__tests__/providers/s3/apis/remove.test.ts b/packages/storage/__tests__/providers/s3/apis/remove.test.ts index b7a40c108c2..f5dcf3813d6 100644 --- a/packages/storage/__tests__/providers/s3/apis/remove.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/remove.test.ts @@ -5,6 +5,7 @@ import { Credentials } from '@aws-sdk/types'; import { Amplify } from '@aws-amplify/core'; import { deleteObject } from '../../../../src/providers/s3/utils/client'; import { remove } from '../../../../src/providers/s3/apis'; +import { StorageOptions } from '../../../../src/types'; jest.mock('../../../../src/providers/s3/utils/client'); jest.mock('@aws-amplify/core', () => ({ @@ -21,7 +22,7 @@ const mockGetConfig = Amplify.getConfig as jest.Mock; const key = 'key'; const bucket = 'bucket'; const region = 'region'; -const targetIdentityId = 'targetIdentityId'; +const defaultIdentityId = 'defaultIdentityId'; const removeResult = { key }; const credentials: Credentials = { accessKeyId: 'accessKeyId', @@ -37,13 +38,13 @@ describe('remove API', () => { beforeAll(() => { mockFetchAuthSession.mockResolvedValue({ credentials, - identityId: targetIdentityId, + identityId: defaultIdentityId, }); mockGetConfig.mockReturnValue({ Storage: { S3: { - bucket: 'bucket', - region: 'region', + bucket, + region, }, }, }); @@ -59,52 +60,35 @@ describe('remove API', () => { afterEach(() => { jest.clearAllMocks(); }); + [ + { + expectedKey: `public/${key}`, + }, + { + options: { accessLevel: 'guest' }, + expectedKey: `public/${key}`, + }, + { + options: { accessLevel: 'private' }, + expectedKey: `private/${defaultIdentityId}/${key}`, + }, + { + options: { accessLevel: 'protected' }, + expectedKey: `protected/${defaultIdentityId}/${key}`, + }, + ].forEach(({ options, expectedKey }) => { + const accessLevel = options?.accessLevel ?? 'default'; - it('Should remove object with default accessLevel', async () => { - expect.assertions(3); - expect(await remove({ key })).toEqual(removeResult); - expect(deleteObject).toBeCalledTimes(1); - expect(deleteObject).toHaveBeenCalledWith(deleteObjectClientConfig, { - Bucket: bucket, - Key: `public/${key}`, - }); - }); - - it('Should remove object with guest accessLevel', async () => { - expect.assertions(3); - expect(await remove({ key, options: { accessLevel: 'guest' } })).toEqual( - removeResult - ); - expect(deleteObject).toBeCalledTimes(1); - expect(deleteObject).toHaveBeenCalledWith(deleteObjectClientConfig, { - Bucket: bucket, - Key: `public/${key}`, - }); - }); - - it('Should remove object with private accessLevel', async () => { - expect.assertions(3); - const accessLevel = 'private'; - expect(await remove({ key, options: { accessLevel } })).toEqual( - removeResult - ); - expect(deleteObject).toBeCalledTimes(1); - expect(deleteObject).toHaveBeenCalledWith(deleteObjectClientConfig, { - Bucket: bucket, - Key: `${accessLevel}/${targetIdentityId}/${key}`, - }); - }); - - it('Should remove object with protected accessLevel', async () => { - expect.assertions(3); - const accessLevel = 'protected'; - expect(await remove({ key, options: { accessLevel } })).toEqual( - removeResult - ); - expect(deleteObject).toBeCalledTimes(1); - expect(deleteObject).toHaveBeenCalledWith(deleteObjectClientConfig, { - Bucket: bucket, - Key: `${accessLevel}/${targetIdentityId}/${key}`, + it(`should remove object with ${accessLevel} accessLevel`, async () => { + expect.assertions(3); + expect( + await remove({ key, options: options as StorageOptions }) + ).toEqual(removeResult); + expect(deleteObject).toBeCalledTimes(1); + expect(deleteObject).toHaveBeenCalledWith(deleteObjectClientConfig, { + Bucket: bucket, + Key: expectedKey, + }); }); }); }); @@ -113,7 +97,7 @@ describe('remove API', () => { afterEach(() => { jest.clearAllMocks(); }); - it('Should return a not found error', async () => { + it('should return a not found error', async () => { mockDeleteObject.mockRejectedValueOnce( Object.assign(new Error(), { $metadata: { httpStatusCode: 404 }, diff --git a/packages/storage/__tests__/providers/s3/apis/uploadData/multipartHandlers.test.ts b/packages/storage/__tests__/providers/s3/apis/uploadData/multipartHandlers.test.ts index be3f70399cd..507ae9600ad 100644 --- a/packages/storage/__tests__/providers/s3/apis/uploadData/multipartHandlers.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/uploadData/multipartHandlers.test.ts @@ -20,6 +20,7 @@ import { UPLOADS_STORAGE_KEY } from '../../../../../src/providers/s3/utils/const import { getKvStorage } from '../../../../../src/providers/s3/apis/uploadData/multipart/uploadCache/kvStorage'; import { byteLength } from '../../../../../src/providers/s3/apis/uploadData/byteLength'; import { CanceledError } from '../../../../../src/errors/CanceledError'; +import { StorageOptions } from '../../../../../src/types'; jest.mock('../../../../../src/providers/s3/utils/client'); @@ -51,7 +52,7 @@ const credentials: Credentials = { sessionToken: 'sessionToken', secretAccessKey: 'secretAccessKey', }; -const identityId = 'identityId'; +const defaultIdentityId = 'defaultIdentityId'; const mockFetchAuthSession = Amplify.Auth.fetchAuthSession as jest.Mock; const bucket = 'bucket'; const region = 'region'; @@ -142,7 +143,7 @@ describe('getMultipartUploadHandlers', () => { beforeAll(() => { mockFetchAuthSession.mockResolvedValue({ credentials, - identityId, + identityId: defaultIdentityId, }); (Amplify.getConfig as jest.Mock).mockReturnValue({ Storage: { @@ -177,41 +178,61 @@ describe('getMultipartUploadHandlers', () => { describe('upload', () => { const getBlob = (size: number) => new Blob(['1'.repeat(size)]); - it.each([ - ['file', new File([getBlob(8 * MB)], 'someName')], - ['blob', getBlob(8 * MB)], - ['string', '1'.repeat(8 * MB)], - ['arrayBuffer', new ArrayBuffer(8 * MB)], - ['arrayBufferView', new Uint8Array(8 * MB)], - ])( - 'should upload a %s type body that splits in 2 parts', - async (_, twoPartsPayload) => { - mockMultipartUploadSuccess(); - const { multipartUploadJob } = getMultipartUploadHandlers({ - key: defaultKey, - data: twoPartsPayload, - }); - const result = await multipartUploadJob(); - expect(mockCreateMultipartUpload).toBeCalledWith( - expect.objectContaining({ - credentials, - region, - abortSignal: expect.any(AbortSignal), - }), - expect.objectContaining({ - Bucket: bucket, - Key: `public/${defaultKey}`, - ContentType: defaultContentType, - }) - ); - expect(result).toEqual( - expect.objectContaining({ key: defaultKey, eTag: 'etag' }) - ); - expect(mockCreateMultipartUpload).toBeCalledTimes(1); - expect(mockUploadPart).toBeCalledTimes(2); - expect(mockCompleteMultipartUpload).toBeCalledTimes(1); - } - ); + [ + { + expectedKey: `public/${defaultKey}`, + }, + { + options: { accessLevel: 'guest' }, + expectedKey: `public/${defaultKey}`, + }, + { + options: { accessLevel: 'private' }, + expectedKey: `private/${defaultIdentityId}/${defaultKey}`, + }, + { + options: { accessLevel: 'protected' }, + expectedKey: `protected/${defaultIdentityId}/${defaultKey}`, + }, + ].forEach(({ options, expectedKey }) => { + const accessLevelMsg = options?.accessLevel ?? 'default'; + it.each([ + ['file', new File([getBlob(8 * MB)], 'someName')], + ['blob', getBlob(8 * MB)], + ['string', '1'.repeat(8 * MB)], + ['arrayBuffer', new ArrayBuffer(8 * MB)], + ['arrayBufferView', new Uint8Array(8 * MB)], + ])( + `should upload a %s type body that splits in 2 parts using ${accessLevelMsg} accessLevel`, + async (_, twoPartsPayload) => { + mockMultipartUploadSuccess(); + const { multipartUploadJob } = getMultipartUploadHandlers({ + key: defaultKey, + data: twoPartsPayload, + options: options as StorageOptions, + }); + const result = await multipartUploadJob(); + expect(mockCreateMultipartUpload).toBeCalledWith( + expect.objectContaining({ + credentials, + region, + abortSignal: expect.any(AbortSignal), + }), + expect.objectContaining({ + Bucket: bucket, + Key: expectedKey, + ContentType: defaultContentType, + }) + ); + expect(result).toEqual( + expect.objectContaining({ key: defaultKey, eTag: 'etag' }) + ); + expect(mockCreateMultipartUpload).toBeCalledTimes(1); + expect(mockUploadPart).toBeCalledTimes(2); + expect(mockCompleteMultipartUpload).toBeCalledTimes(1); + } + ); + }); it('should throw if unsupported payload type is provided', async () => { mockMultipartUploadSuccess();