diff --git a/packages/storage/__tests__/internals/apis/copy.test.ts b/packages/storage/__tests__/internals/apis/copy.test.ts index 2692f4f6a68..b51c145b7e5 100644 --- a/packages/storage/__tests__/internals/apis/copy.test.ts +++ b/packages/storage/__tests__/internals/apis/copy.test.ts @@ -1,12 +1,13 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { AmplifyClassV6 } from '@aws-amplify/core'; import { copy as advancedCopy } from '../../../src/internals'; import { copy as copyInternal } from '../../../src/providers/s3/apis/internal/copy'; +import { createMockAmplifyContext } from '../../testUtils/mockAmplifyContext'; jest.mock('../../../src/providers/s3/apis/internal/copy'); const mockedCopyInternal = jest.mocked(copyInternal); +const mockCtx = createMockAmplifyContext(); describe('copy (internals)', () => { beforeEach(() => { @@ -44,10 +45,10 @@ describe('copy (internals)', () => { customEndpoint, }, }; - const result = await advancedCopy(copyInputWithAdvancedOptions); + const result = await advancedCopy(mockCtx, copyInputWithAdvancedOptions); expect(mockedCopyInternal).toHaveBeenCalledTimes(1); expect(mockedCopyInternal).toHaveBeenCalledWith( - expect.any(AmplifyClassV6), + mockCtx, copyInputWithAdvancedOptions, ); expect(result).toEqual({ diff --git a/packages/storage/__tests__/internals/apis/downloadData.test.ts b/packages/storage/__tests__/internals/apis/downloadData.test.ts index f18ea441e69..026fae3b814 100644 --- a/packages/storage/__tests__/internals/apis/downloadData.test.ts +++ b/packages/storage/__tests__/internals/apis/downloadData.test.ts @@ -3,9 +3,11 @@ import { downloadData as advancedDownloadData } from '../../../src/internals'; import { downloadData as downloadDataInternal } from '../../../src/providers/s3/apis/internal/downloadData'; +import { createMockAmplifyContext } from '../../testUtils/mockAmplifyContext'; jest.mock('../../../src/providers/s3/apis/internal/downloadData'); const mockedDownloadDataInternal = jest.mocked(downloadDataInternal); +const mockCtx = createMockAmplifyContext(); describe('downloadData (internal)', () => { beforeEach(() => { @@ -43,7 +45,7 @@ describe('downloadData (internal)', () => { const onProgress = jest.fn(); const bytesRange = { start: 1024, end: 2048 }; - const output = await advancedDownloadData({ + const output = await advancedDownloadData(mockCtx, { path: 'input/path/to/mock/object', options: { customEndpoint, @@ -57,7 +59,7 @@ describe('downloadData (internal)', () => { }); expect(mockedDownloadDataInternal).toHaveBeenCalledTimes(1); - expect(mockedDownloadDataInternal).toHaveBeenCalledWith({ + expect(mockedDownloadDataInternal).toHaveBeenCalledWith(mockCtx, { path: 'input/path/to/mock/object', options: { customEndpoint, diff --git a/packages/storage/__tests__/internals/apis/getProperties.test.ts b/packages/storage/__tests__/internals/apis/getProperties.test.ts index aa0c2c9815e..c68473e5628 100644 --- a/packages/storage/__tests__/internals/apis/getProperties.test.ts +++ b/packages/storage/__tests__/internals/apis/getProperties.test.ts @@ -1,12 +1,13 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { AmplifyClassV6 } from '@aws-amplify/core'; import { getProperties as advancedGetProperties } from '../../../src/internals'; import { getProperties as getPropertiesInternal } from '../../../src/providers/s3/apis/internal/getProperties'; +import { createMockAmplifyContext } from '../../testUtils/mockAmplifyContext'; jest.mock('../../../src/providers/s3/apis/internal/getProperties'); const mockedGetPropertiesInternal = jest.mocked(getPropertiesInternal); +const mockCtx = createMockAmplifyContext(); describe('getProperties (internal)', () => { beforeEach(() => { @@ -32,7 +33,7 @@ describe('getProperties (internal)', () => { expiration: new Date(), }, }); - const result = await advancedGetProperties({ + const result = await advancedGetProperties(mockCtx, { path: 'input/path/to/mock/object', options: { customEndpoint, @@ -43,19 +44,16 @@ describe('getProperties (internal)', () => { }, }); expect(mockedGetPropertiesInternal).toHaveBeenCalledTimes(1); - expect(mockedGetPropertiesInternal).toHaveBeenCalledWith( - expect.any(AmplifyClassV6), - { - path: 'input/path/to/mock/object', - options: { - customEndpoint, - useAccelerateEndpoint, - bucket, - expectedBucketOwner, - locationCredentialsProvider, - }, + expect(mockedGetPropertiesInternal).toHaveBeenCalledWith(mockCtx, { + path: 'input/path/to/mock/object', + options: { + customEndpoint, + useAccelerateEndpoint, + bucket, + expectedBucketOwner, + locationCredentialsProvider, }, - ); + }); expect(result).toEqual({ path: 'output/path/to/mock/object', }); diff --git a/packages/storage/__tests__/internals/apis/getUrl.test.ts b/packages/storage/__tests__/internals/apis/getUrl.test.ts index fcffafd3f2e..5642585f129 100644 --- a/packages/storage/__tests__/internals/apis/getUrl.test.ts +++ b/packages/storage/__tests__/internals/apis/getUrl.test.ts @@ -1,12 +1,13 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { AmplifyClassV6 } from '@aws-amplify/core'; import { getUrl as advancedGetUrl } from '../../../src/internals'; import { getUrl as getUrlInternal } from '../../../src/providers/s3/apis/internal/getUrl'; +import { createMockAmplifyContext } from '../../testUtils/mockAmplifyContext'; jest.mock('../../../src/providers/s3/apis/internal/getUrl'); const mockedGetUrlInternal = jest.mocked(getUrlInternal); +const mockCtx = createMockAmplifyContext(); const MOCK_URL = new URL('https://s3.aws/mock-presigned-url'); const MOCK_DATE = new Date(); @@ -41,7 +42,7 @@ describe('getUrl (internal)', () => { expiration: new Date(), }, }); - const result = await advancedGetUrl({ + const result = await advancedGetUrl(mockCtx, { path: 'input/path/to/mock/object', options: { customEndpoint, @@ -56,23 +57,20 @@ describe('getUrl (internal)', () => { }, }); expect(mockedGetUrlInternal).toHaveBeenCalledTimes(1); - expect(mockedGetUrlInternal).toHaveBeenCalledWith( - expect.any(AmplifyClassV6), - { - path: 'input/path/to/mock/object', - options: { - customEndpoint, - useAccelerateEndpoint, - bucket, - validateObjectExistence, - expiresIn, - contentDisposition, - contentType, - expectedBucketOwner, - locationCredentialsProvider, - }, + expect(mockedGetUrlInternal).toHaveBeenCalledWith(mockCtx, { + path: 'input/path/to/mock/object', + options: { + customEndpoint, + useAccelerateEndpoint, + bucket, + validateObjectExistence, + expiresIn, + contentDisposition, + contentType, + expectedBucketOwner, + locationCredentialsProvider, }, - ); + }); expect(result).toEqual({ url: MOCK_URL, expiresAt: MOCK_DATE, diff --git a/packages/storage/__tests__/internals/apis/list.test.ts b/packages/storage/__tests__/internals/apis/list.test.ts index 16ea0e5037b..36045b54c6f 100644 --- a/packages/storage/__tests__/internals/apis/list.test.ts +++ b/packages/storage/__tests__/internals/apis/list.test.ts @@ -1,12 +1,13 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { AmplifyClassV6 } from '@aws-amplify/core'; import { list as advancedList } from '../../../src/internals'; import { list as listInternal } from '../../../src/providers/s3/apis/internal/list'; +import { createMockAmplifyContext } from '../../testUtils/mockAmplifyContext'; jest.mock('../../../src/providers/s3/apis/internal/list'); const mockedListInternal = jest.mocked(listInternal); +const mockCtx = createMockAmplifyContext(); describe('list (internals)', () => { beforeEach(() => { @@ -29,7 +30,7 @@ describe('list (internals)', () => { expiration: new Date(), }, }); - const result = await advancedList({ + const result = await advancedList(mockCtx, { path: 'input/path/to/mock/object', options: { customEndpoint, @@ -40,19 +41,16 @@ describe('list (internals)', () => { }, }); expect(mockedListInternal).toHaveBeenCalledTimes(1); - expect(mockedListInternal).toHaveBeenCalledWith( - expect.any(AmplifyClassV6), - { - path: 'input/path/to/mock/object', - options: { - customEndpoint, - useAccelerateEndpoint, - bucket, - expectedBucketOwner, - locationCredentialsProvider, - }, + expect(mockedListInternal).toHaveBeenCalledWith(mockCtx, { + path: 'input/path/to/mock/object', + options: { + customEndpoint, + useAccelerateEndpoint, + bucket, + expectedBucketOwner, + locationCredentialsProvider, }, - ); + }); expect(result).toEqual({ items: [], }); diff --git a/packages/storage/__tests__/internals/apis/listPaths/listPaths.test.ts b/packages/storage/__tests__/internals/apis/listPaths/listPaths.test.ts index dfe1a711c5a..e6b290db450 100644 --- a/packages/storage/__tests__/internals/apis/listPaths/listPaths.test.ts +++ b/packages/storage/__tests__/internals/apis/listPaths/listPaths.test.ts @@ -1,23 +1,16 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { Amplify, AuthTokens, fetchAuthSession } from '@aws-amplify/core'; +import { AmplifyContext, AuthTokens } from '@aws-amplify/core'; import { resolveLocationsForCurrentSession } from '../../../../src/internals/apis/listPaths/resolveLocationsForCurrentSession'; import { getHighestPrecedenceUserGroup } from '../../../../src/internals/apis/listPaths/getHighestPrecedenceUserGroup'; import { listPaths } from '../../../../src/internals'; +import { + StorageValidationErrorCode, + validationErrorMap, +} from '../../../../src/errors/types/validation'; -jest.mock('@aws-amplify/core', () => ({ - ConsoleLogger: jest.fn(), - Amplify: { - getConfig: jest.fn(), - Auth: { - getConfig: jest.fn(), - fetchAuthSession: jest.fn(), - }, - }, - fetchAuthSession: jest.fn(), -})); jest.mock( '../../../../src/internals/apis/listPaths/resolveLocationsForCurrentSession', ); @@ -32,8 +25,20 @@ const credentials = { }; const identityId = 'identityId'; -const mockGetConfig = jest.mocked(Amplify.getConfig); -const mockFetchAuthSession = jest.mocked(fetchAuthSession); +const mockGetConfig = jest.fn(); +const mockFetchAuthSession = jest.fn(); +// listPaths now receives a required AmplifyContext. Back resourcesConfig with +// a jest.fn so tests can vary config per-case, and expose fetchAuthSession as a +// jest.fn for session/token control. +const mockCtx: AmplifyContext = { + get resourcesConfig() { + return mockGetConfig(); + }, + libraryOptions: {}, + fetchAuthSession: mockFetchAuthSession, + clearCredentials: jest.fn(), + getTokens: jest.fn(), +}; const mockResolveLocationsFromCurrentSession = resolveLocationsForCurrentSession as jest.Mock; const mockGetHighestPrecedenceUserGroup = jest.mocked( @@ -99,7 +104,7 @@ describe('listPaths', () => { Storage: { S3: { buckets: undefined } }, }); - const result = await listPaths(); + const result = await listPaths(mockCtx); expect(result).toEqual({ locations: [] }); }); @@ -118,7 +123,7 @@ describe('listPaths', () => { }, ]); - const result = await listPaths(); + const result = await listPaths(mockCtx); expect(result).toEqual({ locations: [ @@ -157,7 +162,7 @@ describe('listPaths', () => { prefix: '/path1', }, }); - await listPaths(); + await listPaths(mockCtx); expect(mockResolveLocationsFromCurrentSession).toHaveBeenCalled(); expect(mockResolveLocationsFromCurrentSession).toHaveBeenCalledWith({ @@ -189,7 +194,7 @@ describe('listPaths', () => { }); mockGetHighestPrecedenceUserGroup.mockReturnValue('admin'); - await listPaths(); + await listPaths(mockCtx); expect(mockResolveLocationsFromCurrentSession).toHaveBeenCalled(); expect(mockResolveLocationsFromCurrentSession).toHaveBeenCalledWith({ @@ -199,4 +204,36 @@ describe('listPaths', () => { userGroup: 'admin', }); }); + + it('should throw StorageValidationError with NoS3Config when Storage.S3 is missing', async () => { + mockGetConfig.mockReturnValue({ + ...mockAuthConfig, + Storage: undefined, + }); + + await expect(listPaths(mockCtx)).rejects.toMatchObject({ + name: StorageValidationErrorCode.NoS3Config, + message: + validationErrorMap[StorageValidationErrorCode.NoS3Config].message, + }); + }); + + it('should throw StorageValidationError with NoAuthConfig when Auth.Cognito is missing', async () => { + mockGetConfig.mockReturnValue({ + Auth: undefined, + Storage: { + S3: { + bucket: 'bucket1', + region: 'region1', + buckets: mockBuckets, + }, + }, + }); + + await expect(listPaths(mockCtx)).rejects.toMatchObject({ + name: StorageValidationErrorCode.NoAuthConfig, + message: + validationErrorMap[StorageValidationErrorCode.NoAuthConfig].message, + }); + }); }); diff --git a/packages/storage/__tests__/internals/apis/remove.test.ts b/packages/storage/__tests__/internals/apis/remove.test.ts index 2adab6dd0ef..3b1caf74b18 100644 --- a/packages/storage/__tests__/internals/apis/remove.test.ts +++ b/packages/storage/__tests__/internals/apis/remove.test.ts @@ -1,12 +1,13 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { AmplifyClassV6 } from '@aws-amplify/core'; import { remove as advancedRemove } from '../../../src/internals'; import { remove as removeInternal } from '../../../src/providers/s3/apis/internal/remove'; +import { createMockAmplifyContext } from '../../testUtils/mockAmplifyContext'; jest.mock('../../../src/providers/s3/apis/internal/remove'); const mockedRemoveInternal = jest.mocked(removeInternal); +const mockCtx = createMockAmplifyContext(); describe('remove (internal)', () => { beforeEach(() => { @@ -33,7 +34,7 @@ describe('remove (internal)', () => { }, }); - const result = await advancedRemove({ + const result = await advancedRemove(mockCtx, { path: 'input/path/to/mock/object', options: { customEndpoint, @@ -45,19 +46,16 @@ describe('remove (internal)', () => { }); expect(mockedRemoveInternal).toHaveBeenCalledTimes(1); - expect(mockedRemoveInternal).toHaveBeenCalledWith( - expect.any(AmplifyClassV6), - { - path: 'input/path/to/mock/object', - options: { - customEndpoint, - useAccelerateEndpoint, - bucket, - expectedBucketOwner, - locationCredentialsProvider, - }, + expect(mockedRemoveInternal).toHaveBeenCalledWith(mockCtx, { + path: 'input/path/to/mock/object', + options: { + customEndpoint, + useAccelerateEndpoint, + bucket, + expectedBucketOwner, + locationCredentialsProvider, }, - ); + }); expect(result).toEqual({ path: 'output/path/to/mock/object', }); diff --git a/packages/storage/__tests__/internals/apis/uploadData.test.ts b/packages/storage/__tests__/internals/apis/uploadData.test.ts index 0f728a00a78..357665548d5 100644 --- a/packages/storage/__tests__/internals/apis/uploadData.test.ts +++ b/packages/storage/__tests__/internals/apis/uploadData.test.ts @@ -1,17 +1,17 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { Amplify } from '@aws-amplify/core'; - import { uploadData as advancedUploadData } from '../../../src/internals'; import { uploadData as uploadDataInternal } from '../../../src/providers/s3/apis/internal/uploadData'; +import { createMockAmplifyContext } from '../../testUtils/mockAmplifyContext'; jest.mock('../../../src/providers/s3/apis/internal/uploadData'); const mockedUploadDataInternal = jest.mocked(uploadDataInternal); const mockedUploadTask = 'UPLOAD_TASK'; +const mockCtx = createMockAmplifyContext(); const expectedCtx = { - amplify: Amplify, + amplify: mockCtx, readFile: expect.any(Function), toBase64: expect.any(Function), }; @@ -43,7 +43,7 @@ describe('uploadData (internal)', () => { const onProgress = jest.fn(); const metadata = { foo: 'bar' }; - const result = advancedUploadData({ + const result = advancedUploadData(mockCtx, { path: 'input/path/to/mock/object', data: 'data', options: { diff --git a/packages/storage/__tests__/providers/s3/apis/copy.test.ts b/packages/storage/__tests__/providers/s3/apis/copy.test.ts index 606786ebfc2..ebc7cb855c2 100644 --- a/packages/storage/__tests__/providers/s3/apis/copy.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/copy.test.ts @@ -1,17 +1,32 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { Amplify } from '@aws-amplify/core'; +import { + clearGlobalContext, + setGlobalContext, +} from '@aws-amplify/core/internals/utils'; import { CopyInput, CopyWithPathInput } from '../../../../src'; import { copy } from '../../../../src/providers/s3/apis'; import { copy as internalCopyImpl } from '../../../../src/providers/s3/apis/internal/copy'; +import { createMockAmplifyContext } from '../../../testUtils/mockAmplifyContext'; jest.mock('../../../../src/providers/s3/apis/internal/copy'); const mockInternalCopyImpl = jest.mocked(internalCopyImpl); +const mockCtx = createMockAmplifyContext(); describe('client-side copy', () => { + beforeAll(() => { + // The public API falls back to the global AmplifyContext when no ctx is + // passed explicitly; establish it so resolveCtxArgs can resolve it. + setGlobalContext(mockCtx); + }); + + afterAll(() => { + clearGlobalContext(); + }); + beforeEach(() => { jest.clearAllMocks(); }); @@ -28,7 +43,7 @@ describe('client-side copy', () => { }, }; expect(copy(input)).toEqual(mockInternalResult); - expect(mockInternalCopyImpl).toBeCalledWith(Amplify, input); + expect(mockInternalCopyImpl).toBeCalledWith(mockCtx, input); }); it('should pass through input with path and output to internal implementation', async () => { @@ -39,6 +54,18 @@ describe('client-side copy', () => { destination: { path: 'abc' }, }; expect(copy(input)).toEqual(mockInternalResult); - expect(mockInternalCopyImpl).toBeCalledWith(Amplify, input); + expect(mockInternalCopyImpl).toBeCalledWith(mockCtx, input); + }); + + it('should pass explicit AmplifyContext to internal implementation when called with two args', () => { + const explicitCtx = createMockAmplifyContext(); + const mockInternalResult = 'RESULT' as any; + mockInternalCopyImpl.mockReturnValue(mockInternalResult); + const input: CopyWithPathInput = { + source: { path: 'source-path' }, + destination: { path: 'dest-path' }, + }; + expect(copy(explicitCtx, input)).toEqual(mockInternalResult); + expect(mockInternalCopyImpl).toBeCalledWith(explicitCtx, input); }); }); diff --git a/packages/storage/__tests__/providers/s3/apis/downloadData.test.ts b/packages/storage/__tests__/providers/s3/apis/downloadData.test.ts index baf27558169..e6a24bc3db1 100644 --- a/packages/storage/__tests__/providers/s3/apis/downloadData.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/downloadData.test.ts @@ -1,14 +1,31 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +import { + clearGlobalContext, + setGlobalContext, +} from '@aws-amplify/core/internals/utils'; + import { downloadData } from '../../../../src/providers/s3/apis'; import { downloadData as internalDownloadDataImpl } from '../../../../src/providers/s3/apis/internal/downloadData'; +import { createMockAmplifyContext } from '../../../testUtils/mockAmplifyContext'; jest.mock('../../../../src/providers/s3/apis/internal/downloadData'); const mockInternalDownloadDataImpl = jest.mocked(internalDownloadDataImpl); +const mockCtx = createMockAmplifyContext(); describe('client-side downloadData', () => { + beforeAll(() => { + // The public API falls back to the global AmplifyContext when no ctx is + // passed explicitly; establish it so resolveCtxArgs can resolve it. + setGlobalContext(mockCtx); + }); + + afterAll(() => { + clearGlobalContext(); + }); + beforeEach(() => { jest.clearAllMocks(); }); @@ -24,7 +41,7 @@ describe('client-side downloadData', () => { }, }; expect(downloadData(input)).toEqual(mockInternalResult); - expect(mockInternalDownloadDataImpl).toBeCalledWith(input); + expect(mockInternalDownloadDataImpl).toBeCalledWith(mockCtx, input); }); it('should pass through input with path and output to internal implementation', async () => { @@ -35,6 +52,18 @@ describe('client-side downloadData', () => { data: 'data', }; expect(downloadData(input)).toEqual(mockInternalResult); - expect(mockInternalDownloadDataImpl).toBeCalledWith(input); + expect(mockInternalDownloadDataImpl).toBeCalledWith(mockCtx, input); + }); + + it('should pass explicit AmplifyContext to internal implementation when called with two args', () => { + const explicitCtx = createMockAmplifyContext(); + const mockInternalResult = 'RESULT' as any; + mockInternalDownloadDataImpl.mockReturnValue(mockInternalResult); + const input = { + path: 'path', + data: 'data', + }; + expect(downloadData(explicitCtx, input)).toEqual(mockInternalResult); + expect(mockInternalDownloadDataImpl).toBeCalledWith(explicitCtx, input); }); }); diff --git a/packages/storage/__tests__/providers/s3/apis/getProperties.test.ts b/packages/storage/__tests__/providers/s3/apis/getProperties.test.ts index 70367b21e6a..1546ec24b36 100644 --- a/packages/storage/__tests__/providers/s3/apis/getProperties.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/getProperties.test.ts @@ -1,7 +1,10 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { Amplify } from '@aws-amplify/core'; +import { + clearGlobalContext, + setGlobalContext, +} from '@aws-amplify/core/internals/utils'; import { GetPropertiesInput, @@ -9,12 +12,24 @@ import { } from '../../../../src'; import { getProperties } from '../../../../src/providers/s3/apis'; import { getProperties as internalGetPropertiesImpl } from '../../../../src/providers/s3/apis/internal/getProperties'; +import { createMockAmplifyContext } from '../../../testUtils/mockAmplifyContext'; jest.mock('../../../../src/providers/s3/apis/internal/getProperties'); const mockInternalGetPropertiesImpl = jest.mocked(internalGetPropertiesImpl); +const mockCtx = createMockAmplifyContext(); describe('client-side getProperties', () => { + beforeAll(() => { + // The public API falls back to the global AmplifyContext when no ctx is + // passed explicitly; establish it so resolveCtxArgs can resolve it. + setGlobalContext(mockCtx); + }); + + afterAll(() => { + clearGlobalContext(); + }); + beforeEach(() => { jest.clearAllMocks(); }); @@ -26,7 +41,7 @@ describe('client-side getProperties', () => { key: 'source-key', }; expect(getProperties(input)).toEqual(mockInternalResult); - expect(mockInternalGetPropertiesImpl).toBeCalledWith(Amplify, input); + expect(mockInternalGetPropertiesImpl).toBeCalledWith(mockCtx, input); }); it('should pass through input with path and output to internal implementation', async () => { @@ -36,6 +51,17 @@ describe('client-side getProperties', () => { path: 'abc', }; expect(getProperties(input)).toEqual(mockInternalResult); - expect(mockInternalGetPropertiesImpl).toBeCalledWith(Amplify, input); + expect(mockInternalGetPropertiesImpl).toBeCalledWith(mockCtx, input); + }); + + it('should pass explicit AmplifyContext to internal implementation when called with two args', () => { + const explicitCtx = createMockAmplifyContext(); + const mockInternalResult = 'RESULT' as any; + mockInternalGetPropertiesImpl.mockReturnValue(mockInternalResult); + const input: GetPropertiesWithPathInput = { + path: 'abc', + }; + expect(getProperties(explicitCtx, input)).toEqual(mockInternalResult); + expect(mockInternalGetPropertiesImpl).toBeCalledWith(explicitCtx, input); }); }); diff --git a/packages/storage/__tests__/providers/s3/apis/getUrl.test.ts b/packages/storage/__tests__/providers/s3/apis/getUrl.test.ts index b7e43285d49..4ceffcae30a 100644 --- a/packages/storage/__tests__/providers/s3/apis/getUrl.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/getUrl.test.ts @@ -1,17 +1,32 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { Amplify } from '@aws-amplify/core'; +import { + clearGlobalContext, + setGlobalContext, +} from '@aws-amplify/core/internals/utils'; import { GetUrlInput, GetUrlWithPathInput } from '../../../../src'; import { getUrl } from '../../../../src/providers/s3/apis'; import { getUrl as internalGetUrlImpl } from '../../../../src/providers/s3/apis/internal/getUrl'; +import { createMockAmplifyContext } from '../../../testUtils/mockAmplifyContext'; jest.mock('../../../../src/providers/s3/apis/internal/getUrl'); const mockInternalGetUrlImpl = jest.mocked(internalGetUrlImpl); +const mockCtx = createMockAmplifyContext(); describe('client-side getUrl', () => { + beforeAll(() => { + // The public API falls back to the global AmplifyContext when no ctx is + // passed explicitly; establish it so resolveCtxArgs can resolve it. + setGlobalContext(mockCtx); + }); + + afterAll(() => { + clearGlobalContext(); + }); + beforeEach(() => { jest.clearAllMocks(); }); @@ -23,7 +38,7 @@ describe('client-side getUrl', () => { key: 'source-key', }; expect(getUrl(input)).toEqual(mockInternalResult); - expect(mockInternalGetUrlImpl).toBeCalledWith(Amplify, input); + expect(mockInternalGetUrlImpl).toBeCalledWith(mockCtx, input); }); it('should pass through input with path and output to internal implementation', async () => { @@ -33,6 +48,17 @@ describe('client-side getUrl', () => { path: 'abc', }; expect(getUrl(input)).toEqual(mockInternalResult); - expect(mockInternalGetUrlImpl).toBeCalledWith(Amplify, input); + expect(mockInternalGetUrlImpl).toBeCalledWith(mockCtx, input); + }); + + it('should pass explicit AmplifyContext to internal implementation when called with two args', () => { + const explicitCtx = createMockAmplifyContext(); + const mockInternalResult = 'RESULT' as any; + mockInternalGetUrlImpl.mockReturnValue(mockInternalResult); + const input: GetUrlWithPathInput = { + path: 'abc', + }; + expect(getUrl(explicitCtx, input)).toEqual(mockInternalResult); + expect(mockInternalGetUrlImpl).toBeCalledWith(explicitCtx, input); }); }); diff --git a/packages/storage/__tests__/providers/s3/apis/internal/copy.test.ts b/packages/storage/__tests__/providers/s3/apis/internal/copy.test.ts index c8423f26a26..5fa76530354 100644 --- a/packages/storage/__tests__/providers/s3/apis/internal/copy.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/internal/copy.test.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { AWSCredentials } from '@aws-amplify/core/internals/utils'; -import { Amplify, StorageAccessLevel } from '@aws-amplify/core'; +import { StorageAccessLevel } from '@aws-amplify/core'; import { StorageError } from '../../../../../src/errors/StorageError'; import { StorageValidationErrorCode } from '../../../../../src/errors/types/validation'; @@ -16,22 +16,14 @@ import { } from '../../../../../src/providers/s3/types'; import './testUtils'; import { BucketInfo } from '../../../../../src/providers/s3/types/options'; +import { createMockAmplifyContext } from '../../../../testUtils/mockAmplifyContext'; jest.mock('../../../../../src/providers/s3/utils/client/s3data'); -jest.mock('@aws-amplify/core', () => ({ - ConsoleLogger: jest.fn().mockImplementation(function ConsoleLogger() { - return { debug: jest.fn() }; - }), - Amplify: { - getConfig: jest.fn(), - Auth: { - fetchAuthSession: jest.fn(), - }, - }, -})); const mockCopyObject = copyObject as jest.Mock; -const mockFetchAuthSession = Amplify.Auth.fetchAuthSession as jest.Mock; -const mockGetConfig = Amplify.getConfig as jest.Mock; +const mockGetConfig = jest.fn(); +// Live getter delegates to mockGetConfig so per-test config variance works. +const mockCtx = createMockAmplifyContext({ getConfig: mockGetConfig }); +const mockFetchAuthSession = jest.mocked(mockCtx.fetchAuthSession); const sourceKey = 'sourceKey'; const destinationKey = 'destinationKey'; @@ -75,7 +67,7 @@ describe('copy API', () => { describe('Happy Cases', () => { describe('With key', () => { - const copyWrapper = async (input: CopyInput) => copy(Amplify, input); + const copyWrapper = async (input: CopyInput) => copy(mockCtx, input); beforeEach(() => { mockCopyObject.mockImplementation(() => { return { @@ -296,7 +288,7 @@ describe('copy API', () => { describe('With path', () => { const copyWrapper = async (input: CopyWithPathInput) => - copy(Amplify, input); + copy(mockCtx, input); beforeEach(() => { mockCopyObject.mockImplementation(() => { @@ -495,7 +487,7 @@ describe('copy API', () => { expect.assertions(3); const missingSourceKey = 'SourceKeyNotFound'; try { - await copy(Amplify, { + await copy(mockCtx, { source: { key: missingSourceKey }, destination: { key: destinationKey }, }); @@ -518,7 +510,7 @@ describe('copy API', () => { expect.assertions(2); try { // @ts-expect-error mismatch copy input not allowed - await copy(Amplify, { + await copy(mockCtx, { source: { path: 'sourcePath' }, destination: { key: 'destinationKey' }, }); @@ -533,7 +525,7 @@ describe('copy API', () => { expect.assertions(2); try { // @ts-expect-error mismatch copy input not allowed - await copy(Amplify, { + await copy(mockCtx, { source: { key: 'sourcePath' }, destination: { path: 'destinationKey' }, }); @@ -546,7 +538,7 @@ describe('copy API', () => { it('should throw an error when only source has bucket option', async () => { expect.assertions(2); try { - await copy(Amplify, { + await copy(mockCtx, { source: { path: 'source', bucket: 'bucket-1' }, destination: { path: 'destination', @@ -563,7 +555,7 @@ describe('copy API', () => { it('should throw an error when only one destination has bucket option', async () => { expect.assertions(2); try { - await copy(Amplify, { + await copy(mockCtx, { source: { key: 'source' }, destination: { key: 'destination', diff --git a/packages/storage/__tests__/providers/s3/apis/internal/downloadData.test.ts b/packages/storage/__tests__/providers/s3/apis/internal/downloadData.test.ts index 7cc077c1aaa..35c9c688d80 100644 --- a/packages/storage/__tests__/providers/s3/apis/internal/downloadData.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/internal/downloadData.test.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { AWSCredentials } from '@aws-amplify/core/internals/utils'; -import { Amplify, StorageAccessLevel } from '@aws-amplify/core'; +import { StorageAccessLevel } from '@aws-amplify/core'; import { getObject } from '../../../../../src/providers/s3/utils/client/s3data'; import { downloadData } from '../../../../../src/providers/s3/apis/internal/downloadData'; @@ -25,20 +25,10 @@ import { } from '../../../../../src/providers/s3/types/outputs'; import './testUtils'; import { BucketInfo } from '../../../../../src/providers/s3/types/options'; +import { createMockAmplifyContext } from '../../../../testUtils/mockAmplifyContext'; jest.mock('../../../../../src/providers/s3/utils/client/s3data'); jest.mock('../../../../../src/providers/s3/utils'); -jest.mock('@aws-amplify/core', () => ({ - ConsoleLogger: jest.fn().mockImplementation(function ConsoleLogger() { - return { debug: jest.fn() }; - }), - Amplify: { - getConfig: jest.fn(), - Auth: { - fetchAuthSession: jest.fn(), - }, - }, -})); const credentials: AWSCredentials = { accessKeyId: 'accessKeyId', sessionToken: 'sessionToken', @@ -61,10 +51,12 @@ const mockDownloadResultBase = { contentType: 'contentType', }; -const mockFetchAuthSession = Amplify.Auth.fetchAuthSession as jest.Mock; const mockCreateDownloadTask = createDownloadTask as jest.Mock; const mockValidateStorageInput = validateStorageOperationInput as jest.Mock; -const mockGetConfig = jest.mocked(Amplify.getConfig); +const mockGetConfig = jest.fn(); +// Live getter delegates to mockGetConfig so per-test config variance works. +const mockCtx = createMockAmplifyContext({ getConfig: mockGetConfig }); +const mockFetchAuthSession = jest.mocked(mockCtx.fetchAuthSession); describe('downloadData with key', () => { beforeAll(() => { @@ -98,7 +90,7 @@ describe('downloadData with key', () => { key: inputKey, options: { accessLevel: 'protected', targetIdentityId }, }; - expect(downloadData(mockDownloadInput)).toBe('downloadTask'); + expect(downloadData(mockCtx, mockDownloadInput)).toBe('downloadTask'); }); const testCases: { @@ -131,7 +123,7 @@ describe('downloadData with key', () => { async ({ options, expectedKey }) => { (getObject as jest.Mock).mockResolvedValueOnce({ Body: 'body' }); const onProgress = jest.fn(); - downloadData({ + downloadData(mockCtx, { key: inputKey, options: { ...options, @@ -173,7 +165,7 @@ describe('downloadData with key', () => { VersionId: 'versionId', ContentType: 'contentType', }); - downloadData({ key: inputKey }); + downloadData(mockCtx, { key: inputKey }); const { job } = mockCreateDownloadTask.mock.calls[0][0]; const { key, @@ -206,7 +198,7 @@ describe('downloadData with key', () => { const end = 100; (getObject as jest.Mock).mockResolvedValueOnce({ Body: 'body' }); - downloadData({ + downloadData(mockCtx, { key: inputKey, options: { bytesRange: { start, end }, @@ -233,7 +225,7 @@ describe('downloadData with key', () => { region: 'region-1', }; - downloadData({ + downloadData(mockCtx, { key: inputKey, options: { bucket: bucketInfo, @@ -262,7 +254,7 @@ describe('downloadData with key', () => { (getObject as jest.Mock).mockResolvedValueOnce({ Body: 'body' }); const abortController = new AbortController(); - downloadData({ + downloadData(mockCtx, { key: inputKey, options: { bucket: 'default-bucket', @@ -291,7 +283,7 @@ describe('downloadData with key', () => { describe('ExpectedBucketOwner passed in options', () => { it('should include expectedBucketOwner in headers when provided', async () => { (getObject as jest.Mock).mockResolvedValueOnce({ Body: 'body' }); - downloadData({ + downloadData(mockCtx, { key: inputKey, options: { expectedBucketOwner: validBucketOwner, @@ -314,7 +306,7 @@ describe('downloadData with key', () => { describe('ResponseCacheControl passed in options', () => { it('should include cacheControl in headers when provided', async () => { (getObject as jest.Mock).mockResolvedValueOnce({ Body: 'body' }); - downloadData({ + downloadData(mockCtx, { path: inputKey, options: { cacheControl: 'no-store', @@ -335,7 +327,7 @@ describe('downloadData with key', () => { it('should NOT include cacheControl in headers when not provided', async () => { (getObject as jest.Mock).mockResolvedValueOnce({ Body: 'body' }); - downloadData({ + downloadData(mockCtx, { path: inputKey, }); @@ -385,7 +377,7 @@ describe('downloadData with path', () => { path: inputPath, options: { useAccelerateEndpoint: true }, }; - expect(downloadData(mockDownloadInput)).toBe('downloadTask'); + expect(downloadData(mockCtx, mockDownloadInput)).toBe('downloadTask'); }); test.each([ @@ -402,7 +394,7 @@ describe('downloadData with path', () => { async ({ path, expectedKey }) => { (getObject as jest.Mock).mockResolvedValueOnce({ Body: 'body' }); const onProgress = jest.fn(); - downloadData({ + downloadData(mockCtx, { path, options: { useAccelerateEndpoint: true, @@ -449,7 +441,7 @@ describe('downloadData with path', () => { VersionId: 'versionId', ContentType: 'contentType', }); - downloadData({ path: inputPath }); + downloadData(mockCtx, { path: inputPath }); const { job } = mockCreateDownloadTask.mock.calls[0][0]; const { path, @@ -482,7 +474,7 @@ describe('downloadData with path', () => { const end = 100; (getObject as jest.Mock).mockResolvedValueOnce({ Body: 'body' }); - downloadData({ + downloadData(mockCtx, { path: inputPath, options: { bytesRange: { start, end }, @@ -509,7 +501,7 @@ describe('downloadData with path', () => { region: 'region-1', }; - downloadData({ + downloadData(mockCtx, { path: inputPath, options: { bucket: bucketInfo, @@ -538,7 +530,7 @@ describe('downloadData with path', () => { (getObject as jest.Mock).mockResolvedValueOnce({ Body: 'body' }); const abortController = new AbortController(); - downloadData({ + downloadData(mockCtx, { path: inputPath, options: { bucket: 'default-bucket', @@ -567,7 +559,7 @@ describe('downloadData with path', () => { describe('ExpectedBucketOwner passed in options', () => { it('should include expectedBucketOwner in headers when provided', async () => { (getObject as jest.Mock).mockResolvedValueOnce({ Body: 'body' }); - downloadData({ + downloadData(mockCtx, { path: inputKey, options: { expectedBucketOwner: validBucketOwner, @@ -590,7 +582,7 @@ describe('downloadData with path', () => { describe('ResponseCacheControl passed in options', () => { it('should include cacheControl in headers when provided', async () => { (getObject as jest.Mock).mockResolvedValueOnce({ Body: 'body' }); - downloadData({ + downloadData(mockCtx, { path: inputKey, options: { cacheControl: 'no-store', @@ -611,7 +603,7 @@ describe('downloadData with path', () => { it('should NOT include cacheControl in headers when not provided', async () => { (getObject as jest.Mock).mockResolvedValueOnce({ Body: 'body' }); - downloadData({ + downloadData(mockCtx, { path: inputKey, }); diff --git a/packages/storage/__tests__/providers/s3/apis/internal/getProperties.test.ts b/packages/storage/__tests__/providers/s3/apis/internal/getProperties.test.ts index 01d7a73ef2c..c16f0926162 100644 --- a/packages/storage/__tests__/providers/s3/apis/internal/getProperties.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/internal/getProperties.test.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { AWSCredentials } from '@aws-amplify/core/internals/utils'; -import { Amplify, StorageAccessLevel } from '@aws-amplify/core'; +import { StorageAccessLevel } from '@aws-amplify/core'; import { headObject } from '../../../../../src/providers/s3/utils/client/s3data'; import { getProperties } from '../../../../../src/providers/s3/apis/internal/getProperties'; @@ -14,22 +14,14 @@ import { } from '../../../../../src/providers/s3/types'; import './testUtils'; import { BucketInfo } from '../../../../../src/providers/s3/types/options'; +import { createMockAmplifyContext } from '../../../../testUtils/mockAmplifyContext'; jest.mock('../../../../../src/providers/s3/utils/client/s3data'); -jest.mock('@aws-amplify/core', () => ({ - ConsoleLogger: jest.fn().mockImplementation(function ConsoleLogger() { - return { debug: jest.fn() }; - }), - Amplify: { - getConfig: jest.fn(), - Auth: { - fetchAuthSession: jest.fn(), - }, - }, -})); const mockHeadObject = headObject as jest.MockedFunction; -const mockFetchAuthSession = Amplify.Auth.fetchAuthSession as jest.Mock; -const mockGetConfig = jest.mocked(Amplify.getConfig); +const mockGetConfig = jest.fn(); +// Live getter delegates to mockGetConfig so per-test config variance works. +const mockCtx = createMockAmplifyContext({ getConfig: mockGetConfig }); +const mockFetchAuthSession = jest.mocked(mockCtx.fetchAuthSession); const bucket = 'bucket'; const region = 'region'; @@ -56,7 +48,7 @@ const expectedResult = { describe('getProperties with key', () => { const getPropertiesWrapper = (input: GetPropertiesInput) => - getProperties(Amplify, input); + getProperties(mockCtx, input); beforeAll(() => { mockFetchAuthSession.mockResolvedValue({ credentials, @@ -243,7 +235,7 @@ describe('getProperties with key', () => { describe('Happy cases: With path', () => { const getPropertiesWrapper = (input: GetPropertiesWithPathInput) => - getProperties(Amplify, input); + getProperties(mockCtx, input); beforeAll(() => { mockFetchAuthSession.mockResolvedValue({ credentials, @@ -415,7 +407,7 @@ describe('Happy cases: With path', () => { describe(`getProperties with path and Expected Bucket Owner`, () => { const getPropertiesWrapper = (input: GetPropertiesWithPathInput) => - getProperties(Amplify, input); + getProperties(mockCtx, input); beforeAll(() => { mockFetchAuthSession.mockResolvedValue({ credentials, diff --git a/packages/storage/__tests__/providers/s3/apis/internal/getUrl.test.ts b/packages/storage/__tests__/providers/s3/apis/internal/getUrl.test.ts index 594e3508e06..2ca8f8c59e6 100644 --- a/packages/storage/__tests__/providers/s3/apis/internal/getUrl.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/internal/getUrl.test.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { AWSCredentials } from '@aws-amplify/core/internals/utils'; -import { Amplify, StorageAccessLevel } from '@aws-amplify/core'; +import { StorageAccessLevel } from '@aws-amplify/core'; import { getUrl } from '../../../../../src/providers/s3/apis/internal/getUrl'; import { @@ -16,24 +16,16 @@ import { } from '../../../../../src/providers/s3/types'; import './testUtils'; import { BucketInfo } from '../../../../../src/providers/s3/types/options'; +import { createMockAmplifyContext } from '../../../../testUtils/mockAmplifyContext'; jest.mock('../../../../../src/providers/s3/utils/client/s3data'); -jest.mock('@aws-amplify/core', () => ({ - ConsoleLogger: jest.fn().mockImplementation(function ConsoleLogger() { - return { debug: jest.fn() }; - }), - Amplify: { - getConfig: jest.fn(), - Auth: { - fetchAuthSession: jest.fn(), - }, - }, -})); const bucket = 'bucket'; const region = 'region'; -const mockFetchAuthSession = jest.mocked(Amplify.Auth.fetchAuthSession); -const mockGetConfig = jest.mocked(Amplify.getConfig); +const mockGetConfig = jest.fn(); +// Live getter delegates to mockGetConfig so per-test config variance works. +const mockCtx = createMockAmplifyContext({ getConfig: mockGetConfig }); +const mockFetchAuthSession = jest.mocked(mockCtx.fetchAuthSession); const credentials: AWSCredentials = { accessKeyId: 'accessKeyId', sessionToken: 'sessionToken', @@ -46,7 +38,7 @@ const validBucketOwner = '111122223333'; const invalidBucketOwner = '123'; describe('getUrl test with key', () => { - const getUrlWrapper = (input: GetUrlInput) => getUrl(Amplify, input); + const getUrlWrapper = (input: GetUrlInput) => getUrl(mockCtx, input); beforeAll(() => { mockFetchAuthSession.mockResolvedValue({ credentials, @@ -335,7 +327,7 @@ describe('getUrl test with key', () => { }); describe('getUrl test with path', () => { - const getUrlWrapper = (input: GetUrlWithPathInput) => getUrl(Amplify, input); + const getUrlWrapper = (input: GetUrlWithPathInput) => getUrl(mockCtx, input); beforeAll(() => { mockFetchAuthSession.mockResolvedValue({ credentials, @@ -687,7 +679,7 @@ describe('getUrl test with path', () => { }); describe(`getURL with path and Expected Bucket Owner`, () => { - const getUrlWrapper = (input: GetUrlWithPathInput) => getUrl(Amplify, input); + const getUrlWrapper = (input: GetUrlWithPathInput) => getUrl(mockCtx, input); beforeAll(() => { mockFetchAuthSession.mockResolvedValue({ credentials, @@ -801,7 +793,7 @@ describe(`getURL with path and Expected Bucket Owner`, () => { }); describe('getUrl PUT method with expiresIn and credential expiration', () => { - const getUrlWrapper = (input: GetUrlWithPathInput) => getUrl(Amplify, input); + const getUrlWrapper = (input: GetUrlWithPathInput) => getUrl(mockCtx, input); beforeAll(() => { mockGetConfig.mockReturnValue({ Storage: { diff --git a/packages/storage/__tests__/providers/s3/apis/internal/list.test.ts b/packages/storage/__tests__/providers/s3/apis/internal/list.test.ts index e861652a90e..2daae74b99f 100644 --- a/packages/storage/__tests__/providers/s3/apis/internal/list.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/internal/list.test.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { AWSCredentials } from '@aws-amplify/core/internals/utils'; -import { Amplify, StorageAccessLevel } from '@aws-amplify/core'; +import { StorageAccessLevel } from '@aws-amplify/core'; import { listObjectsV2 } from '../../../../../src/providers/s3/utils/client/s3data'; import { list } from '../../../../../src/providers/s3/apis/internal/list'; @@ -17,21 +17,13 @@ import { } from '../../../../../src/providers/s3/types'; import './testUtils'; import { ListObjectsV2CommandInput } from '../../../../../src/providers/s3/utils/client/s3data/types'; +import { createMockAmplifyContext } from '../../../../testUtils/mockAmplifyContext'; jest.mock('../../../../../src/providers/s3/utils/client/s3data'); -jest.mock('@aws-amplify/core', () => ({ - ConsoleLogger: jest.fn().mockImplementation(function ConsoleLogger() { - return { debug: jest.fn() }; - }), - Amplify: { - getConfig: jest.fn(), - Auth: { - fetchAuthSession: jest.fn(), - }, - }, -})); -const mockFetchAuthSession = Amplify.Auth.fetchAuthSession as jest.Mock; -const mockGetConfig = jest.mocked(Amplify.getConfig); +const mockGetConfig = jest.fn(); +// Live getter delegates to mockGetConfig so per-test config variance works. +const mockCtx = createMockAmplifyContext({ getConfig: mockGetConfig }); +const mockFetchAuthSession = jest.mocked(mockCtx.fetchAuthSession); const mockListObject = listObjectsV2 as jest.Mock; const inputKey = 'path/itemsKey'; const bucket = 'bucket'; @@ -108,9 +100,9 @@ describe('list API', () => { }); }); describe('Prefix: Happy Cases:', () => { - const listAllWrapper = (input: ListAllInput) => list(Amplify, input); + const listAllWrapper = (input: ListAllInput) => list(mockCtx, input); const listPaginatedWrapper = (input: ListPaginateInput) => - list(Amplify, input); + list(mockCtx, input); afterEach(() => { jest.clearAllMocks(); }); @@ -394,9 +386,9 @@ describe('list API', () => { describe('Path: Happy Cases:', () => { const listAllWrapper = (input: ListAllWithPathInput) => - list(Amplify, input); + list(mockCtx, input); const listPaginatedWrapper = (input: ListPaginateWithPathInput) => - list(Amplify, input); + list(mockCtx, input); const resolvePath = ( path: string | (({ identityId }: { identityId: string }) => string), ) => @@ -658,7 +650,7 @@ describe('list API', () => { }), ); try { - await list(Amplify, {}); + await list(mockCtx, {}); } catch (error: any) { expect.assertions(3); expect(listObjectsV2).toHaveBeenCalledTimes(1); @@ -677,11 +669,11 @@ describe('list API', () => { describe.each([ { type: 'Prefix', - mockListFunction: () => list(Amplify, { prefix: 'test/' }), + mockListFunction: () => list(mockCtx, { prefix: 'test/' }), }, { type: 'Path', - mockListFunction: () => list(Amplify, { path: 'test/' }), + mockListFunction: () => list(mockCtx, { path: 'test/' }), }, ])('$type response validation check', ({ mockListFunction }) => { it.each([ @@ -760,7 +752,7 @@ describe('list API', () => { }); it('should return excludedSubpaths when "exclude" strategy is passed in the request', async () => { - const { items, excludedSubpaths } = (await list(Amplify, { + const { items, excludedSubpaths } = (await list(mockCtx, { path: mockedPath, options: { subpathStrategy: { strategy: 'exclude' }, @@ -794,7 +786,7 @@ describe('list API', () => { }; }); - const { items, excludedSubpaths } = (await list(Amplify, { + const { items, excludedSubpaths } = (await list(mockCtx, { path: mockedPath, options: { subpathStrategy: { strategy: 'exclude' }, @@ -816,7 +808,7 @@ describe('list API', () => { }); it('should return excludedSubpaths when "exclude" strategy and pageSize are passed in the request', async () => { - const { items, excludedSubpaths } = (await list(Amplify, { + const { items, excludedSubpaths } = (await list(mockCtx, { path: mockedPath, options: { subpathStrategy: { strategy: 'exclude' }, @@ -838,7 +830,7 @@ describe('list API', () => { }); it('should listObjectsV2 contain a custom Delimiter when "exclude" with delimiter is passed', async () => { - (await list(Amplify, { + (await list(mockCtx, { path: mockedPath, options: { subpathStrategy: { @@ -860,7 +852,7 @@ describe('list API', () => { }); it('should listObjectsV2 contain an undefined Delimiter when "include" strategy is passed', async () => { - await list(Amplify, { + await list(mockCtx, { path: mockedPath, options: { subpathStrategy: { @@ -881,7 +873,7 @@ describe('list API', () => { }); it('should listObjectsV2 contain an undefined Delimiter when no options are passed', async () => { - await list(Amplify, { + await list(mockCtx, { path: mockedPath, }); expect(listObjectsV2).toHaveBeenCalledTimes(1); @@ -899,9 +891,9 @@ describe('list API', () => { describe(`List with path and Expected Bucket Owner`, () => { describe(`v1`, () => { - const listAllWrapper = (input: ListAllInput) => list(Amplify, input); + const listAllWrapper = (input: ListAllInput) => list(mockCtx, input); const listPaginatedWrapper = (input: ListPaginateInput) => - list(Amplify, input); + list(mockCtx, input); const resolvePath = ( path: string | (({ identityId }: { identityId: string }) => string), ) => @@ -960,9 +952,9 @@ describe('list API', () => { describe(`v2`, () => { const listAllWrapper = (input: ListAllWithPathInput) => - list(Amplify, input); + list(mockCtx, input); const listPaginatedWrapper = (input: ListPaginateWithPathInput) => - list(Amplify, input); + list(mockCtx, input); const resolvePath = ( path: string | (({ identityId }: { identityId: string }) => string), ) => @@ -1029,7 +1021,7 @@ describe('list API', () => { { type: 'Prefix', listFunction: (options?: any) => - list(Amplify, { + list(mockCtx, { prefix: 'some folder with unprintable unicode/', options, }), @@ -1038,7 +1030,7 @@ describe('list API', () => { { type: 'Path', listFunction: (options?: any) => - list(Amplify, { + list(mockCtx, { path: 'public/some folder with unprintable unicode/', options, }), diff --git a/packages/storage/__tests__/providers/s3/apis/internal/remove.test.ts b/packages/storage/__tests__/providers/s3/apis/internal/remove.test.ts index 12a136ee44a..a623d5ddfb8 100644 --- a/packages/storage/__tests__/providers/s3/apis/internal/remove.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/internal/remove.test.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { AWSCredentials } from '@aws-amplify/core/internals/utils'; -import { Amplify, StorageAccessLevel } from '@aws-amplify/core'; +import { StorageAccessLevel } from '@aws-amplify/core'; import { deleteObject, @@ -20,26 +20,18 @@ import { } from '../../../../../src/providers/s3/types'; import { CanceledError } from '../../../../../src/errors/CanceledError'; import './testUtils'; +import { createMockAmplifyContext } from '../../../../testUtils/mockAmplifyContext'; jest.mock('../../../../../src/providers/s3/utils/client/s3data'); -jest.mock('@aws-amplify/core', () => ({ - ConsoleLogger: jest.fn().mockImplementation(function ConsoleLogger() { - return { debug: jest.fn() }; - }), - Amplify: { - getConfig: jest.fn(), - Auth: { - fetchAuthSession: jest.fn(), - }, - }, -})); const mockDeleteObject = deleteObject as jest.Mock; const mockDeleteObjects = deleteObjects as jest.Mock; const mockListObjectsV2 = listObjectsV2 as jest.Mock; const mockHeadObject = headObject as jest.Mock; -const mockFetchAuthSession = Amplify.Auth.fetchAuthSession as jest.Mock; -const mockGetConfig = jest.mocked(Amplify.getConfig); +const mockGetConfig = jest.fn(); +// Live getter delegates to mockGetConfig so per-test config variance works. +const mockCtx = createMockAmplifyContext({ getConfig: mockGetConfig }); +const mockFetchAuthSession = jest.mocked(mockCtx.fetchAuthSession); const inputKey = 'key'; const bucket = 'bucket'; @@ -77,7 +69,7 @@ describe('remove API', () => { describe('Happy Cases', () => { describe('With Key', () => { - const removeWrapper = (input: RemoveInput) => remove(Amplify, input); + const removeWrapper = (input: RemoveInput) => remove(mockCtx, input); beforeEach(() => { mockDeleteObject.mockImplementation(() => { @@ -218,7 +210,7 @@ describe('remove API', () => { describe('With Path', () => { const removeWrapper = (input: RemoveWithPathInput) => - remove(Amplify, input); + remove(mockCtx, input); beforeEach(() => { mockDeleteObject.mockImplementation(() => { @@ -546,7 +538,7 @@ describe('remove API', () => { expect.assertions(3); const key = 'wrongKey'; try { - await remove(Amplify, { key }); + await remove(mockCtx, { key }); } catch (error: any) { expect(deleteObject).toHaveBeenCalledTimes(1); await expect(deleteObject).toBeLastCalledWithConfigAndInput( @@ -563,7 +555,7 @@ describe('remove API', () => { it('should throw InvalidStorageOperationInput error when the path is empty', async () => { expect.assertions(1); try { - await remove(Amplify, { path: '' }); + await remove(mockCtx, { path: '' }); } catch (error: any) { expect(error.name).toBe( StorageValidationErrorCode.InvalidStorageOperationInput, @@ -574,7 +566,7 @@ describe('remove API', () => { it('should throw InvalidStoragePathInput error when the path has leading slash', async () => { expect.assertions(1); try { - await remove(Amplify, { path: '/invalid/path' }); + await remove(mockCtx, { path: '/invalid/path' }); } catch (error: any) { expect(error.name).toBe('InvalidStoragePathInput'); } diff --git a/packages/storage/__tests__/providers/s3/apis/internal/uploadData/index.test.ts b/packages/storage/__tests__/providers/s3/apis/internal/uploadData/index.test.ts index 7a4980974e5..4415163744a 100644 --- a/packages/storage/__tests__/providers/s3/apis/internal/uploadData/index.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/internal/uploadData/index.test.ts @@ -1,8 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { AmplifyClassV6 } from '@aws-amplify/core'; - import { FoundationContext } from '../../../../../../src/foundation/types'; import { uploadData } from '../../../../../../src/providers/s3/apis/internal/uploadData'; import { MAX_OBJECT_SIZE } from '../../../../../../src/providers/s3/utils/constants'; @@ -17,6 +15,7 @@ import { UploadDataInput, UploadDataWithPathInput, } from '../../../../../../src'; +import { createMockAmplifyContext } from '../../../../../testUtils/mockAmplifyContext'; jest.mock('../../../../../../src/providers/s3/utils/'); jest.mock( @@ -26,7 +25,7 @@ jest.mock( '../../../../../../src/providers/s3/apis/internal/uploadData/multipart', ); -const mockAmplifyInstance = {} as AmplifyClassV6; +const mockAmplifyInstance = createMockAmplifyContext(); const mockCtx: FoundationContext = { amplify: mockAmplifyInstance, readFile: jest.fn(), diff --git a/packages/storage/__tests__/providers/s3/apis/internal/uploadData/multipartHandlers.test.ts b/packages/storage/__tests__/providers/s3/apis/internal/uploadData/multipartHandlers.test.ts index ac7a4dcc671..082c1fbfa31 100644 --- a/packages/storage/__tests__/providers/s3/apis/internal/uploadData/multipartHandlers.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/internal/uploadData/multipartHandlers.test.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { AWSCredentials } from '@aws-amplify/core/internals/utils'; -import { Amplify, defaultStorage } from '@aws-amplify/core'; +import { AmplifyContext, defaultStorage } from '@aws-amplify/core'; import { abortMultipartUpload, @@ -50,7 +50,23 @@ const credentials: AWSCredentials = { secretAccessKey: 'secretAccessKey', }; const defaultIdentityId = 'defaultIdentityId'; -const mockFetchAuthSession = Amplify.Auth.fetchAuthSession as jest.Mock; +const mockGetConfig = jest.fn(); +const mockFetchAuthSession = jest.fn(); +let mockLibraryOptions: AmplifyContext['libraryOptions'] = {}; +// Multipart upload handlers resolve config/credentials off the AmplifyContext +// supplied via the FoundationContext. Back resourcesConfig/libraryOptions so +// tests can vary them, and fetchAuthSession so credentials can be controlled. +const mockAmplifyCtx: AmplifyContext = { + get resourcesConfig() { + return mockGetConfig(); + }, + get libraryOptions() { + return mockLibraryOptions; + }, + fetchAuthSession: mockFetchAuthSession, + clearCredentials: jest.fn(), + getTokens: jest.fn(), +}; const bucket = 'bucket'; const region = 'region'; const defaultKey = 'key'; @@ -69,7 +85,7 @@ const mockHeadObject = jest.mocked(headObject); const mockCalculateContentCRC32 = jest.mocked(calculateContentCRC32); const mockCtx: FoundationContext = { - amplify: Amplify, + amplify: mockAmplifyCtx, readFile: realReadFile, toBase64: realToBase64, }; @@ -188,7 +204,7 @@ describe('getMultipartUploadHandlers with key', () => { credentials, identityId: defaultIdentityId, }); - (Amplify.getConfig as jest.Mock).mockReturnValue({ + mockGetConfig.mockReturnValue({ Storage: { S3: { bucket, @@ -353,7 +369,7 @@ describe('getMultipartUploadHandlers with key', () => { it('should use md5 if no using crc32', async () => { mockMultipartUploadSuccess(); - Amplify.libraryOptions = { + mockLibraryOptions = { Storage: { S3: { isObjectLockEnabled: true, @@ -1085,7 +1101,7 @@ describe('getMultipartUploadHandlers with path', () => { credentials, identityId: defaultIdentityId, }); - (Amplify.getConfig as jest.Mock).mockReturnValue({ + mockGetConfig.mockReturnValue({ Storage: { S3: { bucket, @@ -1242,7 +1258,7 @@ describe('getMultipartUploadHandlers with path', () => { it('should use md5 if no using crc32', async () => { mockMultipartUploadSuccess(); - Amplify.libraryOptions = { + mockLibraryOptions = { Storage: { S3: { isObjectLockEnabled: true, diff --git a/packages/storage/__tests__/providers/s3/apis/internal/uploadData/putObjectJob.test.ts b/packages/storage/__tests__/providers/s3/apis/internal/uploadData/putObjectJob.test.ts index 2162793b525..be72a332c81 100644 --- a/packages/storage/__tests__/providers/s3/apis/internal/uploadData/putObjectJob.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/internal/uploadData/putObjectJob.test.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { AWSCredentials } from '@aws-amplify/core/internals/utils'; -import { Amplify } from '@aws-amplify/core'; +import { AmplifyContext } from '@aws-amplify/core'; import { putObject } from '../../../../../../src/providers/s3/utils/client/s3data'; import { calculateContentMd5 } from '../../../../../../src/foundation/utils'; @@ -24,16 +24,6 @@ jest.mock('../../../../../../src/foundation/utils', () => { calculateContentMd5: jest.fn(), }; }); -jest.mock('@aws-amplify/core', () => ({ - ConsoleLogger: jest.fn(), - fetchAuthSession: jest.fn(), - Amplify: { - getConfig: jest.fn(), - Auth: { - fetchAuthSession: jest.fn(), - }, - }, -})); const testPath = 'testPath/object'; const credentials: AWSCredentials = { @@ -42,7 +32,23 @@ const credentials: AWSCredentials = { secretAccessKey: 'secretAccessKey', }; const identityId = 'identityId'; -const mockFetchAuthSession = jest.mocked(Amplify.Auth.fetchAuthSession); +const mockGetConfig = jest.fn(); +const mockFetchAuthSession = jest.fn(); +let mockLibraryOptions: AmplifyContext['libraryOptions'] = {}; +// putObjectJob resolves config/credentials off the AmplifyContext supplied via +// the FoundationContext. Back resourcesConfig/libraryOptions so tests can vary +// them, and fetchAuthSession so credential/identityId can be controlled. +const mockAmplifyCtx: AmplifyContext = { + get resourcesConfig() { + return mockGetConfig(); + }, + get libraryOptions() { + return mockLibraryOptions; + }, + fetchAuthSession: mockFetchAuthSession, + clearCredentials: jest.fn(), + getTokens: jest.fn(), +}; const mockPutObject = jest.mocked(putObject); const bucket = 'bucket'; const region = 'region'; @@ -50,7 +56,7 @@ const data = 'data'; const dataLength = data.length; const mockCtx: FoundationContext = { - amplify: Amplify, + amplify: mockAmplifyCtx, readFile: realReadFile, toBase64: realToBase64, }; @@ -59,7 +65,7 @@ mockFetchAuthSession.mockResolvedValue({ credentials, identityId, }); -jest.mocked(Amplify.getConfig).mockReturnValue({ +mockGetConfig.mockReturnValue({ Storage: { S3: { bucket, @@ -157,7 +163,7 @@ describe('putObjectJob with key', () => { .spyOn(CRC32, 'calculateContentCRC32') .mockResolvedValue(undefined as any); - Amplify.libraryOptions = { + mockLibraryOptions = { Storage: { S3: { isObjectLockEnabled: true, @@ -383,7 +389,7 @@ describe('putObjectJob with path', () => { .spyOn(CRC32, 'calculateContentCRC32') .mockResolvedValue(undefined as any); - Amplify.libraryOptions = { + mockLibraryOptions = { Storage: { S3: { isObjectLockEnabled: true, diff --git a/packages/storage/__tests__/providers/s3/apis/list.test.ts b/packages/storage/__tests__/providers/s3/apis/list.test.ts index 578b74a971b..af50c1c95c9 100644 --- a/packages/storage/__tests__/providers/s3/apis/list.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/list.test.ts @@ -1,7 +1,10 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { Amplify } from '@aws-amplify/core'; +import { + clearGlobalContext, + setGlobalContext, +} from '@aws-amplify/core/internals/utils'; import { ListAllInput, @@ -11,12 +14,24 @@ import { } from '../../../../src'; import { list } from '../../../../src/providers/s3/apis'; import { list as internalListImpl } from '../../../../src/providers/s3/apis/internal/list'; +import { createMockAmplifyContext } from '../../../testUtils/mockAmplifyContext'; jest.mock('../../../../src/providers/s3/apis/internal/list'); const mockInternalListImpl = jest.mocked(internalListImpl); +const mockCtx = createMockAmplifyContext(); describe('client-side list', () => { + beforeAll(() => { + // The public API falls back to the global AmplifyContext when no ctx is + // passed explicitly; establish it so resolveCtxArgs can resolve it. + setGlobalContext(mockCtx); + }); + + afterAll(() => { + clearGlobalContext(); + }); + beforeEach(() => { jest.clearAllMocks(); }); @@ -28,7 +43,7 @@ describe('client-side list', () => { prefix: 'source-key', }; expect(list(input)).toEqual(mockInternalResult); - expect(mockInternalListImpl).toBeCalledWith(Amplify, input); + expect(mockInternalListImpl).toBeCalledWith(mockCtx, input); }); it('should pass through list paginate input with key and output to internal implementation', async () => { @@ -42,7 +57,7 @@ describe('client-side list', () => { }, }; expect(list(input)).toEqual(mockInternalResult); - expect(mockInternalListImpl).toBeCalledWith(Amplify, input); + expect(mockInternalListImpl).toBeCalledWith(mockCtx, input); }); it('should pass through list all input with path and output to internal implementation', async () => { @@ -52,7 +67,7 @@ describe('client-side list', () => { path: 'abc', }; expect(list(input)).toEqual(mockInternalResult); - expect(mockInternalListImpl).toBeCalledWith(Amplify, input); + expect(mockInternalListImpl).toBeCalledWith(mockCtx, input); }); it('should pass through list paginate input with path and output to internal implementation', async () => { @@ -66,6 +81,17 @@ describe('client-side list', () => { }, }; expect(list(input)).toEqual(mockInternalResult); - expect(mockInternalListImpl).toBeCalledWith(Amplify, input); + expect(mockInternalListImpl).toBeCalledWith(mockCtx, input); + }); + + it('should pass explicit AmplifyContext to internal implementation when called with two args', () => { + const explicitCtx = createMockAmplifyContext(); + const mockInternalResult = 'RESULT' as any; + mockInternalListImpl.mockReturnValue(mockInternalResult); + const input: ListAllWithPathInput = { + path: 'abc', + }; + expect(list(explicitCtx, input)).toEqual(mockInternalResult); + expect(mockInternalListImpl).toBeCalledWith(explicitCtx, input); }); }); diff --git a/packages/storage/__tests__/providers/s3/apis/remove.test.ts b/packages/storage/__tests__/providers/s3/apis/remove.test.ts index 8c42aec2f02..531c052f8a7 100644 --- a/packages/storage/__tests__/providers/s3/apis/remove.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/remove.test.ts @@ -1,17 +1,32 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { Amplify } from '@aws-amplify/core'; +import { + clearGlobalContext, + setGlobalContext, +} from '@aws-amplify/core/internals/utils'; import { RemoveInput, RemoveWithPathInput } from '../../../../src'; import { remove } from '../../../../src/providers/s3/apis'; import { remove as internalRemoveImpl } from '../../../../src/providers/s3/apis/internal/remove'; +import { createMockAmplifyContext } from '../../../testUtils/mockAmplifyContext'; jest.mock('../../../../src/providers/s3/apis/internal/remove'); const mockInternalRemoveImpl = jest.mocked(internalRemoveImpl); +const mockCtx = createMockAmplifyContext(); describe('client-side remove', () => { + beforeAll(() => { + // The public API falls back to the global AmplifyContext when no ctx is + // passed explicitly; establish it so resolveCtxArgs can resolve it. + setGlobalContext(mockCtx); + }); + + afterAll(() => { + clearGlobalContext(); + }); + beforeEach(() => { jest.clearAllMocks(); }); @@ -23,7 +38,7 @@ describe('client-side remove', () => { key: 'source-key', }; expect(remove(input)).toEqual(mockInternalResult); - expect(mockInternalRemoveImpl).toBeCalledWith(Amplify, input); + expect(mockInternalRemoveImpl).toBeCalledWith(mockCtx, input); }); it('should pass through input with path and output to internal implementation', async () => { @@ -33,6 +48,17 @@ describe('client-side remove', () => { path: 'abc', }; expect(remove(input)).toEqual(mockInternalResult); - expect(mockInternalRemoveImpl).toBeCalledWith(Amplify, input); + expect(mockInternalRemoveImpl).toBeCalledWith(mockCtx, input); + }); + + it('should pass explicit AmplifyContext to internal implementation when called with two args', () => { + const explicitCtx = createMockAmplifyContext(); + const mockInternalResult = 'RESULT' as any; + mockInternalRemoveImpl.mockReturnValue(mockInternalResult); + const input: RemoveWithPathInput = { + path: 'abc', + }; + expect(remove(explicitCtx, input)).toEqual(mockInternalResult); + expect(mockInternalRemoveImpl).toBeCalledWith(explicitCtx, input); }); }); diff --git a/packages/storage/__tests__/providers/s3/apis/server/copy.test.ts b/packages/storage/__tests__/providers/s3/apis/server/copy.test.ts index 06ce54b5b6b..d23f94867e9 100644 --- a/packages/storage/__tests__/providers/s3/apis/server/copy.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/server/copy.test.ts @@ -13,7 +13,27 @@ jest.mock('@aws-amplify/core/internals/adapter-core'); const mockInternalCopyImpl = jest.mocked(internalCopyImpl); const mockGetAmplifyServerContext = jest.mocked(getAmplifyServerContext); const mockInternalResult = 'RESULT' as any; -const mockAmplifyClass = 'AMPLIFY_CLASS' as any; +const mockResourcesConfig = {} as any; +// Realistic `AmplifyClass` shape: getConfig()/libraryOptions/Auth.*, without the +// top-level context methods (fetchAuthSession/clearCredentials/getTokens). +const mockAmplifyClass = { + getConfig: jest.fn(() => mockResourcesConfig), + libraryOptions: {}, + Auth: { + fetchAuthSession: jest.fn(), + clearCredentials: jest.fn(), + getTokens: jest.fn(), + }, +} as any; +// The context the internal impl should receive after resolveServerContext +// adapts the AmplifyClass into an AmplifyContext. +const expectedResolvedCtx = { + resourcesConfig: mockResourcesConfig, + libraryOptions: mockAmplifyClass.libraryOptions, + fetchAuthSession: expect.any(Function), + clearCredentials: expect.any(Function), + getTokens: expect.any(Function), +}; const mockAmplifyContextSpec = { token: { value: Symbol('123') }, }; @@ -40,7 +60,7 @@ describe('server-side copy', () => { }, }; expect(copy(mockAmplifyContextSpec, input)).toEqual(mockInternalResult); - expect(mockInternalCopyImpl).toBeCalledWith(mockAmplifyClass, input); + expect(mockInternalCopyImpl).toBeCalledWith(expectedResolvedCtx, input); }); it('should pass through input with path and output to internal implementation', async () => { @@ -49,6 +69,6 @@ describe('server-side copy', () => { destination: { path: 'abc' }, }; expect(copy(mockAmplifyContextSpec, input)).toEqual(mockInternalResult); - expect(mockInternalCopyImpl).toBeCalledWith(mockAmplifyClass, input); + expect(mockInternalCopyImpl).toBeCalledWith(expectedResolvedCtx, input); }); }); diff --git a/packages/storage/__tests__/providers/s3/apis/server/getProperties.test.ts b/packages/storage/__tests__/providers/s3/apis/server/getProperties.test.ts index 9afd1403d55..fad8257ffc4 100644 --- a/packages/storage/__tests__/providers/s3/apis/server/getProperties.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/server/getProperties.test.ts @@ -16,7 +16,27 @@ jest.mock('@aws-amplify/core/internals/adapter-core'); const mockInternalGetPropertiesImpl = jest.mocked(internalGetPropertiesImpl); const mockGetAmplifyServerContext = jest.mocked(getAmplifyServerContext); const mockInternalResult = 'RESULT' as any; -const mockAmplifyClass = 'AMPLIFY_CLASS' as any; +const mockResourcesConfig = {} as any; +// Realistic `AmplifyClass` shape: getConfig()/libraryOptions/Auth.*, without the +// top-level context methods (fetchAuthSession/clearCredentials/getTokens). +const mockAmplifyClass = { + getConfig: jest.fn(() => mockResourcesConfig), + libraryOptions: {}, + Auth: { + fetchAuthSession: jest.fn(), + clearCredentials: jest.fn(), + getTokens: jest.fn(), + }, +} as any; +// The context the internal impl should receive after resolveServerContext +// adapts the AmplifyClass into an AmplifyContext. +const expectedResolvedCtx = { + resourcesConfig: mockResourcesConfig, + libraryOptions: mockAmplifyClass.libraryOptions, + fetchAuthSession: expect.any(Function), + clearCredentials: expect.any(Function), + getTokens: expect.any(Function), +}; const mockAmplifyContextSpec = { token: { value: Symbol('123') }, }; @@ -41,7 +61,7 @@ describe('server-side getProperties', () => { mockInternalResult, ); expect(mockInternalGetPropertiesImpl).toBeCalledWith( - mockAmplifyClass, + expectedResolvedCtx, input, ); }); @@ -54,7 +74,7 @@ describe('server-side getProperties', () => { mockInternalResult, ); expect(mockInternalGetPropertiesImpl).toBeCalledWith( - mockAmplifyClass, + expectedResolvedCtx, input, ); }); diff --git a/packages/storage/__tests__/providers/s3/apis/server/getUrl.test.ts b/packages/storage/__tests__/providers/s3/apis/server/getUrl.test.ts index 3dfac7a58dc..e946320f620 100644 --- a/packages/storage/__tests__/providers/s3/apis/server/getUrl.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/server/getUrl.test.ts @@ -13,7 +13,27 @@ jest.mock('@aws-amplify/core/internals/adapter-core'); const mockInternalGetUrlImpl = jest.mocked(internalGetUrlImpl); const mockGetAmplifyServerContext = jest.mocked(getAmplifyServerContext); const mockInternalResult = 'RESULT' as any; -const mockAmplifyClass = 'AMPLIFY_CLASS' as any; +const mockResourcesConfig = {} as any; +// Realistic `AmplifyClass` shape: getConfig()/libraryOptions/Auth.*, without the +// top-level context methods (fetchAuthSession/clearCredentials/getTokens). +const mockAmplifyClass = { + getConfig: jest.fn(() => mockResourcesConfig), + libraryOptions: {}, + Auth: { + fetchAuthSession: jest.fn(), + clearCredentials: jest.fn(), + getTokens: jest.fn(), + }, +} as any; +// The context the internal impl should receive after resolveServerContext +// adapts the AmplifyClass into an AmplifyContext. +const expectedResolvedCtx = { + resourcesConfig: mockResourcesConfig, + libraryOptions: mockAmplifyClass.libraryOptions, + fetchAuthSession: expect.any(Function), + clearCredentials: expect.any(Function), + getTokens: expect.any(Function), +}; describe('server-side getUrl', () => { beforeEach(() => { @@ -39,7 +59,7 @@ describe('server-side getUrl', () => { input, ), ).toEqual(mockInternalResult); - expect(mockInternalGetUrlImpl).toBeCalledWith(mockAmplifyClass, input); + expect(mockInternalGetUrlImpl).toBeCalledWith(expectedResolvedCtx, input); }); it('should pass through input with path and output to internal implementation', async () => { @@ -54,6 +74,6 @@ describe('server-side getUrl', () => { input, ), ).toEqual(mockInternalResult); - expect(mockInternalGetUrlImpl).toBeCalledWith(mockAmplifyClass, input); + expect(mockInternalGetUrlImpl).toBeCalledWith(expectedResolvedCtx, input); }); }); diff --git a/packages/storage/__tests__/providers/s3/apis/server/list.test.ts b/packages/storage/__tests__/providers/s3/apis/server/list.test.ts index febd469afa3..87d16c2df26 100644 --- a/packages/storage/__tests__/providers/s3/apis/server/list.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/server/list.test.ts @@ -18,7 +18,27 @@ jest.mock('@aws-amplify/core/internals/adapter-core'); const mockInternalListImpl = jest.mocked(internalListImpl); const mockGetAmplifyServerContext = jest.mocked(getAmplifyServerContext); const mockInternalResult = 'RESULT' as any; -const mockAmplifyClass = 'AMPLIFY_CLASS' as any; +const mockResourcesConfig = {} as any; +// Realistic `AmplifyClass` shape: getConfig()/libraryOptions/Auth.*, without the +// top-level context methods (fetchAuthSession/clearCredentials/getTokens). +const mockAmplifyClass = { + getConfig: jest.fn(() => mockResourcesConfig), + libraryOptions: {}, + Auth: { + fetchAuthSession: jest.fn(), + clearCredentials: jest.fn(), + getTokens: jest.fn(), + }, +} as any; +// The context the internal impl should receive after resolveServerContext +// adapts the AmplifyClass into an AmplifyContext. +const expectedResolvedCtx = { + resourcesConfig: mockResourcesConfig, + libraryOptions: mockAmplifyClass.libraryOptions, + fetchAuthSession: expect.any(Function), + clearCredentials: expect.any(Function), + getTokens: expect.any(Function), +}; const mockAmplifyContextSpec = { token: { value: Symbol('123') }, }; @@ -40,7 +60,7 @@ describe('server-side list', () => { prefix: 'source-key', }; expect(list(mockAmplifyContextSpec, input)).toEqual(mockInternalResult); - expect(mockInternalListImpl).toBeCalledWith(mockAmplifyClass, input); + expect(mockInternalListImpl).toBeCalledWith(expectedResolvedCtx, input); }); it('should pass through list paginate input with key and output to internal implementation', async () => { @@ -52,7 +72,7 @@ describe('server-side list', () => { }, }; expect(list(mockAmplifyContextSpec, input)).toEqual(mockInternalResult); - expect(mockInternalListImpl).toBeCalledWith(mockAmplifyClass, input); + expect(mockInternalListImpl).toBeCalledWith(expectedResolvedCtx, input); }); it('should pass through list all input with path and output to internal implementation', async () => { @@ -60,7 +80,7 @@ describe('server-side list', () => { path: 'abc', }; expect(list(mockAmplifyContextSpec, input)).toEqual(mockInternalResult); - expect(mockInternalListImpl).toBeCalledWith(mockAmplifyClass, input); + expect(mockInternalListImpl).toBeCalledWith(expectedResolvedCtx, input); }); it('should pass through list paginate input with path and output to internal implementation', async () => { @@ -72,6 +92,6 @@ describe('server-side list', () => { }, }; expect(list(mockAmplifyContextSpec, input)).toEqual(mockInternalResult); - expect(mockInternalListImpl).toBeCalledWith(mockAmplifyClass, input); + expect(mockInternalListImpl).toBeCalledWith(expectedResolvedCtx, input); }); }); diff --git a/packages/storage/__tests__/providers/s3/apis/server/remove.test.ts b/packages/storage/__tests__/providers/s3/apis/server/remove.test.ts index 861c3ce0d24..9ab3c9c76e8 100644 --- a/packages/storage/__tests__/providers/s3/apis/server/remove.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/server/remove.test.ts @@ -13,7 +13,27 @@ jest.mock('@aws-amplify/core/internals/adapter-core'); const mockInternalRemoveImpl = jest.mocked(internalRemoveImpl); const mockGetAmplifyServerContext = jest.mocked(getAmplifyServerContext); const mockInternalResult = 'RESULT' as any; -const mockAmplifyClass = 'AMPLIFY_CLASS' as any; +const mockResourcesConfig = {} as any; +// Realistic `AmplifyClass` shape: getConfig()/libraryOptions/Auth.*, without the +// top-level context methods (fetchAuthSession/clearCredentials/getTokens). +const mockAmplifyClass = { + getConfig: jest.fn(() => mockResourcesConfig), + libraryOptions: {}, + Auth: { + fetchAuthSession: jest.fn(), + clearCredentials: jest.fn(), + getTokens: jest.fn(), + }, +} as any; +// The context the internal impl should receive after resolveServerContext +// adapts the AmplifyClass into an AmplifyContext. +const expectedResolvedCtx = { + resourcesConfig: mockResourcesConfig, + libraryOptions: mockAmplifyClass.libraryOptions, + fetchAuthSession: expect.any(Function), + clearCredentials: expect.any(Function), + getTokens: expect.any(Function), +}; const mockAmplifyContextSpec = { token: { value: Symbol('123') }, }; @@ -35,7 +55,7 @@ describe('server-side remove', () => { key: 'source-key', }; expect(remove(mockAmplifyContextSpec, input)).toEqual(mockInternalResult); - expect(mockInternalRemoveImpl).toBeCalledWith(mockAmplifyClass, input); + expect(mockInternalRemoveImpl).toBeCalledWith(expectedResolvedCtx, input); }); it('should pass through input with path and output to internal implementation', async () => { @@ -43,6 +63,6 @@ describe('server-side remove', () => { path: 'abc', }; expect(remove(mockAmplifyContextSpec, input)).toEqual(mockInternalResult); - expect(mockInternalRemoveImpl).toBeCalledWith(mockAmplifyClass, input); + expect(mockInternalRemoveImpl).toBeCalledWith(expectedResolvedCtx, input); }); }); diff --git a/packages/storage/__tests__/providers/s3/apis/server/resolveServerContext.test.ts b/packages/storage/__tests__/providers/s3/apis/server/resolveServerContext.test.ts new file mode 100644 index 00000000000..ec138b71553 --- /dev/null +++ b/packages/storage/__tests__/providers/s3/apis/server/resolveServerContext.test.ts @@ -0,0 +1,120 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { AmplifyContext } from '@aws-amplify/core'; +import { getAmplifyServerContext } from '@aws-amplify/core/internals/adapter-core'; + +import { resolveServerContext } from '../../../../../src/providers/s3/apis/server/resolveServerContext'; + +jest.mock('@aws-amplify/core/internals/adapter-core'); + +const mockGetAmplifyServerContext = jest.mocked(getAmplifyServerContext); + +// Runtime brand used to identify AmplifyContext objects. Kept in sync with +// core's `AMPLIFY_CONTEXT_BRAND` (Symbol.for('amplify.context')). +const AMPLIFY_CONTEXT_BRAND = Symbol.for('amplify.context'); + +describe('resolveServerContext', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('legacy server ContextSpec branch', () => { + const mockResourcesConfig = { Storage: { S3: { bucket: 'b' } } } as any; + const mockSession = { identityId: 'id-123' } as any; + const mockTokens = { accessToken: 'token' } as any; + // Mimics `AmplifyClass`: it exposes resourcesConfig/libraryOptions via + // getConfig()/libraryOptions and a cross-category `Auth` utility, but + // CRUCIALLY does NOT define top-level + // fetchAuthSession/clearCredentials/getTokens methods. + let mockAmplifyClass: any; + const mockContextSpec = { token: { value: Symbol('123') } } as any; + + beforeEach(() => { + mockAmplifyClass = { + getConfig: jest.fn(() => mockResourcesConfig), + libraryOptions: {}, + Auth: { + fetchAuthSession: jest.fn().mockResolvedValue(mockSession), + clearCredentials: jest.fn().mockResolvedValue(undefined), + getTokens: jest.fn().mockResolvedValue(mockTokens), + }, + }; + mockGetAmplifyServerContext.mockReturnValue({ + amplify: mockAmplifyClass, + } as any); + }); + + it('should unwrap the AmplifyClass from the server context', () => { + resolveServerContext(mockContextSpec); + expect(mockGetAmplifyServerContext).toHaveBeenCalledWith(mockContextSpec); + }); + + it('should source resourcesConfig from amplify.getConfig() via live getter', () => { + const resolved = resolveServerContext(mockContextSpec); + expect(resolved.resourcesConfig).toBe(mockResourcesConfig); + expect(mockAmplifyClass.getConfig).toHaveBeenCalledTimes(1); + }); + + it('should source libraryOptions from amplify.libraryOptions', () => { + const resolved = resolveServerContext(mockContextSpec); + expect(resolved.libraryOptions).toBe(mockAmplifyClass.libraryOptions); + }); + + it('should delegate fetchAuthSession to amplify.Auth.fetchAuthSession', async () => { + const resolved = resolveServerContext(mockContextSpec); + const options = { forceRefresh: true }; + const result = await resolved.fetchAuthSession(options); + + expect(mockAmplifyClass.Auth.fetchAuthSession).toHaveBeenCalledWith( + options, + ); + expect(result).toBe(mockSession); + }); + + it('should delegate fetchAuthSession with default options when none provided', async () => { + const resolved = resolveServerContext(mockContextSpec); + await resolved.fetchAuthSession(); + + expect(mockAmplifyClass.Auth.fetchAuthSession).toHaveBeenCalledWith({}); + }); + + it('should delegate clearCredentials to amplify.Auth.clearCredentials', async () => { + const resolved = resolveServerContext(mockContextSpec); + await resolved.clearCredentials(); + + expect(mockAmplifyClass.Auth.clearCredentials).toHaveBeenCalledTimes(1); + }); + + it('should delegate getTokens to amplify.Auth.getTokens', async () => { + const resolved = resolveServerContext(mockContextSpec); + const options = { forceRefresh: true }; + const result = await resolved.getTokens(options); + + expect(mockAmplifyClass.Auth.getTokens).toHaveBeenCalledWith(options); + expect(result).toBe(mockTokens); + }); + }); + + describe('branded AmplifyContext branch', () => { + it('should return an already-branded AmplifyContext unchanged', () => { + const brandedContext = { + resourcesConfig: {}, + libraryOptions: {}, + fetchAuthSession: jest.fn(), + clearCredentials: jest.fn(), + getTokens: jest.fn(), + } as unknown as AmplifyContext; + Object.defineProperty(brandedContext, AMPLIFY_CONTEXT_BRAND, { + value: true, + enumerable: false, + }); + + const resolved = resolveServerContext(brandedContext); + + expect(resolved).toBe(brandedContext); + // The server context must NOT be consulted for a real AmplifyContext. + expect(mockGetAmplifyServerContext).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/packages/storage/__tests__/providers/s3/apis/server/uploadData.test.ts b/packages/storage/__tests__/providers/s3/apis/server/uploadData.test.ts index 8667fe3eb25..6d045e36452 100644 --- a/packages/storage/__tests__/providers/s3/apis/server/uploadData.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/server/uploadData.test.ts @@ -24,12 +24,32 @@ const mockInternalResult: any = { state: 'IN_PROGRESS', result: Promise.resolve({ path: 'x' }), }; -const mockAmplifyClass = 'AMPLIFY_CLASS' as any; +const mockResourcesConfig = {} as any; +// Realistic `AmplifyClass` shape: getConfig()/libraryOptions/Auth.*, without the +// top-level context methods (fetchAuthSession/clearCredentials/getTokens). +const mockAmplifyClass = { + getConfig: jest.fn(() => mockResourcesConfig), + libraryOptions: {}, + Auth: { + fetchAuthSession: jest.fn(), + clearCredentials: jest.fn(), + getTokens: jest.fn(), + }, +} as any; +// The context the internal impl should receive after resolveServerContext +// adapts the AmplifyClass into an AmplifyContext. +const expectedResolvedCtx = { + resourcesConfig: mockResourcesConfig, + libraryOptions: mockAmplifyClass.libraryOptions, + fetchAuthSession: expect.any(Function), + clearCredentials: expect.any(Function), + getTokens: expect.any(Function), +}; const mockAmplifyContextSpec = { token: { value: Symbol('123') }, }; const expectedCtx = { - amplify: mockAmplifyClass, + amplify: expectedResolvedCtx, readFile: expect.any(Function), toBase64: expect.any(Function), }; @@ -91,10 +111,15 @@ describe('server-side uploadData', () => { }; uploadData(mockAmplifyContextSpec as any, input); expect(mockGetAmplifyServerContext).toBeCalledWith(mockAmplifyContextSpec); - // Ensure the amplify passed to internal uploadData is from the server context - expect((mockInternalUploadDataImpl.mock.calls[0][0] as any).amplify).toBe( - mockAmplifyClass, - ); + // Ensure the amplify passed to internal uploadData is the AmplifyContext + // resolved from the server context (bridged to the AmplifyClass), and that + // it exposes the top-level context methods the internal impl relies on. + const passedAmplify = (mockInternalUploadDataImpl.mock.calls[0][0] as any) + .amplify; + expect(passedAmplify.resourcesConfig).toBe(mockResourcesConfig); + expect(typeof passedAmplify.fetchAuthSession).toBe('function'); + expect(typeof passedAmplify.clearCredentials).toBe('function'); + expect(typeof passedAmplify.getTokens).toBe('function'); }); it('should return a task type that does NOT expose pause/resume at the type level', () => { diff --git a/packages/storage/__tests__/providers/s3/apis/uploadData.test.ts b/packages/storage/__tests__/providers/s3/apis/uploadData.test.ts index f11d9605773..8ce263b58b5 100644 --- a/packages/storage/__tests__/providers/s3/apis/uploadData.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/uploadData.test.ts @@ -1,22 +1,38 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { Amplify, defaultStorage } from '@aws-amplify/core'; +import { defaultStorage } from '@aws-amplify/core'; +import { + clearGlobalContext, + setGlobalContext, +} from '@aws-amplify/core/internals/utils'; import { uploadData } from '../../../../src/providers/s3/apis'; import { uploadData as internalUploadDataImpl } from '../../../../src/providers/s3/apis/internal/uploadData'; +import { createMockAmplifyContext } from '../../../testUtils/mockAmplifyContext'; jest.mock('../../../../src/providers/s3/apis/internal/uploadData'); const mockInternalUploadDataImpl = jest.mocked(internalUploadDataImpl); +const mockCtx = createMockAmplifyContext(); const expectedCtx = { - amplify: Amplify, + amplify: mockCtx, readFile: expect.any(Function), toBase64: expect.any(Function), }; describe('client-side uploadData', () => { + beforeAll(() => { + // The public API falls back to the global AmplifyContext when no ctx is + // passed explicitly; establish it so resolveCtxArgs can resolve it. + setGlobalContext(mockCtx); + }); + + afterAll(() => { + clearGlobalContext(); + }); + beforeEach(() => { jest.clearAllMocks(); }); @@ -60,4 +76,28 @@ describe('client-side uploadData', () => { }, }); }); + + it('should pass explicit AmplifyContext to internal implementation when called with two args', () => { + const explicitCtx = createMockAmplifyContext(); + const mockInternalResult = 'RESULT' as any; + mockInternalUploadDataImpl.mockReturnValue(mockInternalResult); + const input = { + path: 'path', + data: 'data', + }; + expect(uploadData(explicitCtx, input)).toEqual(mockInternalResult); + expect(mockInternalUploadDataImpl).toBeCalledWith( + { + amplify: explicitCtx, + readFile: expect.any(Function), + toBase64: expect.any(Function), + }, + { + ...input, + options: { + resumableUploadsCache: defaultStorage, + }, + }, + ); + }); }); diff --git a/packages/storage/__tests__/providers/s3/apis/utils/resolveS3ConfigAndInput.test.ts b/packages/storage/__tests__/providers/s3/apis/utils/resolveS3ConfigAndInput.test.ts index 662640e3340..2a5d4117eb6 100644 --- a/packages/storage/__tests__/providers/s3/apis/utils/resolveS3ConfigAndInput.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/utils/resolveS3ConfigAndInput.test.ts @@ -1,7 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { Amplify } from '@aws-amplify/core'; +import { AmplifyContext } from '@aws-amplify/core'; import { resolveS3ConfigAndInput } from '../../../../../src/providers/s3/utils'; import { resolvePrefix } from '../../../../../src/utils/resolvePrefix'; @@ -17,20 +17,29 @@ import { INVALID_STORAGE_INPUT } from '../../../../../src/errors/constants'; import { BucketInfo } from '../../../../../src/providers/s3/types/options'; import { StorageError } from '../../../../../src/errors/StorageError'; -jest.mock('@aws-amplify/core', () => ({ - ConsoleLogger: jest.fn(), - Amplify: { - getConfig: jest.fn(), - Auth: { - fetchAuthSession: jest.fn(), - }, - }, -})); jest.mock('../../../../../src/utils/resolvePrefix'); -const mockGetConfig = jest.mocked(Amplify.getConfig); +// resolveS3ConfigAndInput reads config/library options off the AmplifyContext +// and calls ctx.fetchAuthSession(). We back resourcesConfig with a jest.fn so +// individual tests can vary the returned config (and assert it was read) just +// like the previous Amplify.getConfig() mock, and libraryOptions with a mutable +// holder that tests can reassign. +const mockGetConfig = jest.fn(); +let mockLibraryOptions: AmplifyContext['libraryOptions'] = {}; +const mockFetchAuthSession = jest.fn(); const mockDefaultResolvePrefix = resolvePrefix as jest.Mock; -const mockFetchAuthSession = Amplify.Auth.fetchAuthSession as jest.Mock; + +const mockCtx: AmplifyContext = { + get resourcesConfig() { + return mockGetConfig(); + }, + get libraryOptions() { + return mockLibraryOptions; + }, + fetchAuthSession: mockFetchAuthSession, + clearCredentials: jest.fn(), + getTokens: jest.fn(), +}; const bucket = 'bucket'; const region = 'region'; @@ -44,7 +53,7 @@ const targetIdentityId = 'targetIdentityId'; describe('resolveS3ConfigAndInput', () => { beforeEach(() => { jest.clearAllMocks(); - Amplify.libraryOptions = {}; + mockLibraryOptions = {}; }); mockFetchAuthSession.mockResolvedValue({ credentials, @@ -63,7 +72,7 @@ describe('resolveS3ConfigAndInput', () => { it('should call fetchAuthSession for credentials and identityId', async () => { expect.assertions(1); - await resolveS3ConfigAndInput(Amplify, {}); + await resolveS3ConfigAndInput(mockCtx, {}); expect(mockFetchAuthSession).toHaveBeenCalled(); }); @@ -74,7 +83,7 @@ describe('resolveS3ConfigAndInput', () => { }); const { s3Config: { credentials: credentialsProvider }, - } = await resolveS3ConfigAndInput(Amplify, {}); + } = await resolveS3ConfigAndInput(mockCtx, {}); if (typeof credentialsProvider === 'function') { await expect(credentialsProvider()).rejects.toMatchObject( validationErrorMap[StorageValidationErrorCode.NoCredentials], @@ -88,12 +97,12 @@ describe('resolveS3ConfigAndInput', () => { mockFetchAuthSession.mockResolvedValueOnce({ credentials, }); - expect(async () => resolveS3ConfigAndInput(Amplify, {})).not.toThrow(); + expect(async () => resolveS3ConfigAndInput(mockCtx, {})).not.toThrow(); }); it('should resolve bucket from S3 config', async () => { const { bucket: resolvedBucket } = await resolveS3ConfigAndInput( - Amplify, + mockCtx, {}, ); expect(resolvedBucket).toEqual(bucket); @@ -108,13 +117,13 @@ describe('resolveS3ConfigAndInput', () => { }, }, }); - await expect(resolveS3ConfigAndInput(Amplify, {})).rejects.toMatchObject( + await expect(resolveS3ConfigAndInput(mockCtx, {})).rejects.toMatchObject( validationErrorMap[StorageValidationErrorCode.NoBucket], ); }); it('should resolve region from S3 config', async () => { - const { s3Config } = await resolveS3ConfigAndInput(Amplify, {}); + const { s3Config } = await resolveS3ConfigAndInput(mockCtx, {}); expect(s3Config.region).toEqual(region); expect(mockGetConfig).toHaveBeenCalled(); }); @@ -127,7 +136,7 @@ describe('resolveS3ConfigAndInput', () => { }, }, }); - await expect(resolveS3ConfigAndInput(Amplify, {})).rejects.toMatchObject( + await expect(resolveS3ConfigAndInput(mockCtx, {})).rejects.toMatchObject( validationErrorMap[StorageValidationErrorCode.NoRegion], ); }); @@ -142,41 +151,41 @@ describe('resolveS3ConfigAndInput', () => { }, }, }); - const { s3Config } = await resolveS3ConfigAndInput(Amplify, {}); + const { s3Config } = await resolveS3ConfigAndInput(mockCtx, {}); expect(s3Config.customEndpoint).toEqual('http://localhost:20005'); expect(s3Config.forcePathStyle).toEqual(true); expect(mockGetConfig).toHaveBeenCalled(); }); it('should resolve isObjectLockEnabled from S3 library options', async () => { - Amplify.libraryOptions = { + mockLibraryOptions = { Storage: { S3: { isObjectLockEnabled: true, }, }, }; - const { isObjectLockEnabled } = await resolveS3ConfigAndInput(Amplify, {}); + const { isObjectLockEnabled } = await resolveS3ConfigAndInput(mockCtx, {}); expect(isObjectLockEnabled).toEqual(true); }); it('should use default prefix resolver', async () => { mockDefaultResolvePrefix.mockResolvedValueOnce('prefix'); - const { keyPrefix } = await resolveS3ConfigAndInput(Amplify, {}); + const { keyPrefix } = await resolveS3ConfigAndInput(mockCtx, {}); expect(mockDefaultResolvePrefix).toHaveBeenCalled(); expect(keyPrefix).toEqual('prefix'); }); it('should use prefix resolver from S3 library options if supplied', async () => { const customResolvePrefix = jest.fn().mockResolvedValueOnce('prefix'); - Amplify.libraryOptions = { + mockLibraryOptions = { Storage: { S3: { prefixResolver: customResolvePrefix, }, }, }; - const { keyPrefix } = await resolveS3ConfigAndInput(Amplify, {}); + const { keyPrefix } = await resolveS3ConfigAndInput(mockCtx, {}); expect(customResolvePrefix).toHaveBeenCalled(); expect(keyPrefix).toEqual('prefix'); expect(mockDefaultResolvePrefix).not.toHaveBeenCalled(); @@ -184,7 +193,7 @@ describe('resolveS3ConfigAndInput', () => { it('should resolve prefix with given access level', async () => { mockDefaultResolvePrefix.mockResolvedValueOnce('prefix'); - const { keyPrefix } = await resolveS3ConfigAndInput(Amplify, { + const { keyPrefix } = await resolveS3ConfigAndInput(mockCtx, { options: { accessLevel: 'someLevel' as any }, }); expect(mockDefaultResolvePrefix).toHaveBeenCalledWith({ @@ -196,14 +205,14 @@ describe('resolveS3ConfigAndInput', () => { it('should resolve prefix with default access level from S3 library options', async () => { mockDefaultResolvePrefix.mockResolvedValueOnce('prefix'); - Amplify.libraryOptions = { + mockLibraryOptions = { Storage: { S3: { defaultAccessLevel: 'someLevel' as any, }, }, }; - const { keyPrefix } = await resolveS3ConfigAndInput(Amplify, {}); + const { keyPrefix } = await resolveS3ConfigAndInput(mockCtx, {}); expect(mockDefaultResolvePrefix).toHaveBeenCalledWith({ accessLevel: 'someLevel', targetIdentityId, @@ -213,7 +222,7 @@ describe('resolveS3ConfigAndInput', () => { it('should resolve prefix with `guest` access level if no access level is given', async () => { mockDefaultResolvePrefix.mockResolvedValueOnce('prefix'); - const { keyPrefix } = await resolveS3ConfigAndInput(Amplify, {}); + const { keyPrefix } = await resolveS3ConfigAndInput(mockCtx, {}); expect(mockDefaultResolvePrefix).toHaveBeenCalledWith({ accessLevel: 'guest', // default access level targetIdentityId, @@ -234,7 +243,7 @@ describe('resolveS3ConfigAndInput', () => { }, }, }); - const { s3Config } = await resolveS3ConfigAndInput(Amplify, { + const { s3Config } = await resolveS3ConfigAndInput(mockCtx, { options: { locationCredentialsProvider: mockLocationCredentialsProvider, }, @@ -252,7 +261,7 @@ describe('resolveS3ConfigAndInput', () => { }); it('should not throw when path is pass as a string', async () => { - const { s3Config } = await resolveS3ConfigAndInput(Amplify, { + const { s3Config } = await resolveS3ConfigAndInput(mockCtx, { path: 'my-path', options: { locationCredentialsProvider: mockLocationCredentialsProvider, @@ -291,7 +300,7 @@ describe('resolveS3ConfigAndInput', () => { const testCases = [...deprecatedInputs, ...callbackPathInputs]; it.each(testCases)('should throw when input is %s', async input => { - const { s3Config } = await resolveS3ConfigAndInput(Amplify, { + const { s3Config } = await resolveS3ConfigAndInput(mockCtx, { ...input, options: { locationCredentialsProvider: mockLocationCredentialsProvider, @@ -319,7 +328,7 @@ describe('resolveS3ConfigAndInput', () => { const { bucket: resolvedBucket, s3Config: { region: resolvedRegion }, - } = await resolveS3ConfigAndInput(Amplify, { + } = await resolveS3ConfigAndInput(mockCtx, { options: { bucket: bucketInfo }, }); @@ -330,7 +339,7 @@ describe('resolveS3ConfigAndInput', () => { it('should throw when unable to lookup bucket from the config when bucket API option is passed', async () => { try { - await resolveS3ConfigAndInput(Amplify, { + await resolveS3ConfigAndInput(mockCtx, { options: { bucket: 'error-bucket' }, }); } catch (error: any) { diff --git a/packages/storage/__tests__/testUtils/mockAmplifyContext.ts b/packages/storage/__tests__/testUtils/mockAmplifyContext.ts new file mode 100644 index 00000000000..2c459c82060 --- /dev/null +++ b/packages/storage/__tests__/testUtils/mockAmplifyContext.ts @@ -0,0 +1,50 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + AMPLIFY_CONTEXT_BRAND, + AmplifyContext, + ResourcesConfig, +} from '@aws-amplify/core'; + +export interface MockAmplifyContextOptions { + /** When provided, resourcesConfig becomes a live getter delegating to this fn. */ + getConfig?(): ResourcesConfig; +} + +/** + * Creates a mock AmplifyContext for testing. + */ +export function createMockAmplifyContext( + resourcesConfigOrOpts?: ResourcesConfig | MockAmplifyContextOptions, +): AmplifyContext { + // Determine whether we received the options form or raw ResourcesConfig. + const isOpts = + resourcesConfigOrOpts != null && + 'getConfig' in resourcesConfigOrOpts && + typeof (resourcesConfigOrOpts as MockAmplifyContextOptions).getConfig === + 'function'; + + const ctx: AmplifyContext = { + // Use a live getter when getConfig is supplied so per-test config changes propagate. + get resourcesConfig() { + if (isOpts) { + return (resourcesConfigOrOpts as MockAmplifyContextOptions) + .getConfig!(); + } + + return (resourcesConfigOrOpts as ResourcesConfig) ?? {}; + }, + libraryOptions: {}, + fetchAuthSession: jest.fn().mockResolvedValue({}), + clearCredentials: jest.fn().mockResolvedValue(undefined), + getTokens: jest.fn().mockResolvedValue(undefined), + }; + + Object.defineProperty(ctx, AMPLIFY_CONTEXT_BRAND, { + value: true, + enumerable: false, + }); + + return ctx; +} diff --git a/packages/storage/src/client/apis/uploadData.ts b/packages/storage/src/client/apis/uploadData.ts index ac3115819a3..63008dc2b11 100644 --- a/packages/storage/src/client/apis/uploadData.ts +++ b/packages/storage/src/client/apis/uploadData.ts @@ -1,7 +1,8 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { Amplify, defaultStorage } from '@aws-amplify/core'; +import { AmplifyContext, defaultStorage } from '@aws-amplify/core'; +import { resolveCtxArgs } from '@aws-amplify/core/internals/utils'; import { readFile } from '../utils/readFile'; import { toBase64 } from '../utils/toBase64'; @@ -122,10 +123,30 @@ export function uploadData( * ``` */ export function uploadData(input: UploadDataInput): UploadDataOutput; +/** + * @param ctx - The AmplifyContext to operate on. + * @param input - The `UploadDataWithPathInput` object. + */ +export function uploadData( + ctx: AmplifyContext, + input: UploadDataWithPathInput, +): UploadDataWithPathOutput; +/** + * @param ctx - The AmplifyContext to operate on. + * @param input - The `UploadDataInput` object. + */ +export function uploadData( + ctx: AmplifyContext, + input: UploadDataInput, +): UploadDataOutput; + +// Overload signatures above are the public contract; the impl is intentionally untyped and shape is enforced by resolveCtxArgs. +export function uploadData(...args: any[]) { + const [ctx, input] = + resolveCtxArgs<[UploadDataInput | UploadDataWithPathInput]>(args); -export function uploadData(input: UploadDataInput | UploadDataWithPathInput) { return uploadDataInternal( - { amplify: Amplify, readFile, toBase64 }, + { amplify: ctx, readFile, toBase64 }, { ...input, options: { diff --git a/packages/storage/src/errors/types/validation.ts b/packages/storage/src/errors/types/validation.ts index a56662adec4..af10593d6b3 100644 --- a/packages/storage/src/errors/types/validation.ts +++ b/packages/storage/src/errors/types/validation.ts @@ -28,6 +28,8 @@ export enum StorageValidationErrorCode { InvalidCustomEndpoint = 'InvalidCustomEndpoint', ForcePathStyleEndpointNotSupported = 'ForcePathStyleEndpointNotSupported', DnsIncompatibleBucketName = 'DnsIncompatibleBucketName', + NoS3Config = 'NoS3Config', + NoAuthConfig = 'NoAuthConfig', } export const validationErrorMap: AmplifyErrorMap = { @@ -107,4 +109,10 @@ export const validationErrorMap: AmplifyErrorMap = { [StorageValidationErrorCode.DnsIncompatibleBucketName]: { message: `The bucket name isn't DNS compatible.`, }, + [StorageValidationErrorCode.NoS3Config]: { + message: 'Storage S3 configuration is missing.', + }, + [StorageValidationErrorCode.NoAuthConfig]: { + message: 'Auth Cognito configuration is missing.', + }, }; diff --git a/packages/storage/src/foundation/types/context.ts b/packages/storage/src/foundation/types/context.ts index f036bb4a05a..d7738677cdd 100644 --- a/packages/storage/src/foundation/types/context.ts +++ b/packages/storage/src/foundation/types/context.ts @@ -1,7 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { AmplifyClassV6 } from '@aws-amplify/core'; +import { AmplifyContext } from '@aws-amplify/core'; /** * Read a `Blob` (or `File`) into an `ArrayBuffer`. Implementation differs @@ -23,7 +23,7 @@ export type ToBase64 = (input: string | ArrayBufferView) => string; * Per-invocation context passed into foundation-layer functions by the * client or server layer. It carries dependencies that are either * environment-specific or environment-scoped (e.g. a request-scoped - * `AmplifyClassV6` on the server). + * `AmplifyContext` on the server). * * All environment-specific behavior the foundation needs MUST be injected * through this object so the foundation layer stays free of any @@ -31,10 +31,11 @@ export type ToBase64 = (input: string | ArrayBufferView) => string; */ export interface FoundationContext { /** - * The Amplify instance. On the client this is the global singleton; - * on the server this is a request-scoped instance from the server adapter. + * The AmplifyContext providing configuration and auth operations. + * On the client this is the global context created by `Amplify.configure()`; + * on the server this is a context resolved per request via the server adapter. */ - amplify: AmplifyClassV6; + amplify: AmplifyContext; /** * Reads a `Blob` into an `ArrayBuffer`. diff --git a/packages/storage/src/internals/apis/copy.ts b/packages/storage/src/internals/apis/copy.ts index 3286ab99462..3a1c2e8ee38 100644 --- a/packages/storage/src/internals/apis/copy.ts +++ b/packages/storage/src/internals/apis/copy.ts @@ -1,7 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { Amplify } from '@aws-amplify/core'; +import { AmplifyContext } from '@aws-amplify/core'; import { copy as copyInternal } from '../../providers/s3/apis/internal/copy'; import { CopyInput } from '../types/inputs'; @@ -10,8 +10,8 @@ import { CopyOutput } from '../types/outputs'; /** * @internal */ -export const copy = (input: CopyInput) => - copyInternal(Amplify, { +export const copy = (ctx: AmplifyContext, input: CopyInput) => + copyInternal(ctx, { source: { path: input.source.path, bucket: input.source.bucket, diff --git a/packages/storage/src/internals/apis/downloadData.ts b/packages/storage/src/internals/apis/downloadData.ts index bd862d9d9b4..f5d77730954 100644 --- a/packages/storage/src/internals/apis/downloadData.ts +++ b/packages/storage/src/internals/apis/downloadData.ts @@ -1,6 +1,8 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +import { AmplifyContext } from '@aws-amplify/core'; + import { downloadData as downloadDataInternal } from '../../providers/s3/apis/internal/downloadData'; import { DownloadDataInput } from '../types/inputs'; import { DownloadDataOutput } from '../types/outputs'; @@ -8,8 +10,11 @@ import { DownloadDataOutput } from '../types/outputs'; /** * @internal */ -export const downloadData = (input: DownloadDataInput): DownloadDataOutput => - downloadDataInternal({ +export const downloadData = ( + ctx: AmplifyContext, + input: DownloadDataInput, +): DownloadDataOutput => + downloadDataInternal(ctx, { path: input.path, options: { useAccelerateEndpoint: input?.options?.useAccelerateEndpoint, diff --git a/packages/storage/src/internals/apis/getProperties.ts b/packages/storage/src/internals/apis/getProperties.ts index 213e184edae..d0a3d5a20ee 100644 --- a/packages/storage/src/internals/apis/getProperties.ts +++ b/packages/storage/src/internals/apis/getProperties.ts @@ -1,7 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { Amplify } from '@aws-amplify/core'; +import { AmplifyContext } from '@aws-amplify/core'; import { getProperties as getPropertiesInternal } from '../../providers/s3/apis/internal/getProperties'; import { GetPropertiesInput } from '../types/inputs'; @@ -11,9 +11,10 @@ import { GetPropertiesOutput } from '../types/outputs'; * @internal */ export const getProperties = ( + ctx: AmplifyContext, input: GetPropertiesInput, ): Promise => - getPropertiesInternal(Amplify, { + getPropertiesInternal(ctx, { path: input.path, options: { useAccelerateEndpoint: input?.options?.useAccelerateEndpoint, diff --git a/packages/storage/src/internals/apis/getUrl.ts b/packages/storage/src/internals/apis/getUrl.ts index 9a32dae1286..18cd06812f4 100644 --- a/packages/storage/src/internals/apis/getUrl.ts +++ b/packages/storage/src/internals/apis/getUrl.ts @@ -1,7 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { Amplify } from '@aws-amplify/core'; +import { AmplifyContext } from '@aws-amplify/core'; import { getUrl as getUrlInternal } from '../../providers/s3/apis/internal/getUrl'; import { GetUrlInput } from '../types/inputs'; @@ -10,8 +10,8 @@ import { GetUrlOutput } from '../types/outputs'; /** * @internal */ -export const getUrl = (input: GetUrlInput) => - getUrlInternal(Amplify, { +export const getUrl = (ctx: AmplifyContext, input: GetUrlInput) => + getUrlInternal(ctx, { path: input.path, options: { useAccelerateEndpoint: input?.options?.useAccelerateEndpoint, diff --git a/packages/storage/src/internals/apis/list.ts b/packages/storage/src/internals/apis/list.ts index 60c9184bd7f..d2d61ff69f6 100644 --- a/packages/storage/src/internals/apis/list.ts +++ b/packages/storage/src/internals/apis/list.ts @@ -1,7 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { Amplify } from '@aws-amplify/core'; +import { AmplifyContext } from '@aws-amplify/core'; import { list as listInternal } from '../../providers/s3/apis/internal/list'; import { ListAllInput, ListInput, ListPaginateInput } from '../types/inputs'; @@ -14,18 +14,25 @@ import { ListOutput } from '../types/outputs'; /** * @internal */ -export function list(input: ListAllInput): Promise; +export function list( + ctx: AmplifyContext, + input: ListAllInput, +): Promise; /** * @internal */ export function list( + ctx: AmplifyContext, input: ListPaginateInput, ): Promise; /** * @internal */ -export function list(input: ListInput): Promise { - return listInternal(Amplify, { +export function list( + ctx: AmplifyContext, + input: ListInput, +): Promise { + return listInternal(ctx, { path: input.path, options: { bucket: input.options?.bucket, diff --git a/packages/storage/src/internals/apis/listPaths/listPaths.ts b/packages/storage/src/internals/apis/listPaths/listPaths.ts index 2add687dfa4..68d055edd4d 100644 --- a/packages/storage/src/internals/apis/listPaths/listPaths.ts +++ b/packages/storage/src/internals/apis/listPaths/listPaths.ts @@ -1,22 +1,34 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { Amplify, fetchAuthSession } from '@aws-amplify/core'; +import { AmplifyContext } from '@aws-amplify/core'; +import { assertValidationError } from '../../../errors/utils/assertValidationError'; +import { StorageValidationErrorCode } from '../../../errors/types/validation'; import { ListPathsOutput } from '../../types/credentials'; import { resolveLocationsForCurrentSession } from './resolveLocationsForCurrentSession'; import { getHighestPrecedenceUserGroup } from './getHighestPrecedenceUserGroup'; -export const listPaths = async (): Promise => { - const { buckets } = Amplify.getConfig().Storage!.S3!; - const { groups } = Amplify.getConfig().Auth!.Cognito; +export const listPaths = async ( + ctx: AmplifyContext, +): Promise => { + const { Storage, Auth } = ctx.resourcesConfig; + + const s3Config = Storage?.S3; + assertValidationError(!!s3Config, StorageValidationErrorCode.NoS3Config); + + const authConfig = Auth?.Cognito; + assertValidationError(!!authConfig, StorageValidationErrorCode.NoAuthConfig); + + const { buckets } = s3Config; + const { groups } = authConfig; if (!buckets) { return { locations: [] }; } - const { tokens, identityId } = await fetchAuthSession(); + const { tokens, identityId } = await ctx.fetchAuthSession(); const currentUserGroups = tokens?.accessToken.payload['cognito:groups'] as | string[] | undefined; diff --git a/packages/storage/src/internals/apis/remove.ts b/packages/storage/src/internals/apis/remove.ts index 33f0eddae90..cb59d4f25ce 100644 --- a/packages/storage/src/internals/apis/remove.ts +++ b/packages/storage/src/internals/apis/remove.ts @@ -1,7 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { Amplify } from '@aws-amplify/core'; +import { AmplifyContext } from '@aws-amplify/core'; import { remove as removeInternal } from '../../providers/s3/apis/internal/remove'; import { RemoveOperation } from '../../providers/s3/types'; @@ -11,9 +11,12 @@ import { RemoveOutput } from '../types/outputs'; /** * @internal */ -export const remove = (input: RemoveInput): RemoveOperation => { +export const remove = ( + ctx: AmplifyContext, + input: RemoveInput, +): RemoveOperation => { return removeInternal( - Amplify, + ctx, { path: input.path, options: { diff --git a/packages/storage/src/internals/apis/uploadData.ts b/packages/storage/src/internals/apis/uploadData.ts index f093ec60ead..25b82f5e66e 100644 --- a/packages/storage/src/internals/apis/uploadData.ts +++ b/packages/storage/src/internals/apis/uploadData.ts @@ -1,7 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { Amplify } from '@aws-amplify/core'; +import { AmplifyContext } from '@aws-amplify/core'; import { readFile } from '../../client/utils/readFile'; import { toBase64 } from '../../client/utils/toBase64'; @@ -12,11 +12,11 @@ import { uploadData as uploadDataInternal } from '../../providers/s3/apis/intern /** * @internal */ -export const uploadData = (input: UploadDataInput) => { +export const uploadData = (ctx: AmplifyContext, input: UploadDataInput) => { const { data, path, options } = input; return uploadDataInternal( - { amplify: Amplify, readFile, toBase64 }, + { amplify: ctx, readFile, toBase64 }, { path, data, diff --git a/packages/storage/src/providers/s3/apis/copy.ts b/packages/storage/src/providers/s3/apis/copy.ts index 763ff45829b..ee2d135908c 100644 --- a/packages/storage/src/providers/s3/apis/copy.ts +++ b/packages/storage/src/providers/s3/apis/copy.ts @@ -1,7 +1,8 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { Amplify } from '@aws-amplify/core'; +import { AmplifyContext } from '@aws-amplify/core'; +import { resolveCtxArgs } from '@aws-amplify/core/internals/utils'; import { CopyInput, @@ -36,7 +37,26 @@ export function copy(input: CopyWithPathInput): Promise; * source or destination key is not defined. */ export function copy(input: CopyInput): Promise; +/** + * @param ctx - The AmplifyContext to operate on. + * @param input - The `CopyWithPathInput` object. + */ +export function copy( + ctx: AmplifyContext, + input: CopyWithPathInput, +): Promise; +/** + * @param ctx - The AmplifyContext to operate on. + * @param input - The `CopyInput` object. + */ +export function copy( + ctx: AmplifyContext, + input: CopyInput, +): Promise; + +// Overload signatures above are the public contract; the impl is intentionally untyped and shape is enforced by resolveCtxArgs. +export function copy(...args: any[]) { + const [ctx, input] = resolveCtxArgs<[CopyInput | CopyWithPathInput]>(args); -export function copy(input: CopyInput | CopyWithPathInput) { - return copyInternal(Amplify, input); + return copyInternal(ctx, input); } diff --git a/packages/storage/src/providers/s3/apis/downloadData.ts b/packages/storage/src/providers/s3/apis/downloadData.ts index 0eeca69899f..21580cd80e7 100644 --- a/packages/storage/src/providers/s3/apis/downloadData.ts +++ b/packages/storage/src/providers/s3/apis/downloadData.ts @@ -1,6 +1,9 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +import { AmplifyContext } from '@aws-amplify/core'; +import { resolveCtxArgs } from '@aws-amplify/core/internals/utils'; + import { DownloadDataInput, DownloadDataOutput, @@ -77,8 +80,26 @@ export function downloadData( *``` */ export function downloadData(input: DownloadDataInput): DownloadDataOutput; +/** + * @param ctx - The AmplifyContext to operate on. + * @param input - The `DownloadDataWithPathInput` object. + */ export function downloadData( - input: DownloadDataInput | DownloadDataWithPathInput, -) { - return downloadDataInternal(input); + ctx: AmplifyContext, + input: DownloadDataWithPathInput, +): DownloadDataWithPathOutput; +/** + * @param ctx - The AmplifyContext to operate on. + * @param input - The `DownloadDataInput` object. + */ +export function downloadData( + ctx: AmplifyContext, + input: DownloadDataInput, +): DownloadDataOutput; +// Overload signatures above are the public contract; the impl is intentionally untyped and shape is enforced by resolveCtxArgs. +export function downloadData(...args: any[]) { + const [ctx, input] = + resolveCtxArgs<[DownloadDataInput | DownloadDataWithPathInput]>(args); + + return downloadDataInternal(ctx, input); } diff --git a/packages/storage/src/providers/s3/apis/getProperties.ts b/packages/storage/src/providers/s3/apis/getProperties.ts index 630d0b1c467..3f0b82a0631 100644 --- a/packages/storage/src/providers/s3/apis/getProperties.ts +++ b/packages/storage/src/providers/s3/apis/getProperties.ts @@ -1,7 +1,8 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { Amplify } from '@aws-amplify/core'; +import { AmplifyContext } from '@aws-amplify/core'; +import { resolveCtxArgs } from '@aws-amplify/core/internals/utils'; import { GetPropertiesInput, @@ -39,9 +40,27 @@ export function getProperties( export function getProperties( input: GetPropertiesInput, ): Promise; - +/** + * @param ctx - The AmplifyContext to operate on. + * @param input - The `GetPropertiesWithPathInput` object. + */ +export function getProperties( + ctx: AmplifyContext, + input: GetPropertiesWithPathInput, +): Promise; +/** + * @param ctx - The AmplifyContext to operate on. + * @param input - The `GetPropertiesInput` object. + */ export function getProperties( - input: GetPropertiesInput | GetPropertiesWithPathInput, -) { - return getPropertiesInternal(Amplify, input); + ctx: AmplifyContext, + input: GetPropertiesInput, +): Promise; + +// Overload signatures above are the public contract; the impl is intentionally untyped and shape is enforced by resolveCtxArgs. +export function getProperties(...args: any[]) { + const [ctx, input] = + resolveCtxArgs<[GetPropertiesInput | GetPropertiesWithPathInput]>(args); + + return getPropertiesInternal(ctx, input); } diff --git a/packages/storage/src/providers/s3/apis/getUrl.ts b/packages/storage/src/providers/s3/apis/getUrl.ts index 39869eedf4c..441c6094abf 100644 --- a/packages/storage/src/providers/s3/apis/getUrl.ts +++ b/packages/storage/src/providers/s3/apis/getUrl.ts @@ -1,7 +1,8 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { Amplify } from '@aws-amplify/core'; +import { AmplifyContext } from '@aws-amplify/core'; +import { resolveCtxArgs } from '@aws-amplify/core/internals/utils'; import { GetUrlInput, @@ -51,7 +52,27 @@ export function getUrl( * */ export function getUrl(input: GetUrlInput): Promise; +/** + * @param ctx - The AmplifyContext to operate on. + * @param input - The `GetUrlWithPathInput` object. + */ +export function getUrl( + ctx: AmplifyContext, + input: GetUrlWithPathInput, +): Promise; +/** + * @param ctx - The AmplifyContext to operate on. + * @param input - The `GetUrlInput` object. + */ +export function getUrl( + ctx: AmplifyContext, + input: GetUrlInput, +): Promise; + +// Overload signatures above are the public contract; the impl is intentionally untyped and shape is enforced by resolveCtxArgs. +export function getUrl(...args: any[]) { + const [ctx, input] = + resolveCtxArgs<[GetUrlInput | GetUrlWithPathInput]>(args); -export function getUrl(input: GetUrlInput | GetUrlWithPathInput) { - return getUrlInternal(Amplify, input); + return getUrlInternal(ctx, input); } diff --git a/packages/storage/src/providers/s3/apis/internal/copy.ts b/packages/storage/src/providers/s3/apis/internal/copy.ts index 281ff3d3191..796851efa96 100644 --- a/packages/storage/src/providers/s3/apis/internal/copy.ts +++ b/packages/storage/src/providers/s3/apis/internal/copy.ts @@ -1,7 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { AmplifyClassV6 } from '@aws-amplify/core'; +import { AmplifyContext } from '@aws-amplify/core'; import { StorageAction } from '@aws-amplify/core/internals/utils'; import { @@ -47,7 +47,7 @@ const storageBucketAssertion = ( }; export const copy = async ( - amplify: AmplifyClassV6, + amplify: AmplifyContext, input: CopyInput | CopyWithPathInputWithAdvancedOptions, ): Promise => { return isCopyInputWithPath(input) @@ -56,7 +56,7 @@ export const copy = async ( }; const copyWithPath = async ( - amplify: AmplifyClassV6, + amplify: AmplifyContext, input: CopyWithPathInputWithAdvancedOptions, ): Promise => { const { source, destination } = input; @@ -123,7 +123,7 @@ const copyWithPath = async ( /** @deprecated Use {@link copyWithPath} instead. */ export const copyWithKey = async ( - amplify: AmplifyClassV6, + amplify: AmplifyContext, input: CopyInput, ): Promise => { const { source, destination } = input; diff --git a/packages/storage/src/providers/s3/apis/internal/downloadData.ts b/packages/storage/src/providers/s3/apis/internal/downloadData.ts index 80283acfcb1..375c1b9db02 100644 --- a/packages/storage/src/providers/s3/apis/internal/downloadData.ts +++ b/packages/storage/src/providers/s3/apis/internal/downloadData.ts @@ -1,7 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { Amplify } from '@aws-amplify/core'; +import { AmplifyContext } from '@aws-amplify/core'; import { StorageAction } from '@aws-amplify/core/internals/utils'; import { resolveS3ConfigAndInput } from '../../utils/resolveS3ConfigAndInput'; @@ -24,11 +24,12 @@ import { import { DownloadDataInput as DownloadDataWithPathInputWithAdvancedOptions } from '../../../../internals/types/inputs'; export const downloadData = ( + ctx: AmplifyContext, input: DownloadDataInput | DownloadDataWithPathInputWithAdvancedOptions, ) => { const abortController = new AbortController(); const downloadTask = createDownloadTask({ - job: downloadDataJob(input, abortController.signal), + job: downloadDataJob(ctx, input, abortController.signal), onCancel: (message?: string) => { abortController.abort(message); }, @@ -39,6 +40,7 @@ export const downloadData = ( const downloadDataJob = ( + ctx: AmplifyContext, downloadDataInput: DownloadDataInput | DownloadDataWithPathInput, abortSignal: AbortSignal, ) => @@ -47,7 +49,7 @@ const downloadDataJob = > => { const { options: downloadDataOptions } = downloadDataInput; const { bucket, keyPrefix, s3Config, identityId } = - await resolveS3ConfigAndInput(Amplify, downloadDataInput); + await resolveS3ConfigAndInput(ctx, downloadDataInput); const { inputType, objectKey } = validateStorageOperationInput( downloadDataInput, identityId, diff --git a/packages/storage/src/providers/s3/apis/internal/getProperties.ts b/packages/storage/src/providers/s3/apis/internal/getProperties.ts index 981c32cb827..61839b8479a 100644 --- a/packages/storage/src/providers/s3/apis/internal/getProperties.ts +++ b/packages/storage/src/providers/s3/apis/internal/getProperties.ts @@ -1,7 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { AmplifyClassV6 } from '@aws-amplify/core'; +import { AmplifyContext } from '@aws-amplify/core'; import { StorageAction } from '@aws-amplify/core/internals/utils'; import { @@ -22,7 +22,7 @@ import { STORAGE_INPUT_KEY } from '../../utils/constants'; import { GetPropertiesInput as GetPropertiesWithPathInputWithAdvancedOptions } from '../../../../internals'; export const getProperties = async ( - amplify: AmplifyClassV6, + amplify: AmplifyContext, input: GetPropertiesInput | GetPropertiesWithPathInputWithAdvancedOptions, action?: StorageAction, ): Promise => { diff --git a/packages/storage/src/providers/s3/apis/internal/getUrl.ts b/packages/storage/src/providers/s3/apis/internal/getUrl.ts index 303fb59529f..5917d17ce7b 100644 --- a/packages/storage/src/providers/s3/apis/internal/getUrl.ts +++ b/packages/storage/src/providers/s3/apis/internal/getUrl.ts @@ -1,7 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { AmplifyClassV6 } from '@aws-amplify/core'; +import { AmplifyContext } from '@aws-amplify/core'; import { StorageAction } from '@aws-amplify/core/internals/utils'; import { GetUrlInput, GetUrlOutput, GetUrlWithPathOutput } from '../../types'; @@ -28,7 +28,7 @@ import { GetUrlInput as GetUrlWithPathInputWithAdvancedOptions } from '../../../ import { getProperties } from './getProperties'; export const getUrl = async ( - amplify: AmplifyClassV6, + amplify: AmplifyContext, input: GetUrlInput | GetUrlWithPathInputWithAdvancedOptions, ): Promise => { const { options: getUrlOptions } = input; diff --git a/packages/storage/src/providers/s3/apis/internal/list.ts b/packages/storage/src/providers/s3/apis/internal/list.ts index 968e175b329..dbda9bfc703 100644 --- a/packages/storage/src/providers/s3/apis/internal/list.ts +++ b/packages/storage/src/providers/s3/apis/internal/list.ts @@ -1,7 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { AmplifyClassV6 } from '@aws-amplify/core'; +import { AmplifyContext } from '@aws-amplify/core'; import { StorageAction } from '@aws-amplify/core/internals/utils'; import { @@ -46,7 +46,7 @@ interface ListInputArgs { } export const list = async ( - amplify: AmplifyClassV6, + amplify: AmplifyContext, input: ListAllInput | ListPaginateInput | ListWithPathInputAndAdvancedOptions, ): Promise< | ListAllOutput diff --git a/packages/storage/src/providers/s3/apis/internal/remove.ts b/packages/storage/src/providers/s3/apis/internal/remove.ts index 11d04361840..cdfe97fcc45 100644 --- a/packages/storage/src/providers/s3/apis/internal/remove.ts +++ b/packages/storage/src/providers/s3/apis/internal/remove.ts @@ -1,7 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { AmplifyClassV6 } from '@aws-amplify/core'; +import { AmplifyContext } from '@aws-amplify/core'; import { StorageAction } from '@aws-amplify/core/internals/utils'; import { @@ -28,15 +28,15 @@ import { RemoveInput as RemoveWithPathInputWithAdvancedOptions } from '../../../ import { CanceledError } from '../../../../errors/CanceledError'; export function remove( - amplify: AmplifyClassV6, + amplify: AmplifyContext, input: RemoveInput, ): RemoveOperation; export function remove( - amplify: AmplifyClassV6, + amplify: AmplifyContext, input: RemoveWithPathInputWithAdvancedOptions, ): RemoveOperation; export function remove( - amplify: AmplifyClassV6, + amplify: AmplifyContext, input: RemoveInput | RemoveWithPathInputWithAdvancedOptions, ): RemoveOperation { return createAbortableTask(executeRemove(amplify, input)); @@ -44,7 +44,7 @@ export function remove( const executeRemove = ( - amplify: AmplifyClassV6, + amplify: AmplifyContext, input: RemoveInput | RemoveWithPathInputWithAdvancedOptions, ) => async (abortController: AbortController) => { diff --git a/packages/storage/src/providers/s3/apis/internal/uploadData/multipart/uploadHandlers.ts b/packages/storage/src/providers/s3/apis/internal/uploadData/multipart/uploadHandlers.ts index ed87c0b574a..24992073113 100644 --- a/packages/storage/src/providers/s3/apis/internal/uploadData/multipart/uploadHandlers.ts +++ b/packages/storage/src/providers/s3/apis/internal/uploadData/multipart/uploadHandlers.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { - AmplifyClassV6, + AmplifyContext, KeyValueStorageInterface, StorageAccessLevel, } from '@aws-amplify/core'; @@ -370,7 +370,7 @@ export const getMultipartUploadHandlers = ( }; const resolveAccessLevel = ( - amplify: AmplifyClassV6, + amplify: AmplifyContext, accessLevel?: StorageAccessLevel, ) => accessLevel ?? diff --git a/packages/storage/src/providers/s3/apis/list.ts b/packages/storage/src/providers/s3/apis/list.ts index cd58dbdaacd..0f51171efa5 100644 --- a/packages/storage/src/providers/s3/apis/list.ts +++ b/packages/storage/src/providers/s3/apis/list.ts @@ -1,6 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { Amplify } from '@aws-amplify/core'; +import { AmplifyContext } from '@aws-amplify/core'; +import { resolveCtxArgs } from '@aws-amplify/core/internals/utils'; import { ListAllInput, @@ -57,13 +58,52 @@ export function list(input?: ListPaginateInput): Promise; * @throws validation: `StorageValidationErrorCode` - thrown when there are issues with credentials */ export function list(input?: ListAllInput): Promise; - +/** + * @param ctx - The AmplifyContext to operate on. + * @param input - The `ListPaginateWithPathInput` object. + */ +export function list( + ctx: AmplifyContext, + input: ListPaginateWithPathInput, +): Promise; +/** + * @param ctx - The AmplifyContext to operate on. + * @param input - The `ListAllWithPathInput` object. + */ +export function list( + ctx: AmplifyContext, + input: ListAllWithPathInput, +): Promise; +/** + * @param ctx - The AmplifyContext to operate on. + * @param input - The `ListPaginateInput` object. + */ export function list( - input?: - | ListAllInput - | ListPaginateInput - | ListAllWithPathInput - | ListPaginateWithPathInput, -) { - return listInternal(Amplify, input ?? {}); + ctx: AmplifyContext, + input?: ListPaginateInput, +): Promise; +/** + * @param ctx - The AmplifyContext to operate on. + * @param input - The `ListAllInput` object. + */ +export function list( + ctx: AmplifyContext, + input?: ListAllInput, +): Promise; + +// Overload signatures above are the public contract; the impl is intentionally untyped and shape is enforced by resolveCtxArgs. +export function list(...args: any[]) { + const [ctx, input] = + resolveCtxArgs< + [ + ( + | ListAllInput + | ListPaginateInput + | ListAllWithPathInput + | ListPaginateWithPathInput + )?, + ] + >(args); + + return listInternal(ctx, input ?? {}); } diff --git a/packages/storage/src/providers/s3/apis/remove.ts b/packages/storage/src/providers/s3/apis/remove.ts index a3c449f172d..641946179ae 100644 --- a/packages/storage/src/providers/s3/apis/remove.ts +++ b/packages/storage/src/providers/s3/apis/remove.ts @@ -1,7 +1,8 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { Amplify } from '@aws-amplify/core'; +import { AmplifyContext } from '@aws-amplify/core'; +import { resolveCtxArgs } from '@aws-amplify/core/internals/utils'; import { RemoveInput, @@ -36,11 +37,31 @@ export function remove( * when there is no key or its empty. */ export function remove(input: RemoveInput): RemoveOperation; +/** + * @param ctx - The AmplifyContext to operate on. + * @param input - The `RemoveWithPathInput` object. + */ +export function remove( + ctx: AmplifyContext, + input: RemoveWithPathInput, +): RemoveOperation; +/** + * @param ctx - The AmplifyContext to operate on. + * @param input - The `RemoveInput` object. + */ +export function remove( + ctx: AmplifyContext, + input: RemoveInput, +): RemoveOperation; -export function remove(input: RemoveInput | RemoveWithPathInput) { +// Overload signatures above are the public contract; the impl is intentionally untyped and shape is enforced by resolveCtxArgs. +export function remove(...args: any[]) { + const [ctx, input] = + resolveCtxArgs<[RemoveInput | RemoveWithPathInput]>(args); + // Narrowing is required: removeInternal is overloaded and TypeScript cannot resolve the union argument without discriminating. if ('key' in input) { - return removeInternal(Amplify, input); + return removeInternal(ctx, input); } else { - return removeInternal(Amplify, input); + return removeInternal(ctx, input); } } diff --git a/packages/storage/src/providers/s3/apis/server/copy.ts b/packages/storage/src/providers/s3/apis/server/copy.ts index e9486e10431..77a155eccca 100644 --- a/packages/storage/src/providers/s3/apis/server/copy.ts +++ b/packages/storage/src/providers/s3/apis/server/copy.ts @@ -1,9 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { - AmplifyServer, - getAmplifyServerContext, -} from '@aws-amplify/core/internals/adapter-core'; +import { AmplifyContext } from '@aws-amplify/core'; +import { AmplifyServer } from '@aws-amplify/core/internals/adapter-core'; import { CopyInput, @@ -13,10 +11,12 @@ import { } from '../../types'; import { copy as copyInternal } from '../internal/copy'; +import { resolveServerContext } from './resolveServerContext'; + /** * Copy an object from a source to a destination object within the same bucket. * - * @param contextSpec - The isolated server context. + * @param ctxOrContextSpec - The isolated server context. * @param input - The `CopyWithPathInput` object. * @returns Output containing the destination object path. * @throws service: `S3Exception` - Thrown when checking for existence of the object @@ -24,7 +24,7 @@ import { copy as copyInternal } from '../internal/copy'; * source or destination path is not defined. */ export function copy( - contextSpec: AmplifyServer.ContextSpec, + ctxOrContextSpec: AmplifyContext | AmplifyServer.ContextSpec, input: CopyWithPathInput, ): Promise; /** @@ -34,7 +34,7 @@ export function copy( * Copy an object from a source to a destination object within the same bucket. Can optionally copy files across * different accessLevel or identityId (if source object's accessLevel is 'protected'). * - * @param contextSpec - The isolated server context. + * @param ctxOrContextSpec - The isolated server context. * @param input - The `CopyInput` object. * @returns Output containing the destination object key. * @throws service: `S3Exception` - Thrown when checking for existence of the object @@ -42,13 +42,15 @@ export function copy( * source or destination key is not defined. */ export function copy( - contextSpec: AmplifyServer.ContextSpec, + ctxOrContextSpec: AmplifyContext | AmplifyServer.ContextSpec, input: CopyInput, ): Promise; export function copy( - contextSpec: AmplifyServer.ContextSpec, + ctxOrContextSpec: AmplifyContext | AmplifyServer.ContextSpec, input: CopyInput | CopyWithPathInput, ) { - return copyInternal(getAmplifyServerContext(contextSpec).amplify, input); + const ctx = resolveServerContext(ctxOrContextSpec); + + return copyInternal(ctx, input); } diff --git a/packages/storage/src/providers/s3/apis/server/getProperties.ts b/packages/storage/src/providers/s3/apis/server/getProperties.ts index 87a77a297a4..b8bdb0e1007 100644 --- a/packages/storage/src/providers/s3/apis/server/getProperties.ts +++ b/packages/storage/src/providers/s3/apis/server/getProperties.ts @@ -1,10 +1,8 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { - AmplifyServer, - getAmplifyServerContext, -} from '@aws-amplify/core/internals/adapter-core'; +import { AmplifyContext } from '@aws-amplify/core'; +import { AmplifyServer } from '@aws-amplify/core/internals/adapter-core'; import { GetPropertiesInput, @@ -14,18 +12,20 @@ import { } from '../../types'; import { getProperties as getPropertiesInternal } from '../internal/getProperties'; +import { resolveServerContext } from './resolveServerContext'; + /** * Gets the properties of a file. The properties include S3 system metadata and * the user metadata that was provided when uploading the file. * - * @param contextSpec - The isolated server context. + * @param ctxOrContextSpec - The isolated server context. * @param input - The `GetPropertiesWithPathInput` object. * @returns Requested object properties. * @throws An `S3Exception` when the underlying S3 service returned error. * @throws A `StorageValidationErrorCode` when API call parameters are invalid. */ export function getProperties( - contextSpec: AmplifyServer.ContextSpec, + ctxOrContextSpec: AmplifyContext | AmplifyServer.ContextSpec, input: GetPropertiesWithPathInput, ): Promise; /** @@ -35,23 +35,22 @@ export function getProperties( * Gets the properties of a file. The properties include S3 system metadata and * the user metadata that was provided when uploading the file. * - * @param contextSpec - The isolated server context. + * @param ctxOrContextSpec - The isolated server context. * @param input - The `GetPropertiesInput` object. * @returns Requested object properties. * @throws An `S3Exception` when the underlying S3 service returned error. * @throws A `StorageValidationErrorCode` when API call parameters are invalid. */ export function getProperties( - contextSpec: AmplifyServer.ContextSpec, + ctxOrContextSpec: AmplifyContext | AmplifyServer.ContextSpec, input: GetPropertiesInput, ): Promise; export function getProperties( - contextSpec: AmplifyServer.ContextSpec, + ctxOrContextSpec: AmplifyContext | AmplifyServer.ContextSpec, input: GetPropertiesInput | GetPropertiesWithPathInput, ) { - return getPropertiesInternal( - getAmplifyServerContext(contextSpec).amplify, - input, - ); + const ctx = resolveServerContext(ctxOrContextSpec); + + return getPropertiesInternal(ctx, input); } diff --git a/packages/storage/src/providers/s3/apis/server/getUrl.ts b/packages/storage/src/providers/s3/apis/server/getUrl.ts index f9f4e80d07c..423fd38624b 100644 --- a/packages/storage/src/providers/s3/apis/server/getUrl.ts +++ b/packages/storage/src/providers/s3/apis/server/getUrl.ts @@ -1,10 +1,8 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { - AmplifyServer, - getAmplifyServerContext, -} from '@aws-amplify/core/internals/adapter-core'; +import { AmplifyContext } from '@aws-amplify/core'; +import { AmplifyServer } from '@aws-amplify/core/internals/adapter-core'; import { GetUrlInput, @@ -14,6 +12,8 @@ import { } from '../../types'; import { getUrl as getUrlInternal } from '../internal/getUrl'; +import { resolveServerContext } from './resolveServerContext'; + /** * Get a temporary presigned URL to download the specified S3 object. * The presigned URL expires when the associated role used to sign the request expires or @@ -23,7 +23,7 @@ import { getUrl as getUrlInternal } from '../internal/getUrl'; * to true, this method will verify the given object already exists in S3 before returning a presigned * URL, and will throw `StorageError` if the object does not exist. * - * @param contextSpec - The isolated server context. + * @param ctxOrContextSpec - The isolated server context. * @param input - The `GetUrlWithPathInput` object. * @returns Presigned URL and timestamp when the URL may expire. * @throws service: `S3Exception` - thrown when checking for existence of the object @@ -32,7 +32,7 @@ import { getUrl as getUrlInternal } from '../internal/getUrl'; * */ export function getUrl( - contextSpec: AmplifyServer.ContextSpec, + ctxOrContextSpec: AmplifyContext | AmplifyServer.ContextSpec, input: GetUrlWithPathInput, ): Promise; /** @@ -47,7 +47,7 @@ export function getUrl( * to true, this method will verify the given object already exists in S3 before returning a presigned * URL, and will throw `StorageError` if the object does not exist. * - * @param contextSpec - The isolated server context. + * @param ctxOrContextSpec - The isolated server context. * @param input - The `GetUrlInput` object. * @returns Presigned URL and timestamp when the URL may expire. * @throws service: `S3Exception` - thrown when checking for existence of the object @@ -56,13 +56,15 @@ export function getUrl( * */ export function getUrl( - contextSpec: AmplifyServer.ContextSpec, + ctxOrContextSpec: AmplifyContext | AmplifyServer.ContextSpec, input: GetUrlInput, ): Promise; export function getUrl( - contextSpec: AmplifyServer.ContextSpec, + ctxOrContextSpec: AmplifyContext | AmplifyServer.ContextSpec, input: GetUrlInput | GetUrlWithPathInput, ) { - return getUrlInternal(getAmplifyServerContext(contextSpec).amplify, input); + const ctx = resolveServerContext(ctxOrContextSpec); + + return getUrlInternal(ctx, input); } diff --git a/packages/storage/src/providers/s3/apis/server/list.ts b/packages/storage/src/providers/s3/apis/server/list.ts index 66d0ad4cd22..e03f4c78d83 100644 --- a/packages/storage/src/providers/s3/apis/server/list.ts +++ b/packages/storage/src/providers/s3/apis/server/list.ts @@ -1,9 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { - AmplifyServer, - getAmplifyServerContext, -} from '@aws-amplify/core/internals/adapter-core'; +import { AmplifyContext } from '@aws-amplify/core'; +import { AmplifyServer } from '@aws-amplify/core/internals/adapter-core'; import { ListAllInput, @@ -17,29 +15,31 @@ import { } from '../../types'; import { list as listInternal } from '../internal/list'; +import { resolveServerContext } from './resolveServerContext'; + /** * List files in pages with the given `path`. * `pageSize` is defaulted to 1000. Additionally, the result will include a `nextToken` if there are more items to retrieve. * @param input - The `ListPaginateWithPathInput` object. - * @param contextSpec - The context spec used to get the Amplify server context. + * @param ctxOrContextSpec - The context spec used to get the Amplify server context. * @returns A list of objects with path and metadata * @throws service: `S3Exception` - S3 service errors thrown when checking for existence of bucket * @throws validation: `StorageValidationErrorCode` - thrown when there are issues with credentials */ export function list( - contextSpec: AmplifyServer.ContextSpec, + ctxOrContextSpec: AmplifyContext | AmplifyServer.ContextSpec, input: ListPaginateWithPathInput, ): Promise; /** * List all files from S3 for a given `path`. You can set `listAll` to true in `options` to get all the files from S3. * @param input - The `ListAllWithPathInput` object. - * @param contextSpec - The context spec used to get the Amplify server context. + * @param ctxOrContextSpec - The context spec used to get the Amplify server context. * @returns A list of all objects with path and metadata * @throws service: `S3Exception` - S3 service errors thrown when checking for existence of bucket * @throws validation: `StorageValidationErrorCode` - thrown when there are issues with credentials */ export function list( - contextSpec: AmplifyServer.ContextSpec, + ctxOrContextSpec: AmplifyContext | AmplifyServer.ContextSpec, input: ListAllWithPathInput, ): Promise; /** @@ -53,7 +53,7 @@ export function list( * @throws validation: `StorageValidationErrorCode` - thrown when there are issues with credentials */ export function list( - contextSpec: AmplifyServer.ContextSpec, + ctxOrContextSpec: AmplifyContext | AmplifyServer.ContextSpec, input?: ListPaginateInput, ): Promise; /** @@ -66,20 +66,19 @@ export function list( * @throws validation: `StorageValidationErrorCode` - thrown when there are issues with credentials */ export function list( - contextSpec: AmplifyServer.ContextSpec, + ctxOrContextSpec: AmplifyContext | AmplifyServer.ContextSpec, input?: ListAllInput, ): Promise; export function list( - contextSpec: AmplifyServer.ContextSpec, + ctxOrContextSpec: AmplifyContext | AmplifyServer.ContextSpec, input?: | ListAllInput | ListPaginateInput | ListAllWithPathInput | ListPaginateWithPathInput, ) { - return listInternal( - getAmplifyServerContext(contextSpec).amplify, - input ?? {}, - ); + const ctx = resolveServerContext(ctxOrContextSpec); + + return listInternal(ctx, input ?? {}); } diff --git a/packages/storage/src/providers/s3/apis/server/remove.ts b/packages/storage/src/providers/s3/apis/server/remove.ts index 23b62ef6609..8880d3e9138 100644 --- a/packages/storage/src/providers/s3/apis/server/remove.ts +++ b/packages/storage/src/providers/s3/apis/server/remove.ts @@ -1,10 +1,8 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { - AmplifyServer, - getAmplifyServerContext, -} from '@aws-amplify/core/internals/adapter-core'; +import { AmplifyContext } from '@aws-amplify/core'; +import { AmplifyServer } from '@aws-amplify/core/internals/adapter-core'; import { RemoveInput, @@ -15,17 +13,19 @@ import { } from '../../types'; import { remove as removeInternal } from '../internal/remove'; +import { resolveServerContext } from './resolveServerContext'; + /** * Remove a file or folder from your S3 bucket. * @param input - The `RemoveWithPathInput` object. - * @param contextSpec - The context spec used to get the Amplify server context. + * @param ctxOrContextSpec - The context spec used to get the Amplify server context. * @return Operation handle with result promise and cancellation capability. * @throws service: `S3Exception` - S3 service errors thrown while while removing the object. * @throws validation: `StorageValidationErrorCode` - Validation errors thrown * when there is no path or path is empty or path has a leading slash. */ export function remove( - contextSpec: AmplifyServer.ContextSpec, + ctxOrContextSpec: AmplifyContext | AmplifyServer.ContextSpec, input: RemoveWithPathInput, ): RemoveOperation; /** @@ -34,24 +34,25 @@ export function remove( * * Remove a file from your S3 bucket. * @param input - The `RemoveInput` object. - * @param contextSpec - The context spec used to get the Amplify server context. + * @param ctxOrContextSpec - The context spec used to get the Amplify server context. * @return Operation handle with result promise and cancellation capability. * @throws service: `S3Exception` - S3 service errors thrown while while removing the object * @throws validation: `StorageValidationErrorCode` - Validation errors thrown * when there is no key or its empty. */ export function remove( - contextSpec: AmplifyServer.ContextSpec, + ctxOrContextSpec: AmplifyContext | AmplifyServer.ContextSpec, input: RemoveInput, ): RemoveOperation; export function remove( - contextSpec: AmplifyServer.ContextSpec, + ctxOrContextSpec: AmplifyContext | AmplifyServer.ContextSpec, input: RemoveInput | RemoveWithPathInput, ) { + const ctx = resolveServerContext(ctxOrContextSpec); if ('key' in input) { - return removeInternal(getAmplifyServerContext(contextSpec).amplify, input); + return removeInternal(ctx, input); } else { - return removeInternal(getAmplifyServerContext(contextSpec).amplify, input); + return removeInternal(ctx, input); } } diff --git a/packages/storage/src/providers/s3/apis/server/resolveServerContext.ts b/packages/storage/src/providers/s3/apis/server/resolveServerContext.ts new file mode 100644 index 00000000000..797df41c735 --- /dev/null +++ b/packages/storage/src/providers/s3/apis/server/resolveServerContext.ts @@ -0,0 +1,47 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { AmplifyContext, isAmplifyContext } from '@aws-amplify/core'; +import { + AmplifyServer, + getAmplifyServerContext, +} from '@aws-amplify/core/internals/adapter-core'; + +/** + * Resolves a server-side argument that may be either the new {@link AmplifyContext} + * or a legacy {@link AmplifyServer.ContextSpec}, into a concrete `AmplifyContext`. + */ +export const resolveServerContext = ( + ctxOrContextSpec: AmplifyContext | AmplifyServer.ContextSpec, +): AmplifyContext => { + // Already a branded AmplifyContext (e.g. a directly-supplied context) — use as-is. + // Use the runtime brand check rather than a structural `'resourcesConfig' in x` + // probe: `AmplifyClass` also has a `resourcesConfig` field, so the structural + // probe would misclassify a server ContextSpec's underlying `AmplifyClass`. + if (isAmplifyContext(ctxOrContextSpec)) { + return ctxOrContextSpec; + } + + // Legacy server ContextSpec: unwrap the `AmplifyClass` and adapt it to the + // `AmplifyContext` shape. `AmplifyClass` exposes resourcesConfig/libraryOptions + // and a cross-category `Auth` utility, but NOT the top-level context methods + // (fetchAuthSession/clearCredentials/getTokens), so bridge them to `Auth.*` here. + const { amplify } = getAmplifyServerContext(ctxOrContextSpec); + + // Annotate the object so the bridged lambdas receive contextual parameter + // types (avoids implicit-any) and the shape is checked against AmplifyContext. + const resolved: AmplifyContext = { + get resourcesConfig() { + return amplify.getConfig(); + }, + libraryOptions: amplify.libraryOptions, + // AmplifyContext.fetchAuthSession has OPTIONAL options while + // AuthClass.Auth.fetchAuthSession requires it, hence the `?? {}` default. + fetchAuthSession: options => amplify.Auth.fetchAuthSession(options ?? {}), + clearCredentials: () => amplify.Auth.clearCredentials(), + // getTokens needs no default — options is required on both interfaces. + getTokens: options => amplify.Auth.getTokens(options), + }; + + return resolved; +}; diff --git a/packages/storage/src/providers/s3/utils/resolveS3ConfigAndInput.ts b/packages/storage/src/providers/s3/utils/resolveS3ConfigAndInput.ts index 7cb4c55316e..54085b87e19 100644 --- a/packages/storage/src/providers/s3/utils/resolveS3ConfigAndInput.ts +++ b/packages/storage/src/providers/s3/utils/resolveS3ConfigAndInput.ts @@ -1,7 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { AmplifyClassV6, StorageAccessLevel } from '@aws-amplify/core'; +import { AmplifyContext, StorageAccessLevel } from '@aws-amplify/core'; import { CredentialsProviderOptions } from '@aws-amplify/core/internals/aws-client-utils'; import { assertValidationError } from '../../../errors/utils/assertValidationError'; @@ -54,7 +54,7 @@ type StorageInput = DeprecatedStorageInput | CallbackPathStorageInput; /** * resolve the common input options for S3 API handlers from Amplify configuration and library options. * - * @param {AmplifyClassV6} amplify The Amplify instance. + * @param {AmplifyContext} amplify The Amplify instance. * @param {S3ApiOptions} apiOptions The input options for S3 provider. * @returns {Promise} The resolved common input options for S3 API handlers. * @throws A `StorageError` with `error.name` from `StorageValidationErrorCode` indicating invalid @@ -63,7 +63,7 @@ type StorageInput = DeprecatedStorageInput | CallbackPathStorageInput; * @internal */ export const resolveS3ConfigAndInput = async ( - amplify: AmplifyClassV6, + amplify: AmplifyContext, apiInput?: StorageInput & { options?: S3ApiOptions }, ): Promise => { const { options: apiOptions } = apiInput ?? {}; @@ -71,7 +71,7 @@ export const resolveS3ConfigAndInput = async ( * IdentityId is always cached in memory so we can safely make calls here. It * should be stable even for unauthenticated users, regardless of credentials. */ - const { identityId } = await amplify.Auth.fetchAuthSession(); + const { identityId } = await amplify.fetchAuthSession(); /** * A credentials provider function instead of a static credentials object is @@ -92,7 +92,7 @@ export const resolveS3ConfigAndInput = async ( // we support refreshing only the credentials. const { credentials } = isLocationCredentialsProvider(apiOptions) ? await apiOptions.locationCredentialsProvider(options) - : await amplify.Auth.fetchAuthSession(); + : await amplify.fetchAuthSession(); assertValidationError( !!credentials, StorageValidationErrorCode.NoCredentials, @@ -106,7 +106,7 @@ export const resolveS3ConfigAndInput = async ( region: defaultRegion, dangerouslyConnectToHttpEndpointForTesting, buckets, - } = amplify.getConfig()?.Storage?.S3 ?? {}; + } = amplify.resourcesConfig?.Storage?.S3 ?? {}; const { bucket = defaultBucket, region = defaultRegion } = (apiOptions?.bucket && resolveBucketConfig(apiOptions, buckets)) || {}; diff --git a/packages/storage/src/server/apis/uploadData.ts b/packages/storage/src/server/apis/uploadData.ts index 5b38630b334..ae9c6815d18 100644 --- a/packages/storage/src/server/apis/uploadData.ts +++ b/packages/storage/src/server/apis/uploadData.ts @@ -1,10 +1,8 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { - AmplifyServer, - getAmplifyServerContext, -} from '@aws-amplify/core/internals/adapter-core'; +import { AmplifyContext } from '@aws-amplify/core'; +import { AmplifyServer } from '@aws-amplify/core/internals/adapter-core'; import { readFile } from '../utils/readFile'; import { toBase64 } from '../utils/toBase64'; @@ -15,6 +13,7 @@ import { UploadDataWithPathInput, } from '../../providers/s3/types'; import { uploadData as uploadDataInternal } from '../../providers/s3/apis/internal/uploadData'; +import { resolveServerContext } from '../../providers/s3/apis/server/resolveServerContext'; /** * Upload data to the specified S3 object path. By default uses a single PUT @@ -24,7 +23,7 @@ import { uploadData as uploadDataInternal } from '../../providers/s3/apis/intern * Server-side `uploadData` is intended for use in SSR contexts such as * Next.js Route Handlers and Server Actions. * - * @param contextSpec - The isolated server context. + * @param ctxOrContextSpec - The isolated server context. * @param input - A `UploadDataWithPathInput` object. * * @returns An `UploadDataServerWithPathOutput` task. Await the `result` @@ -53,7 +52,7 @@ import { uploadData as uploadDataInternal } from '../../providers/s3/apis/intern * ``` */ export function uploadData( - contextSpec: AmplifyServer.ContextSpec, + ctxOrContextSpec: AmplifyContext | AmplifyServer.ContextSpec, input: UploadDataWithPathInput, ): UploadDataServerWithPathOutput; @@ -65,7 +64,7 @@ export function uploadData( * operation to upload when the payload is less than 5MB. Otherwise, uses * multipart upload to upload the payload. * - * @param contextSpec - The isolated server context. + * @param ctxOrContextSpec - The isolated server context. * @param input - A `UploadDataInput` object. * * @returns An `UploadDataServerOutput` task. Await the `result` promise to @@ -75,21 +74,23 @@ export function uploadData( * @throws StorageValidationErrorCode when API call parameters are invalid. */ export function uploadData( - contextSpec: AmplifyServer.ContextSpec, + ctxOrContextSpec: AmplifyContext | AmplifyServer.ContextSpec, input: UploadDataInput, ): UploadDataServerOutput; export function uploadData( - contextSpec: AmplifyServer.ContextSpec, + ctxOrContextSpec: AmplifyContext | AmplifyServer.ContextSpec, input: UploadDataInput | UploadDataWithPathInput, ): UploadDataServerOutput | UploadDataServerWithPathOutput { // The internal uploadData returns an UploadTask which has pause/resume. On // the server path we intentionally hide pause/resume from the type because // they are not supported across isolated server requests. The runtime // object still exposes them as no-ops (delegated to createUploadTask). + const ctx = resolveServerContext(ctxOrContextSpec); + return uploadDataInternal( { - amplify: getAmplifyServerContext(contextSpec).amplify, + amplify: ctx, readFile, toBase64, },