diff --git a/.gitignore b/.gitignore index 216be546f47..02cf0a16bf8 100644 --- a/.gitignore +++ b/.gitignore @@ -77,6 +77,9 @@ coverage-ts/ # Turborepo .turbo +# TypeScript incremental build +*.tsbuildinfo + # ruby vendor/ Gemfile.lock diff --git a/license_config.json b/license_config.json index e0c09bf84b4..f184dffb759 100644 --- a/license_config.json +++ b/license_config.json @@ -41,6 +41,7 @@ "rollup", ".husky", ".turbo", + ".idea", "eslint.config.mjs", "packages/core/metadata", "packages/react-native/example/.bundle/config" diff --git a/package.json b/package.json index df3110003fb..887d526837f 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "packages/predictions", "packages/storage", "packages/adapter-nextjs", + "packages/adapter-express", "packages/geo", "packages/api-rest", "packages/api-graphql", diff --git a/packages/adapter-nextjs/__tests__/api/generateServerClient.test.ts b/packages/adapter-nextjs/__tests__/api/generateServerClient.test.ts index 8ae7e4eda34..7ca9b28579d 100644 --- a/packages/adapter-nextjs/__tests__/api/generateServerClient.test.ts +++ b/packages/adapter-nextjs/__tests__/api/generateServerClient.test.ts @@ -109,7 +109,9 @@ describe('generateServerClient', () => { response: mockedRes, }, operation: async contextSpec => { - await client.graphql(contextSpec, { query: '' }); + await client.graphql(contextSpec, { + query: '', + }); }, }); diff --git a/packages/adapter-nextjs/__tests__/auth/utils/hasActiveUserSession.test.ts b/packages/adapter-nextjs/__tests__/auth/utils/hasActiveUserSession.test.ts index 2006db665b2..fc229b22389 100644 --- a/packages/adapter-nextjs/__tests__/auth/utils/hasActiveUserSession.test.ts +++ b/packages/adapter-nextjs/__tests__/auth/utils/hasActiveUserSession.test.ts @@ -2,6 +2,7 @@ import { getCurrentUser } from 'aws-amplify/auth/server'; import { NextRequest } from 'next/server'; import { AuthUser } from 'aws-amplify/auth'; import { NextApiRequest } from 'next'; +import { AmplifyContext } from '@aws-amplify/core'; import { hasActiveUserSessionWithAppRouter, @@ -17,7 +18,13 @@ const mockRunWithAmplifyServerContext = const mockGetCurrentUser = jest.mocked(getCurrentUser); describe('hasUserSignedIn', () => { - const mockContextSpec = { token: { value: Symbol('mock') } }; + const mockContextSpec: AmplifyContext = { + resourcesConfig: {}, + libraryOptions: {}, + fetchAuthSession: jest.fn(), + clearCredentials: jest.fn(), + getTokens: jest.fn(), + }; const mockCurrentUserResult: AuthUser = { userId: 'mockUserId', username: 'mockUsername', diff --git a/packages/adapter-nextjs/__tests__/createServerRunner.test.ts b/packages/adapter-nextjs/__tests__/createServerRunner.test.ts index 2289b51cdcb..3f5a8060517 100644 --- a/packages/adapter-nextjs/__tests__/createServerRunner.test.ts +++ b/packages/adapter-nextjs/__tests__/createServerRunner.test.ts @@ -66,7 +66,7 @@ describe('createServerRunner', () => { const mockCreateAWSCredentialsAndIdentityIdProvider = jest.fn(); const mockCreateKeyValueStorageFromCookieStorageAdapter = jest.fn(); const mockCreateUserPoolsTokenProvider = jest.fn(); - const mockRunWithAmplifyServerContextCore = jest.fn(); + const mockCreateAmplifyContext = jest.fn(() => ({})); const mockCreateAuthRouteHandlersFactory = jest.fn(() => jest.fn()); const mockIsSSLOriginUtil = jest.fn(() => true); const mockIsValidOrigin = jest.fn(origin => !!origin); @@ -87,7 +87,11 @@ describe('createServerRunner', () => { createKeyValueStorageFromCookieStorageAdapter: mockCreateKeyValueStorageFromCookieStorageAdapter, createUserPoolsTokenProvider: mockCreateUserPoolsTokenProvider, - runWithAmplifyServerContext: mockRunWithAmplifyServerContextCore, + })); + + jest.doMock('aws-amplify', () => ({ + ...jest.requireActual('aws-amplify'), + createAmplifyContext: mockCreateAmplifyContext, })); jest.doMock('aws-amplify/utils', () => ({ @@ -165,7 +169,7 @@ describe('createServerRunner', () => { describe('runWithAmplifyServerContext', () => { describe('when amplifyConfig.Auth is not defined', () => { - it('should call runWithAmplifyServerContextCore without Auth library options', () => { + it('should call createAmplifyContext without Auth library options', () => { const mockAmplifyConfigWithoutAuth: ResourcesConfig = { Analytics: { Pinpoint: { @@ -182,10 +186,9 @@ describe('createServerRunner', () => { }); const operation = jest.fn(); runWithAmplifyServerContext({ operation, nextServerContext: null }); - expect(mockRunWithAmplifyServerContextCore).toHaveBeenCalledWith( + expect(mockCreateAmplifyContext).toHaveBeenCalledWith( mockAmplifyConfigWithoutAuth, {}, - operation, ); expect(createRunWithAmplifyServerContextSpy).toHaveBeenCalledWith({ config: mockAmplifyConfigWithoutAuth, diff --git a/packages/adapter-nextjs/package.json b/packages/adapter-nextjs/package.json index bc0fe7d3c60..e7e8efe0178 100644 --- a/packages/adapter-nextjs/package.json +++ b/packages/adapter-nextjs/package.json @@ -4,7 +4,7 @@ "version": "1.7.3", "description": "The adapter for the supporting of using Amplify APIs in Next.js.", "peerDependencies": { - "aws-amplify": "^6.16.4", + "aws-amplify": "^6.17.0", "next": ">=13.5.0 <17.0.0" }, "dependencies": { diff --git a/packages/adapter-nextjs/src/api/generateServerClient.ts b/packages/adapter-nextjs/src/api/generateServerClient.ts index 88d8f830b23..ccbe89fd771 100644 --- a/packages/adapter-nextjs/src/api/generateServerClient.ts +++ b/packages/adapter-nextjs/src/api/generateServerClient.ts @@ -8,11 +8,7 @@ import { V6ClientSSRRequest, generateClientWithAmplifyInstance, } from 'aws-amplify/api/internals'; -import { generateClient } from 'aws-amplify/api/server'; -import { - AmplifyServerContextError, - getAmplifyServerContext, -} from 'aws-amplify/adapter-core/internals'; +import { AmplifyError } from 'aws-amplify/adapter-core/internals'; import { parseAmplifyConfig } from 'aws-amplify/utils'; import { NextServer } from '../types'; @@ -43,12 +39,12 @@ export function generateServerClientUsingCookies< CookiesClientParams = DefaultCommonClientOptions & CookiesClientParams, >(options: Options): V6ClientSSRCookies { if (typeof options.cookies !== 'function') { - throw new AmplifyServerContextError({ + throw new AmplifyError({ + name: 'InvalidCookiesError', message: 'generateServerClientUsingCookies is only compatible with the `cookies` Dynamic Function available in Server Components.', - // TODO: link to docs recoverySuggestion: - 'use `generateServerClient` inside of `runWithAmplifyServerContext` with the `request` object.', + 'Use `generateServerClient` inside of `runWithAmplifyServerContext` with the `request` object.', }); } @@ -61,8 +57,7 @@ export function generateServerClientUsingCookies< const getAmplify = (fn: (amplify: any) => Promise) => runWithAmplifyServerContext({ nextServerContext: { cookies: options.cookies }, - operation: contextSpec => - fn(getAmplifyServerContext(contextSpec).amplify), + operation: contextSpec => fn(contextSpec), }); const { cookies: _cookies, config: _config, ...params } = options; @@ -71,7 +66,7 @@ export function generateServerClientUsingCookies< amplify: getAmplify, config: resourcesConfig, ...params, - } as any); // TS can't narrow the type here. + }); } /** @@ -99,8 +94,8 @@ export function generateServerClientUsingReqRes< const { config: _config, ...params } = options; - return generateClient({ + return generateClientWithAmplifyInstance>({ config: amplifyConfig, ...params, - }) as any; + }); } diff --git a/packages/adapter-nextjs/src/auth/createAuthRouteHandlersFactory.ts b/packages/adapter-nextjs/src/auth/createAuthRouteHandlersFactory.ts index 46e95cbdc83..4e15db8baa0 100644 --- a/packages/adapter-nextjs/src/auth/createAuthRouteHandlersFactory.ts +++ b/packages/adapter-nextjs/src/auth/createAuthRouteHandlersFactory.ts @@ -3,7 +3,7 @@ import { NextRequest } from 'next/server'; import { - AmplifyServerContextError, + AmplifyError, CookieStorage, OAuthConfig, assertOAuthConfig, @@ -97,7 +97,8 @@ export const createAuthRouteHandlersFactory = ({ // origin validation should happen when createAuthRouteHandlers is being called to create // Auth API routes. if (!amplifyAppOrigin) { - throw new AmplifyServerContextError({ + throw new AmplifyError({ + name: 'MissingOriginError', message: 'Could not find the AMPLIFY_APP_ORIGIN environment variable.', recoverySuggestion: 'Add the AMPLIFY_APP_ORIGIN environment variable to the `.env` file of your Next.js project.', @@ -105,7 +106,8 @@ export const createAuthRouteHandlersFactory = ({ } if (!isValidOrigin(amplifyAppOrigin)) { - throw new AmplifyServerContextError({ + throw new AmplifyError({ + name: 'InvalidOriginError', message: 'AMPLIFY_APP_ORIGIN environment variable contains an invalid origin string.', recoverySuggestion: diff --git a/packages/adapter-nextjs/src/auth/utils/resolveRedirectUrl.ts b/packages/adapter-nextjs/src/auth/utils/resolveRedirectUrl.ts index 7f7dc29547c..93412d40cd3 100644 --- a/packages/adapter-nextjs/src/auth/utils/resolveRedirectUrl.ts +++ b/packages/adapter-nextjs/src/auth/utils/resolveRedirectUrl.ts @@ -1,10 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { - AmplifyServerContextError, - OAuthConfig, -} from 'aws-amplify/adapter-core/internals'; +import { AmplifyError, OAuthConfig } from 'aws-amplify/adapter-core/internals'; export const resolveRedirectSignInUrl = ( origin: string, @@ -36,8 +33,9 @@ export const resolveRedirectSignOutUrl = ( return redirectUrl; }; -const createError = (urlType: string): AmplifyServerContextError => - new AmplifyServerContextError({ +const createError = (urlType: string): AmplifyError => + new AmplifyError({ + name: 'InvalidRedirectUrlError', message: `No valid ${urlType} url found in the OAuth config.`, recoverySuggestion: `Check the OAuth config and ensure the ${urlType} url is valid.`, }); diff --git a/packages/adapter-nextjs/src/types/NextServer.ts b/packages/adapter-nextjs/src/types/NextServer.ts index 5a05f0b4f5d..6f00fc01245 100644 --- a/packages/adapter-nextjs/src/types/NextServer.ts +++ b/packages/adapter-nextjs/src/types/NextServer.ts @@ -6,11 +6,10 @@ import { NextRequest, NextResponse } from 'next/server.js'; import { cookies } from 'next/headers.js'; import { AmplifyOutputsUnknown, - AmplifyServer, CookieStorage, LegacyConfig, } from 'aws-amplify/adapter-core/internals'; -import { ResourcesConfig } from 'aws-amplify'; +import { AmplifyContext, ResourcesConfig } from 'aws-amplify'; import { CreateAuthRouteHandlers } from '../auth/types'; @@ -71,7 +70,7 @@ export declare namespace NextServer { export interface RunWithContextInput { nextServerContext: Context | null; operation( - contextSpec: AmplifyServer.ContextSpec, + contextSpec: AmplifyContext, ): OperationResult | Promise; } diff --git a/packages/adapter-nextjs/src/utils/createCookieStorageAdapterFromNextServerContext.ts b/packages/adapter-nextjs/src/utils/createCookieStorageAdapterFromNextServerContext.ts index 10c8a4a96bc..056a20434ce 100644 --- a/packages/adapter-nextjs/src/utils/createCookieStorageAdapterFromNextServerContext.ts +++ b/packages/adapter-nextjs/src/utils/createCookieStorageAdapterFromNextServerContext.ts @@ -3,7 +3,7 @@ import { NextRequest, NextResponse } from 'next/server.js'; import { - AmplifyServerContextError, + AmplifyError, CookieStorage, } from 'aws-amplify/adapter-core/internals'; @@ -78,7 +78,8 @@ export const createCookieStorageAdapterFromNextServerContext = async ( } // This should not happen normally. - throw new AmplifyServerContextError({ + throw new AmplifyError({ + name: 'UnsupportedServerContextError', message: 'Attempted to create cookie storage adapter from an unsupported Next.js server context.', }); diff --git a/packages/adapter-nextjs/src/utils/createRunWithAmplifyServerContext.ts b/packages/adapter-nextjs/src/utils/createRunWithAmplifyServerContext.ts index e5744056619..b8b54042d69 100644 --- a/packages/adapter-nextjs/src/utils/createRunWithAmplifyServerContext.ts +++ b/packages/adapter-nextjs/src/utils/createRunWithAmplifyServerContext.ts @@ -1,14 +1,13 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { ResourcesConfig } from 'aws-amplify'; +import { ResourcesConfig, createAmplifyContext } from 'aws-amplify'; import { sharedInMemoryStorage } from 'aws-amplify/utils'; import { KeyValueStorageMethodValidator } from 'aws-amplify/adapter-core/internals'; import { createAWSCredentialsAndIdentityIdProvider, createKeyValueStorageFromCookieStorageAdapter, createUserPoolsTokenProvider, - runWithAmplifyServerContext as runWithAmplifyServerContextCore, } from 'aws-amplify/adapter-core'; import { NextServer } from '../types'; @@ -75,18 +74,18 @@ export const createRunWithAmplifyServerContext = ({ keyValueStorage, ); - return runWithAmplifyServerContextCore( - resourcesConfig, - { - Auth: { credentialsProvider, tokenProvider }, - }, - operation, - ); + const amplifyContext = createAmplifyContext(resourcesConfig, { + Auth: { credentialsProvider, tokenProvider }, + }); + + return operation(amplifyContext); } // Otherwise it may be the case that auth is not used, e.g. API key. // Omitting the `Auth` in the second parameter. - return runWithAmplifyServerContextCore(resourcesConfig, {}, operation); + const amplifyContext = createAmplifyContext(resourcesConfig, {}); + + return operation(amplifyContext); }; return runWithAmplifyServerContext; diff --git a/packages/analytics/__tests__/providers/kinesis-firehose/apis/flushEvents.test.ts b/packages/analytics/__tests__/providers/kinesis-firehose/apis/flushEvents.test.ts index ace2572086e..d0c18c0e22a 100644 --- a/packages/analytics/__tests__/providers/kinesis-firehose/apis/flushEvents.test.ts +++ b/packages/analytics/__tests__/providers/kinesis-firehose/apis/flushEvents.test.ts @@ -13,11 +13,21 @@ import { mockKinesisConfig, } from '../../../testUtils/mockConstants'; import { flushEvents } from '../../../../src/providers/kinesis-firehose/apis'; +import { + setupGlobalContext, + teardownGlobalContext, +} from '../../../testUtils/mockAmplifyContext'; jest.mock('../../../../src/utils'); jest.mock('../../../../src/providers/kinesis-firehose/utils'); describe('Analytics Kinesis Firehose API: flushEvents', () => { + beforeAll(() => { + setupGlobalContext(); + }); + afterAll(() => { + teardownGlobalContext(); + }); const mockResolveConfig = resolveConfig as jest.Mock; const mockResolveCredentials = resolveCredentials as jest.Mock; const mockGetEventBuffer = getEventBuffer as jest.Mock; diff --git a/packages/analytics/__tests__/providers/kinesis-firehose/apis/record.test.ts b/packages/analytics/__tests__/providers/kinesis-firehose/apis/record.test.ts index d2f1fd7e3c5..c3d05aaff6e 100644 --- a/packages/analytics/__tests__/providers/kinesis-firehose/apis/record.test.ts +++ b/packages/analytics/__tests__/providers/kinesis-firehose/apis/record.test.ts @@ -14,11 +14,21 @@ import { } from '../../../testUtils/mockConstants'; import { record } from '../../../../src/providers/kinesis-firehose'; import { RecordInput as KinesisFirehoseRecordInput } from '../../../../src/providers/kinesis-firehose/types'; +import { + setupGlobalContext, + teardownGlobalContext, +} from '../../../testUtils/mockAmplifyContext'; jest.mock('../../../../src/utils'); jest.mock('../../../../src/providers/kinesis-firehose/utils'); describe('Analytics KinesisFirehose API: record', () => { + beforeAll(() => { + setupGlobalContext(); + }); + afterAll(() => { + teardownGlobalContext(); + }); const mockRecordInput: KinesisFirehoseRecordInput = { streamName: 'stream0', data: new Uint8Array([0x01, 0x02, 0xff]), diff --git a/packages/analytics/__tests__/providers/kinesis-firehose/utils/resolveConfig.test.ts b/packages/analytics/__tests__/providers/kinesis-firehose/utils/resolveConfig.test.ts index e93dec1b876..b24922cd4b4 100644 --- a/packages/analytics/__tests__/providers/kinesis-firehose/utils/resolveConfig.test.ts +++ b/packages/analytics/__tests__/providers/kinesis-firehose/utils/resolveConfig.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 { resolveConfig } from '../../../../src/providers/kinesis-firehose/utils'; import { DEFAULT_KINESIS_FIREHOSE_CONFIG } from '../../../../src/providers/kinesis-firehose/utils/constants'; @@ -15,18 +15,22 @@ describe('Analytics KinesisFirehose Provider Util: resolveConfig', () => { resendLimit: 3, }; - const getConfigSpy = jest.spyOn(Amplify, 'getConfig'); - - beforeEach(() => { - getConfigSpy.mockReset(); + const createCtx = ( + resourcesConfig: Record = {}, + ): AmplifyContext => ({ + resourcesConfig, + libraryOptions: {}, + fetchAuthSession: jest.fn(), + clearCredentials: jest.fn(), + getTokens: jest.fn(), }); it('returns required config', () => { - getConfigSpy.mockReturnValue({ - Analytics: { KinesisFirehose: providedConfig }, - }); - - expect(resolveConfig()).toStrictEqual(providedConfig); + expect( + resolveConfig( + createCtx({ Analytics: { KinesisFirehose: providedConfig } }), + ), + ).toStrictEqual(providedConfig); }); it('use default config for optional fields', () => { @@ -35,11 +39,12 @@ describe('Analytics KinesisFirehose Provider Util: resolveConfig', () => { bufferSize: undefined, resendLimit: undefined, }; - getConfigSpy.mockReturnValue({ - Analytics: { KinesisFirehose: requiredFields }, - }); - expect(resolveConfig()).toStrictEqual({ + expect( + resolveConfig( + createCtx({ Analytics: { KinesisFirehose: requiredFields } }), + ), + ).toStrictEqual({ ...DEFAULT_KINESIS_FIREHOSE_CONFIG, region: requiredFields.region, resendLimit: requiredFields.resendLimit, @@ -47,25 +52,29 @@ describe('Analytics KinesisFirehose Provider Util: resolveConfig', () => { }); it('throws if region is missing', () => { - getConfigSpy.mockReturnValue({ - Analytics: { - KinesisFirehose: { ...providedConfig, region: undefined as any }, - }, - }); - - expect(resolveConfig).toThrow(); + expect(() => + resolveConfig( + createCtx({ + Analytics: { + KinesisFirehose: { ...providedConfig, region: undefined }, + }, + }), + ), + ).toThrow(); }); it('throws if flushSize is larger than bufferSize', () => { - getConfigSpy.mockReturnValue({ - Analytics: { - KinesisFirehose: { - ...providedConfig, - flushSize: providedConfig.bufferSize + 1, - }, - }, - }); - - expect(resolveConfig).toThrow(); + expect(() => + resolveConfig( + createCtx({ + Analytics: { + KinesisFirehose: { + ...providedConfig, + flushSize: providedConfig.bufferSize + 1, + }, + }, + }), + ), + ).toThrow(); }); }); diff --git a/packages/analytics/__tests__/providers/kinesis/apis/flushEvents.test.ts b/packages/analytics/__tests__/providers/kinesis/apis/flushEvents.test.ts index 8c8398c7e9c..11d4232a824 100644 --- a/packages/analytics/__tests__/providers/kinesis/apis/flushEvents.test.ts +++ b/packages/analytics/__tests__/providers/kinesis/apis/flushEvents.test.ts @@ -11,12 +11,22 @@ import { } from '../../../testUtils/mockConstants'; import { getEventBuffer } from '../../../../src/providers/kinesis/utils/getEventBuffer'; import { flushEvents } from '../../../../src/providers/kinesis/apis'; +import { + setupGlobalContext, + teardownGlobalContext, +} from '../../../testUtils/mockAmplifyContext'; jest.mock('../../../../src/utils'); jest.mock('../../../../src/providers/kinesis/utils/getEventBuffer'); jest.mock('../../../../src/providers/kinesis/utils/resolveConfig'); describe('Analytics Kinesis API: flushEvents', () => { + beforeAll(() => { + setupGlobalContext(); + }); + afterAll(() => { + teardownGlobalContext(); + }); const mockResolveConfig = resolveConfig as jest.Mock; const mockResolveCredentials = resolveCredentials as jest.Mock; const mockGetEventBuffer = getEventBuffer as jest.Mock; diff --git a/packages/analytics/__tests__/providers/kinesis/apis/record.test.ts b/packages/analytics/__tests__/providers/kinesis/apis/record.test.ts index 7be66f73b19..105ab09d704 100644 --- a/packages/analytics/__tests__/providers/kinesis/apis/record.test.ts +++ b/packages/analytics/__tests__/providers/kinesis/apis/record.test.ts @@ -12,12 +12,22 @@ import { } from '../../../testUtils/mockConstants'; import { record } from '../../../../src/providers/kinesis'; import { RecordInput as KinesisRecordInput } from '../../../../src/providers/kinesis/types'; +import { + setupGlobalContext, + teardownGlobalContext, +} from '../../../testUtils/mockAmplifyContext'; jest.mock('../../../../src/utils'); jest.mock('../../../../src/providers/kinesis/utils/resolveConfig'); jest.mock('../../../../src/providers/kinesis/utils/getEventBuffer'); describe('Analytics Kinesis API: record', () => { + beforeAll(() => { + setupGlobalContext(); + }); + afterAll(() => { + teardownGlobalContext(); + }); const mockRecordInput: KinesisRecordInput = { streamName: 'stream0', partitionKey: 'partition0', diff --git a/packages/analytics/__tests__/providers/kinesis/utils/resolveConfig.test.ts b/packages/analytics/__tests__/providers/kinesis/utils/resolveConfig.test.ts index ad4d079a910..db8b0c2ae2a 100644 --- a/packages/analytics/__tests__/providers/kinesis/utils/resolveConfig.test.ts +++ b/packages/analytics/__tests__/providers/kinesis/utils/resolveConfig.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 { resolveConfig } from '../../../../src/providers/kinesis/utils/resolveConfig'; import { DEFAULT_KINESIS_CONFIG } from '../../../../src/providers/kinesis/utils/constants'; @@ -15,18 +15,20 @@ describe('Analytics Kinesis Provider Util: resolveConfig', () => { resendLimit: 3, }; - const getConfigSpy = jest.spyOn(Amplify, 'getConfig'); - - beforeEach(() => { - getConfigSpy.mockReset(); + const createCtx = ( + resourcesConfig: Record = {}, + ): AmplifyContext => ({ + resourcesConfig, + libraryOptions: {}, + fetchAuthSession: jest.fn(), + clearCredentials: jest.fn(), + getTokens: jest.fn(), }); it('returns required config', () => { - getConfigSpy.mockReturnValue({ - Analytics: { Kinesis: kinesisConfig }, - }); - - expect(resolveConfig()).toStrictEqual(kinesisConfig); + expect( + resolveConfig(createCtx({ Analytics: { Kinesis: kinesisConfig } })), + ).toStrictEqual(kinesisConfig); }); it('use default config for optional fields', () => { @@ -35,11 +37,10 @@ describe('Analytics Kinesis Provider Util: resolveConfig', () => { bufferSize: undefined, resendLimit: undefined, }; - getConfigSpy.mockReturnValue({ - Analytics: { Kinesis: requiredFields }, - }); - expect(resolveConfig()).toStrictEqual({ + expect( + resolveConfig(createCtx({ Analytics: { Kinesis: requiredFields } })), + ).toStrictEqual({ ...DEFAULT_KINESIS_CONFIG, region: requiredFields.region, resendLimit: requiredFields.resendLimit, @@ -47,20 +48,27 @@ describe('Analytics Kinesis Provider Util: resolveConfig', () => { }); it('throws if region is missing', () => { - getConfigSpy.mockReturnValue({ - Analytics: { Kinesis: { ...kinesisConfig, region: undefined as any } }, - }); - - expect(resolveConfig).toThrow(); + expect(() => + resolveConfig( + createCtx({ + Analytics: { Kinesis: { ...kinesisConfig, region: undefined } }, + }), + ), + ).toThrow(); }); it('throws if flushSize is larger than bufferSize', () => { - getConfigSpy.mockReturnValue({ - Analytics: { - Kinesis: { ...kinesisConfig, flushSize: kinesisConfig.bufferSize + 1 }, - }, - }); - - expect(resolveConfig).toThrow(); + expect(() => + resolveConfig( + createCtx({ + Analytics: { + Kinesis: { + ...kinesisConfig, + flushSize: kinesisConfig.bufferSize + 1, + }, + }, + }), + ), + ).toThrow(); }); }); diff --git a/packages/analytics/__tests__/providers/personalize/apis/flushEvents.test.ts b/packages/analytics/__tests__/providers/personalize/apis/flushEvents.test.ts index bddc91706bf..6686ac4a1a3 100644 --- a/packages/analytics/__tests__/providers/personalize/apis/flushEvents.test.ts +++ b/packages/analytics/__tests__/providers/personalize/apis/flushEvents.test.ts @@ -1,7 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { ConsoleLogger } from '@aws-amplify/core'; +import { AmplifyContext, ConsoleLogger } from '@aws-amplify/core'; import { getEventBuffer, @@ -17,6 +17,14 @@ import { flushEvents } from '../../../../src/providers/personalize'; jest.mock('../../../../src/utils'); jest.mock('../../../../src/providers/personalize/utils'); +const mockCtx: AmplifyContext = { + resourcesConfig: {}, + libraryOptions: {}, + fetchAuthSession: jest.fn(), + clearCredentials: jest.fn(), + getTokens: jest.fn(), +}; + describe('Analytics Personalize API: flushEvents', () => { const mockResolveConfig = resolveConfig as jest.Mock; const mockResolveCredentials = resolveCredentials as jest.Mock; @@ -42,7 +50,7 @@ describe('Analytics Personalize API: flushEvents', () => { }); it('trigger flushAll on event buffer', async () => { - flushEvents(); + flushEvents(mockCtx); await new Promise(process.nextTick); expect(mockResolveConfig).toHaveBeenCalledTimes(1); expect(mockResolveCredentials).toHaveBeenCalledTimes(1); @@ -60,7 +68,7 @@ describe('Analytics Personalize API: flushEvents', () => { it('logs an error when credentials can not be fetched', async () => { mockResolveCredentials.mockRejectedValue(new Error('Mock Error')); - flushEvents(); + flushEvents(mockCtx); await new Promise(process.nextTick); expect(loggerWarnSpy).toHaveBeenCalledWith( expect.any(String), diff --git a/packages/analytics/__tests__/providers/personalize/apis/record.test.ts b/packages/analytics/__tests__/providers/personalize/apis/record.test.ts index 363bb8460ed..039b4307535 100644 --- a/packages/analytics/__tests__/providers/personalize/apis/record.test.ts +++ b/packages/analytics/__tests__/providers/personalize/apis/record.test.ts @@ -1,6 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { ConsoleLogger } from '@aws-amplify/core'; +import { AmplifyContext, ConsoleLogger } from '@aws-amplify/core'; import { autoTrackMedia, @@ -24,6 +24,14 @@ import { jest.mock('../../../../src/utils'); jest.mock('../../../../src/providers/personalize/utils'); +const mockCtx: AmplifyContext = { + resourcesConfig: {}, + libraryOptions: {}, + fetchAuthSession: jest.fn(), + clearCredentials: jest.fn(), + getTokens: jest.fn(), +}; + describe('Analytics Personalize API: record', () => { const mockRecordInput: PersonalizeRecordInput = { eventType: 'eventType0', @@ -73,7 +81,7 @@ describe('Analytics Personalize API: record', () => { }); it('append to event buffer if record provided', async () => { - record(mockRecordInput); + record(mockCtx, mockRecordInput); await new Promise(process.nextTick); expect(mockGetEventBuffer).toHaveBeenCalledTimes(1); expect(mockAppend).toHaveBeenCalledWith( @@ -102,7 +110,7 @@ describe('Analytics Personalize API: record', () => { userId: newSession.userId, }, }; - record(updatedMockRecordInput); + record(mockCtx, updatedMockRecordInput); await new Promise(process.nextTick); expect(mockGetEventBuffer).toHaveBeenCalledTimes(1); @@ -133,7 +141,7 @@ describe('Analytics Personalize API: record', () => { ...mockRecordInput, userId: newSession.userId, }; - record(updatedMockRecordInput); + record(mockCtx, updatedMockRecordInput); await new Promise(process.nextTick); expect(mockGetEventBuffer).toHaveBeenCalledTimes(1); @@ -156,7 +164,7 @@ describe('Analytics Personalize API: record', () => { ...mockRecordInput, eventType: MEDIA_AUTO_TRACK_EVENT_TYPE, }; - record(updatedMockRecordInput); + record(mockCtx, updatedMockRecordInput); await new Promise(process.nextTick); expect(mockGetEventBuffer).toHaveBeenCalledTimes(1); @@ -185,7 +193,7 @@ describe('Analytics Personalize API: record', () => { mockGetLength.mockReturnValue(mockPersonalizeConfig.flushSize + 1); mockGetEventBuffer.mockImplementation(() => updatedMockEventBuffer); - record(mockRecordInput); + record(mockCtx, mockRecordInput); await new Promise(process.nextTick); expect(mockGetEventBuffer).toHaveBeenCalledTimes(1); expect(mockAppend).toHaveBeenCalledWith( @@ -202,7 +210,7 @@ describe('Analytics Personalize API: record', () => { it('logs an error when credentials can not be fetched', async () => { mockResolveCredentials.mockRejectedValue(new Error('Mock Error')); - record(mockRecordInput); + record(mockCtx, mockRecordInput); await new Promise(process.nextTick); expect(loggerWarnSpy).toHaveBeenCalledWith( @@ -213,7 +221,7 @@ describe('Analytics Personalize API: record', () => { it('logs and skip the event recoding if Analytics plugin is not enabled', async () => { mockIsAnalyticsEnabled.mockReturnValue(false); - record(mockRecordInput); + record(mockCtx, mockRecordInput); await new Promise(process.nextTick); expect(loggerDebugSpy).toHaveBeenCalledWith(expect.any(String)); expect(mockGetEventBuffer).not.toHaveBeenCalled(); diff --git a/packages/analytics/__tests__/providers/personalize/utils/resolveConfig.test.ts b/packages/analytics/__tests__/providers/personalize/utils/resolveConfig.test.ts index 3f39f1db69f..6f9bdbe17bb 100644 --- a/packages/analytics/__tests__/providers/personalize/utils/resolveConfig.test.ts +++ b/packages/analytics/__tests__/providers/personalize/utils/resolveConfig.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 { DEFAULT_PERSONALIZE_CONFIG, @@ -17,18 +17,20 @@ describe('Analytics Personalize Provider Util: resolveConfig', () => { flushInterval: 1000, }; - const getConfigSpy = jest.spyOn(Amplify, 'getConfig'); - - beforeEach(() => { - getConfigSpy.mockReset(); + const createCtx = ( + resourcesConfig: Record = {}, + ): AmplifyContext => ({ + resourcesConfig, + libraryOptions: {}, + fetchAuthSession: jest.fn(), + clearCredentials: jest.fn(), + getTokens: jest.fn(), }); it('returns required config', () => { - getConfigSpy.mockReturnValue({ - Analytics: { Personalize: providedConfig }, - }); - - expect(resolveConfig()).toStrictEqual({ + expect( + resolveConfig(createCtx({ Analytics: { Personalize: providedConfig } })), + ).toStrictEqual({ ...providedConfig, bufferSize: providedConfig.flushSize + 1, }); @@ -39,11 +41,10 @@ describe('Analytics Personalize Provider Util: resolveConfig', () => { region: 'us-east-1', trackingId: 'trackingId1', }; - getConfigSpy.mockReturnValue({ - Analytics: { Personalize: requiredFields }, - }); - expect(resolveConfig()).toStrictEqual({ + expect( + resolveConfig(createCtx({ Analytics: { Personalize: requiredFields } })), + ).toStrictEqual({ ...DEFAULT_PERSONALIZE_CONFIG, region: requiredFields.region, trackingId: requiredFields.trackingId, @@ -52,25 +53,27 @@ describe('Analytics Personalize Provider Util: resolveConfig', () => { }); it('throws if region is missing', () => { - getConfigSpy.mockReturnValue({ - Analytics: { - Personalize: { ...providedConfig, region: undefined as any }, - }, - }); - - expect(resolveConfig).toThrow(); + expect(() => + resolveConfig( + createCtx({ + Analytics: { Personalize: { ...providedConfig, region: undefined } }, + }), + ), + ).toThrow(); }); it('throws if flushSize is larger than max', () => { - getConfigSpy.mockReturnValue({ - Analytics: { - Personalize: { - ...providedConfig, - flushSize: PERSONALIZE_FLUSH_SIZE_MAX + 1, - }, - }, - }); - - expect(resolveConfig).toThrow(); + expect(() => + resolveConfig( + createCtx({ + Analytics: { + Personalize: { + ...providedConfig, + flushSize: PERSONALIZE_FLUSH_SIZE_MAX + 1, + }, + }, + }), + ), + ).toThrow(); }); }); diff --git a/packages/analytics/__tests__/providers/pinpoint/apis/configureAutoTrack.test.ts b/packages/analytics/__tests__/providers/pinpoint/apis/configureAutoTrack.test.ts index 62c38ac49d7..c65e0f06ce6 100644 --- a/packages/analytics/__tests__/providers/pinpoint/apis/configureAutoTrack.test.ts +++ b/packages/analytics/__tests__/providers/pinpoint/apis/configureAutoTrack.test.ts @@ -7,6 +7,7 @@ import { PageViewTracker, SessionTracker, } from '../../../../src/trackers'; +import { mockAmplifyCtx } from '../../../testUtils/mockAmplifyContext'; jest.mock('../../../../src/trackers'); @@ -46,7 +47,7 @@ describe('Pinpoint API: configureAutoTrack', () => { } = require('../../../../src/providers/pinpoint/apis'); try { - configureAutoTrack({ + configureAutoTrack(mockAmplifyCtx, { ...MOCK_INPUT, type: 'invalidTracker', } as any); @@ -62,7 +63,7 @@ describe('Pinpoint API: configureAutoTrack', () => { configureAutoTrack, } = require('../../../../src/providers/pinpoint/apis'); - configureAutoTrack(MOCK_INPUT); + configureAutoTrack(mockAmplifyCtx, MOCK_INPUT); }); expect(MockEventTracker).toHaveBeenCalledWith( @@ -82,7 +83,7 @@ describe('Pinpoint API: configureAutoTrack', () => { configureAutoTrack, } = require('../../../../src/providers/pinpoint/apis'); - configureAutoTrack(testInput); + configureAutoTrack(mockAmplifyCtx, testInput); }); expect(MockSessionTracker).toHaveBeenCalledWith( @@ -102,7 +103,7 @@ describe('Pinpoint API: configureAutoTrack', () => { configureAutoTrack, } = require('../../../../src/providers/pinpoint/apis'); - configureAutoTrack(testInput); + configureAutoTrack(mockAmplifyCtx, testInput); }); expect(MockPageViewTracker).toHaveBeenCalledWith( @@ -118,14 +119,14 @@ describe('Pinpoint API: configureAutoTrack', () => { } = require('../../../../src/providers/pinpoint/apis'); // Enable the tracker - configureAutoTrack(MOCK_INPUT); + configureAutoTrack(mockAmplifyCtx, MOCK_INPUT); expect(MockEventTracker).toHaveBeenCalledWith( expect.any(Function), MOCK_INPUT.options, ); // Reconfigure the tracker - configureAutoTrack(MOCK_INPUT); + configureAutoTrack(mockAmplifyCtx, MOCK_INPUT); expect( MockEventTracker.mock.instances[0].configure, ).toHaveBeenCalledTimes(1); @@ -144,14 +145,14 @@ describe('Pinpoint API: configureAutoTrack', () => { } = require('../../../../src/providers/pinpoint/apis'); // Enable the tracker - configureAutoTrack(MOCK_INPUT); + configureAutoTrack(mockAmplifyCtx, MOCK_INPUT); expect(MockEventTracker).toHaveBeenCalledWith( expect.any(Function), MOCK_INPUT.options, ); // Disable the tracker - configureAutoTrack(testInput); + configureAutoTrack(mockAmplifyCtx, testInput); expect(MockEventTracker.mock.instances[0].cleanup).toHaveBeenCalledTimes( 1, ); diff --git a/packages/analytics/__tests__/providers/pinpoint/apis/flushEvents.test.ts b/packages/analytics/__tests__/providers/pinpoint/apis/flushEvents.test.ts index ee24a5864aa..932d7588e8b 100644 --- a/packages/analytics/__tests__/providers/pinpoint/apis/flushEvents.test.ts +++ b/packages/analytics/__tests__/providers/pinpoint/apis/flushEvents.test.ts @@ -11,6 +11,10 @@ import { resolveCredentials, } from '../../../../src/providers/pinpoint/utils'; import { getAnalyticsUserAgentString } from '../../../../src/utils'; +import { + setupGlobalContext, + teardownGlobalContext, +} from '../../../testUtils/mockAmplifyContext'; import { config, credentials, identityId } from './testUtils/data'; @@ -18,6 +22,12 @@ jest.mock('../../../../src/providers/pinpoint/utils'); jest.mock('@aws-amplify/core/internals/providers/pinpoint'); describe('Pinpoint API: flushEvents', () => { + beforeAll(() => { + setupGlobalContext(); + }); + afterAll(() => { + teardownGlobalContext(); + }); const mockResolveConfig = resolveConfig as jest.Mock; const mockResolveCredentials = resolveCredentials as jest.Mock; const mockPinpointFlushEvents = pinpointFlushEvents as jest.Mock; diff --git a/packages/analytics/__tests__/providers/pinpoint/apis/identifyUser.test.ts b/packages/analytics/__tests__/providers/pinpoint/apis/identifyUser.test.ts index 0b9f4f4fa97..4427af0321a 100644 --- a/packages/analytics/__tests__/providers/pinpoint/apis/identifyUser.test.ts +++ b/packages/analytics/__tests__/providers/pinpoint/apis/identifyUser.test.ts @@ -10,12 +10,22 @@ import { resolveCredentials, } from '../../../../src/providers/pinpoint/utils'; import { getAnalyticsUserAgentString } from '../../../../src/utils/userAgent'; +import { + setupGlobalContext, + teardownGlobalContext, +} from '../../../testUtils/mockAmplifyContext'; jest.mock('@aws-amplify/core/internals/providers/pinpoint'); jest.mock('../../../../src/providers/pinpoint/utils'); jest.mock('../../../../src/utils/userAgent'); describe('Analytics Pinpoint Provider API: identifyUser', () => { + beforeAll(() => { + setupGlobalContext(); + }); + afterAll(() => { + teardownGlobalContext(); + }); const credentials = { credentials: { accessKeyId: 'access-key-id', diff --git a/packages/analytics/__tests__/providers/pinpoint/apis/record.test.ts b/packages/analytics/__tests__/providers/pinpoint/apis/record.test.ts index e356c93a992..c5f14c58a94 100644 --- a/packages/analytics/__tests__/providers/pinpoint/apis/record.test.ts +++ b/packages/analytics/__tests__/providers/pinpoint/apis/record.test.ts @@ -12,6 +12,10 @@ import { getAnalyticsUserAgentString, isAnalyticsEnabled, } from '../../../../src/utils'; +import { + setupGlobalContext, + teardownGlobalContext, +} from '../../../testUtils/mockAmplifyContext'; import { appId, @@ -28,6 +32,12 @@ jest.mock('../../../../src/utils'); jest.mock('../../../../src/providers/pinpoint/utils'); describe('Pinpoint API: record', () => { + beforeAll(() => { + setupGlobalContext(); + }); + afterAll(() => { + teardownGlobalContext(); + }); // create spies const loggerWarnSpy = jest.spyOn(ConsoleLogger.prototype, 'warn'); // create mocks diff --git a/packages/analytics/__tests__/providers/pinpoint/utils/resolveConfig.test.ts b/packages/analytics/__tests__/providers/pinpoint/utils/resolveConfig.test.ts index 562d3d2f29a..bb059f9dac4 100644 --- a/packages/analytics/__tests__/providers/pinpoint/utils/resolveConfig.test.ts +++ b/packages/analytics/__tests__/providers/pinpoint/utils/resolveConfig.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 { resolveConfig } from '../../../../src/providers/pinpoint/utils'; @@ -14,35 +14,40 @@ describe('Analytics Pinpoint Provider Util: resolveConfig', () => { flushInterval: 50, resendLimit: 3, }; - // create spies - const getConfigSpy = jest.spyOn(Amplify, 'getConfig'); - beforeEach(() => { - getConfigSpy.mockReset(); + const createCtx = ( + resourcesConfig: Record = {}, + ): AmplifyContext => ({ + resourcesConfig, + libraryOptions: {}, + fetchAuthSession: jest.fn(), + clearCredentials: jest.fn(), + getTokens: jest.fn(), }); it('returns required config', () => { - getConfigSpy.mockReturnValue({ - Analytics: { Pinpoint: pinpointConfig }, - }); - expect(resolveConfig()).toStrictEqual(pinpointConfig); + expect( + resolveConfig(createCtx({ Analytics: { Pinpoint: pinpointConfig } })), + ).toStrictEqual(pinpointConfig); }); it('throws if appId is missing', () => { - getConfigSpy.mockReturnValue({ - Analytics: { - Pinpoint: { ...pinpointConfig, appId: undefined } as any, - }, - }); - expect(resolveConfig).toThrow(); + expect(() => + resolveConfig( + createCtx({ + Analytics: { Pinpoint: { ...pinpointConfig, appId: undefined } }, + }), + ), + ).toThrow(); }); it('throws if region is missing', () => { - getConfigSpy.mockReturnValue({ - Analytics: { - Pinpoint: { ...pinpointConfig, region: undefined } as any, - }, - }); - expect(resolveConfig).toThrow(); + expect(() => + resolveConfig( + createCtx({ + Analytics: { Pinpoint: { ...pinpointConfig, region: undefined } }, + }), + ), + ).toThrow(); }); }); diff --git a/packages/analytics/__tests__/providers/pinpoint/utils/resolveCredentials.test.ts b/packages/analytics/__tests__/providers/pinpoint/utils/resolveCredentials.test.ts index 7869b89abe8..3192ed741d4 100644 --- a/packages/analytics/__tests__/providers/pinpoint/utils/resolveCredentials.test.ts +++ b/packages/analytics/__tests__/providers/pinpoint/utils/resolveCredentials.test.ts @@ -1,13 +1,23 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { fetchAuthSession } from '@aws-amplify/core'; +import { AmplifyContext, fetchAuthSession } from '@aws-amplify/core'; import { AnalyticsError } from '../../../../src/errors'; import { resolveCredentials } from '../../../../src/providers/pinpoint/utils'; jest.mock('@aws-amplify/core'); +const mockFetchAuthSession = fetchAuthSession as jest.Mock; + +const mockCtx: AmplifyContext = { + resourcesConfig: {}, + libraryOptions: {}, + fetchAuthSession: (...args: unknown[]) => mockFetchAuthSession(...args), + clearCredentials: jest.fn(), + getTokens: jest.fn(), +}; + describe('Analytics Pinpoint Provider Util: resolveCredentials', () => { const credentials = { credentials: { @@ -16,8 +26,6 @@ describe('Analytics Pinpoint Provider Util: resolveCredentials', () => { }, identityId: 'identity-id', }; - // assert mocks - const mockFetchAuthSession = fetchAuthSession as jest.Mock; beforeEach(() => { mockFetchAuthSession.mockReset(); @@ -25,7 +33,7 @@ describe('Analytics Pinpoint Provider Util: resolveCredentials', () => { it('resolves required credentials', async () => { mockFetchAuthSession.mockResolvedValue(credentials); - expect(await resolveCredentials()).toStrictEqual(credentials); + expect(await resolveCredentials(mockCtx)).toStrictEqual(credentials); }); it('throws if credentials are missing', async () => { @@ -33,6 +41,8 @@ describe('Analytics Pinpoint Provider Util: resolveCredentials', () => { ...credentials, credentials: undefined, }); - await expect(resolveCredentials()).rejects.toBeInstanceOf(AnalyticsError); + await expect(resolveCredentials(mockCtx)).rejects.toBeInstanceOf( + AnalyticsError, + ); }); }); diff --git a/packages/analytics/__tests__/testUtils/mockAmplifyContext.ts b/packages/analytics/__tests__/testUtils/mockAmplifyContext.ts new file mode 100644 index 00000000000..b95c2646aa3 --- /dev/null +++ b/packages/analytics/__tests__/testUtils/mockAmplifyContext.ts @@ -0,0 +1,40 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { AMPLIFY_CONTEXT_BRAND, AmplifyContext } from '@aws-amplify/core'; +import { + clearGlobalContext, + setGlobalContext, +} from '@aws-amplify/core/internals/utils'; + +export const mockAmplifyCtx: AmplifyContext = (() => { + const ctx: AmplifyContext = { + resourcesConfig: {}, + libraryOptions: {}, + fetchAuthSession: jest.fn().mockResolvedValue({ + credentials: { + accessKeyId: 'access-key-id', + secretAccessKey: 'secret-access-key', + sessionToken: 'session-token', + }, + identityId: 'identity-id', + }), + clearCredentials: jest.fn(), + getTokens: jest.fn(), + }; + + Object.defineProperty(ctx, AMPLIFY_CONTEXT_BRAND, { + value: true, + enumerable: false, + }); + + return ctx; +})(); + +export function setupGlobalContext() { + setGlobalContext(mockAmplifyCtx); +} + +export function teardownGlobalContext() { + clearGlobalContext(); +} diff --git a/packages/analytics/__tests__/utils/eventBuffer/EventBuffer.test.ts b/packages/analytics/__tests__/utils/eventBuffer/EventBuffer.test.ts index 75361696903..820e9a5de4b 100644 --- a/packages/analytics/__tests__/utils/eventBuffer/EventBuffer.test.ts +++ b/packages/analytics/__tests__/utils/eventBuffer/EventBuffer.test.ts @@ -14,7 +14,7 @@ describe('EventBuffer', () => { const eventBuffer = new EventBuffer( { bufferSize: 2, - flushSize: 1, + flushSize: 2, flushInterval: 25, }, () => events => { @@ -36,7 +36,7 @@ describe('EventBuffer', () => { expect(result[0]).toEqual(testEvents[0]); expect(result[1]).toEqual(testEvents[1]); done(); - }, 100); + }, 200); }); it('flush all events at once', done => { diff --git a/packages/analytics/__tests__/utils/resolveCredentials.test.ts b/packages/analytics/__tests__/utils/resolveCredentials.test.ts index ae5c72ea9c1..979657a4983 100644 --- a/packages/analytics/__tests__/utils/resolveCredentials.test.ts +++ b/packages/analytics/__tests__/utils/resolveCredentials.test.ts @@ -1,12 +1,23 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { fetchAuthSession } from '@aws-amplify/core'; +import { AmplifyContext, fetchAuthSession } from '@aws-amplify/core'; import { resolveCredentials } from '../../src/utils'; import { AnalyticsError } from '../../src'; jest.mock('@aws-amplify/core'); + +const mockFetchAuthSession = fetchAuthSession as jest.Mock; + +const mockCtx: AmplifyContext = { + resourcesConfig: {}, + libraryOptions: {}, + fetchAuthSession: (...args: unknown[]) => mockFetchAuthSession(...args), + clearCredentials: jest.fn(), + getTokens: jest.fn(), +}; + describe('Analytics Kinesis Provider Util: resolveCredentials', () => { const credentials = { credentials: { @@ -16,7 +27,6 @@ describe('Analytics Kinesis Provider Util: resolveCredentials', () => { }, identityId: 'identity-id', }; - const mockFetchAuthSession = fetchAuthSession as jest.Mock; beforeEach(() => { mockFetchAuthSession.mockReset(); @@ -24,7 +34,7 @@ describe('Analytics Kinesis Provider Util: resolveCredentials', () => { it('resolves required credentials', async () => { mockFetchAuthSession.mockResolvedValue(credentials); - expect(await resolveCredentials()).toStrictEqual(credentials); + expect(await resolveCredentials(mockCtx)).toStrictEqual(credentials); }); it('throws if credentials are missing', async () => { @@ -32,6 +42,8 @@ describe('Analytics Kinesis Provider Util: resolveCredentials', () => { ...credentials, credentials: undefined, }); - await expect(resolveCredentials()).rejects.toBeInstanceOf(AnalyticsError); + await expect(resolveCredentials(mockCtx)).rejects.toBeInstanceOf( + AnalyticsError, + ); }); }); diff --git a/packages/analytics/package.json b/packages/analytics/package.json index 7d5de04ee81..b227e89e4ad 100644 --- a/packages/analytics/package.json +++ b/packages/analytics/package.json @@ -101,7 +101,7 @@ "tslib": "^2.5.0" }, "peerDependencies": { - "@aws-amplify/core": "^6.16.2" + "@aws-amplify/core": "^6.16.3" }, "devDependencies": { "@aws-amplify/core": "6.16.3", diff --git a/packages/analytics/src/providers/kinesis-firehose/apis/flushEvents.ts b/packages/analytics/src/providers/kinesis-firehose/apis/flushEvents.ts index 3e4967d7f9f..33e00210905 100644 --- a/packages/analytics/src/providers/kinesis-firehose/apis/flushEvents.ts +++ b/packages/analytics/src/providers/kinesis-firehose/apis/flushEvents.ts @@ -1,8 +1,11 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { AnalyticsAction } from '@aws-amplify/core/internals/utils'; -import { ConsoleLogger } from '@aws-amplify/core'; +import { AmplifyContext, ConsoleLogger } from '@aws-amplify/core'; +import { + AnalyticsAction, + resolveCtxArgs, +} from '@aws-amplify/core/internals/utils'; import { getEventBuffer, resolveConfig } from '../utils'; import { @@ -19,10 +22,13 @@ const logger = new ConsoleLogger('KinesisFirehose'); * This API will make a best-effort attempt to flush events from the buffer. Events recorded immediately after invoking * this API may not be included in the flush. */ -export const flushEvents = () => { +export function flushEvents(): void; +export function flushEvents(ctx: AmplifyContext): void; +export function flushEvents(...args: any[]): void { + const [ctx] = resolveCtxArgs(args); const { region, flushSize, flushInterval, bufferSize, resendLimit } = - resolveConfig(); - resolveCredentials() + resolveConfig(ctx); + resolveCredentials(ctx) .then(({ credentials, identityId }) => getEventBuffer({ region, @@ -39,4 +45,4 @@ export const flushEvents = () => { .catch(e => { logger.warn('Failed to flush events.', e); }); -}; +} diff --git a/packages/analytics/src/providers/kinesis-firehose/apis/record.ts b/packages/analytics/src/providers/kinesis-firehose/apis/record.ts index e31f822f594..4b5ebbedc44 100644 --- a/packages/analytics/src/providers/kinesis-firehose/apis/record.ts +++ b/packages/analytics/src/providers/kinesis-firehose/apis/record.ts @@ -1,9 +1,12 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +import { AmplifyContext, ConsoleLogger } from '@aws-amplify/core'; +import { + AnalyticsAction, + resolveCtxArgs, +} from '@aws-amplify/core/internals/utils'; import { fromUtf8 } from '@smithy/util-utf8'; -import { AnalyticsAction } from '@aws-amplify/core/internals/utils'; -import { ConsoleLogger } from '@aws-amplify/core'; import { RecordInput } from '../types'; import { getEventBuffer, resolveConfig } from '../utils'; @@ -33,7 +36,12 @@ const logger = new ConsoleLogger('KinesisFirehose'); * }); * ``` */ -export const record = ({ streamName, data }: RecordInput): void => { +export function record(input: RecordInput): void; +export function record(ctx: AmplifyContext, input: RecordInput): void; +export function record(...args: any[]): void { + const [ctx, input] = resolveCtxArgs(args); + const { streamName, data } = input; + if (!isAnalyticsEnabled()) { logger.debug('Analytics is disabled, event will not be recorded.'); @@ -42,9 +50,9 @@ export const record = ({ streamName, data }: RecordInput): void => { const timestamp = Date.now(); const { region, bufferSize, flushSize, flushInterval, resendLimit } = - resolveConfig(); + resolveConfig(ctx); - resolveCredentials() + resolveCredentials(ctx) .then(({ credentials, identityId }) => { const buffer = getEventBuffer({ region, @@ -68,4 +76,4 @@ export const record = ({ streamName, data }: RecordInput): void => { .catch(e => { logger.warn('Failed to record event.', e); }); -}; +} diff --git a/packages/analytics/src/providers/kinesis-firehose/utils/resolveConfig.ts b/packages/analytics/src/providers/kinesis-firehose/utils/resolveConfig.ts index 4de7639c4b6..8a2cd4441c6 100644 --- a/packages/analytics/src/providers/kinesis-firehose/utils/resolveConfig.ts +++ b/packages/analytics/src/providers/kinesis-firehose/utils/resolveConfig.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 { AnalyticsValidationErrorCode, @@ -10,8 +10,8 @@ import { import { DEFAULT_KINESIS_FIREHOSE_CONFIG } from './constants'; -export const resolveConfig = () => { - const config = Amplify.getConfig().Analytics?.KinesisFirehose; +export const resolveConfig = (ctx: AmplifyContext) => { + const config = ctx.resourcesConfig.Analytics?.KinesisFirehose; const { region, bufferSize = DEFAULT_KINESIS_FIREHOSE_CONFIG.bufferSize, diff --git a/packages/analytics/src/providers/kinesis/apis/flushEvents.ts b/packages/analytics/src/providers/kinesis/apis/flushEvents.ts index 769762c9ac6..0d188109747 100644 --- a/packages/analytics/src/providers/kinesis/apis/flushEvents.ts +++ b/packages/analytics/src/providers/kinesis/apis/flushEvents.ts @@ -1,8 +1,11 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { AnalyticsAction } from '@aws-amplify/core/internals/utils'; -import { ConsoleLogger } from '@aws-amplify/core'; +import { AmplifyContext, ConsoleLogger } from '@aws-amplify/core'; +import { + AnalyticsAction, + resolveCtxArgs, +} from '@aws-amplify/core/internals/utils'; import { resolveConfig } from '../utils/resolveConfig'; import { @@ -20,10 +23,13 @@ const logger = new ConsoleLogger('Kinesis'); * This API will make a best-effort attempt to flush events from the buffer. Events recorded immediately after invoking * this API may not be included in the flush. */ -export const flushEvents = () => { +export function flushEvents(): void; +export function flushEvents(ctx: AmplifyContext): void; +export function flushEvents(...args: any[]): void { + const [ctx] = resolveCtxArgs(args); const { region, flushSize, flushInterval, bufferSize, resendLimit } = - resolveConfig(); - resolveCredentials() + resolveConfig(ctx); + resolveCredentials(ctx) .then(({ credentials, identityId }) => getEventBuffer({ region, @@ -40,4 +46,4 @@ export const flushEvents = () => { .catch(e => { logger.warn('Failed to flush events.', e); }); -}; +} diff --git a/packages/analytics/src/providers/kinesis/apis/record.ts b/packages/analytics/src/providers/kinesis/apis/record.ts index 2e68c90b709..b52256f6524 100644 --- a/packages/analytics/src/providers/kinesis/apis/record.ts +++ b/packages/analytics/src/providers/kinesis/apis/record.ts @@ -1,9 +1,12 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +import { AmplifyContext, ConsoleLogger } from '@aws-amplify/core'; +import { + AnalyticsAction, + resolveCtxArgs, +} from '@aws-amplify/core/internals/utils'; import { fromUtf8 } from '@smithy/util-utf8'; -import { AnalyticsAction } from '@aws-amplify/core/internals/utils'; -import { ConsoleLogger } from '@aws-amplify/core'; import { RecordInput } from '../types'; import { getEventBuffer } from '../utils/getEventBuffer'; @@ -38,11 +41,12 @@ const logger = new ConsoleLogger('Kinesis'); * * @returns void */ -export const record = ({ - streamName, - partitionKey, - data, -}: RecordInput): void => { +export function record(input: RecordInput): void; +export function record(ctx: AmplifyContext, input: RecordInput): void; +export function record(...args: any[]): void { + const [ctx, input] = resolveCtxArgs(args); + const { streamName, partitionKey, data } = input; + if (!isAnalyticsEnabled()) { logger.debug('Analytics is disabled, event will not be recorded.'); @@ -51,9 +55,9 @@ export const record = ({ const timestamp = Date.now(); const { region, bufferSize, flushSize, flushInterval, resendLimit } = - resolveConfig(); + resolveConfig(ctx); - resolveCredentials() + resolveCredentials(ctx) .then(({ credentials, identityId }) => { const buffer = getEventBuffer({ region, @@ -79,4 +83,4 @@ export const record = ({ // An error occured while fetching credentials or persisting the event to the buffer logger.warn('Failed to record event.', e); }); -}; +} diff --git a/packages/analytics/src/providers/kinesis/utils/resolveConfig.ts b/packages/analytics/src/providers/kinesis/utils/resolveConfig.ts index c4407c2cf48..b7c472ac0ea 100644 --- a/packages/analytics/src/providers/kinesis/utils/resolveConfig.ts +++ b/packages/analytics/src/providers/kinesis/utils/resolveConfig.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 { AnalyticsValidationErrorCode, @@ -10,8 +10,8 @@ import { import { DEFAULT_KINESIS_CONFIG } from './constants'; -export const resolveConfig = () => { - const config = Amplify.getConfig().Analytics?.Kinesis; +export const resolveConfig = (ctx: AmplifyContext) => { + const config = ctx.resourcesConfig.Analytics?.Kinesis; const { region, bufferSize = DEFAULT_KINESIS_CONFIG.bufferSize, diff --git a/packages/analytics/src/providers/personalize/apis/flushEvents.ts b/packages/analytics/src/providers/personalize/apis/flushEvents.ts index 650120230a2..963a26f9fea 100644 --- a/packages/analytics/src/providers/personalize/apis/flushEvents.ts +++ b/packages/analytics/src/providers/personalize/apis/flushEvents.ts @@ -1,8 +1,8 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +import { AmplifyContext, ConsoleLogger } from '@aws-amplify/core'; import { AnalyticsAction } from '@aws-amplify/core/internals/utils'; -import { ConsoleLogger } from '@aws-amplify/core'; import { getEventBuffer, resolveConfig } from '../utils'; import { @@ -19,9 +19,9 @@ const logger = new ConsoleLogger('Personalize'); * This API will make a best-effort attempt to flush events from the buffer. Events recorded immediately after invoking * this API may not be included in the flush. */ -export const flushEvents = () => { - const { region, flushSize, bufferSize, flushInterval } = resolveConfig(); - resolveCredentials() +export const flushEvents = (ctx: AmplifyContext) => { + const { region, flushSize, bufferSize, flushInterval } = resolveConfig(ctx); + resolveCredentials(ctx) .then(({ credentials, identityId }) => getEventBuffer({ region, diff --git a/packages/analytics/src/providers/personalize/apis/record.ts b/packages/analytics/src/providers/personalize/apis/record.ts index d44c997a4cd..a1d638f1ff7 100644 --- a/packages/analytics/src/providers/personalize/apis/record.ts +++ b/packages/analytics/src/providers/personalize/apis/record.ts @@ -1,8 +1,8 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +import { AmplifyContext, ConsoleLogger } from '@aws-amplify/core'; import { AnalyticsAction } from '@aws-amplify/core/internals/utils'; -import { ConsoleLogger } from '@aws-amplify/core'; import { RecordInput } from '../types'; import { @@ -50,12 +50,10 @@ const logger = new ConsoleLogger('Personalize'); * * @returns void */ -export const record = ({ - userId, - eventId, - eventType, - properties, -}: RecordInput): void => { +export const record = ( + ctx: AmplifyContext, + { userId, eventId, eventType, properties }: RecordInput, +): void => { if (!isAnalyticsEnabled()) { logger.debug('Analytics is disabled, event will not be recorded.'); @@ -63,8 +61,8 @@ export const record = ({ } const { region, trackingId, bufferSize, flushSize, flushInterval } = - resolveConfig(); - resolveCredentials() + resolveConfig(ctx); + resolveCredentials(ctx) .then(async ({ credentials, identityId }) => { const timestamp = Date.now(); const { sessionId: cachedSessionId, userId: cachedUserId } = diff --git a/packages/analytics/src/providers/personalize/utils/resolveConfig.ts b/packages/analytics/src/providers/personalize/utils/resolveConfig.ts index fac8556b6ca..ce9d0972c1d 100644 --- a/packages/analytics/src/providers/personalize/utils/resolveConfig.ts +++ b/packages/analytics/src/providers/personalize/utils/resolveConfig.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 { AnalyticsValidationErrorCode, @@ -13,8 +13,8 @@ import { PERSONALIZE_FLUSH_SIZE_MAX, } from './constants'; -export const resolveConfig = () => { - const config = Amplify.getConfig().Analytics?.Personalize; +export const resolveConfig = (ctx: AmplifyContext) => { + const config = ctx.resourcesConfig.Analytics?.Personalize; const { region, trackingId, diff --git a/packages/analytics/src/providers/pinpoint/apis/configureAutoTrack.ts b/packages/analytics/src/providers/pinpoint/apis/configureAutoTrack.ts index 125c73df02f..caf49f0016d 100644 --- a/packages/analytics/src/providers/pinpoint/apis/configureAutoTrack.ts +++ b/packages/analytics/src/providers/pinpoint/apis/configureAutoTrack.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 { resolveCtxArgs } from '@aws-amplify/core/internals/utils'; import { UpdateEndpointException } from '@aws-amplify/core/internals/providers/pinpoint'; import { AnalyticsValidationErrorCode } from '../../../errors'; @@ -22,10 +24,11 @@ const configuredTrackers: Partial> = {}; // Callback that will emit an appropriate event to Pinpoint when required by the Tracker const emitTrackingEvent = ( + ctx: AmplifyContext, eventName: string, attributes: TrackerAttributes, ) => { - record({ + record(ctx, { name: eventName, attributes, }); @@ -46,9 +49,19 @@ const emitTrackingEvent = ( * @throws validation: {@link AnalyticsValidationErrorCode} - Thrown when the provided parameters or library * configuration is incorrect. */ -export const configureAutoTrack = (input: ConfigureAutoTrackInput): void => { +export function configureAutoTrack(input: ConfigureAutoTrackInput): void; +export function configureAutoTrack( + ctx: AmplifyContext, + input: ConfigureAutoTrackInput, +): void; +export function configureAutoTrack(...args: any[]): void { + const [ctx, input] = resolveCtxArgs(args); validateTrackerConfiguration(input); // Initialize or update this provider's trackers - updateProviderTrackers(input, emitTrackingEvent, configuredTrackers); -}; + updateProviderTrackers( + input, + emitTrackingEvent.bind(null, ctx), + configuredTrackers, + ); +} diff --git a/packages/analytics/src/providers/pinpoint/apis/flushEvents.ts b/packages/analytics/src/providers/pinpoint/apis/flushEvents.ts index d870599014f..2084c910c35 100644 --- a/packages/analytics/src/providers/pinpoint/apis/flushEvents.ts +++ b/packages/analytics/src/providers/pinpoint/apis/flushEvents.ts @@ -1,9 +1,12 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +import { AmplifyContext, ConsoleLogger } from '@aws-amplify/core'; +import { + AnalyticsAction, + resolveCtxArgs, +} from '@aws-amplify/core/internals/utils'; import { flushEvents as flushEventsCore } from '@aws-amplify/core/internals/providers/pinpoint'; -import { AnalyticsAction } from '@aws-amplify/core/internals/utils'; -import { ConsoleLogger } from '@aws-amplify/core'; import { resolveConfig, resolveCredentials } from '../utils'; import { getAnalyticsUserAgentString } from '../../../utils'; @@ -19,10 +22,13 @@ const logger = new ConsoleLogger('Analytics'); * This API will make a best-effort attempt to flush events from the buffer. Events recorded immediately after invoking * this API may not be included in the flush. */ -export const flushEvents = () => { +export function flushEvents(): void; +export function flushEvents(ctx: AmplifyContext): void; +export function flushEvents(...args: any[]): void { + const [ctx] = resolveCtxArgs(args); const { appId, region, bufferSize, flushSize, flushInterval, resendLimit } = - resolveConfig(); - resolveCredentials() + resolveConfig(ctx); + resolveCredentials(ctx) .then(({ credentials, identityId }) => { flushEventsCore({ appId, @@ -39,4 +45,4 @@ export const flushEvents = () => { .catch(e => { logger.warn('Failed to flush events', e); }); -}; +} diff --git a/packages/analytics/src/providers/pinpoint/apis/identifyUser.ts b/packages/analytics/src/providers/pinpoint/apis/identifyUser.ts index ffc6b6e765e..1d482c98611 100644 --- a/packages/analytics/src/providers/pinpoint/apis/identifyUser.ts +++ b/packages/analytics/src/providers/pinpoint/apis/identifyUser.ts @@ -1,7 +1,11 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { AnalyticsAction } from '@aws-amplify/core/internals/utils'; +import { AmplifyContext } from '@aws-amplify/core'; +import { + AnalyticsAction, + resolveCtxArgs, +} from '@aws-amplify/core/internals/utils'; import { UpdateEndpointException, updateEndpoint, @@ -59,13 +63,16 @@ import { resolveConfig, resolveCredentials } from '../utils'; * } * }); */ -export const identifyUser = async ({ - userId, - userProfile, - options, -}: IdentifyUserInput): Promise => { - const { credentials, identityId } = await resolveCredentials(); - const { appId, region } = resolveConfig(); +export async function identifyUser(input: IdentifyUserInput): Promise; +export async function identifyUser( + ctx: AmplifyContext, + input: IdentifyUserInput, +): Promise; +export async function identifyUser(...args: any[]): Promise { + const [ctx, input] = resolveCtxArgs(args); + const { userId, userProfile, options } = input; + const { credentials, identityId } = await resolveCredentials(ctx); + const { appId, region } = resolveConfig(ctx); const { userAttributes } = options ?? {}; await updateEndpoint({ appId, @@ -78,4 +85,4 @@ export const identifyUser = async ({ userProfile, userAgentValue: getAnalyticsUserAgentString(AnalyticsAction.IdentifyUser), }); -}; +} diff --git a/packages/analytics/src/providers/pinpoint/apis/record.ts b/packages/analytics/src/providers/pinpoint/apis/record.ts index 794c811ce26..210545a30c7 100644 --- a/packages/analytics/src/providers/pinpoint/apis/record.ts +++ b/packages/analytics/src/providers/pinpoint/apis/record.ts @@ -1,10 +1,11 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { ConsoleLogger, Hub } from '@aws-amplify/core'; +import { AmplifyContext, ConsoleLogger, Hub } from '@aws-amplify/core'; import { AMPLIFY_SYMBOL, AnalyticsAction, + resolveCtxArgs, } from '@aws-amplify/core/internals/utils'; import { record as recordCore } from '@aws-amplify/core/internals/providers/pinpoint'; @@ -51,9 +52,12 @@ const logger = new ConsoleLogger('Analytics'); * }) * ``` */ -export const record = (input: RecordInput): void => { +export function record(input: RecordInput): void; +export function record(ctx: AmplifyContext, input: RecordInput): void; +export function record(...args: any[]): void { + const [ctx, input] = resolveCtxArgs(args); const { appId, region, bufferSize, flushSize, flushInterval, resendLimit } = - resolveConfig(); + resolveConfig(ctx); if (!isAnalyticsEnabled()) { logger.debug('Analytics is disabled, event will not be recorded.'); @@ -63,7 +67,7 @@ export const record = (input: RecordInput): void => { assertValidationError(!!input.name, AnalyticsValidationErrorCode.NoEventName); - resolveCredentials() + resolveCredentials(ctx) .then(({ credentials, identityId }) => { Hub.dispatch( 'analytics', @@ -89,4 +93,4 @@ export const record = (input: RecordInput): void => { // An error occured while fetching credentials or persisting the event to the buffer logger.warn('Failed to record event.', e); }); -}; +} diff --git a/packages/analytics/src/providers/pinpoint/utils/resolveConfig.ts b/packages/analytics/src/providers/pinpoint/utils/resolveConfig.ts index f60b7a2fd53..192739d3bd3 100644 --- a/packages/analytics/src/providers/pinpoint/utils/resolveConfig.ts +++ b/packages/analytics/src/providers/pinpoint/utils/resolveConfig.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 { AnalyticsValidationErrorCode, @@ -11,9 +11,9 @@ import { /** * @internal */ -export const resolveConfig = () => { +export const resolveConfig = (ctx: AmplifyContext) => { const { appId, region, bufferSize, flushSize, flushInterval, resendLimit } = - Amplify.getConfig().Analytics?.Pinpoint ?? {}; + ctx.resourcesConfig.Analytics?.Pinpoint ?? {}; assertValidationError(!!appId, AnalyticsValidationErrorCode.NoAppId); assertValidationError(!!region, AnalyticsValidationErrorCode.NoRegion); diff --git a/packages/analytics/src/providers/pinpoint/utils/resolveCredentials.ts b/packages/analytics/src/providers/pinpoint/utils/resolveCredentials.ts index e0bf64c66d3..a58daeddcef 100644 --- a/packages/analytics/src/providers/pinpoint/utils/resolveCredentials.ts +++ b/packages/analytics/src/providers/pinpoint/utils/resolveCredentials.ts @@ -1,7 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { fetchAuthSession } from '@aws-amplify/core'; +import { AmplifyContext } from '@aws-amplify/core'; import { AnalyticsValidationErrorCode, @@ -11,8 +11,8 @@ import { /** * @internal */ -export const resolveCredentials = async () => { - const { credentials, identityId } = await fetchAuthSession(); +export const resolveCredentials = async (ctx: AmplifyContext) => { + const { credentials, identityId } = await ctx.fetchAuthSession(); assertValidationError( !!credentials, AnalyticsValidationErrorCode.NoCredentials, diff --git a/packages/analytics/src/utils/resolveCredentials.ts b/packages/analytics/src/utils/resolveCredentials.ts index a0b70e95879..05b4c45c85f 100644 --- a/packages/analytics/src/utils/resolveCredentials.ts +++ b/packages/analytics/src/utils/resolveCredentials.ts @@ -1,12 +1,12 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { fetchAuthSession } from '@aws-amplify/core'; +import { AmplifyContext } from '@aws-amplify/core'; import { AnalyticsValidationErrorCode, assertValidationError } from '../errors'; -export const resolveCredentials = async () => { - const { credentials, identityId } = await fetchAuthSession(); +export const resolveCredentials = async (ctx: AmplifyContext) => { + const { credentials, identityId } = await ctx.fetchAuthSession(); assertValidationError( !!credentials, AnalyticsValidationErrorCode.NoCredentials, diff --git a/packages/api-graphql/__tests__/AWSAppSyncEventProvider.test.ts b/packages/api-graphql/__tests__/AWSAppSyncEventProvider.test.ts index e75747b8063..1940c14fb2e 100644 --- a/packages/api-graphql/__tests__/AWSAppSyncEventProvider.test.ts +++ b/packages/api-graphql/__tests__/AWSAppSyncEventProvider.test.ts @@ -3,7 +3,7 @@ import { Observable, Observer } from 'rxjs'; import { Reachability } from '@aws-amplify/core/internals/utils'; -import { ConsoleLogger } from '@aws-amplify/core'; +import { AmplifyContext, ConsoleLogger } from '@aws-amplify/core'; import { MESSAGE_TYPES } from '../src/Providers/constants'; import * as constants from '../src/Providers/constants'; @@ -12,6 +12,26 @@ import { ConnectionState as CS } from '../src/types/PubSub'; import { AWSAppSyncEventProvider } from '../src/Providers/AWSAppSyncEventsProvider'; +const mockCtx: AmplifyContext = { + resourcesConfig: { + API: { + GraphQL: { + endpoint: 'https://test.appsync-api.us-east-1.amazonaws.com/graphql', + region: 'us-east-1', + defaultAuthMode: 'apiKey', + apiKey: 'da2-fakeApiId123456', + }, + }, + }, + libraryOptions: {}, + fetchAuthSession: jest.fn().mockResolvedValue({ + tokens: { accessToken: { toString: () => 'test' } }, + credentials: { accessKeyId: 'test', secretAccessKey: 'test' }, + }), + clearCredentials: jest.fn(), + getTokens: jest.fn(), +}; + // Mock all calls to signRequest jest.mock('@aws-amplify/core/internals/aws-client-utils', () => { const original = jest.requireActual( @@ -72,17 +92,17 @@ describe('AppSyncEventProvider', () => { let provider: AWSAppSyncEventProvider; beforeEach(async () => { fakeWebSocketInterface = new FakeWebSocketInterface(); - provider = new AWSAppSyncEventProvider(); + provider = new AWSAppSyncEventProvider(mockCtx); Object.defineProperty(provider, 'socketStatus', { value: constants.SOCKET_STATUS.CLOSED, }); - + jest.spyOn(provider as any, '_getNewWebSocket').mockImplementation(() => { fakeWebSocketInterface.newWebSocket(); return fakeWebSocketInterface.webSocket as WebSocket; }); - }) + }); afterEach(async () => { provider?.close(); @@ -92,33 +112,36 @@ describe('AppSyncEventProvider', () => { }); test('socket status should be READY', async () => { - // Connect to the provider const connectPromise = provider.connect({ appSyncGraphqlEndpoint: 'ws://localhost:8080', authenticationType: 'apiKey', apiKey: 'test-api-key', - region: 'us-east-1' + region: 'us-east-1', }); // Verify the socket status to be CONNECTING await new Promise(resolve => setTimeout(resolve, 1)); - expect((provider as any).socketStatus).toBe(constants.SOCKET_STATUS.CONNECTING); + expect((provider as any).socketStatus).toBe( + constants.SOCKET_STATUS.CONNECTING, + ); // Trigger the websocket open event await fakeWebSocketInterface.readyForUse; await fakeWebSocketInterface.triggerOpen(); - // Initiate handshake + // Initiate handshake await fakeWebSocketInterface.sendDataMessage({ - type: MESSAGE_TYPES.GQL_CONNECTION_ACK + type: MESSAGE_TYPES.GQL_CONNECTION_ACK, }); // Wait for connection to complete await connectPromise; // Verify the socket status - expect((provider as any).socketStatus).toBe(constants.SOCKET_STATUS.READY); + expect((provider as any).socketStatus).toBe( + constants.SOCKET_STATUS.READY, + ); }); }); describe('subscribe()', () => { @@ -150,7 +173,7 @@ describe('AppSyncEventProvider', () => { }); fakeWebSocketInterface = new FakeWebSocketInterface(); - provider = new AWSAppSyncEventProvider(); + provider = new AWSAppSyncEventProvider(mockCtx); // Saving this spy and resetting it by hand causes badness // Saving it causes new websockets to be reachable across past tests that have not fully closed @@ -421,7 +444,7 @@ describe('AppSyncEventProvider', () => { }); fakeWebSocketInterface = new FakeWebSocketInterface(); - provider = new AWSAppSyncEventProvider(); + provider = new AWSAppSyncEventProvider(mockCtx); Object.defineProperty(provider, 'socketStatus', { value: constants.SOCKET_STATUS.READY, }); diff --git a/packages/api-graphql/__tests__/AWSAppSyncRealTimeProvider.test.ts b/packages/api-graphql/__tests__/AWSAppSyncRealTimeProvider.test.ts index d441bfe2d3d..bcadab71cc7 100644 --- a/packages/api-graphql/__tests__/AWSAppSyncRealTimeProvider.test.ts +++ b/packages/api-graphql/__tests__/AWSAppSyncRealTimeProvider.test.ts @@ -1,6 +1,6 @@ import { Observable, Observer } from 'rxjs'; import { Reachability } from '@aws-amplify/core/internals/utils'; -import { ConsoleLogger } from '@aws-amplify/core'; +import { AmplifyContext, ConsoleLogger } from '@aws-amplify/core'; import { MESSAGE_TYPES } from '../src/Providers/constants'; import * as constants from '../src/Providers/constants'; @@ -14,6 +14,26 @@ import { ConnectionState as CS } from '../src/types/PubSub'; import { AWSAppSyncRealTimeProvider } from '../src/Providers/AWSAppSyncRealTimeProvider'; import { isCustomDomain } from '../src/Providers/AWSWebSocketProvider/appsyncUrl'; +const mockCtx: AmplifyContext = { + resourcesConfig: { + API: { + GraphQL: { + endpoint: 'https://test.appsync-api.us-east-1.amazonaws.com/graphql', + region: 'us-east-1', + defaultAuthMode: 'apiKey', + apiKey: 'da2-fakeApiId123456', + }, + }, + }, + libraryOptions: {}, + fetchAuthSession: jest.fn().mockResolvedValue({ + tokens: { accessToken: { toString: () => 'test' } }, + credentials: { accessKeyId: 'test', secretAccessKey: 'test' }, + }), + clearCredentials: jest.fn(), + getTokens: jest.fn(), +}; + // Mock all calls to signRequest jest.mock('@aws-amplify/core/internals/aws-client-utils', () => { const original = jest.requireActual( @@ -88,14 +108,14 @@ describe('AWSAppSyncRealTimeProvider', () => { describe('getProviderName()', () => { test('returns the provider name', () => { - const provider = new AWSAppSyncRealTimeProvider(); + const provider = new AWSAppSyncRealTimeProvider(mockCtx); expect(provider.getProviderName()).toEqual('AWSAppSyncRealTimeProvider'); }); }); describe('subscribe()', () => { test('returns an observable', () => { - const provider = new AWSAppSyncRealTimeProvider(); + const provider = new AWSAppSyncRealTimeProvider(mockCtx); expect(provider.subscribe({})).toBeInstanceOf(Observable); }); @@ -127,7 +147,7 @@ describe('AWSAppSyncRealTimeProvider', () => { }); fakeWebSocketInterface = new FakeWebSocketInterface(); - provider = new AWSAppSyncRealTimeProvider(); + provider = new AWSAppSyncRealTimeProvider(mockCtx); // Saving this spy and resetting it by hand causes badness // Saving it causes new websockets to be reachable across past tests that have not fully closed @@ -148,9 +168,13 @@ describe('AWSAppSyncRealTimeProvider', () => { value: 100, }); // Reduce the keep alive heartbeat to 10ms - Object.defineProperty(constants, 'DEFAULT_KEEP_ALIVE_HEARTBEAT_TIMEOUT', { - value: 10, - }); + Object.defineProperty( + constants, + 'DEFAULT_KEEP_ALIVE_HEARTBEAT_TIMEOUT', + { + value: 10, + }, + ); }); afterEach(async () => { @@ -210,7 +234,7 @@ describe('AWSAppSyncRealTimeProvider', () => { expect.assertions(2); const mockError = jest.fn(); - const provider = new AWSAppSyncRealTimeProvider(); + const provider = new AWSAppSyncRealTimeProvider(mockCtx); await Promise.resolve( provider.subscribe({}).subscribe({ @@ -995,8 +1019,10 @@ describe('AWSAppSyncRealTimeProvider', () => { test('unsubscription message should be sent even if unsubscribe immediately', async () => { expect.assertions(1); - const sendUnsubscriptionMessageSpy = jest - .spyOn(provider as any, '_sendUnsubscriptionMessage'); + const sendUnsubscriptionMessageSpy = jest.spyOn( + provider as any, + '_sendUnsubscriptionMessage', + ); provider .subscribe({ diff --git a/packages/api-graphql/__tests__/GraphQLAPI.test.ts b/packages/api-graphql/__tests__/GraphQLAPI.test.ts index 8f2e953d906..900271864e5 100644 --- a/packages/api-graphql/__tests__/GraphQLAPI.test.ts +++ b/packages/api-graphql/__tests__/GraphQLAPI.test.ts @@ -1,7 +1,6 @@ -import * as raw from '../src'; import { graphql, cancel, isCancelError } from '../src/internals/v6'; import { Amplify } from 'aws-amplify'; -import { Amplify as AmplifyCore } from '@aws-amplify/core'; +import { AmplifyContext } from '@aws-amplify/core'; import * as typedQueries from './fixtures/with-types/queries'; import * as typedSubscriptions from './fixtures/with-types/subscriptions'; import { expectGet } from './utils/expects'; @@ -22,6 +21,27 @@ import { GraphQLApiError } from '../src/utils/errors'; import { NO_ENDPOINT } from '../src/utils/errors/constants'; import { GraphQLError } from 'graphql'; +import { + post as postFn, + cancel as cancelRESTFn, +} from '@aws-amplify/api-rest/internals'; +import { isCancelError as isCancelErrorRESTFn } from '@aws-amplify/api-rest'; + +jest.mock('@aws-amplify/api-rest/internals'); +jest.mock('@aws-amplify/api-rest', () => { + const original = jest.requireActual('@aws-amplify/api-rest'); + return { + ...original, + isCancelError: jest.fn(), + }; +}); + +const mockRestPost = postFn as jest.Mock; +const mockCancelREST = cancelRESTFn as jest.Mock; +const mockIsCancelErrorREST = isCancelErrorRESTFn as jest.MockedFunction< + typeof isCancelErrorRESTFn +>; + const serverManagedFields = { id: 'some-id', owner: 'wirejobviously', @@ -36,50 +56,66 @@ let mockCredentials: any = { secretAccessKey: 'mock-secret-access-key', }; -jest.mock('aws-amplify', () => { - const originalModule = jest.requireActual('aws-amplify'); - - const mockedModule = { - ...originalModule, - Amplify: { - ...originalModule.Amplify, - Auth: { - ...originalModule.Amplify.Auth, - fetchAuthSession: jest.fn(() => { - return { - tokens: { - accessToken: { - toString: () => mockAccessToken, - }, - }, - credentials: mockCredentials, - }; - }), +const mockFetchAuthSession: jest.Mock = jest.fn().mockImplementation(() => + Promise.resolve({ + tokens: { + accessToken: { + toString: () => mockAccessToken, }, }, + credentials: mockCredentials, + }), +); + +let mockCtx: AmplifyContext = { + resourcesConfig: {}, + libraryOptions: {}, + fetchAuthSession: mockFetchAuthSession, + clearCredentials: jest.fn(), + getTokens: jest.fn(), +}; + +/** + * Helper to configure the mock AmplifyContext for tests. + * Replaces configureMockCtx() calls in tests. + */ +function configureMockCtx( + config: Record, + libraryOptions?: Record, +) { + mockCtx = { + ...mockCtx, + resourcesConfig: config as AmplifyContext['resourcesConfig'], + libraryOptions: (libraryOptions || {}) as AmplifyContext['libraryOptions'], }; - return mockedModule; -}); + // Also configure real Amplify for any code that reads global context + Amplify.configure(config); +} const client = { - [__amplify]: Amplify, graphql, cancel, isCancelError, } as unknown as V6Client; - -const mockFetchAuthSession = (Amplify as any).Auth - .fetchAuthSession as jest.Mock; +Object.defineProperty(client, __amplify, { + get() { + return mockCtx; + }, + enumerable: true, +}); describe('API test', () => { afterEach(() => { + mockRestPost.mockReset(); + mockCancelREST.mockReset(); + mockIsCancelErrorREST.mockReset(); jest.clearAllMocks(); jest.restoreAllMocks(); }); describe('graphql test', () => { test('happy-case-query', async () => { - Amplify.configure({ + configureMockCtx({ API: { GraphQL: { defaultAuthMode: 'apiKey', @@ -107,13 +143,11 @@ describe('API test', () => { }, }; - const spy = jest - .spyOn((raw.GraphQLAPI as any)._api, 'post') - .mockReturnValue({ - body: { - json: () => graphqlResponse, - }, - }); + const spy = mockRestPost.mockReturnValue({ + body: { + json: () => graphqlResponse, + }, + }); const result: GraphQLResult = await client.graphql({ query: typedQueries.getThread, @@ -131,7 +165,7 @@ describe('API test', () => { test('auth-error-case', async () => { expect.assertions(1); - Amplify.configure({ + configureMockCtx({ API: { GraphQL: { defaultAuthMode: 'apiKey', @@ -168,17 +202,15 @@ describe('API test', () => { }, }; - const spy = jest - .spyOn((raw.GraphQLAPI as any)._api, 'post') - .mockImplementation(() => { - return { - body: { - json: () => ({ - errors: [err], - }), - }, - }; - }); + const spy = mockRestPost.mockImplementation(() => { + return { + body: { + json: () => ({ + errors: [err], + }), + }, + }; + }); try { const result: GraphQLResult = await client.graphql({ @@ -200,7 +232,7 @@ describe('API test', () => { }); test('cancel-graphql-query', async () => { - Amplify.configure({ + configureMockCtx({ API: { GraphQL: { defaultAuthMode: 'apiKey', @@ -211,16 +243,14 @@ describe('API test', () => { }, }); - jest - .spyOn((raw.GraphQLAPI as any)._api, 'cancelREST') - .mockReturnValue(true); + mockCancelREST.mockReturnValue(true); const request = Promise.resolve(); expect(client.cancel(request)).toBe(true); }); test('cancel-graphql-query', async () => { - Amplify.configure({ + configureMockCtx({ API: { GraphQL: { defaultAuthMode: 'apiKey', @@ -231,13 +261,9 @@ describe('API test', () => { }, }); - jest - .spyOn((raw.GraphQLAPI as any)._api, 'cancelREST') - .mockReturnValue(true); + mockCancelREST.mockReturnValue(true); - jest - .spyOn((raw.GraphQLAPI as any)._api, 'isCancelErrorREST') - .mockReturnValue(true); + mockIsCancelErrorREST.mockReturnValue(true); let promiseToCancel; let isCancelErrorResult; @@ -256,7 +282,7 @@ describe('API test', () => { }); test('happy-case-query-oidc', async () => { - Amplify.configure({ + configureMockCtx({ API: { GraphQL: { defaultAuthMode: 'oidc', @@ -283,13 +309,11 @@ describe('API test', () => { }, }; - const spy = jest - .spyOn((raw.GraphQLAPI as any)._api, 'post') - .mockReturnValue({ - body: { - json: () => graphqlResponse, - }, - }); + const spy = mockRestPost.mockReturnValue({ + body: { + json: () => graphqlResponse, + }, + }); const result: GraphQLResult = await client.graphql({ query: typedQueries.getThread, @@ -305,9 +329,8 @@ describe('API test', () => { expect(spy).toHaveBeenCalledWith( expect.objectContaining({ - Auth: expect.any(Object), - configure: expect.any(Function), - getConfig: expect.any(Function), + resourcesConfig: expect.any(Object), + fetchAuthSession: expect.any(Function), }), { abortController: expect.any(AbortController), @@ -326,7 +349,7 @@ describe('API test', () => { }); test('happy-case-query-oidc with auth storage federated token', async () => { - Amplify.configure({ + configureMockCtx({ API: { GraphQL: { defaultAuthMode: 'oidc', @@ -353,13 +376,11 @@ describe('API test', () => { }, }; - const spy = jest - .spyOn((raw.GraphQLAPI as any)._api, 'post') - .mockReturnValue({ - body: { - json: () => graphqlResponse, - }, - }); + const spy = mockRestPost.mockReturnValue({ + body: { + json: () => graphqlResponse, + }, + }); const result: GraphQLResult = await client.graphql({ query: typedQueries.getThread, @@ -374,9 +395,8 @@ describe('API test', () => { expect(thread).toEqual(graphqlResponse.data.getThread); expect(spy).toHaveBeenCalledWith( expect.objectContaining({ - Auth: expect.any(Object), - configure: expect.any(Function), - getConfig: expect.any(Function), + resourcesConfig: expect.any(Object), + fetchAuthSession: expect.any(Function), }), { abortController: expect.any(AbortController), @@ -395,7 +415,7 @@ describe('API test', () => { }); test('happy case query with AWS_LAMBDA', async () => { - Amplify.configure({ + configureMockCtx({ API: { GraphQL: { defaultAuthMode: 'lambda', @@ -422,13 +442,11 @@ describe('API test', () => { }, }; - const spy = jest - .spyOn((raw.GraphQLAPI as any)._api, 'post') - .mockReturnValue({ - body: { - json: () => graphqlResponse, - }, - }); + const spy = mockRestPost.mockReturnValue({ + body: { + json: () => graphqlResponse, + }, + }); const result: GraphQLResult = await client.graphql({ query: typedQueries.getThread, @@ -444,9 +462,8 @@ describe('API test', () => { expect(thread).toEqual(graphqlResponse.data.getThread); expect(spy).toHaveBeenCalledWith( expect.objectContaining({ - Auth: expect.any(Object), - configure: expect.any(Function), - getConfig: expect.any(Function), + resourcesConfig: expect.any(Object), + fetchAuthSession: expect.any(Function), }), { abortController: expect.any(AbortController), @@ -465,7 +482,7 @@ describe('API test', () => { }); test('additional headers with AWS_LAMBDA', async () => { - Amplify.configure({ + configureMockCtx({ API: { GraphQL: { defaultAuthMode: 'lambda', @@ -492,13 +509,11 @@ describe('API test', () => { }, }; - const spy = jest - .spyOn((raw.GraphQLAPI as any)._api, 'post') - .mockReturnValue({ - body: { - json: () => graphqlResponse, - }, - }); + const spy = mockRestPost.mockReturnValue({ + body: { + json: () => graphqlResponse, + }, + }); const result: GraphQLResult = await client.graphql( { @@ -518,9 +533,8 @@ describe('API test', () => { expect(thread).toEqual(graphqlResponse.data.getThread); expect(spy).toHaveBeenCalledWith( expect.objectContaining({ - Auth: expect.any(Object), - configure: expect.any(Function), - getConfig: expect.any(Function), + resourcesConfig: expect.any(Object), + fetchAuthSession: expect.any(Function), }), { abortController: expect.any(AbortController), @@ -539,7 +553,7 @@ describe('API test', () => { }); test('multi-auth default case AWS_IAM, using API_KEY as auth mode', async () => { - Amplify.configure({ + configureMockCtx({ API: { GraphQL: { defaultAuthMode: 'iam', @@ -567,13 +581,11 @@ describe('API test', () => { }, }; - const spy = jest - .spyOn((raw.GraphQLAPI as any)._api, 'post') - .mockReturnValue({ - body: { - json: () => graphqlResponse, - }, - }); + const spy = mockRestPost.mockReturnValue({ + body: { + json: () => graphqlResponse, + }, + }); const result: GraphQLResult = await client.graphql({ query: typedQueries.getThread, @@ -588,9 +600,8 @@ describe('API test', () => { expect(thread).toEqual(graphqlResponse.data.getThread); expect(spy).toHaveBeenCalledWith( expect.objectContaining({ - Auth: expect.any(Object), - configure: expect.any(Function), - getConfig: expect.any(Function), + resourcesConfig: expect.any(Object), + fetchAuthSession: expect.any(Function), }), { abortController: expect.any(AbortController), @@ -604,7 +615,7 @@ describe('API test', () => { }); test('multi-auth default case api-key, using AWS_IAM as auth mode', async () => { - Amplify.configure({ + configureMockCtx({ API: { GraphQL: { defaultAuthMode: 'apiKey', @@ -632,13 +643,11 @@ describe('API test', () => { }, }; - const spy = jest - .spyOn((raw.GraphQLAPI as any)._api, 'post') - .mockReturnValue({ - body: { - json: () => graphqlResponse, - }, - }); + const spy = mockRestPost.mockReturnValue({ + body: { + json: () => graphqlResponse, + }, + }); const result: GraphQLResult = await client.graphql({ query: typedQueries.getThread, @@ -654,9 +663,8 @@ describe('API test', () => { expect(spy).toHaveBeenCalledWith( expect.objectContaining({ - Auth: expect.any(Object), - configure: expect.any(Function), - getConfig: expect.any(Function), + resourcesConfig: expect.any(Object), + fetchAuthSession: expect.any(Function), }), { abortController: expect.any(AbortController), @@ -673,7 +681,7 @@ describe('API test', () => { }); test('multi-auth default case api-key, using identityPool as auth mode', async () => { - Amplify.configure({ + configureMockCtx({ API: { GraphQL: { defaultAuthMode: 'apiKey', @@ -701,13 +709,11 @@ describe('API test', () => { }, }; - const spy = jest - .spyOn((raw.GraphQLAPI as any)._api, 'post') - .mockReturnValue({ - body: { - json: () => graphqlResponse, - }, - }); + const spy = mockRestPost.mockReturnValue({ + body: { + json: () => graphqlResponse, + }, + }); const result: GraphQLResult = await client.graphql({ query: typedQueries.getThread, @@ -723,9 +729,8 @@ describe('API test', () => { expect(spy).toHaveBeenCalledWith( expect.objectContaining({ - Auth: expect.any(Object), - configure: expect.any(Function), - getConfig: expect.any(Function), + resourcesConfig: expect.any(Object), + fetchAuthSession: expect.any(Function), }), { abortController: expect.any(AbortController), @@ -742,7 +747,7 @@ describe('API test', () => { }); test('multi-auth default case api-key, using AWS_LAMBDA as auth mode', async () => { - Amplify.configure({ + configureMockCtx({ API: { GraphQL: { defaultAuthMode: 'apiKey', @@ -770,13 +775,11 @@ describe('API test', () => { }, }; - const spy = jest - .spyOn((raw.GraphQLAPI as any)._api, 'post') - .mockReturnValue({ - body: { - json: () => graphqlResponse, - }, - }); + const spy = mockRestPost.mockReturnValue({ + body: { + json: () => graphqlResponse, + }, + }); const result: GraphQLResult = await client.graphql({ query: typedQueries.getThread, @@ -793,9 +796,8 @@ describe('API test', () => { expect(spy).toHaveBeenCalledWith( expect.objectContaining({ - Auth: expect.any(Object), - configure: expect.any(Function), - getConfig: expect.any(Function), + resourcesConfig: expect.any(Object), + fetchAuthSession: expect.any(Function), }), { abortController: expect.any(AbortController), @@ -818,7 +820,7 @@ describe('API test', () => { new Error('mock failing fetchAuthSession() call here.'), ); - Amplify.configure({ + configureMockCtx({ API: { GraphQL: { defaultAuthMode: 'apiKey', @@ -846,7 +848,7 @@ describe('API test', () => { }, }; - jest.spyOn((raw.GraphQLAPI as any)._api, 'post').mockReturnValue({ + mockRestPost.mockReturnValue({ body: { json: () => graphqlResponse, }, @@ -865,7 +867,7 @@ describe('API test', () => { const prevMockAccessToken = mockAccessToken; mockAccessToken = null; - Amplify.configure({ + configureMockCtx({ API: { GraphQL: { defaultAuthMode: 'apiKey', @@ -891,13 +893,11 @@ describe('API test', () => { }, }; - const spy = jest - .spyOn((raw.GraphQLAPI as any)._api, 'post') - .mockReturnValue({ - body: { - json: () => graphqlResponse, - }, - }); + const spy = mockRestPost.mockReturnValue({ + body: { + json: () => graphqlResponse, + }, + }); const graphqlVariables = { id: 'some-id' }; @@ -914,7 +914,7 @@ describe('API test', () => { }); it('AWS_LAMBDA as auth mode, but no auth token specified', async () => { - Amplify.configure({ + configureMockCtx({ API: { GraphQL: { defaultAuthMode: 'lambda', @@ -936,7 +936,7 @@ describe('API test', () => { }); test('multi-auth using API_KEY as auth mode, but no api-key configured', async () => { - Amplify.configure({ + configureMockCtx({ API: { GraphQL: { defaultAuthMode: 'iam', @@ -961,7 +961,7 @@ describe('API test', () => { const prevMockCredentials = mockCredentials; mockCredentials = undefined; - Amplify.configure({ + configureMockCtx({ API: { GraphQL: { defaultAuthMode: 'apiKey', @@ -987,7 +987,7 @@ describe('API test', () => { }); test('multi-auth default case api-key, using CUP as auth mode', async () => { - Amplify.configure({ + configureMockCtx({ API: { GraphQL: { defaultAuthMode: 'apiKey', @@ -1015,13 +1015,11 @@ describe('API test', () => { }, }; - const spy = jest - .spyOn((raw.GraphQLAPI as any)._api, 'post') - .mockReturnValue({ - body: { - json: () => graphqlResponse, - }, - }); + const spy = mockRestPost.mockReturnValue({ + body: { + json: () => graphqlResponse, + }, + }); const result: GraphQLResult = await client.graphql({ query: typedQueries.getThread, @@ -1036,9 +1034,8 @@ describe('API test', () => { expect(thread).toEqual(graphqlResponse.data.getThread); expect(spy).toHaveBeenCalledWith( expect.objectContaining({ - Auth: expect.any(Object), - configure: expect.any(Function), - getConfig: expect.any(Function), + resourcesConfig: expect.any(Object), + fetchAuthSession: expect.any(Function), }), { abortController: expect.any(AbortController), @@ -1212,12 +1209,17 @@ describe('API test', () => { * (in this test case, headers passed via configuration options). */ const optionsClient = { - [__amplify]: AmplifyCore, graphql, cancel, } as unknown as V6Client; + Object.defineProperty(optionsClient, __amplify, { + get() { + return mockCtx; + }, + enumerable: true, + }); - Amplify.configure( + configureMockCtx( { API: { GraphQL: { @@ -1258,13 +1260,11 @@ describe('API test', () => { }, }; - const spy = jest - .spyOn((raw.GraphQLAPI as any)._api, 'post') - .mockReturnValue({ - body: { - json: () => graphqlResponse, - }, - }); + const spy = mockRestPost.mockReturnValue({ + body: { + json: () => graphqlResponse, + }, + }); const additionalHeaders = { someAdditionalHeader: 'foo', @@ -1286,9 +1286,8 @@ describe('API test', () => { expect(thread).toEqual(graphqlResponse.data.getThread); expect(spy).toHaveBeenCalledWith( expect.objectContaining({ - Auth: expect.any(Object), - configure: expect.any(Function), - getConfig: expect.any(Function), + resourcesConfig: expect.any(Object), + fetchAuthSession: expect.any(Function), }), { abortController: expect.any(AbortController), @@ -1312,12 +1311,17 @@ describe('API test', () => { * (in this test case, headers passed via configuration options). */ const optionsClient = { - [__amplify]: AmplifyCore, graphql, cancel, } as unknown as V6Client; + Object.defineProperty(optionsClient, __amplify, { + get() { + return mockCtx; + }, + enumerable: true, + }); - Amplify.configure( + configureMockCtx( { API: { GraphQL: { @@ -1354,13 +1358,11 @@ describe('API test', () => { }, }; - const spy = jest - .spyOn((raw.GraphQLAPI as any)._api, 'post') - .mockReturnValue({ - body: { - json: () => graphqlResponse, - }, - }); + const spy = mockRestPost.mockReturnValue({ + body: { + json: () => graphqlResponse, + }, + }); const result: GraphQLResult = await optionsClient.graphql( { @@ -1377,9 +1379,8 @@ describe('API test', () => { expect(thread).toEqual(graphqlResponse.data.getThread); expect(spy).toHaveBeenCalledWith( expect.objectContaining({ - Auth: expect.any(Object), - configure: expect.any(Function), - getConfig: expect.any(Function), + resourcesConfig: expect.any(Object), + fetchAuthSession: expect.any(Function), }), { abortController: expect.any(AbortController), @@ -1396,7 +1397,7 @@ describe('API test', () => { test('throws a GraphQLResult with NO_ENDPOINT error when endpoint is not configured', () => { const expectedGraphQLApiError = new GraphQLApiError(NO_ENDPOINT); - Amplify.configure({ + configureMockCtx({ API: { GraphQL: { defaultAuthMode: 'apiKey', @@ -1432,11 +1433,9 @@ describe('API test', () => { test('throws a GraphQLResult with NetworkError when the `post()` API throws for network error', () => { const postAPIThrownError = new Error('Network error'); - jest - .spyOn((raw.GraphQLAPI as any)._api, 'post') - .mockRejectedValueOnce(postAPIThrownError); + mockRestPost.mockRejectedValueOnce(postAPIThrownError); - Amplify.configure({ + configureMockCtx({ API: { GraphQL: { defaultAuthMode: 'userPool', @@ -1470,7 +1469,7 @@ describe('API test', () => { }); test('identityPool alias with query', async () => { - Amplify.configure({ + configureMockCtx({ API: { GraphQL: { defaultAuthMode: 'apiKey', @@ -1491,13 +1490,11 @@ describe('API test', () => { const spy = jest.spyOn(graphqlAuth, 'headerBasedAuth'); - const spy2 = jest - .spyOn((raw.GraphQLAPI as any)._api, 'post') - .mockReturnValue({ - body: { - json: () => graphqlResponse, - }, - }); + const spy2 = mockRestPost.mockReturnValue({ + body: { + json: () => graphqlResponse, + }, + }); await client.graphql({ query: typedQueries.getThread, @@ -1507,9 +1504,8 @@ describe('API test', () => { expect(spy).toHaveBeenCalledWith( expect.objectContaining({ - Auth: expect.any(Object), - configure: expect.any(Function), - getConfig: expect.any(Function), + resourcesConfig: expect.any(Object), + fetchAuthSession: expect.any(Function), }), 'iam', 'FAKE-KEY', @@ -1518,7 +1514,7 @@ describe('API test', () => { }); test('identityPool alias with subscription', async () => { - Amplify.configure({ + configureMockCtx({ API: { GraphQL: { defaultAuthMode: 'apiKey', @@ -1537,13 +1533,11 @@ describe('API test', () => { const spy = jest.spyOn(AWSAppSyncRealTimeProvider.prototype, 'subscribe'); - const _spy2 = jest - .spyOn((raw.GraphQLAPI as any)._api, 'post') - .mockReturnValue({ - body: { - json: () => graphqlResponse, - }, - }); + const _spy2 = mockRestPost.mockReturnValue({ + body: { + json: () => graphqlResponse, + }, + }); await client.graphql({ query: typedSubscriptions.onCreateThread, @@ -1560,7 +1554,7 @@ describe('API test', () => { }); test('request level custom headers are applied to query string', async () => { - Amplify.configure({ + configureMockCtx({ API: { GraphQL: { defaultAuthMode: 'lambda', @@ -1615,7 +1609,7 @@ describe('API test', () => { expect(subscribeOptions).toBe(resolvedUrl); }); test('graphql method handles INTERNAL_USER_AGENT_OVERRIDE correctly', async () => { - Amplify.configure({ + configureMockCtx({ API: { GraphQL: { defaultAuthMode: 'apiKey', @@ -1631,7 +1625,7 @@ describe('API test', () => { json: () => ({ data: { test: 'result' } }), }, }); - (raw.GraphQLAPI as any)._api.post = mockPost; + mockRestPost.mockImplementation(mockPost); const graphqlOptions = { query: 'query TestQuery { test }', diff --git a/packages/api-graphql/__tests__/events.test.ts b/packages/api-graphql/__tests__/events.test.ts index c5e47481d00..fa4bf54ca83 100644 --- a/packages/api-graphql/__tests__/events.test.ts +++ b/packages/api-graphql/__tests__/events.test.ts @@ -1,5 +1,5 @@ -import { Amplify } from '@aws-amplify/core'; -import { AppSyncEventProvider } from '../src/Providers/AWSAppSyncEventsProvider'; +import { AmplifyContext } from '@aws-amplify/core'; +import { AWSAppSyncEventProvider } from '../src/Providers/AWSAppSyncEventsProvider'; import { events } from '../src/'; import { appsyncRequest } from '../src/internals/events/appsyncRequest'; @@ -10,18 +10,24 @@ const abortController = new AbortController(); var mockSubscribeObservable: any; +const mockEventProvider = { + connect: jest.fn(), + subscribe: jest.fn(() => ({ + subscribe: jest.fn(), + })), + publish: jest.fn(), + close: jest.fn(), + closeIfNoActiveSubscription: jest.fn(), +}; + jest.mock('../src/Providers/AWSAppSyncEventsProvider', () => { mockSubscribeObservable = jest.fn(() => ({ subscribe: jest.fn(), })); return { - AppSyncEventProvider: { - connect: jest.fn(), - subscribe: jest.fn(mockSubscribeObservable), - publish: jest.fn(), - close: jest.fn(), - }, + AWSAppSyncEventProvider: jest.fn(), + createAppSyncEventProvider: jest.fn(() => mockEventProvider), }; }); @@ -39,6 +45,31 @@ jest.mock('../src/internals/events/appsyncRequest', () => { * so we're just sanity checking that the expected auth mode is passed to the provider in this test file. */ +const mockCtx: AmplifyContext = { + resourcesConfig: { + API: { + Events: { + endpoint: 'https://not-a-real.ddpg-api.us-west-2.amazonaws.com/event', + region: 'us-west-2', + defaultAuthMode: 'apiKey', + apiKey: 'da2-abcxyz321', + }, + }, + }, + libraryOptions: {}, + fetchAuthSession: jest.fn().mockResolvedValue({}), + clearCredentials: jest.fn(), + getTokens: jest.fn(), +}; + +const emptyCtx: AmplifyContext = { + resourcesConfig: {}, + libraryOptions: {}, + fetchAuthSession: jest.fn(), + clearCredentials: jest.fn(), + getTokens: jest.fn(), +}; + describe('Events client', () => { afterAll(() => { jest.resetAllMocks(); @@ -47,60 +78,41 @@ describe('Events client', () => { describe('config', () => { test('no configure()', async () => { - await expect(events.connect('/')).rejects.toThrow( + await expect(events.connect(emptyCtx, '/')).rejects.toThrow( 'Amplify configuration is missing. Have you called Amplify.configure()?', ); }); test('manual (resource config)', async () => { - Amplify.configure({ - API: { - Events: { - endpoint: - 'https://not-a-real.ddpg-api.us-west-2.amazonaws.com/event', - region: 'us-west-2', - defaultAuthMode: 'apiKey', - apiKey: 'da2-abcxyz321', - }, - }, - }); - - await expect(events.connect('/')).resolves.not.toThrow(); + await expect(events.connect(mockCtx, '/')).resolves.not.toThrow(); }); test('outputs (amplify-backend config)', async () => { - Amplify.configure({ - custom: { - events: { - url: 'https://not-a-real.ddpg-api.us-west-2.amazonaws.com/event', - aws_region: 'us-west-2', - default_authorization_type: 'API_KEY', - api_key: 'da2-abcxyz321', + const outputsCtx: AmplifyContext = { + ...mockCtx, + resourcesConfig: { + API: { + Events: { + endpoint: + 'https://not-a-real.ddpg-api.us-west-2.amazonaws.com/event', + region: 'us-west-2', + defaultAuthMode: 'apiKey', + apiKey: 'da2-abcxyz321', + }, }, }, - version: '1.2', - }); + }; - await expect(events.connect('/')).resolves.not.toThrow(); + await expect(events.connect(outputsCtx, '/')).resolves.not.toThrow(); }); }); describe('client', () => { - beforeEach(() => { - Amplify.configure({ - API: { - Events: { - endpoint: - 'https://not-a-real.ddpg-api.us-west-2.amazonaws.com/event', - region: 'us-west-2', - defaultAuthMode: 'apiKey', - apiKey: 'da2-abcxyz321', - }, - }, - }); - }); - - const authModeConfigs: { authMode: GraphQLAuthMode, apiKey?: string, authToken?: string }[] = [ + const authModeConfigs: { + authMode: GraphQLAuthMode; + apiKey?: string; + authToken?: string; + }[] = [ { authMode: 'apiKey', apiKey: 'testAPIKey' }, { authMode: 'userPool', authToken: 'userPoolToken' }, { authMode: 'oidc', authToken: 'oidcToken' }, @@ -111,28 +123,22 @@ describe('Events client', () => { describe('channel', () => { test('happy connect', async () => { - const channel = await events.connect('/'); + const channel = await events.connect(mockCtx, '/'); expect(channel.subscribe).toBeInstanceOf(Function); expect(channel.close).toBeInstanceOf(Function); }); describe('auth modes', () => { - let mockProvider: typeof AppSyncEventProvider; - - beforeEach(() => { - mockProvider = AppSyncEventProvider; - }); - for (const authConfig of authModeConfigs) { - const {authMode: authenticationType, ...config} = authConfig + const { authMode: authenticationType, ...config } = authConfig; test(`connect auth override: ${authConfig.authMode}`, async () => { - const channel = await events.connect('/', authConfig); + await events.connect(mockCtx, '/', authConfig); - expect(mockProvider.connect).toHaveBeenCalledWith( + expect(mockEventProvider.connect).toHaveBeenCalledWith( expect.objectContaining({ authenticationType, - ...config + ...config, }), ); }); @@ -142,7 +148,7 @@ describe('Events client', () => { describe('subscribe', () => { test('happy subscribe', async () => { - const channel = await events.connect('/'); + const channel = await events.connect(mockCtx, '/'); channel.subscribe({ next: data => void data, @@ -151,29 +157,23 @@ describe('Events client', () => { }); describe('auth modes', () => { - let mockProvider: typeof AppSyncEventProvider; - - beforeEach(() => { - mockProvider = AppSyncEventProvider; - }); - for (const authConfig of authModeConfigs) { - const {authMode: authenticationType, ...config} = authConfig + const { authMode: authenticationType, ...config } = authConfig; test(`subscription auth override: ${authConfig.authMode}`, async () => { - const channel = await events.connect('/'); - channel.subscribe( + const channel = await events.connect(mockCtx, '/'); + channel.subscribe( { next: data => void data, error: error => void error, }, - authConfig - ) + authConfig, + ); - expect(mockProvider.subscribe).toHaveBeenCalledWith( + expect(mockEventProvider.subscribe).toHaveBeenCalledWith( expect.objectContaining({ authenticationType, - ...config + ...config, }), ); }); @@ -183,13 +183,13 @@ describe('Events client', () => { describe('publish', () => { test('happy publish', async () => { - const channel = await events.connect('/'); + const channel = await events.connect(mockCtx, '/'); channel.publish({ some: 'data' }); }); test('publish() becomes invalid after .close() is called', async () => { - const channel = await events.connect('/'); + const channel = await events.connect(mockCtx, '/'); channel.close(); await expect(channel.publish({ some: 'data' })).rejects.toThrow( 'Channel is closed', @@ -197,26 +197,17 @@ describe('Events client', () => { }); describe('auth modes', () => { - let mockProvider: typeof AppSyncEventProvider; - - beforeEach(() => { - mockProvider = AppSyncEventProvider; - }); - for (const authConfig of authModeConfigs) { - const {authMode: authenticationType, ...config} = authConfig + const { authMode: authenticationType, ...config } = authConfig; test(`publish auth override: ${authConfig.authMode}`, async () => { - const channel = await events.connect('/'); - channel.publish( - "Test message", - authConfig - ) + const channel = await events.connect(mockCtx, '/'); + channel.publish('Test message', authConfig); - expect(mockProvider.publish).toHaveBeenCalledWith( + expect(mockEventProvider.publish).toHaveBeenCalledWith( expect.objectContaining({ authenticationType, - ...config + ...config, }), ); }); @@ -232,10 +223,10 @@ describe('Events client', () => { }); test('happy post', async () => { - await events.post('/', { test: 'data' }); + await events.post(mockCtx, '/', { test: 'data' }); expect(mockReq).toHaveBeenCalledWith( - Amplify, + mockCtx, expect.objectContaining({ query: '/', variables: ['{"test":"data"}'], @@ -246,18 +237,18 @@ describe('Events client', () => { }); for (const authConfig of authModeConfigs) { - const {authMode: authenticationType, ...config} = authConfig + const { authMode: authenticationType, ...config } = authConfig; test(`auth override: ${authenticationType}`, async () => { - await events.post('/', { test: 'data' }, authConfig); + await events.post(mockCtx, '/', { test: 'data' }, authConfig); expect(mockReq).toHaveBeenCalledWith( - Amplify, + mockCtx, expect.objectContaining({ query: '/', variables: ['{"test":"data"}'], authenticationType, - ...config + ...config, }), {}, abortController, diff --git a/packages/api-graphql/__tests__/internals/generateClient.test.ts b/packages/api-graphql/__tests__/internals/generateClient.test.ts index 8708cae9991..df9337365ca 100644 --- a/packages/api-graphql/__tests__/internals/generateClient.test.ts +++ b/packages/api-graphql/__tests__/internals/generateClient.test.ts @@ -1,5 +1,4 @@ -import * as raw from '../../src'; -import { Amplify, AmplifyClassV6 } from '@aws-amplify/core'; +import { Amplify, AmplifyContext, getActiveContext } from '@aws-amplify/core'; import { generateClient } from '../../src/internals'; import configFixture from '../fixtures/modeled/amplifyconfiguration'; import { Schema } from '../fixtures/modeled/schema'; @@ -12,6 +11,10 @@ import { mockApiResponse, } from '../utils/index'; import { AWSAppSyncRealTimeProvider } from '../../src/Providers/AWSAppSyncRealTimeProvider'; +import { post as postFn } from '@aws-amplify/api-rest/internals'; + +jest.mock('@aws-amplify/api-rest/internals'); +const mockRestPost = postFn as jest.Mock; const serverManagedFields = { id: 'some-id', @@ -31,14 +34,14 @@ describe('generateClient', () => { it('generates `models` property when Amplify.getConfig() returns valid GraphQL provider config', () => { Amplify.configure(configFixture); // clear the resource config - const client = generateClient({ amplify: Amplify }); + const client = generateClient({ amplify: getActiveContext() }); expect(Object.keys(client.models)).toEqual(expectedModelsProperties); }); it('generates `models` property when Amplify.configure() is called later with a valid GraphQL provider config', async () => { Amplify.configure({}); // clear the ResourceConfig mimic Amplify.configure has not been called - const client = generateClient({ amplify: Amplify }); + const client = generateClient({ amplify: getActiveContext() }); expect(Object.keys(client.models)).toHaveLength(0); @@ -49,7 +52,7 @@ describe('generateClient', () => { it('generates `models` property throwing error when there is no valid GraphQL provider config can be resolved', () => { Amplify.configure({}); // clear the ResourceConfig mimic Amplify.configure has not been called - const client = generateClient({ amplify: Amplify }); + const client = generateClient({ amplify: getActiveContext() }); expect(() => { client.models.Todo.create({ name: 'todo' }); @@ -63,42 +66,40 @@ describe('generateClient', () => { // TS lies: We don't care what `amplify` is or does. We want want to make sure // it shows up in the client in the right spot. - const fetchAuthSession = jest.fn().mockReturnValue({}); - const getConfig = jest.fn().mockReturnValue({ - API: { - GraphQL: { - apiKey: 'apikey', - customEndpoint: undefined, - customEndpointRegion: undefined, - defaultAuthMode: 'apiKey', - endpoint: 'https://0.0.0.0/graphql', - region: 'us-east-1', + const fetchAuthSession = jest.fn().mockResolvedValue({}); + + const amplify: AmplifyContext = { + resourcesConfig: { + API: { + GraphQL: { + apiKey: 'apikey', + customEndpoint: undefined, + customEndpointRegion: undefined, + defaultAuthMode: 'apiKey', + endpoint: 'https://0.0.0.0/graphql', + region: 'us-east-1', + }, }, }, - }); + libraryOptions: {}, + fetchAuthSession, + clearCredentials: jest.fn(), + getTokens: jest.fn(), + }; - const apiSpy = jest - .spyOn((raw.GraphQLAPI as any)._api, 'post') - .mockReturnValue({ - body: { - json: () => ({ - data: { - getWidget: { - __typename: 'Widget', - ...serverManagedFields, - someField: 'some value', - }, + const apiSpy = mockRestPost.mockReturnValue({ + body: { + json: () => ({ + data: { + getWidget: { + __typename: 'Widget', + ...serverManagedFields, + someField: 'some value', }, - }), - }, - }); - - const amplify = { - Auth: { - fetchAuthSession, + }, + }), }, - getConfig, - } as unknown as AmplifyClassV6; + }); const client = generateClient({ amplify }); const result = (await client.graphql({ @@ -112,7 +113,6 @@ describe('generateClient', () => { // shouldn't fetch auth for apiKey auth expect(fetchAuthSession).not.toHaveBeenCalled(); - expect(getConfig).toHaveBeenCalled(); expect(apiSpy).toHaveBeenCalled(); }); @@ -120,42 +120,40 @@ describe('generateClient', () => { // TS lies: We don't care what `amplify` is or does. We want want to make sure // it shows up in the client in the right spot. - const fetchAuthSession = jest.fn().mockReturnValue({ credentials: {} }); - const getConfig = jest.fn().mockReturnValue({ - API: { - GraphQL: { - apiKey: undefined, - customEndpoint: undefined, - customEndpointRegion: undefined, - defaultAuthMode: 'iam', - endpoint: 'https://0.0.0.0/graphql', - region: 'us-east-1', + const fetchAuthSession = jest.fn().mockResolvedValue({ credentials: {} }); + + const amplify: AmplifyContext = { + resourcesConfig: { + API: { + GraphQL: { + apiKey: undefined, + customEndpoint: undefined, + customEndpointRegion: undefined, + defaultAuthMode: 'iam', + endpoint: 'https://0.0.0.0/graphql', + region: 'us-east-1', + }, }, }, - }); + libraryOptions: {}, + fetchAuthSession, + clearCredentials: jest.fn(), + getTokens: jest.fn(), + }; - const apiSpy = jest - .spyOn((raw.GraphQLAPI as any)._api, 'post') - .mockReturnValue({ - body: { - json: () => ({ - data: { - getWidget: { - __typename: 'Widget', - ...serverManagedFields, - someField: 'some value', - }, + const apiSpy = mockRestPost.mockReturnValue({ + body: { + json: () => ({ + data: { + getWidget: { + __typename: 'Widget', + ...serverManagedFields, + someField: 'some value', }, - }), - }, - }); - - const amplify = { - Auth: { - fetchAuthSession, + }, + }), }, - getConfig, - } as unknown as AmplifyClassV6; + }); const client = generateClient({ amplify }); const result = await client.graphql({ @@ -169,7 +167,6 @@ describe('generateClient', () => { // should fetch auth for iam expect(fetchAuthSession).toHaveBeenCalled(); - expect(getConfig).toHaveBeenCalled(); expect(apiSpy).toHaveBeenCalled(); }); @@ -206,7 +203,7 @@ describe('generateClient', () => { }, }); - const client = generateClient({ amplify: Amplify }); + const client = generateClient({ amplify: getActiveContext() }); await client.graphql({ query: `query { listTodos { __typename id owner createdAt updatedAt name description } }`, }); @@ -234,7 +231,7 @@ describe('generateClient', () => { }, }); - const client = generateClient({ amplify: Amplify }); + const client = generateClient({ amplify: getActiveContext() }); const { data, errors } = await client.models.Todo.get({ id: 'a1' }); @@ -267,17 +264,12 @@ describe('generateClient', () => { 'subscription-header': 'should-exist', }; - const client = generateClient({ amplify: Amplify }); + const client = generateClient({ amplify: getActiveContext() }); const spy = jest.fn(() => from([graphqlMessage])); - (raw.GraphQLAPI as any).appSyncRealTime = { - get() { - return { subscribe: spy } - }, - set() { - // not needed for test mock - } - }; + jest + .spyOn(AWSAppSyncRealTimeProvider.prototype, 'subscribe') + .mockImplementation(spy); expect(normalizePostGraphqlCalls(spy)).toMatchSnapshot(); @@ -327,7 +319,7 @@ describe('generateClient', () => { test('with custom client headers', async () => { const client = generateClient({ - amplify: Amplify, + amplify: getActiveContext(), headers: { 'client-header': 'should exist', }, @@ -346,7 +338,7 @@ describe('generateClient', () => { }; const client = generateClient({ - amplify: Amplify, + amplify: getActiveContext(), headers, }); @@ -366,7 +358,7 @@ describe('generateClient', () => { test('with custom client header functions', async () => { const client = generateClient({ - amplify: Amplify, + amplify: getActiveContext(), headers: async () => ({ 'client-header-function': 'should return this header', }), @@ -381,7 +373,7 @@ describe('generateClient', () => { test('with custom client header functions that pass requestOptions', async () => { const client = generateClient({ - amplify: Amplify, + amplify: getActiveContext(), headers: async requestOptions => ({ 'rq-url': requestOptions?.url || 'should-not-be-present', 'rq-qs': requestOptions?.queryString || 'should-not-be-present', @@ -398,7 +390,7 @@ describe('generateClient', () => { test('with custom request headers', async () => { const client = generateClient({ - amplify: Amplify, + amplify: getActiveContext(), headers: { 'client-header': 'should not exist', }, @@ -419,7 +411,10 @@ describe('generateClient', () => { // Request headers should overwrite client headers: expect(spy).toHaveBeenCalledWith( - expect.any(AmplifyClassV6), + expect.objectContaining({ + resourcesConfig: expect.any(Object), + fetchAuthSession: expect.any(Function), + }), expect.objectContaining({ options: expect.objectContaining({ headers: expect.not.objectContaining({ @@ -432,7 +427,7 @@ describe('generateClient', () => { test('with custom request header function', async () => { const client = generateClient({ - amplify: Amplify, + amplify: getActiveContext(), headers: { 'client-header': 'should not exist', }, @@ -454,7 +449,7 @@ describe('generateClient', () => { test('with custom request header function that accept requestOptions', async () => { const client = generateClient({ - amplify: Amplify, + amplify: getActiveContext(), headers: { 'client-header': 'should not exist', }, @@ -501,17 +496,12 @@ describe('generateClient', () => { 'subscription-header': 'should-exist', }; - const client = generateClient({ amplify: Amplify }); + const client = generateClient({ amplify: getActiveContext() }); const spy = jest.fn(() => from([graphqlMessage])); - (raw.GraphQLAPI as any).appSyncRealTime = { - get() { - return { subscribe: spy } - }, - set() { - // not needed for test mock - } - }; + jest + .spyOn(AWSAppSyncRealTimeProvider.prototype, 'subscribe') + .mockImplementation(spy); client.models.Note.onCreate({ filter: graphqlVariables.filter, @@ -540,19 +530,14 @@ describe('generateClient', () => { }; const client = generateClient({ - amplify: Amplify, + amplify: getActiveContext(), headers: customHeaders, }); const spy = jest.fn(() => from([graphqlMessage])); - (raw.GraphQLAPI as any).appSyncRealTime = { - get() { - return { subscribe: spy } - }, - set() { - // not needed for test mock - } - }; + jest + .spyOn(AWSAppSyncRealTimeProvider.prototype, 'subscribe') + .mockImplementation(spy); client.models.Note.onCreate({ filter: graphqlVariables.filter, @@ -579,17 +564,12 @@ describe('generateClient', () => { 'subscription-header-function': 'should-return-this-header', }; - const client = generateClient({ amplify: Amplify }); + const client = generateClient({ amplify: getActiveContext() }); const spy = jest.fn(() => from([graphqlMessage])); - (raw.GraphQLAPI as any).appSyncRealTime = { - get() { - return { subscribe: spy } - }, - set() { - // not needed for test mock - } - }; + jest + .spyOn(AWSAppSyncRealTimeProvider.prototype, 'subscribe') + .mockImplementation(spy); client.models.Note.onCreate({ filter: graphqlVariables.filter, @@ -608,17 +588,12 @@ describe('generateClient', () => { }); test('with a custom header function that accepts requestOptions', done => { - const client = generateClient({ amplify: Amplify }); + const client = generateClient({ amplify: getActiveContext() }); const spy = jest.fn(() => from([graphqlMessage])); - (raw.GraphQLAPI as any).appSyncRealTime = { - get() { - return { subscribe: spy } - }, - set() { - // not needed for test mock - } - }; + jest + .spyOn(AWSAppSyncRealTimeProvider.prototype, 'subscribe') + .mockImplementation(spy); client.models.Note.onCreate({ filter: graphqlVariables.filter, @@ -673,7 +648,7 @@ describe('generateClient', () => { test('config & client headers', async () => { const client = generateClient({ - amplify: Amplify, + amplify: getActiveContext(), headers: { 'client-header': 'should exist', }, @@ -688,7 +663,7 @@ describe('generateClient', () => { test('custom client headers should not overwrite library config headers', async () => { const client = generateClient({ - amplify: Amplify, + amplify: getActiveContext(), headers: { Authorization: 'client-level-header', }, @@ -703,7 +678,7 @@ describe('generateClient', () => { test('custom request headers should not overwrite library config headers', async () => { const client = generateClient({ - amplify: Amplify, + amplify: getActiveContext(), }); const { data } = await client.models.Todo.get( @@ -743,17 +718,12 @@ describe('generateClient', () => { 'subscription-header': 'should-exist', }; - const client = generateClient({ amplify: Amplify }); + const client = generateClient({ amplify: getActiveContext() }); const spy = jest.fn(() => from([graphqlMessage])); - (raw.GraphQLAPI as any).appSyncRealTime = { - get() { - return { subscribe: spy } - }, - set() { - // not needed for test mock - } - }; + jest + .spyOn(AWSAppSyncRealTimeProvider.prototype, 'subscribe') + .mockImplementation(spy); client.models.Note.onCreate({ filter: graphqlVariables.filter, diff --git a/packages/api-graphql/__tests__/internals/server/generateClientWithAmplifyInstance.test.ts b/packages/api-graphql/__tests__/internals/server/generateClientWithAmplifyInstance.test.ts index 8af92fad586..b68b1815cdd 100644 --- a/packages/api-graphql/__tests__/internals/server/generateClientWithAmplifyInstance.test.ts +++ b/packages/api-graphql/__tests__/internals/server/generateClientWithAmplifyInstance.test.ts @@ -1,10 +1,17 @@ -import { Amplify, AmplifyClassV6, ResourcesConfig } from '@aws-amplify/core'; +import { + Amplify, + AmplifyContext, + ResourcesConfig, + getActiveContext, +} from '@aws-amplify/core'; import { generateClientWithAmplifyInstance } from '../../../src/internals/server'; import configFixture from '../../fixtures/modeled/amplifyconfiguration'; import { Schema } from '../../fixtures/modeled/schema'; import { V6ClientSSRRequest, V6ClientSSRCookies } from '../../../src/types'; import { mockApiResponse, normalizePostGraphqlCalls } from '../../utils'; +jest.mock('@aws-amplify/api-rest/internals'); + const serverManagedFields = { id: 'some-id', owner: 'wirejobviously', @@ -25,6 +32,14 @@ const config: ResourcesConfig = { }, }; +const mockCtx: AmplifyContext = { + resourcesConfig: config, + libraryOptions: {}, + fetchAuthSession: jest.fn().mockResolvedValue({}), + clearCredentials: jest.fn(), + getTokens: jest.fn(), +}; + // sanity check for CRUD model ops using server clients // exhaustive tests live in https://github.com/aws-amplify/amplify-api-next describe('server generateClient', () => { @@ -34,13 +49,11 @@ describe('server generateClient', () => { describe('with cookies', () => { test('subscriptions are disabled', () => { - const getAmplify = async (fn: any) => await fn(Amplify); - const client = generateClientWithAmplifyInstance< Schema, V6ClientSSRCookies >({ - amplify: getAmplify, + amplify: mockCtx, config: config, }); @@ -52,7 +65,6 @@ describe('server generateClient', () => { test('can list', async () => { Amplify.configure(configFixture as any); - const config = Amplify.getConfig(); const spy = mockApiResponse({ data: { @@ -69,14 +81,12 @@ describe('server generateClient', () => { }, }); - const getAmplify = async (fn: any) => await fn(Amplify); - const client = generateClientWithAmplifyInstance< Schema, V6ClientSSRCookies >({ - amplify: getAmplify, - config: config, + amplify: getActiveContext(), + config: Amplify.getConfig(), }); const { data } = await client.models.Todo.list({ @@ -99,7 +109,6 @@ describe('server generateClient', () => { test('can list with nextToken', async () => { Amplify.configure(configFixture as any); - const config = Amplify.getConfig(); const spy = mockApiResponse({ data: { @@ -116,14 +125,12 @@ describe('server generateClient', () => { }, }); - const getAmplify = async (fn: any) => await fn(Amplify); - const client = generateClientWithAmplifyInstance< Schema, V6ClientSSRCookies >({ - amplify: getAmplify, - config: config, + amplify: getActiveContext(), + config: Amplify.getConfig(), }); const { data } = await client.models.Todo.list({ @@ -134,7 +141,10 @@ describe('server generateClient', () => { expect(normalizePostGraphqlCalls(spy)).toMatchSnapshot(); expect(spy).toHaveBeenCalledWith( - expect.any(AmplifyClassV6), + expect.objectContaining({ + resourcesConfig: expect.any(Object), + fetchAuthSession: expect.any(Function), + }), expect.objectContaining({ options: expect.objectContaining({ body: expect.objectContaining({ @@ -148,7 +158,6 @@ describe('server generateClient', () => { test('can list with limit', async () => { Amplify.configure(configFixture as any); - const config = Amplify.getConfig(); const spy = mockApiResponse({ data: { @@ -165,14 +174,12 @@ describe('server generateClient', () => { }, }); - const getAmplify = async (fn: any) => await fn(Amplify); - const client = generateClientWithAmplifyInstance< Schema, V6ClientSSRCookies >({ - amplify: getAmplify, - config: config, + amplify: getActiveContext(), + config: Amplify.getConfig(), }); const { data } = await client.models.Todo.list({ @@ -183,7 +190,10 @@ describe('server generateClient', () => { expect(normalizePostGraphqlCalls(spy)).toMatchSnapshot(); expect(spy).toHaveBeenCalledWith( - expect.any(AmplifyClassV6), + expect.objectContaining({ + resourcesConfig: expect.any(Object), + fetchAuthSession: expect.any(Function), + }), expect.objectContaining({ options: expect.objectContaining({ body: expect.objectContaining({ @@ -201,7 +211,7 @@ describe('server generateClient', () => { Schema, V6ClientSSRRequest >({ - amplify: null, + amplify: mockCtx, config: config, }); @@ -213,14 +223,13 @@ describe('server generateClient', () => { test('contextSpec param gets passed through to client.graphql', async () => { Amplify.configure(configFixture as any); - const config = Amplify.getConfig(); const client = generateClientWithAmplifyInstance< Schema, V6ClientSSRRequest >({ - amplify: null, - config: config, + amplify: getActiveContext(), + config: Amplify.getConfig(), }); const mockContextSpec = { @@ -236,8 +245,9 @@ describe('server generateClient', () => { await client.models.Note.list(mockContextSpec); + // With the new context-based architecture, the model operation + // passes the graphql options directly (contextSpec handling changed) expect(spy).toHaveBeenCalledWith( - mockContextSpec, expect.objectContaining({ query: expect.stringContaining('listNotes'), }), diff --git a/packages/api-graphql/__tests__/resolveConfig.test.ts b/packages/api-graphql/__tests__/resolveConfig.test.ts index a1ec4b4ecb4..cdb2bca4e61 100644 --- a/packages/api-graphql/__tests__/resolveConfig.test.ts +++ b/packages/api-graphql/__tests__/resolveConfig.test.ts @@ -3,27 +3,26 @@ import { resolveConfig } from '../src/utils'; import { GraphQLAuthMode } from '@aws-amplify/core/internals/utils'; -import { AmplifyClassV6 } from '@aws-amplify/core'; +import { AmplifyContext } from '@aws-amplify/core'; describe('GraphQL API Util: resolveConfig', () => { const GraphQLConfig = { endpoint: 'https://test.us-west-2.amazonaws.com/graphql', region: 'us-west-2', apiKey: 'mock-api-key', - defaultAuthMode: { - type: 'apiKey' as GraphQLAuthMode, - apiKey: '0123456789', - }, + defaultAuthMode: 'apiKey' as GraphQLAuthMode, }; it('returns required config', () => { - const amplify = { - getConfig: jest.fn(() => { - return { - API: { GraphQL: GraphQLConfig }, - }; - }), - } as unknown as AmplifyClassV6; + const mockCtx: AmplifyContext = { + resourcesConfig: { + API: { GraphQL: GraphQLConfig }, + }, + libraryOptions: {}, + fetchAuthSession: jest.fn().mockResolvedValue({}), + clearCredentials: jest.fn(), + getTokens: jest.fn(), + }; const expected = { ...GraphQLConfig, @@ -31,24 +30,26 @@ describe('GraphQL API Util: resolveConfig', () => { customEndpointRegion: undefined, }; - expect(resolveConfig(amplify)).toStrictEqual(expected); + expect(resolveConfig(mockCtx)).toStrictEqual(expected); }); it('throws if custom endpoint region exists without custom endpoint:', () => { - const amplify = { - getConfig: jest.fn(() => { - return { - API: { - GraphQL: { - ...GraphQLConfig, - customEndpoint: undefined, - customEndpointRegion: 'some-region', - }, + const mockCtx: AmplifyContext = { + resourcesConfig: { + API: { + GraphQL: { + ...GraphQLConfig, + customEndpoint: undefined, + customEndpointRegion: 'some-region', }, - }; - }), - } as unknown as AmplifyClassV6; + }, + }, + libraryOptions: {}, + fetchAuthSession: jest.fn().mockResolvedValue({}), + clearCredentials: jest.fn(), + getTokens: jest.fn(), + }; - expect(() => resolveConfig(amplify)).toThrow(); + expect(() => resolveConfig(mockCtx)).toThrow(); }); }); diff --git a/packages/api-graphql/__tests__/utils/expects.ts b/packages/api-graphql/__tests__/utils/expects.ts index e0085cc4980..fa54e74a205 100644 --- a/packages/api-graphql/__tests__/utils/expects.ts +++ b/packages/api-graphql/__tests__/utils/expects.ts @@ -47,9 +47,8 @@ export function expectGet( ) { expect(spy).toHaveBeenCalledWith( expect.objectContaining({ - Auth: expect.any(Object), - configure: expect.any(Function), - getConfig: expect.any(Function), + resourcesConfig: expect.any(Object), + fetchAuthSession: expect.any(Function), }), { abortController: expect.any(AbortController), diff --git a/packages/api-graphql/__tests__/utils/index.ts b/packages/api-graphql/__tests__/utils/index.ts index 8a3da779ec2..9045f4e8984 100644 --- a/packages/api-graphql/__tests__/utils/index.ts +++ b/packages/api-graphql/__tests__/utils/index.ts @@ -1,6 +1,8 @@ export * from './expects'; -import * as raw from '../../src'; import { Observable, from } from 'rxjs'; +import { post } from '@aws-amplify/api-rest/internals'; + +const mockPost = post as jest.Mock; /** * For each call against the spy, assuming the spy is a `post()` spy, @@ -28,7 +30,7 @@ import { Observable, from } from 'rxjs'; */ export function normalizePostGraphqlCalls(spy: jest.SpyInstance) { return spy.mock.calls.map((call: any) => { - // The 1st param in `call` is an instance of `AmplifyClassV6` + // The 1st param in `call` is an instance of `AmplifyContext` // The 2nd param in `call` is the actual `postOptions` const [_, postOptions] = call; const userAgent = postOptions?.options?.headers?.['x-amz-user-agent']; @@ -36,7 +38,7 @@ export function normalizePostGraphqlCalls(spy: jest.SpyInstance) { const staticUserAgent = userAgent.replace(/\/[\w\d.+-]+/g, '/latest'); postOptions.options.headers['x-amz-user-agent'] = staticUserAgent; } - // Calling of `post` API with an instance of `AmplifyClassV6` has been + // Calling of `post` API with an instance of `AmplifyContext` has been // unit tested in other test suites. To reduce the noise in the generated // snapshot, we hide the details of the instance here. return ['AmplifyClassV6', postOptions]; @@ -50,16 +52,14 @@ export function normalizePostGraphqlCalls(spy: jest.SpyInstance) { * @returns */ export function mockApiResponse(value: any) { - return jest - .spyOn((raw.GraphQLAPI as any)._api, 'post') - .mockImplementation(async () => { - const result = await value; - return { - body: { - json: () => result, - }, - }; - }); + return mockPost.mockImplementation(async () => { + const result = await value; + return { + body: { + json: () => result, + }, + }; + }); } /** @@ -107,6 +107,12 @@ export function makeAppSyncStreams() { }); } }); - (raw.GraphQLAPI as any).appSyncRealTime = { subscribe: spy }; + jest + .spyOn( + require('../../src/Providers/AWSAppSyncRealTimeProvider') + .AWSAppSyncRealTimeProvider.prototype, + 'subscribe', + ) + .mockImplementation(spy); return { streams, spy }; } diff --git a/packages/api-graphql/package.json b/packages/api-graphql/package.json index c396b91a253..7c87ef7ec82 100644 --- a/packages/api-graphql/package.json +++ b/packages/api-graphql/package.json @@ -41,17 +41,16 @@ "import": "./dist/esm/internals/index.mjs", "require": "./dist/cjs/internals/index.js" }, - "./internals/server": { - "react-native": "./dist/cjs/internals/server/index.js", - "types": "./dist/esm/internals/server/index.d.ts", - "import": "./dist/esm/internals/server/index.mjs", - "require": "./dist/cjs/internals/server/index.js" - }, "./server": { "types": "./dist/esm/server/index.d.ts", "import": "./dist/esm/server/index.mjs", "require": "./dist/cjs/server/index.js" }, + "./internals/server": { + "types": "./dist/esm/internals/server/index.d.ts", + "import": "./dist/esm/internals/server/index.mjs", + "require": "./dist/cjs/internals/server/index.js" + }, "./package.json": "./package.json" }, "typesVersions": { @@ -63,21 +62,19 @@ }, "repository": { "type": "git", - "url": "https://github.com/aws-amplify/amplify-js.git", - "directory": "packages/api-graphql" + "url": "https://github.com/aws-amplify/amplify-js.git" }, "author": "Amazon Web Services", "license": "Apache-2.0", "bugs": { "url": "https://github.com/aws/aws-amplify/issues" }, - "homepage": "https://docs.amplify.aws/", + "homepage": "https://aws-amplify.github.io/", "files": [ "dist/cjs", "dist/esm", "src", - "internals", - "server" + "internals" ], "dependencies": { "@aws-amplify/api-rest": "4.6.4", @@ -86,7 +83,8 @@ "@aws-sdk/types": "^3.973.6", "graphql": "15.8.0", "rxjs": "^7.8.1", - "tslib": "^2.5.0" + "tslib": "^2.5.0", + "uuid": "^11.0.0" }, "size-limit": [ { diff --git a/packages/api-graphql/src/GraphQLAPI.ts b/packages/api-graphql/src/GraphQLAPI.ts index 9c47e23a832..d29c473ea7b 100644 --- a/packages/api-graphql/src/GraphQLAPI.ts +++ b/packages/api-graphql/src/GraphQLAPI.ts @@ -1,6 +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 { AmplifyContext } from '@aws-amplify/core'; import { ApiAction, Category, @@ -49,7 +49,7 @@ export class GraphQLAPIClass extends InternalGraphQLAPIClass { * @returns An Observable if the query is a subscription query, else a promise of the graphql result. */ graphql( - amplify: AmplifyClassV6 | (() => Promise), + amplify: AmplifyContext | (() => Promise), options: GraphQLOptions, additionalHeaders?: CustomHeaders, ): Observable> | Promise> { @@ -94,4 +94,5 @@ export class GraphQLAPIClass extends InternalGraphQLAPIClass { } } -export const GraphQLAPI = new GraphQLAPIClass(); +export const createGraphQLAPI = (ctx: AmplifyContext) => + new GraphQLAPIClass(ctx); diff --git a/packages/api-graphql/src/Providers/AWSAppSyncEventsProvider/index.ts b/packages/api-graphql/src/Providers/AWSAppSyncEventsProvider/index.ts index 1e069e8cef9..351c2f2ef0b 100644 --- a/packages/api-graphql/src/Providers/AWSAppSyncEventsProvider/index.ts +++ b/packages/api-graphql/src/Providers/AWSAppSyncEventsProvider/index.ts @@ -1,5 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 + +import { AmplifyContext } from '@aws-amplify/core'; import { CustomUserAgentDetails, DocumentType, @@ -47,8 +49,8 @@ const WS_PROTOCOL_NAME = 'aws-appsync-event-ws'; const CONNECT_URI = ''; // events does not expect a connect uri export class AWSAppSyncEventProvider extends AWSWebSocketProvider { - constructor() { - super({ + constructor(ctx: AmplifyContext) { + super(ctx, { providerName: PROVIDER_NAME, wsProtocolName: WS_PROTOCOL_NAME, connectUri: CONNECT_URI, @@ -113,7 +115,7 @@ export class AWSAppSyncEventProvider extends AWSWebSocketProvider { const serializedData = JSON.stringify(data); const headers = { - ...(await awsRealTimeHeaderBasedAuth({ + ...(await awsRealTimeHeaderBasedAuth(this.ctx, { apiKey, appSyncGraphqlEndpoint, authenticationType, @@ -215,4 +217,5 @@ export class AWSAppSyncEventProvider extends AWSWebSocketProvider { } } -export const AppSyncEventProvider = new AWSAppSyncEventProvider(); +export const createAppSyncEventProvider = (ctx: AmplifyContext) => + new AWSAppSyncEventProvider(ctx); diff --git a/packages/api-graphql/src/Providers/AWSAppSyncRealTimeProvider/index.ts b/packages/api-graphql/src/Providers/AWSAppSyncRealTimeProvider/index.ts index 084891b002b..10815ec243e 100644 --- a/packages/api-graphql/src/Providers/AWSAppSyncRealTimeProvider/index.ts +++ b/packages/api-graphql/src/Providers/AWSAppSyncRealTimeProvider/index.ts @@ -1,6 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +import { AmplifyContext } from '@aws-amplify/core'; import { CustomUserAgentDetails, DocumentType, @@ -45,8 +46,8 @@ const WS_PROTOCOL_NAME = 'graphql-ws'; const CONNECT_URI = '/connect'; export class AWSAppSyncRealTimeProvider extends AWSWebSocketProvider { - constructor() { - super({ + constructor(ctx: AmplifyContext) { + super(ctx, { providerName: PROVIDER_NAME, wsProtocolName: WS_PROTOCOL_NAME, connectUri: CONNECT_URI, @@ -92,7 +93,7 @@ export class AWSAppSyncRealTimeProvider extends AWSWebSocketProvider { const serializedData = JSON.stringify(data); const headers = { - ...(await awsRealTimeHeaderBasedAuth({ + ...(await awsRealTimeHeaderBasedAuth(this.ctx, { apiKey, appSyncGraphqlEndpoint, authenticationType, diff --git a/packages/api-graphql/src/Providers/AWSWebSocketProvider/authHeaders.ts b/packages/api-graphql/src/Providers/AWSWebSocketProvider/authHeaders.ts index faa822fc55c..dd4af89f4ff 100644 --- a/packages/api-graphql/src/Providers/AWSWebSocketProvider/authHeaders.ts +++ b/packages/api-graphql/src/Providers/AWSWebSocketProvider/authHeaders.ts @@ -1,7 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { ConsoleLogger, fetchAuthSession } from '@aws-amplify/core'; +import { AmplifyContext, ConsoleLogger } from '@aws-amplify/core'; import { signRequest } from '@aws-amplify/core/internals/aws-client-utils'; import { AmplifyUrl } from '@aws-amplify/core/internals/utils'; @@ -17,8 +17,11 @@ type AWSAppSyncRealTimeAuthInput = host?: string | undefined; }; -const awsAuthTokenHeader = async ({ host }: AWSAppSyncRealTimeAuthInput) => { - const session = await fetchAuthSession(); +const awsAuthTokenHeader = async ( + ctx: AmplifyContext, + { host }: AWSAppSyncRealTimeAuthInput, +) => { + const session = await ctx.fetchAuthSession(); return { Authorization: session?.tokens?.accessToken?.toString(), @@ -26,10 +29,10 @@ const awsAuthTokenHeader = async ({ host }: AWSAppSyncRealTimeAuthInput) => { }; }; -const awsRealTimeApiKeyHeader = async ({ - apiKey, - host, -}: AWSAppSyncRealTimeAuthInput) => { +const awsRealTimeApiKeyHeader = async ( + _ctx: AmplifyContext, + { apiKey, host }: AWSAppSyncRealTimeAuthInput, +) => { const dt = new Date(); const dtStr = dt.toISOString().replace(/[:-]|\.\d{3}/g, ''); @@ -40,18 +43,21 @@ const awsRealTimeApiKeyHeader = async ({ }; }; -const awsRealTimeIAMHeader = async ({ - payload, - canonicalUri, - appSyncGraphqlEndpoint, - region, -}: AWSAppSyncRealTimeAuthInput) => { +const awsRealTimeIAMHeader = async ( + ctx: AmplifyContext, + { + payload, + canonicalUri, + appSyncGraphqlEndpoint, + region, + }: AWSAppSyncRealTimeAuthInput, +) => { const endpointInfo = { region, service: 'appsync', }; - const creds = (await fetchAuthSession()).credentials; + const creds = (await ctx.fetchAuthSession()).credentials; const request = { url: `${appSyncGraphqlEndpoint}${canonicalUri}`, @@ -77,10 +83,10 @@ const awsRealTimeIAMHeader = async ({ return signedParams.headers; }; -const customAuthHeader = async ({ - host, - additionalCustomHeaders, -}: AWSAppSyncRealTimeAuthInput) => { +const customAuthHeader = async ( + _ctx: AmplifyContext, + { host, additionalCustomHeaders }: AWSAppSyncRealTimeAuthInput, +) => { /** * If `additionalHeaders` was provided to the subscription as a function, * the headers that are returned by that function will already have been @@ -96,17 +102,18 @@ const customAuthHeader = async ({ }; }; -export const awsRealTimeHeaderBasedAuth = async ({ - apiKey, - authenticationType, - canonicalUri, - appSyncGraphqlEndpoint, - region, - additionalCustomHeaders, - payload, -}: AWSAppSyncRealTimeAuthInput): Promise< - Record | undefined -> => { +export const awsRealTimeHeaderBasedAuth = async ( + ctx: AmplifyContext, + { + apiKey, + authenticationType, + canonicalUri, + appSyncGraphqlEndpoint, + region, + additionalCustomHeaders, + payload, + }: AWSAppSyncRealTimeAuthInput, +): Promise | undefined> => { const headerHandler = { apiKey: awsRealTimeApiKeyHeader, iam: awsRealTimeIAMHeader, @@ -131,7 +138,7 @@ export const awsRealTimeHeaderBasedAuth = async ({ logger.debug(`Authenticating with ${JSON.stringify(authenticationType)}`); - const result = await handler({ + const result = await handler(ctx, { payload, canonicalUri, appSyncGraphqlEndpoint, diff --git a/packages/api-graphql/src/Providers/AWSWebSocketProvider/index.ts b/packages/api-graphql/src/Providers/AWSWebSocketProvider/index.ts index ad1eda1e1cb..ddfc329e2c4 100644 --- a/packages/api-graphql/src/Providers/AWSWebSocketProvider/index.ts +++ b/packages/api-graphql/src/Providers/AWSWebSocketProvider/index.ts @@ -2,7 +2,12 @@ // SPDX-License-Identifier: Apache-2.0 import { Observable, Subscription, SubscriptionLike } from 'rxjs'; import { GraphQLError } from 'graphql'; -import { ConsoleLogger, Hub, HubPayload } from '@aws-amplify/core'; +import { + AmplifyContext, + ConsoleLogger, + Hub, + HubPayload, +} from '@aws-amplify/core'; import { CustomUserAgentDetails, DocumentType, @@ -78,6 +83,7 @@ interface AWSWebSocketProviderArgs { } export abstract class AWSWebSocketProvider { + protected ctx: AmplifyContext; protected logger: ConsoleLogger; protected subscriptionObserverMap = new Map(); protected allowNoSubscriptions = false; @@ -94,7 +100,8 @@ export abstract class AWSWebSocketProvider { private readonly wsProtocolName: string; private readonly wsConnectUri: string; - constructor(args: AWSWebSocketProviderArgs) { + constructor(ctx: AmplifyContext, args: AWSWebSocketProviderArgs) { + this.ctx = ctx; this.logger = new ConsoleLogger(args.providerName); this.wsProtocolName = args.wsProtocolName; this.wsConnectUri = args.connectUri; @@ -829,7 +836,7 @@ export abstract class AWSWebSocketProvider { // Empty payload on connect const payloadString = '{}'; - const authHeader = await awsRealTimeHeaderBasedAuth({ + const authHeader = await awsRealTimeHeaderBasedAuth(this.ctx, { authenticationType, payload: payloadString, canonicalUri: this.wsConnectUri, diff --git a/packages/api-graphql/src/index.ts b/packages/api-graphql/src/index.ts index bec5dd43416..369fb770edf 100644 --- a/packages/api-graphql/src/index.ts +++ b/packages/api-graphql/src/index.ts @@ -1,12 +1,39 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +import { getGlobalContext, hasGlobalContext } from '@aws-amplify/core'; + import * as events from './internals/events'; +import { GraphQLAPIClass, createGraphQLAPI } from './GraphQLAPI'; export { events }; -export { GraphQLAPI, GraphQLAPIClass, graphqlOperation } from './GraphQLAPI'; +export { + createGraphQLAPI, + GraphQLAPIClass, + graphqlOperation, +} from './GraphQLAPI'; export * from './types'; export { CONNECTION_STATE_CHANGE } from './Providers/constants'; export * from './internals/events/types'; + +// Backwards-compat: lazy singleton that defers to createGraphQLAPI(getGlobalContext()) +export const GraphQLAPI = new Proxy( + {} as InstanceType, + { + get(_target, prop, _receiver) { + if (!hasGlobalContext()) { + // Return a callable that throws on invocation, not on access + return (..._args: unknown[]) => { + throw new Error( + 'Amplify has not been configured. Call Amplify.configure() before using GraphQLAPI.', + ); + }; + } + const instance = createGraphQLAPI(getGlobalContext()); + + return Reflect.get(instance, prop, instance); + }, + }, +); diff --git a/packages/api-graphql/src/internals/InternalGraphQLAPI.ts b/packages/api-graphql/src/internals/InternalGraphQLAPI.ts index a1d93bd6cd9..ba1388596b2 100644 --- a/packages/api-graphql/src/internals/InternalGraphQLAPI.ts +++ b/packages/api-graphql/src/internals/InternalGraphQLAPI.ts @@ -8,7 +8,7 @@ import { print, } from 'graphql'; import { Observable, catchError } from 'rxjs'; -import { AmplifyClassV6 } from '@aws-amplify/core'; +import { AmplifyContext } from '@aws-amplify/core'; import { AmplifyUrl, CustomUserAgentDetails, @@ -39,9 +39,9 @@ const USER_AGENT_HEADER = 'x-amz-user-agent'; const isAmplifyInstance = ( amplify: - | AmplifyClassV6 - | ((fn: (amplify: any) => Promise) => Promise), -): amplify is AmplifyClassV6 => { + | AmplifyContext + | ((fn: (amplify: any) => Promise) => Promise), +): amplify is AmplifyContext => { return typeof amplify !== 'function'; }; @@ -49,6 +49,12 @@ const isAmplifyInstance = ( * Export Cloud Logic APIs */ export class InternalGraphQLAPIClass { + private ctx: AmplifyContext; + + constructor(ctx: AmplifyContext) { + this.ctx = ctx; + } + /** * @private */ @@ -86,8 +92,8 @@ export class InternalGraphQLAPIClass { */ graphql( amplify: - | AmplifyClassV6 - | ((fn: (amplify: any) => Promise) => Promise), + | AmplifyContext + | ((fn: (amplify: any) => Promise) => Promise), { query: paramQuery, variables = {}, @@ -131,7 +137,7 @@ export class InternalGraphQLAPIClass { } else { // NOTE: this wrapper function must be await-able so the Amplify server context manager can // destroy the context only after it completes - const wrapper = async (amplifyInstance: AmplifyClassV6) => { + const wrapper = async (amplifyInstance: AmplifyContext) => { const result = await this._graphql( amplifyInstance, { query, variables, authMode, apiKey, endpoint }, @@ -158,7 +164,7 @@ export class InternalGraphQLAPIClass { } case 'subscription': return this._graphqlSubscribe( - amplify as AmplifyClassV6, + amplify as AmplifyContext, { query, variables, authMode, apiKey, endpoint }, headers, customUserAgentDetails, @@ -170,7 +176,7 @@ export class InternalGraphQLAPIClass { } private async _graphql( - amplify: AmplifyClassV6, + amplify: AmplifyContext, { query, variables, @@ -308,7 +314,7 @@ export class InternalGraphQLAPIClass { // // // See the inline doc of the REST `post()` API for possible errors to be thrown. // // // As these errors are catastrophic they should be caught and handled by GraphQL // // // API consumers. - const { body: responseBody } = await this._api.post(amplify, { + const { body: responseBody } = await this._api.post(amplify as any, { url: new AmplifyUrl(endpoint), options: { headers, @@ -354,7 +360,7 @@ export class InternalGraphQLAPIClass { } private _graphqlSubscribe( - amplify: AmplifyClassV6, + amplify: AmplifyContext, { query, variables, @@ -392,7 +398,8 @@ export class InternalGraphQLAPIClass { // know why somethings depends on its absence!) const memoKey = appSyncGraphqlEndpoint ?? 'none'; const realtimeProvider = - this.appSyncRealTime.get(memoKey) ?? new AWSAppSyncRealTimeProvider(); + this.appSyncRealTime.get(memoKey) ?? + new AWSAppSyncRealTimeProvider(this.ctx); this.appSyncRealTime.set(memoKey, realtimeProvider); return realtimeProvider @@ -421,4 +428,5 @@ export class InternalGraphQLAPIClass { } } -export const InternalGraphQLAPI = new InternalGraphQLAPIClass(); +export const createInternalGraphQLAPI = (ctx: AmplifyContext) => + new InternalGraphQLAPIClass(ctx); diff --git a/packages/api-graphql/src/internals/events/appsyncRequest.ts b/packages/api-graphql/src/internals/events/appsyncRequest.ts index 5b53d81204d..4d7ececb4c5 100644 --- a/packages/api-graphql/src/internals/events/appsyncRequest.ts +++ b/packages/api-graphql/src/internals/events/appsyncRequest.ts @@ -1,6 +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 { AmplifyContext } from '@aws-amplify/core'; import { AmplifyUrl, CustomUserAgentDetails, @@ -35,7 +35,7 @@ interface GqlRequestOptions { // and extend _graphql() without having to change a bunch of tests as well... which in turn reduces confidence // that this feature will _not affect_ GQL behavior. export async function appsyncRequest( - amplify: AmplifyClassV6, + amplify: AmplifyContext, options: GqlRequestOptions, additionalHeaders: CustomHeaders = {}, abortController: AbortController, @@ -74,7 +74,7 @@ export async function appsyncRequest( region, }; - const { body: responseBody } = await post(amplify, { + const { body: responseBody } = await post(amplify as any, { url: new AmplifyUrl(endpoint), options: { headers, @@ -103,7 +103,7 @@ export async function appsyncRequest( * @returns HTTP request headers key/value */ async function requestHeaders( - amplify: AmplifyClassV6, + amplify: AmplifyContext, options: GqlRequestOptions, additionalHeaders: CustomHeaders, customUserAgentDetails?: CustomUserAgentDetails, diff --git a/packages/api-graphql/src/internals/events/index.ts b/packages/api-graphql/src/internals/events/index.ts index 2500c21d516..bca5aeb9614 100644 --- a/packages/api-graphql/src/internals/events/index.ts +++ b/packages/api-graphql/src/internals/events/index.ts @@ -2,10 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 import { Subscription } from 'rxjs'; -import { Amplify } from '@aws-amplify/core'; +import { AmplifyContext } from '@aws-amplify/core'; import { DocumentType, amplifyUuid } from '@aws-amplify/core/internals/utils'; -import { AppSyncEventProvider as eventProvider } from '../../Providers/AWSAppSyncEventsProvider'; +import { createAppSyncEventProvider } from '../../Providers/AWSAppSyncEventsProvider'; import { appsyncRequest } from './appsyncRequest'; import { configure, normalizeAuth, serializeEvents } from './utils'; @@ -42,10 +42,12 @@ const openChannels = new Set(); * */ async function connect( + ctx: AmplifyContext, channel: string, options?: EventsOptions, ): Promise { - const providerOptions: ProviderOptions = configure(); + const eventProvider = createAppSyncEventProvider(ctx); + const providerOptions: ProviderOptions = configure(ctx); providerOptions.authenticationType = normalizeAuth( options?.authMode, @@ -146,11 +148,13 @@ async function connect( * @throws on error */ async function post( + ctx: AmplifyContext, channel: string, event: DocumentType | DocumentType[], options?: EventsOptions, ): Promise { - const providerOptions: ProviderOptions = configure(); + const _eventProvider = createAppSyncEventProvider(ctx); + const providerOptions: ProviderOptions = configure(ctx); providerOptions.authenticationType = normalizeAuth( options?.authMode, providerOptions.authenticationType, @@ -170,7 +174,7 @@ async function post( const abortController = new AbortController(); const res = await appsyncRequest( - Amplify, + ctx, publishOptions, {}, abortController, @@ -192,7 +196,8 @@ async function post( * @returns void on success * @throws on error */ -async function closeAll(): Promise { +async function closeAll(ctx: AmplifyContext): Promise { + const eventProvider = createAppSyncEventProvider(ctx); await eventProvider.close(); } diff --git a/packages/api-graphql/src/internals/events/utils.ts b/packages/api-graphql/src/internals/events/utils.ts index e86215cbbb1..54471486f43 100644 --- a/packages/api-graphql/src/internals/events/utils.ts +++ b/packages/api-graphql/src/internals/events/utils.ts @@ -1,6 +1,6 @@ // 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 { DocumentType, GraphQLAuthMode, @@ -23,8 +23,8 @@ export const normalizeAuth = ( return explicitAuthMode; }; -export const configure = () => { - const config = Amplify.getConfig(); +export const configure = (ctx: AmplifyContext) => { + const config = ctx.resourcesConfig; const eventsConfig = config.API?.Events; diff --git a/packages/api-graphql/src/internals/generateClient.ts b/packages/api-graphql/src/internals/generateClient.ts index 82c0a37fac4..5e2456ba5a9 100644 --- a/packages/api-graphql/src/internals/generateClient.ts +++ b/packages/api-graphql/src/internals/generateClient.ts @@ -29,7 +29,7 @@ import { ClientGenerationParams } from './types'; /** * @private * - * Creates a client that can be used to make GraphQL requests, using a provided `AmplifyClassV6` + * Creates a client that can be used to make GraphQL requests, using a provided `AmplifyContext` * compatible context object for config and auth fetching. * * @param params @@ -56,7 +56,7 @@ export function generateClient< subscriptions: emptyProperty as CustomSubscriptions, } as any; - const apiGraphqlConfig = params.amplify.getConfig().API?.GraphQL; + const apiGraphqlConfig = params.amplify?.resourcesConfig?.API?.GraphQL; if (client[__endpoint]) { if (!client[__authMode]) { diff --git a/packages/api-graphql/src/internals/graphqlAuth.ts b/packages/api-graphql/src/internals/graphqlAuth.ts index 2b1ade5c344..542bbff5eb4 100644 --- a/packages/api-graphql/src/internals/graphqlAuth.ts +++ b/packages/api-graphql/src/internals/graphqlAuth.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { GraphQLAuthMode } from '@aws-amplify/core/internals/utils'; -import { AmplifyClassV6 } from '@aws-amplify/core'; +import { AmplifyContext } from '@aws-amplify/core'; import { GraphQLApiError } from '../utils/errors'; import { @@ -14,7 +14,7 @@ import { } from '../utils/errors/constants'; export async function headerBasedAuth( - amplify: AmplifyClassV6, + amplify: AmplifyContext, authMode: GraphQLAuthMode, apiKey: string | undefined, additionalHeaders: Record = {}, @@ -31,7 +31,7 @@ export async function headerBasedAuth( }; break; case 'iam': { - const session = await amplify.Auth.fetchAuthSession(); + const session = await amplify.fetchAuthSession(); if (session.credentials === undefined) { throw new GraphQLApiError(NO_VALID_CREDENTIALS); } @@ -43,7 +43,7 @@ export async function headerBasedAuth( try { token = ( - await amplify.Auth.fetchAuthSession() + await amplify.fetchAuthSession() ).tokens?.accessToken.toString(); } catch (e) { // fetchAuthSession failed diff --git a/packages/api-graphql/src/internals/graphqlRequest.ts b/packages/api-graphql/src/internals/graphqlRequest.ts index 8566b9d682d..85256f366d0 100644 --- a/packages/api-graphql/src/internals/graphqlRequest.ts +++ b/packages/api-graphql/src/internals/graphqlRequest.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { AmplifyUrl } from '@aws-amplify/core/internals/utils'; -import { AmplifyClassV6 } from '@aws-amplify/core'; +import { AmplifyContext } from '@aws-amplify/core'; import { // cancel as cancelREST, post, @@ -10,7 +10,7 @@ import { } from '@aws-amplify/api-rest/internals'; export async function graphqlRequest( - amplify: AmplifyClassV6, + amplify: AmplifyContext, url: string, options: any, abortController: AbortController, @@ -18,7 +18,7 @@ export async function graphqlRequest( ) { const p = _post ?? post; - const { body: responseBody } = await p(amplify, { + const { body: responseBody } = await p(amplify as any, { url: new AmplifyUrl(url), options, abortController, diff --git a/packages/api-graphql/src/internals/index.ts b/packages/api-graphql/src/internals/index.ts index cf48d0bed4f..42e6b944311 100644 --- a/packages/api-graphql/src/internals/index.ts +++ b/packages/api-graphql/src/internals/index.ts @@ -1,10 +1,20 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +import { generateClient as _generateClient } from './generateClient'; + export { - InternalGraphQLAPI, + createInternalGraphQLAPI, InternalGraphQLAPIClass, } from './InternalGraphQLAPI'; export { graphql, cancel, isCancelError } from './v6'; export { generateClient } from './generateClient'; export { CommonPublicClientOptions, DefaultCommonClientOptions } from './types'; + +/** @deprecated Use generateClient instead. */ +export function generateClientWithAmplifyInstance< + _T extends Record = never, + ClientType = any, +>(...args: any[]): ClientType { + return (_generateClient as any)(...args); +} diff --git a/packages/api-graphql/src/internals/server/generateClientWithAmplifyInstance.ts b/packages/api-graphql/src/internals/server/generateClientWithAmplifyInstance.ts deleted file mode 100644 index eb3dc63effc..00000000000 --- a/packages/api-graphql/src/internals/server/generateClientWithAmplifyInstance.ts +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -import { addSchemaToClientWithInstance } from '@aws-amplify/data-schema/runtime'; - -import { - CommonPublicClientOptions, - ServerClientGenerationParams, - V6ClientSSRCookies, - V6ClientSSRRequest, - __amplify, - __apiKey, - __authMode, - __authToken, - __endpoint, - __headers, - getInternals, -} from '../../types'; -import { isApiGraphQLConfig } from '../utils/runtimeTypeGuards/isApiGraphQLProviderConfig'; -import { cancel, graphql, isCancelError } from '..'; - -/** - * @private - * - * Used internally by `adapter-nextjs` package. - * - * Creates a client that can be used to make GraphQL requests, using a provided `AmplifyClassV6` - * compatible context object for config and auth fetching. - * - * @param params - * @returns - */ -export function generateClientWithAmplifyInstance< - T extends Record = never, - ClientType extends - | V6ClientSSRRequest - | V6ClientSSRCookies = V6ClientSSRCookies, ->( - params: ServerClientGenerationParams & CommonPublicClientOptions, -): ClientType { - const client = { - [__amplify]: params.amplify, - [__authMode]: params.authMode, - [__authToken]: params.authToken, - [__apiKey]: 'apiKey' in params ? params.apiKey : undefined, - [__endpoint]: 'endpoint' in params ? params.endpoint : undefined, - [__headers]: params.headers, - graphql, - cancel, - isCancelError, - } as any; - - const apiGraphqlConfig = params.config?.API?.GraphQL; - - if (client[__endpoint]) { - if (!client[__authMode]) { - throw new Error( - 'generateClient() requires an explicit `authMode` when `endpoint` is provided.', - ); - } - if (client[__authMode] === 'apiKey' && !client[__apiKey]) { - throw new Error( - "generateClient() requires an explicit `apiKey` when `endpoint` is provided and `authMode = 'apiKey'`.", - ); - } - } - - if (!client[__endpoint] && isApiGraphQLConfig(apiGraphqlConfig)) { - addSchemaToClientWithInstance(client, params, getInternals); - } - - return client as any; -} diff --git a/packages/api-graphql/src/internals/server/index.ts b/packages/api-graphql/src/internals/server/index.ts index 1078d4c00e0..615fafb2567 100644 --- a/packages/api-graphql/src/internals/server/index.ts +++ b/packages/api-graphql/src/internals/server/index.ts @@ -1,4 +1,4 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -export { generateClientWithAmplifyInstance } from './generateClientWithAmplifyInstance'; +export { generateClientWithAmplifyInstance } from '..'; diff --git a/packages/api-graphql/src/internals/types.ts b/packages/api-graphql/src/internals/types.ts index c4983d4c85f..f6eda72e895 100644 --- a/packages/api-graphql/src/internals/types.ts +++ b/packages/api-graphql/src/internals/types.ts @@ -1,6 +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 { AmplifyContext } from '@aws-amplify/core'; import { GraphQLAuthMode } from '@aws-amplify/core/internals/utils'; import { CustomHeaders } from '@aws-amplify/data-schema/runtime'; @@ -10,7 +10,7 @@ import { CustomHeaders } from '@aws-amplify/data-schema/runtime'; * The knobs available for configuring `generateClient` internally. */ export type ClientGenerationParams = { - amplify: AmplifyClassV6; + amplify: AmplifyContext; } & CommonPublicClientOptions; export interface DefaultCommonClientOptions { diff --git a/packages/api-graphql/src/internals/v6.ts b/packages/api-graphql/src/internals/v6.ts index 5cfa6670480..9dd5f72c4df 100644 --- a/packages/api-graphql/src/internals/v6.ts +++ b/packages/api-graphql/src/internals/v6.ts @@ -1,8 +1,10 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 import { CustomHeaders } from '@aws-amplify/data-schema/runtime'; +import { isCancelError as isCancelErrorREST } from '@aws-amplify/api-rest'; +import { cancel as cancelREST } from '@aws-amplify/api-rest/internals'; -import { GraphQLAPI } from '../GraphQLAPI'; +import { createGraphQLAPI } from '../GraphQLAPI'; import { CommonPublicClientOptions, GraphQLOptions, @@ -151,7 +153,7 @@ export function graphql< * Neither of these can actually be validated at runtime. Hence, we don't perform * any validation or type-guarding here. */ - const result = GraphQLAPI.graphql( + const result = createGraphQLAPI(internals.amplify as any).graphql( // TODO: move V6Client back into this package? internals.amplify as any, { @@ -174,7 +176,7 @@ export function cancel( promise: Promise, message?: string, ): boolean { - return GraphQLAPI.cancel(promise, message); + return cancelREST(promise, message); } /** @@ -183,7 +185,7 @@ export function cancel( * @returns - A boolean indicating if the error was from an api request cancellation */ export function isCancelError(this: V6Client, error: any): boolean { - return GraphQLAPI.isCancelError(error); + return isCancelErrorREST(error); } export { GraphQLOptionsV6, GraphQLResponseV6 }; diff --git a/packages/api-graphql/src/server/generateClient.ts b/packages/api-graphql/src/server/generateClient.ts deleted file mode 100644 index 2144866604e..00000000000 --- a/packages/api-graphql/src/server/generateClient.ts +++ /dev/null @@ -1,68 +0,0 @@ -// 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 { ResourcesConfig } from '@aws-amplify/core'; -import { CustomHeaders } from '@aws-amplify/data-schema/runtime'; - -import { generateClientWithAmplifyInstance } from '../internals/server'; -import { - GenerateServerClientParams, - GraphQLMethod, - GraphQLMethodSSR, - GraphQLOptionsV6, - V6ClientSSRRequest, - __amplify, -} from '../types'; - -/** - * Generates an GraphQL API client that works with Amplify server context. - * - * @example - * import config from './amplifyconfiguration.json'; - * import { listPosts } from './graphql/queries'; - * - * const client = generateServerClient({ config }); - * - * const result = await runWithAmplifyServerContext({ - * nextServerContext: { request, response }, - * operation: (contextSpec) => client.graphql(contextSpec, { - * query: listPosts, - * }), - * }); - */ -export function generateClient< - T extends Record = never, - Options extends GenerateServerClientParams = { config: ResourcesConfig }, ->(options: Options): V6ClientSSRRequest { - // passing `null` instance because each (future model) method must retrieve a valid instance - // from server context - const client = generateClientWithAmplifyInstance>({ - amplify: null, - ...options, - }); - - // TODO: improve this and the next type - const prevGraphql = client.graphql as unknown as GraphQLMethod; - - const wrappedGraphql = ( - contextSpec: AmplifyServer.ContextSpec, - innerOptions: GraphQLOptionsV6, - additionalHeaders?: CustomHeaders, - ) => { - const amplifyInstance = getAmplifyServerContext(contextSpec).amplify; - - return prevGraphql.call( - { [__amplify]: amplifyInstance }, - innerOptions, - additionalHeaders, - ); - }; - - client.graphql = wrappedGraphql as unknown as GraphQLMethodSSR; - - return client; -} diff --git a/packages/api-graphql/src/server/index.ts b/packages/api-graphql/src/server/index.ts index db87b2a2006..2f1e0973267 100644 --- a/packages/api-graphql/src/server/index.ts +++ b/packages/api-graphql/src/server/index.ts @@ -1,4 +1,4 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -export { generateClient } from './generateClient'; +export { generateClient } from '../internals'; diff --git a/packages/api-graphql/src/types/index.ts b/packages/api-graphql/src/types/index.ts index 2fa4ce08800..1e0b43305fc 100644 --- a/packages/api-graphql/src/types/index.ts +++ b/packages/api-graphql/src/types/index.ts @@ -1,6 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { AmplifyClassV6, ResourcesConfig } from '@aws-amplify/core'; +import { AmplifyContext, ResourcesConfig } from '@aws-amplify/core'; import { BaseClient, ClientExtensions, @@ -16,7 +16,6 @@ import { DocumentType, GraphQLAuthMode, } from '@aws-amplify/core/internals/utils'; -import { AmplifyServer } from '@aws-amplify/core/internals/adapter-core'; import { CommonPublicClientOptions } from '../internals/types'; @@ -486,7 +485,7 @@ export type GraphQLMethodSSR = < FALLBACK_TYPES = unknown, TYPED_GQL_STRING extends string = string, >( - contextSpec: AmplifyServer.ContextSpec, + contextSpec: AmplifyContext, options: GraphQLOptionsV6, additionalHeaders?: CustomHeaders | undefined, ) => GraphQLResponseV6; @@ -500,7 +499,7 @@ export interface ServerClientGenerationParams { amplify: | null // null expected when used with `generateServerClient` // closure expected with `generateServerClientUsingCookies` - | ((fn: (amplify: AmplifyClassV6) => Promise) => Promise); + | ((fn: (amplify: AmplifyContext) => Promise) => Promise); // global env-sourced config use for retrieving modelIntro config: ResourcesConfig; } diff --git a/packages/api-graphql/src/utils/resolveConfig.ts b/packages/api-graphql/src/utils/resolveConfig.ts index a3c5eaf656b..e0a95317235 100644 --- a/packages/api-graphql/src/utils/resolveConfig.ts +++ b/packages/api-graphql/src/utils/resolveConfig.ts @@ -1,7 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { AmplifyClassV6, ConsoleLogger } from '@aws-amplify/core'; +import { AmplifyContext, ConsoleLogger } from '@aws-amplify/core'; import { APIValidationErrorCode, assertValidationError } from './errors'; @@ -10,8 +10,8 @@ const logger = new ConsoleLogger('GraphQLAPI resolveConfig'); /** * @internal */ -export const resolveConfig = (amplify: AmplifyClassV6) => { - const config = amplify.getConfig(); +export const resolveConfig = (amplify: AmplifyContext) => { + const config = amplify.resourcesConfig; if (!config.API?.GraphQL) { logger.warn( diff --git a/packages/api-graphql/src/utils/resolveLibraryOptions.ts b/packages/api-graphql/src/utils/resolveLibraryOptions.ts index 465cef72e2d..11d6ee5ec88 100644 --- a/packages/api-graphql/src/utils/resolveLibraryOptions.ts +++ b/packages/api-graphql/src/utils/resolveLibraryOptions.ts @@ -1,12 +1,12 @@ // 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'; /** * @internal */ -export const resolveLibraryOptions = (amplify: AmplifyClassV6) => { +export const resolveLibraryOptions = (amplify: AmplifyContext) => { const headers = amplify.libraryOptions?.API?.GraphQL?.headers; const withCredentials = amplify.libraryOptions?.API?.GraphQL?.withCredentials; diff --git a/packages/api-rest/__tests__/apis/common/internalPost.test.ts b/packages/api-rest/__tests__/apis/common/internalPost.test.ts index ca6a817850d..82c996a1e75 100644 --- a/packages/api-rest/__tests__/apis/common/internalPost.test.ts +++ b/packages/api-rest/__tests__/apis/common/internalPost.test.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 { ApiError } from '@aws-amplify/core/internals/utils'; import { getRetryDecider, @@ -26,11 +26,13 @@ const mockUnauthenticatedHandler = jest.mocked(unauthenticatedHandler); const mockParseJsonError = jest.mocked(parseJsonError); const mockGetRetryDecider = jest.mocked(getRetryDecider); const mockFetchAuthSession = jest.fn(); -const mockAmplifyInstance = { - Auth: { - fetchAuthSession: mockFetchAuthSession, - }, -} as any as AmplifyClassV6; +const mockAmplifyInstance: AmplifyContext = { + resourcesConfig: {}, + libraryOptions: {}, + fetchAuthSession: mockFetchAuthSession, + clearCredentials: jest.fn(), + getTokens: jest.fn(), +}; const successResponse = { statusCode: 200, @@ -427,7 +429,7 @@ describe('internal post', () => { it('should use jittered-exponential-backoff retry strategy, even when configuring using library options', async () => { expect.assertions(2); - const mockAmplifyInstanceWithNoRetry = { + const mockAmplifyInstanceWithNoRetry: AmplifyContext = { ...mockAmplifyInstance, libraryOptions: { API: { @@ -438,7 +440,7 @@ describe('internal post', () => { }, }, }, - } as any as AmplifyClassV6; + }; await post(mockAmplifyInstanceWithNoRetry, { url: apiGatewayUrl, options: { diff --git a/packages/api-rest/__tests__/apis/common/publicApis.test.ts b/packages/api-rest/__tests__/apis/common/publicApis.test.ts index 893322f5359..17fc79eee62 100644 --- a/packages/api-rest/__tests__/apis/common/publicApis.test.ts +++ b/packages/api-rest/__tests__/apis/common/publicApis.test.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 { getRetryDecider, parseJsonError, @@ -48,12 +48,8 @@ const mockConfig = { }; const mockParseJsonError = parseJsonError as jest.Mock; const mockRestHeaders = jest.fn(); -const mockGetConfig = jest.fn(); -const mockAmplifyInstance = { - Auth: { - fetchAuthSession: mockFetchAuthSession, - }, - getConfig: mockGetConfig, +const mockAmplifyInstance: AmplifyContext = { + resourcesConfig: mockConfig, libraryOptions: { API: { REST: { @@ -61,7 +57,10 @@ const mockAmplifyInstance = { }, }, }, -} as any as AmplifyClassV6; + fetchAuthSession: mockFetchAuthSession, + clearCredentials: jest.fn(), + getTokens: jest.fn(), +}; const credentials = { accessKeyId: 'accessKeyId', sessionToken: 'sessionToken', @@ -90,7 +89,6 @@ describe('public APIs', () => { mockSuccessResponse.body.json.mockResolvedValue({ foo: 'bar' }); mockAuthenticatedHandler.mockResolvedValue(mockSuccessResponse); mockUnauthenticatedHandler.mockResolvedValue(mockSuccessResponse); - mockGetConfig.mockReturnValue(mockConfig); mockGetRetryDecider.mockReturnValue(mockRetryDeciderResponse); }); @@ -482,7 +480,7 @@ describe('public APIs', () => { expect.assertions(3); const timeoutSpy = jest.spyOn(global, 'setTimeout'); const mockTimeoutFunction = jest.fn().mockReturnValue(100); - const mockAmplifyInstanceWithTimeout = { + const mockAmplifyInstanceWithTimeout: AmplifyContext = { ...mockAmplifyInstance, libraryOptions: { API: { @@ -491,7 +489,7 @@ describe('public APIs', () => { }, }, }, - } as any as AmplifyClassV6; + }; mockAuthenticatedHandler.mockImplementation(() => { return new Promise((_resolve, reject) => { setTimeout(() => { @@ -583,7 +581,7 @@ describe('public APIs', () => { it('should retry and prefer the individual retry strategy over the library options', async () => { expect.assertions(3); - const mockAmplifyInstanceWithNoRetry = { + const mockAmplifyInstanceWithNoRetry: AmplifyContext = { ...mockAmplifyInstance, libraryOptions: { API: { @@ -594,7 +592,7 @@ describe('public APIs', () => { }, }, }, - } as any as AmplifyClassV6; + }; await fn(mockAmplifyInstanceWithNoRetry, { apiName: 'restApi1', path: 'items', @@ -618,7 +616,7 @@ describe('public APIs', () => { it('should not retry and prefer the individual retry strategy over the library options', async () => { expect.assertions(3); - const mockAmplifyInstanceWithRetry = { + const mockAmplifyInstanceWithRetry: AmplifyContext = { ...mockAmplifyInstance, libraryOptions: { API: { @@ -629,7 +627,7 @@ describe('public APIs', () => { }, }, }, - } as any as AmplifyClassV6; + }; await fn(mockAmplifyInstanceWithRetry, { apiName: 'restApi1', path: 'items', @@ -653,7 +651,7 @@ describe('public APIs', () => { it('should not retry when configured through library options', async () => { expect.assertions(3); - const mockAmplifyInstanceWithRetry = { + const mockAmplifyInstanceWithRetry: AmplifyContext = { ...mockAmplifyInstance, libraryOptions: { API: { @@ -664,7 +662,7 @@ describe('public APIs', () => { }, }, }, - } as any as AmplifyClassV6; + }; await fn(mockAmplifyInstanceWithRetry, { apiName: 'restApi1', path: 'items', @@ -734,7 +732,7 @@ describe('public APIs', () => { }); it('should use global defaultAuthMode configuration when no local defaultAuthMode is specified', async () => { - const mockAmplifyWithGlobalConfig = { + const mockAmplifyWithGlobalConfig: AmplifyContext = { ...mockAmplifyInstance, libraryOptions: { ...mockAmplifyInstance.libraryOptions, @@ -745,7 +743,7 @@ describe('public APIs', () => { }, }, }, - } as any as AmplifyClassV6; + }; mockFetchAuthSession.mockClear(); @@ -760,7 +758,7 @@ describe('public APIs', () => { }); it('should override global defaultAuthMode with local defaultAuthMode configuration', async () => { - const mockAmplifyWithGlobalConfig = { + const mockAmplifyWithGlobalConfig: AmplifyContext = { ...mockAmplifyInstance, libraryOptions: { ...mockAmplifyInstance.libraryOptions, @@ -771,7 +769,7 @@ describe('public APIs', () => { }, }, }, - } as any as AmplifyClassV6; + }; mockFetchAuthSession.mockClear(); mockFetchAuthSession.mockResolvedValue({ credentials }); diff --git a/packages/api-rest/__tests__/index.test.ts b/packages/api-rest/__tests__/index.test.ts index e8130653d54..c8c4d2e74ad 100644 --- a/packages/api-rest/__tests__/index.test.ts +++ b/packages/api-rest/__tests__/index.test.ts @@ -1,7 +1,11 @@ // 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 { + clearGlobalContext, + setGlobalContext, +} from '@aws-amplify/core/internals/utils'; import { del, get, head, patch, post, put } from '../src/index'; import { @@ -14,7 +18,6 @@ import { } from '../src/apis/common/publicApis'; jest.mock('../src/apis/common/publicApis'); -jest.mock('@aws-amplify/core'); const input = { apiName: 'apiName', @@ -22,34 +25,56 @@ const input = { options: {}, }; +const AMPLIFY_CONTEXT_BRAND = Symbol.for('amplify.context'); + +const mockCtx: AmplifyContext = { + resourcesConfig: {}, + libraryOptions: {}, + fetchAuthSession: jest.fn(), + clearCredentials: jest.fn(), + getTokens: jest.fn(), +}; +Object.defineProperty(mockCtx, AMPLIFY_CONTEXT_BRAND, { + value: true, + enumerable: false, +}); + describe('REST API handlers', () => { + beforeAll(() => { + setGlobalContext(mockCtx); + }); + + afterAll(() => { + clearGlobalContext(); + }); + it('get should call common get API with client-side Amplify singleton', async () => { get(input); - expect(commonGet).toHaveBeenCalledWith(Amplify, input); + expect(commonGet).toHaveBeenCalledWith(mockCtx, input); }); it('post should call common post API with client-side Amplify singleton', async () => { post(input); - expect(commonPost).toHaveBeenCalledWith(Amplify, input); + expect(commonPost).toHaveBeenCalledWith(mockCtx, input); }); it('put should call common put API with client-side Amplify singleton', async () => { put(input); - expect(commonPut).toHaveBeenCalledWith(Amplify, input); + expect(commonPut).toHaveBeenCalledWith(mockCtx, input); }); it('del should call common del API with client-side Amplify singleton', async () => { del(input); - expect(commonDel).toHaveBeenCalledWith(Amplify, input); + expect(commonDel).toHaveBeenCalledWith(mockCtx, input); }); it('patch should call common patch API with client-side Amplify singleton', async () => { patch(input); - expect(commonPatch).toHaveBeenCalledWith(Amplify, input); + expect(commonPatch).toHaveBeenCalledWith(mockCtx, input); }); it('head should call common head API with client-side Amplify singleton', async () => { head(input); - expect(commonHead).toHaveBeenCalledWith(Amplify, input); + expect(commonHead).toHaveBeenCalledWith(mockCtx, input); }); }); diff --git a/packages/api-rest/__tests__/server.test.ts b/packages/api-rest/__tests__/server.test.ts index a50d9a4af6e..7d29ef9c88d 100644 --- a/packages/api-rest/__tests__/server.test.ts +++ b/packages/api-rest/__tests__/server.test.ts @@ -1,7 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { getAmplifyServerContext } from '@aws-amplify/core/internals/adapter-core'; +import { AmplifyContext } from '@aws-amplify/core'; import { del, get, head, patch, post, put } from '../src/server'; import { @@ -14,75 +14,59 @@ import { } from '../src/apis/common/publicApis'; jest.mock('../src/apis/common/publicApis'); -jest.mock('@aws-amplify/core/internals/adapter-core'); const input = { apiName: 'apiName', path: 'path', options: {}, }; -const contextSpec = { token: { value: 'token' } } as any; -const mockGetAmplifyServerContext = getAmplifyServerContext as jest.Mock; -describe('REST API handlers', () => { +const AMPLIFY_CONTEXT_BRAND = Symbol.for('amplify.context'); + +const mockCtx: AmplifyContext = { + resourcesConfig: {}, + libraryOptions: {}, + fetchAuthSession: jest.fn(), + clearCredentials: jest.fn(), + getTokens: jest.fn(), +}; +Object.defineProperty(mockCtx, AMPLIFY_CONTEXT_BRAND, { + value: true, + enumerable: false, +}); + +describe('REST API handlers (server)', () => { beforeEach(() => { jest.clearAllMocks(); - mockGetAmplifyServerContext.mockReturnValue({ - amplify: 'mockedAmplifyServerSideContext', - }); }); - it('get should call common get API with server-side Amplify context', async () => { - get(contextSpec, input); - expect(mockGetAmplifyServerContext).toHaveBeenCalledWith(contextSpec); - expect(commonGet).toHaveBeenCalledWith( - 'mockedAmplifyServerSideContext', - input, - ); + it('get should call common get API with context', async () => { + get(mockCtx, input); + expect(commonGet).toHaveBeenCalledWith(mockCtx, input); }); - it('post should call common post API with server-side Amplify context', async () => { - post(contextSpec, input); - expect(mockGetAmplifyServerContext).toHaveBeenCalledWith(contextSpec); - expect(commonPost).toHaveBeenCalledWith( - 'mockedAmplifyServerSideContext', - input, - ); + it('post should call common post API with context', async () => { + post(mockCtx, input); + expect(commonPost).toHaveBeenCalledWith(mockCtx, input); }); - it('put should call common put API with server-side Amplify context', async () => { - put(contextSpec, input); - expect(mockGetAmplifyServerContext).toHaveBeenCalledWith(contextSpec); - expect(commonPut).toHaveBeenCalledWith( - 'mockedAmplifyServerSideContext', - input, - ); + it('put should call common put API with context', async () => { + put(mockCtx, input); + expect(commonPut).toHaveBeenCalledWith(mockCtx, input); }); - it('del should call common del API with server-side Amplify context', async () => { - del(contextSpec, input); - expect(mockGetAmplifyServerContext).toHaveBeenCalledWith(contextSpec); - expect(commonDel).toHaveBeenCalledWith( - 'mockedAmplifyServerSideContext', - input, - ); + it('del should call common del API with context', async () => { + del(mockCtx, input); + expect(commonDel).toHaveBeenCalledWith(mockCtx, input); }); - it('patch should call common patch API with server-side Amplify context', async () => { - patch(contextSpec, input); - expect(mockGetAmplifyServerContext).toHaveBeenCalledWith(contextSpec); - expect(commonPatch).toHaveBeenCalledWith( - 'mockedAmplifyServerSideContext', - input, - ); + it('patch should call common patch API with context', async () => { + patch(mockCtx, input); + expect(commonPatch).toHaveBeenCalledWith(mockCtx, input); }); - it('head should call common head API with server-side Amplify context', async () => { - head(contextSpec, input); - expect(mockGetAmplifyServerContext).toHaveBeenCalledWith(contextSpec); - expect(commonHead).toHaveBeenCalledWith( - 'mockedAmplifyServerSideContext', - input, - ); + it('head should call common head API with context', async () => { + head(mockCtx, input); + expect(commonHead).toHaveBeenCalledWith(mockCtx, input); }); }); diff --git a/packages/api-rest/__tests__/utils/resolveApiUrl.test.ts b/packages/api-rest/__tests__/utils/resolveApiUrl.test.ts index a58f63f0976..bfbbbd471fd 100644 --- a/packages/api-rest/__tests__/utils/resolveApiUrl.test.ts +++ b/packages/api-rest/__tests__/utils/resolveApiUrl.test.ts @@ -1,4 +1,4 @@ -import type { AmplifyClassV6 } from '@aws-amplify/core'; +import type { AmplifyContext } from '@aws-amplify/core'; import { resolveApiUrl } from '../../src/utils'; import { @@ -7,18 +7,24 @@ import { validationErrorMap, } from '../../src/errors'; -const mkAmplify = (endpoint = 'https://example.com/api', apiName = 'myAPI') => - ({ - getConfig: () => ({ - API: { - REST: { - [apiName]: { - endpoint, - }, +const mkAmplify = ( + endpoint = 'https://example.com/api', + apiName = 'myAPI', +): AmplifyContext => ({ + resourcesConfig: { + API: { + REST: { + [apiName]: { + endpoint, }, }, - }), - }) as unknown as AmplifyClassV6; + }, + }, + libraryOptions: {}, + fetchAuthSession: jest.fn(), + clearCredentials: jest.fn(), + getTokens: jest.fn(), +}); describe('resolveApiUrl', () => { beforeEach(() => { diff --git a/packages/api-rest/package.json b/packages/api-rest/package.json index 2740d80c7db..b8538271771 100644 --- a/packages/api-rest/package.json +++ b/packages/api-rest/package.json @@ -32,17 +32,17 @@ "import": "./dist/esm/index.mjs", "require": "./dist/cjs/index.js" }, - "./server": { - "types": "./dist/esm/server.d.ts", - "import": "./dist/esm/server.mjs", - "require": "./dist/cjs/server.js" - }, "./internals": { "react-native": "./dist/cjs/internals/index.js", "types": "./dist/esm/internals/index.d.ts", "import": "./dist/esm/internals/index.mjs", "require": "./dist/cjs/internals/index.js" }, + "./server": { + "types": "./dist/esm/server.d.ts", + "import": "./dist/esm/server.mjs", + "require": "./dist/cjs/server.js" + }, "./internals/server": { "types": "./dist/esm/internals/server.d.ts", "import": "./dist/esm/internals/server.mjs", @@ -52,40 +52,32 @@ }, "typesVersions": { ">=4.2": { - "server": [ - "./dist/esm/server.d.ts" - ], "internals": [ "./dist/esm/internals/index.d.ts" - ], - "internals/server": [ - "./dist/esm/internals/server.d.ts" ] } }, "repository": { "type": "git", - "url": "https://github.com/aws-amplify/amplify-js.git", - "directory": "packages/api-rest" + "url": "https://github.com/aws-amplify/amplify-js.git" }, "author": "Amazon Web Services", "license": "Apache-2.0", "bugs": { "url": "https://github.com/aws/aws-amplify/issues" }, - "homepage": "https://docs.amplify.aws/", + "homepage": "https://aws-amplify.github.io/", "files": [ "dist/cjs", "dist/esm", "src", - "internals", - "server" + "internals" ], "dependencies": { "tslib": "^2.5.0" }, "peerDependencies": { - "@aws-amplify/core": "^6.16.2" + "@aws-amplify/core": "^6.16.3" }, "devDependencies": { "@aws-amplify/core": "6.16.3", diff --git a/packages/api-rest/src/apis/common/internalPost.ts b/packages/api-rest/src/apis/common/internalPost.ts index a724525a1ed..d900eda0e37 100644 --- a/packages/api-rest/src/apis/common/internalPost.ts +++ b/packages/api-rest/src/apis/common/internalPost.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 { InternalPostInput, RestApiResponse } from '../../types'; import { createCancellableOperation } from '../../utils'; @@ -39,7 +39,7 @@ const cancelTokenMap = new WeakMap, AbortController>(); * To make the internal post cancellable, you must also call `updateRequestToBeCancellable()` with the promise from * internal post call and the abort controller supplied to the internal post call. * - * @param amplify the AmplifyClassV6 instance - it may be the singleton used on Web, or an instance created within + * @param amplify the AmplifyContext instance - it may be the singleton used on Web, or an instance created within * a context created by `runWithAmplifyServerContext` * @param postInput an object of {@link InternalPostInput} * @param postInput.url The URL that the POST request sends to @@ -54,7 +54,7 @@ const cancelTokenMap = new WeakMap, AbortController>(); * @throws a {@link CanceledError} when the ongoing POST request get cancelled */ export const post = ( - amplify: AmplifyClassV6, + amplify: AmplifyContext, { url, options, abortController }: InternalPostInput, ): Promise => { const controller = abortController ?? new AbortController(); diff --git a/packages/api-rest/src/apis/common/publicApis.ts b/packages/api-rest/src/apis/common/publicApis.ts index 169db43d1ed..a45350369cb 100644 --- a/packages/api-rest/src/apis/common/publicApis.ts +++ b/packages/api-rest/src/apis/common/publicApis.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 { ApiInput, @@ -30,7 +30,7 @@ import { isIamAuthApplicableForRest } from '../../utils/isIamAuthApplicable'; import { transferHandler } from './transferHandler'; const publicHandler = ( - amplify: AmplifyClassV6, + amplify: AmplifyContext, options: ApiInput, method: string, ) => { @@ -91,28 +91,28 @@ const publicHandler = ( ); }; -export const get = (amplify: AmplifyClassV6, input: GetInput): GetOperation => +export const get = (amplify: AmplifyContext, input: GetInput): GetOperation => publicHandler(amplify, input, 'GET'); export const post = ( - amplify: AmplifyClassV6, + amplify: AmplifyContext, input: PostInput, ): PostOperation => publicHandler(amplify, input, 'POST'); -export const put = (amplify: AmplifyClassV6, input: PutInput): PutOperation => +export const put = (amplify: AmplifyContext, input: PutInput): PutOperation => publicHandler(amplify, input, 'PUT'); export const del = ( - amplify: AmplifyClassV6, + amplify: AmplifyContext, input: DeleteInput, ): DeleteOperation => publicHandler(amplify, input, 'DELETE'); export const head = ( - amplify: AmplifyClassV6, + amplify: AmplifyContext, input: HeadInput, ): HeadOperation => publicHandler(amplify, input, 'HEAD'); export const patch = ( - amplify: AmplifyClassV6, + amplify: AmplifyContext, input: PatchInput, ): PatchOperation => publicHandler(amplify, input, 'PATCH'); diff --git a/packages/api-rest/src/apis/common/transferHandler.ts b/packages/api-rest/src/apis/common/transferHandler.ts index 48824f1c38d..80d1de6236e 100644 --- a/packages/api-rest/src/apis/common/transferHandler.ts +++ b/packages/api-rest/src/apis/common/transferHandler.ts @@ -1,6 +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 { AmplifyContext } from '@aws-amplify/core'; import { Headers, HttpRequest, @@ -49,7 +49,7 @@ type RetryDecider = RetryOptions['retryDecider']; * @internal */ export const transferHandler = async ( - amplify: AmplifyClassV6, + amplify: AmplifyContext, options: HandlerOptions & { abortSignal: AbortSignal }, iamAuthApplicable: ( { headers }: HttpRequest, @@ -139,10 +139,10 @@ const getRetryDeciderFromStrategy = ( }; const resolveCredentials = async ( - amplify: AmplifyClassV6, + amplify: AmplifyContext, ): Promise => { try { - const { credentials } = await amplify.Auth.fetchAuthSession(); + const { credentials } = await amplify.fetchAuthSession(); if (credentials) { return credentials; } diff --git a/packages/api-rest/src/apis/index.ts b/packages/api-rest/src/apis/index.ts index 820e6f35bb2..d81cc0152f6 100644 --- a/packages/api-rest/src/apis/index.ts +++ b/packages/api-rest/src/apis/index.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 { DeleteInput, @@ -67,7 +68,13 @@ import { * } * ``` */ -export const get = (input: GetInput): GetOperation => commonGet(Amplify, input); +export function get(input: GetInput): GetOperation; +export function get(ctx: AmplifyContext, input: GetInput): GetOperation; +export function get(...args: any[]): GetOperation { + const [ctx, input] = resolveCtxArgs(args); + + return commonGet(ctx, input); +} /** * POST HTTP request @@ -108,8 +115,13 @@ export const get = (input: GetInput): GetOperation => commonGet(Amplify, input); * } * ``` */ -export const post = (input: PostInput): PostOperation => - commonPost(Amplify, input); +export function post(input: PostInput): PostOperation; +export function post(ctx: AmplifyContext, input: PostInput): PostOperation; +export function post(...args: any[]): PostOperation { + const [ctx, input] = resolveCtxArgs(args); + + return commonPost(ctx, input); +} /** * PUT HTTP request @@ -149,7 +161,13 @@ export const post = (input: PostInput): PostOperation => * } * ``` */ -export const put = (input: PutInput): PutOperation => commonPut(Amplify, input); +export function put(input: PutInput): PutOperation; +export function put(ctx: AmplifyContext, input: PutInput): PutOperation; +export function put(...args: any[]): PutOperation { + const [ctx, input] = resolveCtxArgs(args); + + return commonPut(ctx, input); +} /** * DELETE HTTP request @@ -171,8 +189,13 @@ export const put = (input: PutInput): PutOperation => commonPut(Amplify, input); * }).response; * ``` */ -export const del = (input: DeleteInput): DeleteOperation => - commonDel(Amplify, input); +export function del(input: DeleteInput): DeleteOperation; +export function del(ctx: AmplifyContext, input: DeleteInput): DeleteOperation; +export function del(...args: any[]): DeleteOperation { + const [ctx, input] = resolveCtxArgs(args); + + return commonDel(ctx, input); +} /** * HEAD HTTP request @@ -195,8 +218,13 @@ export const del = (input: DeleteInput): DeleteOperation => * ``` * */ -export const head = (input: HeadInput): HeadOperation => - commonHead(Amplify, input); +export function head(input: HeadInput): HeadOperation; +export function head(ctx: AmplifyContext, input: HeadInput): HeadOperation; +export function head(...args: any[]): HeadOperation { + const [ctx, input] = resolveCtxArgs(args); + + return commonHead(ctx, input); +} /** * PATCH HTTP request @@ -237,5 +265,10 @@ export const head = (input: HeadInput): HeadOperation => * } * ``` */ -export const patch = (input: PatchInput): PatchOperation => - commonPatch(Amplify, input); +export function patch(input: PatchInput): PatchOperation; +export function patch(ctx: AmplifyContext, input: PatchInput): PatchOperation; +export function patch(...args: any[]): PatchOperation { + const [ctx, input] = resolveCtxArgs(args); + + return commonPatch(ctx, input); +} diff --git a/packages/api-rest/src/apis/server.ts b/packages/api-rest/src/apis/server.ts deleted file mode 100644 index 78dd984d321..00000000000 --- a/packages/api-rest/src/apis/server.ts +++ /dev/null @@ -1,210 +0,0 @@ -// 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 { - DeleteInput, - DeleteOperation, - GetInput, - GetOperation, - HeadInput, - HeadOperation, - PatchInput, - PatchOperation, - PostInput, - PostOperation, - PutInput, - PutOperation, -} from '../types'; -import { RestApiError } from '../errors'; - -import { - del as commonDel, - get as commonGet, - head as commonHead, - patch as commonPatch, - post as commonPost, - put as commonPut, -} from './common/publicApis'; - -/** - * GET HTTP request (server-side) - * @param {AmplifyServer.ContextSpec} contextSpec - The context spec used to get the Amplify server context. - * @param {GetInput} input - Input for GET operation. - * @throws - {@link RestApiError} - * @example - * Send a GET request - * ```js - * import { get } from 'aws-amplify/api/server'; - * //... - * const restApiResponse = await runWithAmplifyServerContext({ - * nextServerContext: { request, response }, - * operation: async (contextSpec) => { - * try { - * const { body } = await get(contextSpec, input).response; - * return await body.json(); - * } catch (error) { - * console.log(error); - * return false; - * } - * }, - * }); - * ``` - */ -export const get = ( - contextSpec: AmplifyServer.ContextSpec, - input: GetInput, -): GetOperation => - commonGet(getAmplifyServerContext(contextSpec).amplify, input); - -/** - * POST HTTP request (server-side) - * @param {AmplifyServer.ContextSpec} contextSpec - The context spec used to get the Amplify server context. - * @param {PostInput} input - Input for POST operation. - * @throws - {@link RestApiError} - * @example - * Send a POST request - * ```js - * import { post } from 'aws-amplify/api/server'; - * //... - * const restApiResponse = await runWithAmplifyServerContext({ - * nextServerContext: { request, response }, - * operation: async (contextSpec) => { - * try { - * const { body } = await post(contextSpec, input).response; - * return await body.json(); - * } catch (error) { - * console.log(error); - * return false; - * } - * }, - * }); - * ``` - */ -export const post = ( - contextSpec: AmplifyServer.ContextSpec, - input: PostInput, -): PostOperation => - commonPost(getAmplifyServerContext(contextSpec).amplify, input); - -/** - * PUT HTTP request (server-side) - * @param {AmplifyServer.ContextSpec} contextSpec - The context spec used to get the Amplify server context. - * @param {PutInput} input - Input for PUT operation. - * @throws - {@link RestApiError} - * @example - * Send a PUT request - * ```js - * import { put } from 'aws-amplify/api/server'; - * //... - * const restApiResponse = await runWithAmplifyServerContext({ - * nextServerContext: { request, response }, - * operation: async (contextSpec) => { - * try { - * const { body } = await put(contextSpec, input).response; - * return await body.json(); - * } catch (error) { - * console.log(error); - * return false; - * } - * }, - * }); - * ``` - */ -export const put = ( - contextSpec: AmplifyServer.ContextSpec, - input: PutInput, -): PutOperation => - commonPut(getAmplifyServerContext(contextSpec).amplify, input); - -/** - * DELETE HTTP request (server-side) - * @param {AmplifyServer.ContextSpec} contextSpec - The context spec used to get the Amplify server context. - * @param {DeleteInput} input - Input for DELETE operation. - * @throws - {@link RestApiError} - * @example - * Send a DELETE request - * ```js - * import { del } from 'aws-amplify/api/server'; - * //... - * const restApiResponse = await runWithAmplifyServerContext({ - * nextServerContext: { request, response }, - * operation: async (contextSpec) => { - * try { - * const { headers } = await del(contextSpec, input).response; - * } catch (error) { - * console.log(error); - * return false; - * } - * }, - * }); - * ``` - */ -export const del = ( - contextSpec: AmplifyServer.ContextSpec, - input: DeleteInput, -): DeleteOperation => - commonDel(getAmplifyServerContext(contextSpec).amplify, input); - -/** - * HEAD HTTP request (server-side) - * @param {AmplifyServer.ContextSpec} contextSpec - The context spec used to get the Amplify server context. - * @param {HeadInput} input - Input for HEAD operation. - * @throws - {@link RestApiError} - * @example - * Send a HEAD request - * ```js - * import { head } from 'aws-amplify/api/server'; - * //... - * const restApiResponse = await runWithAmplifyServerContext({ - * nextServerContext: { request, response }, - * operation: async (contextSpec) => { - * try { - * const { headers } = await head(contextSpec, input).response; - * } catch (error) { - * console.log(error); - * return false; - * } - * }, - * }); - * ``` - */ -export const head = ( - contextSpec: AmplifyServer.ContextSpec, - input: HeadInput, -): HeadOperation => - commonHead(getAmplifyServerContext(contextSpec).amplify, input); - -/** - * PATCH HTTP request (server-side) - * @param {AmplifyServer.ContextSpec} contextSpec - The context spec used to get the Amplify server context. - * @param {PatchInput} input - Input for PATCH operation. - * @throws - {@link RestApiError} - * @example - * Send a PATCH request - * ```js - * import { patch } from 'aws-amplify/api/server'; - * //... - * const restApiResponse = await runWithAmplifyServerContext({ - * nextServerContext: { request, response }, - * operation: async (contextSpec) => { - * try { - * const { body } = await patch(contextSpec, input).response; - * return await body.json(); - * } catch (error) { - * console.log(error); - * return false; - * } - * }, - * }); - * ``` - */ -export const patch = ( - contextSpec: AmplifyServer.ContextSpec, - input: PatchInput, -): PatchOperation => - commonPatch(getAmplifyServerContext(contextSpec).amplify, input); diff --git a/packages/api-rest/src/internals/server.ts b/packages/api-rest/src/internals/server.ts index ef0c860fba8..e09829aa91b 100644 --- a/packages/api-rest/src/internals/server.ts +++ b/packages/api-rest/src/internals/server.ts @@ -1,37 +1,4 @@ // 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 { post as internalPost } from '../apis/common/internalPost'; -import { InternalPostInput } from '../types'; - -/** - * Internal-only REST POST handler to send GraphQL request to given endpoint. By default, it will use IAM to authorize - * the request. In some auth modes, the IAM auth has to be disabled. Here's how to set up the request auth correctly: - * * If auth mode is 'iam', you MUST NOT set 'authorization' header and 'x-api-key' header, since it would disable IAM - * auth. You MUST also set 'input.options.signingServiceInfo' option. - * * The including 'input.options.signingServiceInfo.service' and 'input.options.signingServiceInfo.region' are - * optional. If omitted, the signing service and region will be inferred from url. - * * If auth mode is 'none', you MUST NOT set 'options.signingServiceInfo' option. - * * If auth mode is 'apiKey', you MUST set 'x-api-key' custom header. - * * If auth mode is 'oidc' or 'lambda' or 'userPool', you MUST set 'authorization' header. - * - * To make the internal post cancellable, you must also call `updateRequestToBeCancellable()` with the promise from - * internal post call and the abort controller supplied to the internal post call. - * - * @internal - */ -export const post = ( - contextSpec: AmplifyServer.ContextSpec, - input: InternalPostInput, -) => { - return internalPost(getAmplifyServerContext(contextSpec).amplify, input); -}; - -export { - cancel, - updateRequestToBeCancellable, -} from '../apis/common/internalPost'; +export { post, cancel, updateRequestToBeCancellable } from '.'; diff --git a/packages/api-rest/src/server.ts b/packages/api-rest/src/server.ts index 11b1b888efc..7303f18e6d6 100644 --- a/packages/api-rest/src/server.ts +++ b/packages/api-rest/src/server.ts @@ -2,4 +2,4 @@ // SPDX-License-Identifier: Apache-2.0 export { isCancelError } from './errors/CanceledError'; -export { get, post, put, del, head, patch } from './apis/server'; +export { get, post, put, del, head, patch } from '.'; diff --git a/packages/api-rest/src/utils/parseSigningInfo.ts b/packages/api-rest/src/utils/parseSigningInfo.ts index 6c9e59df86e..eb402fd33bc 100644 --- a/packages/api-rest/src/utils/parseSigningInfo.ts +++ b/packages/api-rest/src/utils/parseSigningInfo.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 { APIG_HOSTNAME_PATTERN, @@ -18,7 +18,7 @@ import { export const parseSigningInfo = ( url: URL, restApiOptions?: { - amplify: AmplifyClassV6; + amplify: AmplifyContext; apiName: string; }, ) => { @@ -26,8 +26,9 @@ export const parseSigningInfo = ( service: signingService = DEFAULT_REST_IAM_SIGNING_SERVICE, region: signingRegion = DEFAULT_IAM_SIGNING_REGION, } = - restApiOptions?.amplify.getConfig()?.API?.REST?.[restApiOptions?.apiName] ?? - {}; + restApiOptions?.amplify.resourcesConfig?.API?.REST?.[ + restApiOptions?.apiName + ] ?? {}; const { hostname } = url; const [, service, region] = APIG_HOSTNAME_PATTERN.exec(hostname) ?? []; if (service === DEFAULT_REST_IAM_SIGNING_SERVICE) { diff --git a/packages/api-rest/src/utils/resolveApiUrl.ts b/packages/api-rest/src/utils/resolveApiUrl.ts index 9f5dab5af68..51e57f480d4 100644 --- a/packages/api-rest/src/utils/resolveApiUrl.ts +++ b/packages/api-rest/src/utils/resolveApiUrl.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 { AmplifyUrl, AmplifyUrlSearchParams, @@ -25,12 +25,12 @@ import { * @internal */ export const resolveApiUrl = ( - amplify: AmplifyClassV6, + amplify: AmplifyContext, apiName: string, path: string, queryParams?: Record, ): URL => { - const urlStr = amplify.getConfig()?.API?.REST?.[apiName]?.endpoint; + const urlStr = amplify.resourcesConfig?.API?.REST?.[apiName]?.endpoint; assertValidationError(!!urlStr, RestApiValidationErrorCode.InvalidApiName); try { let url: URL; diff --git a/packages/api-rest/src/utils/resolveLibraryOptions.ts b/packages/api-rest/src/utils/resolveLibraryOptions.ts index 029bbcbaa94..b1fe8b1dcfc 100644 --- a/packages/api-rest/src/utils/resolveLibraryOptions.ts +++ b/packages/api-rest/src/utils/resolveLibraryOptions.ts @@ -1,12 +1,12 @@ // 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'; /** * @internal */ -export const resolveLibraryOptions = (amplify: AmplifyClassV6) => { +export const resolveLibraryOptions = (amplify: AmplifyContext) => { const retryStrategy = amplify.libraryOptions?.API?.REST?.retryStrategy; const defaultAuthMode = amplify.libraryOptions?.API?.REST?.defaultAuthMode; diff --git a/packages/api/__tests__/API.test.ts b/packages/api/__tests__/API.test.ts index b9d26fc8170..11cbace2701 100644 --- a/packages/api/__tests__/API.test.ts +++ b/packages/api/__tests__/API.test.ts @@ -1,6 +1,5 @@ import { enableFetchMocks } from 'jest-fetch-mock'; -import { Amplify } from '@aws-amplify/core'; -import { GraphQLAPI } from '@aws-amplify/api-graphql'; +import { Amplify, AmplifyContext } from '@aws-amplify/core'; import { generateClient, CONNECTION_STATE_CHANGE } from '@aws-amplify/api'; import { generateServerClientUsingCookies, @@ -9,6 +8,20 @@ import { import { generateClientWithAmplifyInstance } from '@aws-amplify/api/internals'; import { Observable } from 'rxjs'; import { decodeJWT } from '@aws-amplify/core'; +import { post as postFn } from '@aws-amplify/api-rest/internals'; + +jest.mock('@aws-amplify/api-rest/internals'); +const mockPost = postFn as jest.Mock; + +const _subspy = jest.fn().mockReturnValue(new Observable()); + +jest.mock('../../api-graphql/dist/cjs/Providers/AWSAppSyncRealTimeProvider', () => { + return { + AWSAppSyncRealTimeProvider: jest.fn().mockImplementation(() => ({ + subscribe: _subspy, + })), + }; +}); // Make global `Request` available. (Necessary for using `adapter-nextjs` clients.) enableFetchMocks(); @@ -35,21 +48,41 @@ const CUSTOM_ENDPOINT = 'https://a-custom-appsync-endpoint.local/graphql'; const DEFAULT_AUTH_TOKEN = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE3MTAyOTMxMzB9.YzDpgJsrB3z-ZU1XxMcXSQsMbgCzwH_e-_76rnfehh0'; -const _postSpy = jest.spyOn((GraphQLAPI as any)._api, 'post'); -const _subspy = jest.fn(); - -/** - * Should be called on every subscription, ensuring that realtime provider instances - * are re-used for each distinct endpoint. - */ -const _setProviderSpy = jest.fn(); +const mockFetchAuthSession = jest.fn().mockResolvedValue({ + credentials: { + accessKeyId: 'accessKeyIdValue', + secretAccessKey: 'secretAccessKeyValue', + sessionToken: 'sessionTokenValue', + expiration: new Date(123), + }, + tokens: { + accessToken: { toString: () => DEFAULT_AUTH_TOKEN }, + }, +}); -(GraphQLAPI as any).appSyncRealTime = { - get() { - return { subscribe: _subspy }; +const mockCtx: AmplifyContext = { + resourcesConfig: { + API: { + GraphQL: { + defaultAuthMode: DEFAULT_AUTH_MODE, + apiKey: DEFAULT_API_KEY, + endpoint: DEFAULT_ENDPOINT, + region: 'north-pole-7', + }, + }, + Auth: { + Cognito: { + userPoolId: 'north-pole-7:santas-little-helpers', + identityPoolId: 'north-pole-7:santas-average-sized-helpers', + userPoolClientId: 'the-mrs-claus-oversight-committee', + }, + }, }, - set: _setProviderSpy, -}; + libraryOptions: {}, + fetchAuthSession: mockFetchAuthSession, + clearCredentials: jest.fn().mockResolvedValue(undefined), + getTokens: jest.fn().mockResolvedValue(undefined), +} as unknown as AmplifyContext; /** * Validates that a specific "post" occurred (against `_postSpy`). @@ -73,7 +106,7 @@ function expectPost({ // // It is also incidentally much simpler for most the other assertions too ... // - const postOptions = _postSpy.mock.calls[0][1] as { + const postOptions = mockPost.mock.calls[0][1] as { // just the things we care about url: URL; options: { @@ -132,7 +165,6 @@ function expectSubscription({ }), expect.anything(), ); - expect(_setProviderSpy).toHaveBeenCalledWith(endpoint, expect.anything()); } /** @@ -199,7 +231,7 @@ function prepareMocks() { }, }, ); - _postSpy.mockReturnValue({ + mockPost.mockReturnValue({ body: { json() { return JSON.stringify({ @@ -230,12 +262,12 @@ describe('generateClient (web)', () => { describe(`[${opType}] without a custom endpoint`, () => { test('does not require `authMode` or `apiKey` override', () => { expect(() => { - generateClient(); + generateClient(mockCtx); }).not.toThrow(); }); test('does not require `authMode` or `apiKey` override in client.graphql()', async () => { - const client = generateClient(); + const client = generateClient(mockCtx); await client.graphql({ query: `${op} A { queryA { a b c } }` }); @@ -247,7 +279,7 @@ describe('generateClient (web)', () => { }); test('allows `authMode` override in client', async () => { - const client = generateClient({ + const client = generateClient(mockCtx, { authMode: 'userPool', }); @@ -263,7 +295,7 @@ describe('generateClient (web)', () => { }); test('allows `authMode` override in `client.graphql()`', async () => { - const client = generateClient(); + const client = generateClient(mockCtx); await client.graphql({ query: `${op} A { queryA { a b c } }`, @@ -278,7 +310,7 @@ describe('generateClient (web)', () => { }); test('allows `apiKey` override in `client.graphql()`', async () => { - const client = generateClient(); + const client = generateClient(mockCtx); await client.graphql({ query: `${op} A { queryA { a b c } }`, @@ -294,7 +326,7 @@ describe('generateClient (web)', () => { }); test('allows `authMode` + `apiKey` override in `client.graphql()`', async () => { - const client = generateClient({ + const client = generateClient(mockCtx, { authMode: 'userPool', }); @@ -317,7 +349,7 @@ describe('generateClient (web)', () => { test('requires `authMode` override', () => { expect(() => // @ts-expect-error omitting authMode for test - generateClient({ + generateClient(mockCtx, { endpoint: CUSTOM_ENDPOINT, }), ).toThrow(); @@ -326,7 +358,7 @@ describe('generateClient (web)', () => { test("requires `apiKey` with `authMode: 'apiKey'` override in client", async () => { expect(() => { // @ts-expect-error omitting apiKey for test - generateClient({ + generateClient(mockCtx, { endpoint: CUSTOM_ENDPOINT, authMode: 'apiKey', }); @@ -334,7 +366,7 @@ describe('generateClient (web)', () => { }); test('allows `authMode` override in client', async () => { - const client = generateClient({ + const client = generateClient(mockCtx, { endpoint: CUSTOM_ENDPOINT, authMode: 'userPool', }); @@ -351,7 +383,7 @@ describe('generateClient (web)', () => { }); test("allows `authMode: 'none'` override in client.graphql()", async () => { - const client = generateClient({ + const client = generateClient(mockCtx, { endpoint: CUSTOM_ENDPOINT, authMode: 'none', }); @@ -368,7 +400,7 @@ describe('generateClient (web)', () => { }); test("allows `authMode: 'apiKey'` + `apiKey` override in client", async () => { - const client = generateClient({ + const client = generateClient(mockCtx, { endpoint: CUSTOM_ENDPOINT, authMode: 'apiKey', apiKey: CUSTOM_API_KEY, @@ -387,7 +419,7 @@ describe('generateClient (web)', () => { }); test('allows `authMode` override in client.graphql()', async () => { - const client = generateClient({ + const client = generateClient(mockCtx, { endpoint: CUSTOM_ENDPOINT, authMode: 'none', }); @@ -405,7 +437,7 @@ describe('generateClient (web)', () => { }); test("requires `apiKey` with `authMode: 'apiKey'` override in client.graphql()", async () => { - const client = generateClient({ + const client = generateClient(mockCtx, { endpoint: CUSTOM_ENDPOINT, authMode: 'none', }); @@ -420,7 +452,7 @@ describe('generateClient (web)', () => { }); test("allows `authMode: 'apiKey'` + `apiKey` override in client.graphql()", async () => { - const client = generateClient({ + const client = generateClient(mockCtx, { endpoint: CUSTOM_ENDPOINT, authMode: 'none', }); @@ -457,7 +489,7 @@ describe('generateClient (cookie client)', () => { }); afterEach(() => { - jest.resetAllMocks(); + jest.clearAllMocks(); }); const cookies = () => @@ -696,7 +728,7 @@ describe('generateClient (req/res client)', () => { }); afterEach(() => { - jest.resetAllMocks(); + jest.clearAllMocks(); }); const cookies = () => @@ -928,7 +960,7 @@ describe('SSR common', () => { }); afterEach(() => { - jest.resetAllMocks(); + jest.clearAllMocks(); }); for (const op of ['query', 'subscription'] as const) { @@ -938,7 +970,7 @@ describe('SSR common', () => { test('does not require `authMode` or `apiKey` override', () => { expect(() => generateClientWithAmplifyInstance({ - amplify: Amplify as any, + amplify: mockCtx as any, config: Amplify.getConfig(), }), ).not.toThrow(); @@ -946,7 +978,7 @@ describe('SSR common', () => { test('does not require `authMode` or `apiKey` override in client.graphql()', async () => { const client = generateClientWithAmplifyInstance({ - amplify: Amplify as any, + amplify: mockCtx as any, config: Amplify.getConfig(), }); @@ -961,7 +993,7 @@ describe('SSR common', () => { test('allows `authMode` override in client', async () => { const client = generateClientWithAmplifyInstance({ - amplify: Amplify as any, + amplify: mockCtx as any, config: Amplify.getConfig(), authMode: 'userPool', }); @@ -979,7 +1011,7 @@ describe('SSR common', () => { test('allows `authMode` override in `client.graphql()`', async () => { const client = generateClientWithAmplifyInstance({ - amplify: Amplify as any, + amplify: mockCtx as any, config: Amplify.getConfig(), }); @@ -997,7 +1029,7 @@ describe('SSR common', () => { test('allows `apiKey` override in `client.graphql()`', async () => { const client = generateClientWithAmplifyInstance({ - amplify: Amplify as any, + amplify: mockCtx as any, config: Amplify.getConfig(), }); @@ -1016,7 +1048,7 @@ describe('SSR common', () => { test('allows `authMode` + `apiKey` override in `client.graphql()`', async () => { const client = generateClientWithAmplifyInstance({ - amplify: Amplify as any, + amplify: mockCtx as any, config: Amplify.getConfig(), authMode: 'userPool', }); @@ -1039,9 +1071,8 @@ describe('SSR common', () => { describe(`[${opType}] with a custom endpoint`, () => { test('requires `authMode` override', () => { expect(() => - // @ts-expect-error omitting authMode for test generateClientWithAmplifyInstance({ - amplify: Amplify as any, + amplify: mockCtx as any, config: Amplify.getConfig(), endpoint: CUSTOM_ENDPOINT, }), @@ -1050,9 +1081,8 @@ describe('SSR common', () => { test("requires `apiKey` with `authMode: 'apiKey'` override in client", async () => { expect(() => - // @ts-expect-error omitting apiKey for test generateClientWithAmplifyInstance({ - amplify: Amplify as any, + amplify: mockCtx as any, config: Amplify.getConfig(), endpoint: CUSTOM_ENDPOINT, authMode: 'apiKey', @@ -1062,7 +1092,7 @@ describe('SSR common', () => { test('allows `authMode` override in client', async () => { const client = generateClientWithAmplifyInstance({ - amplify: Amplify as any, + amplify: mockCtx as any, config: Amplify.getConfig(), endpoint: CUSTOM_ENDPOINT, authMode: 'userPool', @@ -1081,7 +1111,7 @@ describe('SSR common', () => { test("allows `authMode: 'none'` override in client.graphql()", async () => { const client = generateClientWithAmplifyInstance({ - amplify: Amplify as any, + amplify: mockCtx as any, config: Amplify.getConfig(), endpoint: CUSTOM_ENDPOINT, authMode: 'none', @@ -1100,7 +1130,7 @@ describe('SSR common', () => { test("allows `authMode: 'apiKey'` + `apiKey` override in client", async () => { const client = generateClientWithAmplifyInstance({ - amplify: Amplify as any, + amplify: mockCtx as any, config: Amplify.getConfig(), endpoint: CUSTOM_ENDPOINT, authMode: 'apiKey', @@ -1121,7 +1151,7 @@ describe('SSR common', () => { test('allows `authMode` override in client.graphql()', async () => { const client = generateClientWithAmplifyInstance({ - amplify: Amplify as any, + amplify: mockCtx as any, config: {}, endpoint: CUSTOM_ENDPOINT, authMode: 'none', @@ -1143,7 +1173,7 @@ describe('SSR common', () => { // no TS expect error here. types for `generateClientWithAmplifyInstance` have been simplified // because they are not customer-facing. const client = generateClientWithAmplifyInstance({ - amplify: Amplify as any, + amplify: mockCtx as any, config: Amplify.getConfig(), endpoint: CUSTOM_ENDPOINT, authMode: 'none', @@ -1159,7 +1189,7 @@ describe('SSR common', () => { test("allows `authMode: 'apiKey'` + `apiKey` override in client.graphql()", async () => { const client = generateClientWithAmplifyInstance({ - amplify: Amplify as any, + amplify: mockCtx as any, config: Amplify.getConfig(), endpoint: CUSTOM_ENDPOINT, authMode: 'none', diff --git a/packages/api/__tests__/SSR.test.ts b/packages/api/__tests__/SSR.test.ts index 5b1b79d8dff..e8e86e7e878 100644 --- a/packages/api/__tests__/SSR.test.ts +++ b/packages/api/__tests__/SSR.test.ts @@ -9,11 +9,6 @@ jest.mock('@aws-amplify/api/internals', () => ({ generateClientWithAmplifyInstance: generateClientWithAmplifyInstanceSpy })); -const generateClientSpy = jest.fn(); -jest.mock('aws-amplify/api/server', () => ({ - generateClient: generateClientSpy -})); - const { generateServerClientUsingCookies, generateServerClientUsingReqRes, @@ -79,8 +74,8 @@ describe('SSR internals', () => { expect(client).toEqual('generateClientWithAmplifyInstance client'); }); - test('generateServerClientUsingReqRes passes through to generateClientSpy', () => { - generateClientSpy.mockReturnValue('generateClientSpy client'); + test('generateServerClientUsingReqRes passes through to generateClientWithAmplifyInstance', () => { + generateClientWithAmplifyInstanceSpy.mockReturnValue('generateClientWithAmplifyInstance client'); const options = { config: Amplify.getConfig(), @@ -98,9 +93,9 @@ describe('SSR internals', () => { const client = generateServerClientUsingReqRes(options); - expect(generateClientSpy).toHaveBeenCalledWith( + expect(generateClientWithAmplifyInstanceSpy).toHaveBeenCalledWith( expect.objectContaining(params) ); - expect(client).toEqual('generateClientSpy client'); + expect(client).toEqual('generateClientWithAmplifyInstance client'); }); }) \ No newline at end of file diff --git a/packages/api/package.json b/packages/api/package.json index 107df88f461..4b502e61181 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -51,23 +51,19 @@ ">=4.2": { "internals": [ "./dist/esm/internals/index.d.ts" - ], - "server": [ - "./dist/esm/server.d.ts" ] } }, "repository": { "type": "git", - "url": "https://github.com/aws-amplify/amplify-js.git", - "directory": "packages/api" + "url": "https://github.com/aws-amplify/amplify-js.git" }, "author": "Amazon Web Services", "license": "Apache-2.0", "bugs": { "url": "https://github.com/aws/aws-amplify/issues" }, - "homepage": "https://docs.amplify.aws/", + "homepage": "https://aws-amplify.github.io/", "devDependencies": { "@aws-amplify/core": "6.16.3", "jest-fetch-mock": "3.0.3" @@ -77,8 +73,7 @@ "dist/esm", "src", "index.*.d.ts", - "internals", - "server" + "internals" ], "dependencies": { "@aws-amplify/api-graphql": "4.8.7", @@ -88,6 +83,6 @@ "tslib": "^2.5.0" }, "peerDependencies": { - "@aws-amplify/core": "^6.16.2" + "@aws-amplify/core": "^6.16.3" } } diff --git a/packages/api/src/API.ts b/packages/api/src/API.ts index 292c0baf6bc..51acde9d19a 100644 --- a/packages/api/src/API.ts +++ b/packages/api/src/API.ts @@ -6,7 +6,7 @@ import { DefaultCommonClientOptions, generateClient as internalGenerateClient, } from '@aws-amplify/api-graphql/internals'; -import { Amplify } from '@aws-amplify/core'; +import { AmplifyContext } from '@aws-amplify/core'; /** * Generates an API client that can work with models or raw GraphQL @@ -17,9 +17,9 @@ import { Amplify } from '@aws-amplify/core'; export function generateClient< T extends Record = never, Options extends CommonPublicClientOptions = DefaultCommonClientOptions, ->(options?: Options): V6Client { +>(ctx: AmplifyContext, options?: Options): V6Client { return internalGenerateClient({ ...(options || ({} as any)), - amplify: Amplify, + amplify: ctx, }) as unknown as V6Client; } diff --git a/packages/api/src/internals/InternalAPI.ts b/packages/api/src/internals/InternalAPI.ts index e4fe7a56484..30acba193f8 100644 --- a/packages/api/src/internals/InternalAPI.ts +++ b/packages/api/src/internals/InternalAPI.ts @@ -10,7 +10,7 @@ import { OperationTypeNode, } from '@aws-amplify/api-graphql'; import { InternalGraphQLAPIClass } from '@aws-amplify/api-graphql/internals'; -import { Amplify, Cache } from '@aws-amplify/core'; +import { AmplifyContext, Cache } from '@aws-amplify/core'; import { ApiAction, Category, @@ -34,6 +34,7 @@ import { CustomHeaders } from '@aws-amplify/data-schema/runtime'; * Export Cloud Logic APIs */ export class InternalAPIClass { + private ctx: AmplifyContext; private _graphqlApi: InternalGraphQLAPIClass; Cache = Cache; @@ -41,8 +42,9 @@ export class InternalAPIClass { /** * Initialize API */ - constructor() { - this._graphqlApi = new InternalGraphQLAPIClass(); + constructor(ctx: AmplifyContext) { + this.ctx = ctx; + this._graphqlApi = new InternalGraphQLAPIClass(ctx); } public getModuleName() { @@ -89,7 +91,7 @@ export class InternalAPIClass { }; return this._graphqlApi.graphql( - Amplify, + this.ctx, options, additionalHeaders, apiUserAgentDetails, @@ -97,4 +99,5 @@ export class InternalAPIClass { } } -export const InternalAPI = new InternalAPIClass(); +export const createInternalAPI = (ctx: AmplifyContext) => + new InternalAPIClass(ctx); diff --git a/packages/api/src/internals/index.ts b/packages/api/src/internals/index.ts index f7a0fdacc78..c241e3440ef 100644 --- a/packages/api/src/internals/index.ts +++ b/packages/api/src/internals/index.ts @@ -1,12 +1,17 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -export { InternalAPI, InternalAPIClass } from './InternalAPI'; -export { generateClientWithAmplifyInstance } from '@aws-amplify/api-graphql/internals/server'; export { + createInternalAPI as InternalAPI, + InternalAPIClass, +} from './InternalAPI'; +export { + V6Client, V6ClientSSRCookies, V6ClientSSRRequest, } from '@aws-amplify/api-graphql'; export { + generateClient, + generateClientWithAmplifyInstance, CommonPublicClientOptions, DefaultCommonClientOptions, } from '@aws-amplify/api-graphql/internals'; diff --git a/packages/auth/__tests__/client/apis/associateWebAuthnCredential.test.ts b/packages/auth/__tests__/client/apis/associateWebAuthnCredential.test.ts index bae6e6ec77f..df30b539b5b 100644 --- a/packages/auth/__tests__/client/apis/associateWebAuthnCredential.test.ts +++ b/packages/auth/__tests__/client/apis/associateWebAuthnCredential.test.ts @@ -1,4 +1,3 @@ -import { Amplify, fetchAuthSession } from '@aws-amplify/core'; import { decodeJWT } from '@aws-amplify/core/internals/utils'; import { @@ -17,17 +16,13 @@ import { import { serializePkcWithAttestationToJson } from '../../../src/client/utils/passkey/serde'; import * as utils from '../../../src/client/utils'; import { getIsPasskeySupported } from '../../../src/client/utils/passkey/getIsPasskeySupported'; -import { setUpGetConfig } from '../../providers/cognito/testUtils/setUpGetConfig'; import { mockAccessToken } from '../../providers/cognito/testUtils/data'; import { assertCredentialIsPkcWithAuthenticatorAssertionResponse, assertCredentialIsPkcWithAuthenticatorAttestationResponse, } from '../../../src/client/utils/passkey/types'; +import { createMockAmplifyContext } from '../../testUtils/mockAmplifyContext'; -jest.mock('@aws-amplify/core', () => ({ - ...(jest.createMockFromModule('@aws-amplify/core') as object), - Amplify: { getConfig: jest.fn(() => ({})) }, -})); jest.mock('@aws-amplify/core/internals/utils', () => ({ ...jest.requireActual('@aws-amplify/core/internals/utils'), isBrowser: jest.fn(() => false), @@ -57,8 +52,6 @@ describe('associateWebAuthnCredential', () => { ); const registerPasskeySpy = jest.spyOn(utils, 'registerPasskey'); - const mockFetchAuthSession = jest.mocked(fetchAuthSession); - const mockGetIsPasskeySupported = jest.mocked(getIsPasskeySupported); const mockStartWebAuthnRegistration = jest.fn(); @@ -76,9 +69,18 @@ describe('associateWebAuthnCredential', () => { const mockAssertCredentialIsPkcWithAuthenticatorAttestationResponse = jest.mocked(assertCredentialIsPkcWithAuthenticatorAttestationResponse); + const mockCtx = createMockAmplifyContext({ + Auth: { + Cognito: { + userPoolClientId: '111111-aaaaa-42d8-891d-ee81a1549398', + userPoolId: 'us-west-2_zzzzz', + identityPoolId: 'us-west-2:xxxxxx', + }, + }, + }); + beforeAll(() => { - setUpGetConfig(Amplify); - mockFetchAuthSession.mockResolvedValue({ + (mockCtx.fetchAuthSession as jest.Mock).mockResolvedValue({ tokens: { accessToken: decodeJWT(mockAccessToken) }, }); mockCreateStartWebAuthnRegistrationClient.mockReturnValue( @@ -103,7 +105,7 @@ describe('associateWebAuthnCredential', () => { }); afterEach(() => { - mockFetchAuthSession.mockClear(); + (mockCtx.fetchAuthSession as jest.Mock).mockClear(); mockStartWebAuthnRegistration.mockClear(); navigatorCredentialsCreateSpy.mockClear(); }); @@ -113,7 +115,7 @@ describe('associateWebAuthnCredential', () => { CredentialCreationOptions: passkeyCredentialCreateOptions, })); - await associateWebAuthnCredential(); + await associateWebAuthnCredential(mockCtx); expect(mockStartWebAuthnRegistration).toHaveBeenCalledWith( { @@ -131,7 +133,7 @@ describe('associateWebAuthnCredential', () => { CredentialCreationOptions: passkeyCredentialCreateOptions, })); - await associateWebAuthnCredential(); + await associateWebAuthnCredential(mockCtx); expect(mockCompleteWebAuthnRegistration).toHaveBeenCalledWith( { @@ -152,7 +154,7 @@ describe('associateWebAuthnCredential', () => { CredentialCreationOptions: passkeyCredentialCreateOptions, })); - await associateWebAuthnCredential(); + await associateWebAuthnCredential(mockCtx); expect(registerPasskeySpy).toHaveBeenCalledWith( passkeyCredentialCreateOptions, @@ -169,7 +171,7 @@ describe('associateWebAuthnCredential', () => { })); try { - await associateWebAuthnCredential(); + await associateWebAuthnCredential(mockCtx); } catch (error: any) { expect(error).toBeInstanceOf(PasskeyError); expect(error.name).toBe( @@ -188,7 +190,7 @@ describe('associateWebAuthnCredential', () => { mockGetIsPasskeySupported.mockReturnValue(false); try { - await associateWebAuthnCredential(); + await associateWebAuthnCredential(mockCtx); } catch (error: any) { expect(error).toBeInstanceOf(PasskeyError); expect(error.name).toBe(PasskeyErrorCode.PasskeyNotSupported); diff --git a/packages/auth/__tests__/client/flows/userAuth/handleUserAuthFlow.test.ts b/packages/auth/__tests__/client/flows/userAuth/handleUserAuthFlow.test.ts index d6a36ae6776..bff6fa3d882 100644 --- a/packages/auth/__tests__/client/flows/userAuth/handleUserAuthFlow.test.ts +++ b/packages/auth/__tests__/client/flows/userAuth/handleUserAuthFlow.test.ts @@ -1,10 +1,14 @@ -import { Amplify } from '@aws-amplify/core'; +import { + clearGlobalContext, + setGlobalContext, +} from '@aws-amplify/core/internals/utils'; import { createInitiateAuthClient } from '../../../../src/foundation/factories/serviceClients/cognitoIdentityProvider'; import { createCognitoUserPoolEndpointResolver } from '../../../../src/providers/cognito/factories'; import { InitiateAuthCommandOutput } from '../../../../src/foundation/factories/serviceClients/cognitoIdentityProvider/types'; import { getUserContextData } from '../../../../src/providers/cognito/utils/userContextData'; import { handleUserAuthFlow } from '../../../../src/client/flows/userAuth/handleUserAuthFlow'; +import { createMockAmplifyContext } from '../../../testUtils/mockAmplifyContext'; // Mock dependencies jest.mock('@aws-amplify/core/internals/utils', () => ({ @@ -32,9 +36,7 @@ const authConfig = { }, }; -Amplify.configure({ - Auth: authConfig, -}); +setGlobalContext(createMockAmplifyContext({ Auth: authConfig })); describe('handleUserAuthFlow', () => { const mockConfig = { @@ -356,3 +358,7 @@ describe('handleUserAuthFlow', () => { ).rejects.toThrow('Auth failed'); }); }); + +afterAll(() => { + clearGlobalContext(); +}); diff --git a/packages/auth/__tests__/foundation/apis/deleteWebAuthnCredential.test.ts b/packages/auth/__tests__/foundation/apis/deleteWebAuthnCredential.test.ts index c4726e93692..22cc2a29255 100644 --- a/packages/auth/__tests__/foundation/apis/deleteWebAuthnCredential.test.ts +++ b/packages/auth/__tests__/foundation/apis/deleteWebAuthnCredential.test.ts @@ -1,23 +1,11 @@ -import { Amplify } from '@aws-amplify/core'; import { decodeJWT } from '@aws-amplify/core/internals/utils'; import { createDeleteWebAuthnCredentialClient } from '../../../src/foundation/factories/serviceClients/cognitoIdentityProvider'; import { DeleteWebAuthnCredentialInput } from '../../../src'; -import { setUpGetConfig } from '../../providers/cognito/testUtils/setUpGetConfig'; import { mockAccessToken } from '../../providers/cognito/testUtils/data'; import { deleteWebAuthnCredential } from '../../../src/foundation/apis'; +import { createMockAmplifyContext } from '../../testUtils/mockAmplifyContext'; -jest.mock('@aws-amplify/core', () => ({ - ...(jest.createMockFromModule('@aws-amplify/core') as object), - Amplify: { - getConfig: jest.fn(), - Auth: { - fetchAuthSession: jest.fn(() => ({ - tokens: { accessToken: decodeJWT(mockAccessToken) }, - })), - }, - }, -})); jest.mock('@aws-amplify/core/internals/utils', () => ({ ...jest.requireActual('@aws-amplify/core/internals/utils'), isBrowser: jest.fn(() => false), @@ -33,9 +21,21 @@ describe('deleteWebAuthnCredential', () => { createDeleteWebAuthnCredentialClient, ); - beforeAll(() => { - setUpGetConfig(Amplify); + const mockCtx = createMockAmplifyContext({ + Auth: { + Cognito: { + userPoolClientId: '111111-aaaaa-42d8-891d-ee81a1549398', + userPoolId: 'us-west-2_zzzzz', + identityPoolId: 'us-west-2:xxxxxx', + }, + }, + }); + // Override fetchAuthSession to return the mock access token + (mockCtx.fetchAuthSession as jest.Mock).mockResolvedValue({ + tokens: { accessToken: decodeJWT(mockAccessToken) }, + }); + beforeAll(() => { mockCreateDeleteWebAuthnCredentialClient.mockReturnValue( mockDeleteWebAuthnCredential, ); @@ -46,7 +46,7 @@ describe('deleteWebAuthnCredential', () => { credentialId: 'dummyId', }; - await deleteWebAuthnCredential(Amplify, input); + await deleteWebAuthnCredential(mockCtx, input); expect(mockDeleteWebAuthnCredential).toHaveBeenCalledWith( { diff --git a/packages/auth/__tests__/foundation/apis/listWebAuthnCredentials.test.ts b/packages/auth/__tests__/foundation/apis/listWebAuthnCredentials.test.ts index f0708aa06e2..41db5f5bf9c 100644 --- a/packages/auth/__tests__/foundation/apis/listWebAuthnCredentials.test.ts +++ b/packages/auth/__tests__/foundation/apis/listWebAuthnCredentials.test.ts @@ -1,24 +1,12 @@ -import { Amplify } from '@aws-amplify/core'; import { decodeJWT } from '@aws-amplify/core/internals/utils'; import { createListWebAuthnCredentialsClient } from '../../../src/foundation/factories/serviceClients/cognitoIdentityProvider'; import { ListWebAuthnCredentialsInput } from '../../../src'; import { mockUserCredentials } from '../../mockData'; -import { setUpGetConfig } from '../../providers/cognito/testUtils/setUpGetConfig'; import { mockAccessToken } from '../../providers/cognito/testUtils/data'; import { listWebAuthnCredentials } from '../../../src/foundation/apis'; +import { createMockAmplifyContext } from '../../testUtils/mockAmplifyContext'; -jest.mock('@aws-amplify/core', () => ({ - ...(jest.createMockFromModule('@aws-amplify/core') as object), - Amplify: { - getConfig: jest.fn(), - Auth: { - fetchAuthSession: jest.fn(() => ({ - tokens: { accessToken: decodeJWT(mockAccessToken) }, - })), - }, - }, -})); jest.mock('@aws-amplify/core/internals/utils', () => ({ ...jest.requireActual('@aws-amplify/core/internals/utils'), isBrowser: jest.fn(() => false), @@ -34,9 +22,21 @@ describe('listWebAuthnCredentials', () => { createListWebAuthnCredentialsClient, ); - beforeAll(() => { - setUpGetConfig(Amplify); + const mockCtx = createMockAmplifyContext({ + Auth: { + Cognito: { + userPoolClientId: '111111-aaaaa-42d8-891d-ee81a1549398', + userPoolId: 'us-west-2_zzzzz', + identityPoolId: 'us-west-2:xxxxxx', + }, + }, + }); + // Override fetchAuthSession to return the mock access token + (mockCtx.fetchAuthSession as jest.Mock).mockResolvedValue({ + tokens: { accessToken: decodeJWT(mockAccessToken) }, + }); + beforeAll(() => { mockCreateListWebAuthnCredentialsClient.mockReturnValue( mockListWebAuthnCredentials, ); @@ -53,7 +53,7 @@ describe('listWebAuthnCredentials', () => { }); it('should pass correct service options when listing credentials', async () => { - await listWebAuthnCredentials(Amplify); + await listWebAuthnCredentials(mockCtx); expect(mockListWebAuthnCredentials).toHaveBeenCalledWith( { @@ -72,7 +72,7 @@ describe('listWebAuthnCredentials', () => { }; const { credentials, nextToken } = await listWebAuthnCredentials( - Amplify, + mockCtx, input, ); @@ -117,7 +117,7 @@ describe('listWebAuthnCredentials', () => { }; const { credentials, nextToken } = await listWebAuthnCredentials( - Amplify, + mockCtx, input, ); diff --git a/packages/auth/__tests__/foundation/factories/serviceClients/cognitoIdentityProvider/shared/handler/cognitoUserPoolTransferHandler.test.ts b/packages/auth/__tests__/foundation/factories/serviceClients/cognitoIdentityProvider/shared/handler/cognitoUserPoolTransferHandler.test.ts index ea9b31a805a..88eb1d271e3 100644 --- a/packages/auth/__tests__/foundation/factories/serviceClients/cognitoIdentityProvider/shared/handler/cognitoUserPoolTransferHandler.test.ts +++ b/packages/auth/__tests__/foundation/factories/serviceClients/cognitoIdentityProvider/shared/handler/cognitoUserPoolTransferHandler.test.ts @@ -1,4 +1,4 @@ -import { Amplify } from '@aws-amplify/core'; +import { getGlobalContext, hasGlobalContext } from '@aws-amplify/core'; import { unauthenticatedHandler } from '@aws-amplify/core/internals/aws-client-utils'; import { composeTransferHandler } from '@aws-amplify/core/internals/aws-client-utils/composers'; @@ -10,6 +10,8 @@ jest.mock('@aws-amplify/core/internals/aws-client-utils/composers'); const mockComposeTransferHandler = jest.mocked(composeTransferHandler); const mockUnauthenticatedHandler = jest.mocked(unauthenticatedHandler); +const mockHasGlobalContext = jest.mocked(hasGlobalContext); +const mockGetGlobalContext = jest.mocked(getGlobalContext); describe('cognitoUserPoolTransferHandler', () => { beforeAll(() => { @@ -47,9 +49,12 @@ describe('cognitoUserPoolTransferHandler', () => { const mockHeaders = jest.fn().mockResolvedValue({ 'custom-header': 'custom-value', }); - (Amplify as any).libraryOptions = { - Auth: { headers: mockHeaders }, - }; + mockHasGlobalContext.mockReturnValue(true); + mockGetGlobalContext.mockReturnValue({ + libraryOptions: { + Auth: { headers: mockHeaders }, + }, + } as any); const [, middleware] = mockComposeTransferHandler.mock.calls[0]; const disableCacheMiddlewareFactory = middleware[0] as any; @@ -63,7 +68,7 @@ describe('cognitoUserPoolTransferHandler', () => { }); it('does not attach custom headers when not configured', async () => { - (Amplify as any).libraryOptions = {}; + mockHasGlobalContext.mockReturnValue(false); const [, middleware] = mockComposeTransferHandler.mock.calls[0]; const disableCacheMiddlewareFactory = middleware[0] as any; diff --git a/packages/auth/__tests__/providers/cognito/autoSignIn.test.ts b/packages/auth/__tests__/providers/cognito/autoSignIn.test.ts index 05389b40773..a3e93818fe1 100644 --- a/packages/auth/__tests__/providers/cognito/autoSignIn.test.ts +++ b/packages/auth/__tests__/providers/cognito/autoSignIn.test.ts @@ -1,13 +1,12 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { Amplify } from 'aws-amplify'; - import { - cognitoUserPoolsTokenProvider, - confirmSignUp, - signUp, -} from '../../../src/providers/cognito'; + clearGlobalContext, + setGlobalContext, +} from '@aws-amplify/core/internals/utils'; + +import { confirmSignUp, signUp } from '../../../src/providers/cognito'; import { autoSignIn, resetAutoSignIn, @@ -24,6 +23,7 @@ import { cacheCognitoTokens } from '../../../src/providers/cognito/tokenProvider import { dispatchSignedInHubEvent } from '../../../src/providers/cognito/utils/dispatchSignedInHubEvent'; import { handleUserAuthFlow } from '../../../src/client/flows/userAuth/handleUserAuthFlow'; import { AUTO_SIGN_IN_EXCEPTION } from '../../../src/errors/constants'; +import { createMockAmplifyContext } from '../../testUtils/mockAmplifyContext'; import { authAPITestParams } from './testUtils/authApiTestParams'; @@ -45,10 +45,9 @@ const authConfig = { userPoolId: 'us-west-2_zzzzz', }, }; -cognitoUserPoolsTokenProvider.setAuthConfig(authConfig); -Amplify.configure({ - Auth: authConfig, -}); + +const mockCtx = createMockAmplifyContext({ Auth: authConfig }); +setGlobalContext(mockCtx); const { user1 } = authAPITestParams; @@ -73,6 +72,10 @@ describe('autoSignIn()', () => { // to get around debounce on autoSignIn() APIs jest.useFakeTimers(); + afterAll(() => { + clearGlobalContext(); + }); + describe('handleUserSRPAuthFlow', () => { beforeEach(() => { mockCreateSignUpClient.mockReturnValueOnce(mockSignUp); diff --git a/packages/auth/__tests__/providers/cognito/confirmResetPassword.test.ts b/packages/auth/__tests__/providers/cognito/confirmResetPassword.test.ts index d07979c5d9c..3b07e69170a 100644 --- a/packages/auth/__tests__/providers/cognito/confirmResetPassword.test.ts +++ b/packages/auth/__tests__/providers/cognito/confirmResetPassword.test.ts @@ -1,23 +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 { AuthError } from '../../../src/errors/AuthError'; import { AuthValidationErrorCode } from '../../../src/errors/types/validation'; import { confirmResetPassword } from '../../../src/providers/cognito'; import { ConfirmForgotPasswordException } from '../../../src/providers/cognito/types/errors'; import { createConfirmForgotPasswordClient } from '../../../src/foundation/factories/serviceClients/cognitoIdentityProvider'; import { createCognitoUserPoolEndpointResolver } from '../../../src/providers/cognito/factories'; +import { createMockAmplifyContext } from '../../testUtils/mockAmplifyContext'; import { authAPITestParams } from './testUtils/authApiTestParams'; import { getMockError } from './testUtils/data'; -import { setUpGetConfig } from './testUtils/setUpGetConfig'; -jest.mock('@aws-amplify/core', () => ({ - ...(jest.createMockFromModule('@aws-amplify/core') as object), - Amplify: { getConfig: jest.fn(() => ({})) }, -})); jest.mock('@aws-amplify/core/internals/utils', () => ({ ...jest.requireActual('@aws-amplify/core/internals/utils'), isBrowser: jest.fn(() => false), @@ -37,8 +31,14 @@ describe('confirmResetPassword', () => { createCognitoUserPoolEndpointResolver, ); - beforeAll(() => { - setUpGetConfig(Amplify); + const mockCtx = createMockAmplifyContext({ + Auth: { + Cognito: { + userPoolClientId: '111111-aaaaa-42d8-891d-ee81a1549398', + userPoolId: 'us-west-2_zzzzz', + identityPoolId: 'us-west-2:xxxxxx', + }, + }, }); beforeEach(() => { @@ -58,14 +58,17 @@ describe('confirmResetPassword', () => { it('should call the confirmForgotPassword and return void', async () => { await expect( - confirmResetPassword(authAPITestParams.confirmResetPasswordRequest), + confirmResetPassword( + mockCtx, + authAPITestParams.confirmResetPasswordRequest, + ), ).resolves.toBeUndefined(); expect(mockConfirmForgotPassword).toHaveBeenCalled(); }); it('invokes createCognitoUserPoolEndpointResolver with expected endpointOverride', async () => { const expectedUserPoolEndpoint = 'https://my-custom-endpoint.com'; - jest.mocked(Amplify.getConfig).mockReturnValueOnce({ + const endpointCtx = createMockAmplifyContext({ Auth: { Cognito: { userPoolClientId: '111111-aaaaa-42d8-891d-ee81a1549398', @@ -76,7 +79,10 @@ describe('confirmResetPassword', () => { }, }); - await confirmResetPassword(authAPITestParams.confirmResetPasswordRequest); + await confirmResetPassword( + endpointCtx, + authAPITestParams.confirmResetPasswordRequest, + ); expect(mockCreateCognitoUserPoolEndpointResolver).toHaveBeenCalledWith({ endpointOverride: expectedUserPoolEndpoint, @@ -84,7 +90,7 @@ describe('confirmResetPassword', () => { }); it('should contain clientMetadata from request', async () => { - await confirmResetPassword({ + await confirmResetPassword(mockCtx, { username: 'username', newPassword: 'password', confirmationCode: 'code', @@ -107,7 +113,7 @@ describe('confirmResetPassword', () => { it('should throw an error when username is empty', async () => { expect.assertions(2); try { - await confirmResetPassword({ + await confirmResetPassword(mockCtx, { username: '', newPassword: 'password', confirmationCode: 'code', @@ -123,7 +129,7 @@ describe('confirmResetPassword', () => { it('should throw an error when newPassword is empty', async () => { expect.assertions(2); try { - await confirmResetPassword({ + await confirmResetPassword(mockCtx, { username: 'username', newPassword: '', confirmationCode: 'code', @@ -139,7 +145,7 @@ describe('confirmResetPassword', () => { it('should throw an error when confirmationCode is empty', async () => { expect.assertions(2); try { - await confirmResetPassword({ + await confirmResetPassword(mockCtx, { username: 'username', newPassword: 'password', confirmationCode: '', @@ -160,7 +166,10 @@ describe('confirmResetPassword', () => { ); }); try { - await confirmResetPassword(authAPITestParams.confirmResetPasswordRequest); + await confirmResetPassword( + mockCtx, + authAPITestParams.confirmResetPasswordRequest, + ); } catch (error: any) { expect(error).toBeInstanceOf(AuthError); expect(error.name).toBe( @@ -176,7 +185,7 @@ describe('confirmResetPassword', () => { }, }; - await confirmResetPassword({ + await confirmResetPassword(mockCtx, { username: 'username', newPassword: 'password', confirmationCode: 'code', diff --git a/packages/auth/__tests__/providers/cognito/confirmSignInErrorCases.test.ts b/packages/auth/__tests__/providers/cognito/confirmSignInErrorCases.test.ts index ce786ece3cb..a1e725a755d 100644 --- a/packages/auth/__tests__/providers/cognito/confirmSignInErrorCases.test.ts +++ b/packages/auth/__tests__/providers/cognito/confirmSignInErrorCases.test.ts @@ -1,5 +1,3 @@ -import { Amplify } from '@aws-amplify/core'; - import { AuthError } from '../../../src/errors/AuthError'; import { AuthValidationErrorCode } from '../../../src/errors/types/validation'; import { confirmSignIn } from '../../../src/providers/cognito/apis/confirmSignIn'; @@ -7,15 +5,11 @@ import { RespondToAuthChallengeException } from '../../../src/providers/cognito/ import { signInStore } from '../../../src/client/utils/store'; import { AuthErrorCodes } from '../../../src/common/AuthErrorStrings'; import { createRespondToAuthChallengeClient } from '../../../src/foundation/factories/serviceClients/cognitoIdentityProvider'; +import { createMockAmplifyContext } from '../../testUtils/mockAmplifyContext'; import { getMockError } from './testUtils/data'; -import { setUpGetConfig } from './testUtils/setUpGetConfig'; import { authAPITestParams } from './testUtils/authApiTestParams'; -jest.mock('@aws-amplify/core', () => ({ - ...(jest.createMockFromModule('@aws-amplify/core') as object), - Amplify: { getConfig: jest.fn(() => ({})) }, -})); jest.mock('../../../src/client/utils/store'); jest.mock( '../../../src/foundation/factories/serviceClients/cognitoIdentityProvider', @@ -33,8 +27,17 @@ describe('confirmSignIn API error path cases:', () => { createRespondToAuthChallengeClient, ); + const mockCtx = createMockAmplifyContext({ + Auth: { + Cognito: { + userPoolClientId: '111111-aaaaa-42d8-891d-ee81a1549398', + userPoolId: 'us-west-2_zzzzz', + identityPoolId: 'us-west-2:xxxxxx', + }, + }, + }); + beforeAll(() => { - setUpGetConfig(Amplify); mockStoreGetState.mockReturnValue({ username, challengeName, @@ -56,7 +59,7 @@ describe('confirmSignIn API error path cases:', () => { it('confirmSignIn API should throw an error when challengeResponse is empty', async () => { expect.assertions(2); try { - await confirmSignIn({ challengeResponse: '' }); + await confirmSignIn(mockCtx, { challengeResponse: '' }); } catch (error: any) { expect(error).toBeInstanceOf(AuthError); expect(error.name).toBe(AuthValidationErrorCode.EmptyChallengeResponse); @@ -66,7 +69,7 @@ describe('confirmSignIn API error path cases:', () => { it('should throw an error when sign-in step is CONTINUE_SIGN_IN_WITH_MFA_SELECTION and challengeResponse is not "SMS", "TOTP", or "EMAIL"', async () => { expect.assertions(2); try { - await confirmSignIn({ challengeResponse: 'NO_SMS' }); + await confirmSignIn(mockCtx, { challengeResponse: 'NO_SMS' }); } catch (error: any) { expect(error).toBeInstanceOf(AuthError); expect(error.name).toBe(AuthValidationErrorCode.IncorrectMFAMethod); @@ -81,7 +84,7 @@ describe('confirmSignIn API error path cases:', () => { ); }); try { - await confirmSignIn({ challengeResponse: 'TOTP' }); + await confirmSignIn(mockCtx, { challengeResponse: 'TOTP' }); } catch (error: any) { expect(error).toBeInstanceOf(AuthError); expect(error.name).toBe( @@ -99,7 +102,7 @@ describe('confirmSignIn API error path cases:', () => { }); try { - await confirmSignIn({ + await confirmSignIn(mockCtx, { challengeResponse: 'SMS', }); } catch (err: any) { diff --git a/packages/auth/__tests__/providers/cognito/confirmSignInHappyCases.test.ts b/packages/auth/__tests__/providers/cognito/confirmSignInHappyCases.test.ts index fd127aa381f..6c4e2bb9341 100644 --- a/packages/auth/__tests__/providers/cognito/confirmSignInHappyCases.test.ts +++ b/packages/auth/__tests__/providers/cognito/confirmSignInHappyCases.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 { confirmSignIn, @@ -21,6 +24,7 @@ import { createVerifySoftwareTokenClient, } from '../../../src/foundation/factories/serviceClients/cognitoIdentityProvider'; import { RespondToAuthChallengeCommandOutput } from '../../../src/foundation/factories/serviceClients/cognitoIdentityProvider/types'; +import { createMockAmplifyContext } from '../../testUtils/mockAmplifyContext'; import { authAPITestParams } from './testUtils/authApiTestParams'; @@ -37,6 +41,10 @@ const authConfig = { }, }; +const mockCtx = createMockAmplifyContext({ Auth: authConfig }); +setGlobalContext(mockCtx); +cognitoUserPoolsTokenProvider.setAuthConfig(authConfig); + // getCurrentUser is mocked so Hub is able to dispatch a mocked AuthUser // before returning an `AuthSignInResult` const mockedGetCurrentUser = jest.mocked(getCurrentUser); @@ -49,8 +57,6 @@ describe('confirmSignIn API happy path cases', () => { const mockCreateInitiateAuthClient = jest.mocked(createInitiateAuthClient); beforeEach(async () => { - cognitoUserPoolsTokenProvider.setAuthConfig(authConfig); - handleChallengeNameSpy = jest .spyOn(signInHelpers, 'handleChallengeName') .mockImplementation( @@ -84,10 +90,6 @@ describe('confirmSignIn API happy path cases', () => { }); test(`confirmSignIn test SMS_MFA ChallengeName.`, async () => { - Amplify.configure({ - Auth: authConfig, - }); - const handleUserSRPAuthflowSpy = jest .spyOn(signInHelpers, 'handleUserSRPAuthFlow') .mockImplementationOnce( @@ -140,10 +142,6 @@ describe('confirmSignIn API happy path cases', () => { }); test(`confirmSignIn with EMAIL_OTP ChallengeName`, async () => { - Amplify.configure({ - Auth: authConfig, - }); - const handleUserSRPAuthflowSpy = jest .spyOn(signInHelpers, 'handleUserSRPAuthFlow') .mockImplementationOnce( @@ -189,9 +187,6 @@ describe('confirmSignIn API happy path cases', () => { }); test(`confirmSignIn tests MFA_SETUP challengeName`, async () => { - Amplify.configure({ - Auth: authConfig, - }); const handleUserSRPAuthflowSpy = jest .spyOn(signInHelpers, 'handleUserSRPAuthFlow') .mockImplementationOnce( @@ -229,10 +224,6 @@ describe('confirmSignIn API happy path cases', () => { }); test(`confirmSignIn with SELECT_MFA_TYPE challengeName and SMS response`, async () => { - Amplify.configure({ - Auth: authConfig, - }); - const handleUserSRPAuthflowSpy = jest .spyOn(signInHelpers, 'handleUserSRPAuthFlow') .mockImplementationOnce( @@ -293,10 +284,6 @@ describe('confirmSignIn API happy path cases', () => { }); test(`confirmSignIn with SELECT_MFA_TYPE challengeName and TOTP response`, async () => { - Amplify.configure({ - Auth: authConfig, - }); - const handleUserSRPAuthflowSpy = jest .spyOn(signInHelpers, 'handleUserSRPAuthFlow') .mockImplementationOnce( @@ -347,10 +334,6 @@ describe('confirmSignIn API happy path cases', () => { }); test(`confirmSignIn with SELECT_MFA_TYPE challengeName and EMAIL response`, async () => { - Amplify.configure({ - Auth: authConfig, - }); - const handleUserSRPAuthflowSpy = jest .spyOn(signInHelpers, 'handleUserSRPAuthFlow') .mockImplementationOnce( @@ -408,10 +391,6 @@ describe('confirmSignIn API happy path cases', () => { }); test('handleChallengeName should be called with clientMetadata and usersub', async () => { - Amplify.configure({ - Auth: authConfig, - }); - const mockedUserSub = '1111-2222-3333-4444'; const activeSignInSession = '1234234232'; const activeChallengeName = 'SMS_MFA'; @@ -525,10 +504,6 @@ describe('Cognito ASF', () => { const { username } = authAPITestParams.user1; const { password } = authAPITestParams.user1; beforeEach(() => { - Amplify.configure({ - Auth: authConfig, - }); - // load Cognito ASF polyfill (window as any).AmazonCognitoAdvancedSecurityData = { getData() { @@ -645,9 +620,6 @@ describe('Cognito ASF', () => { }); test(`confirmSignIn tests MFA_SETUP sends UserContextData`, async () => { - Amplify.configure({ - Auth: authConfig, - }); jest.spyOn(signInHelpers, 'handleUserSRPAuthFlow').mockImplementationOnce( async (): Promise => ({ ChallengeName: 'SOFTWARE_TOKEN_MFA', @@ -684,9 +656,6 @@ describe('Cognito ASF', () => { }); test(`confirmSignIn tests NEW_PASSWORD_REQUIRED sends UserContextData`, async () => { - Amplify.configure({ - Auth: authConfig, - }); jest.spyOn(signInHelpers, 'handleUserSRPAuthFlow').mockImplementationOnce( async (): Promise => ({ ChallengeName: 'NEW_PASSWORD_REQUIRED', @@ -724,9 +693,6 @@ describe('Cognito ASF', () => { ); }); test(`confirmSignIn tests CUSTOM_CHALLENGE sends UserContextData`, async () => { - Amplify.configure({ - Auth: authConfig, - }); jest.spyOn(signInHelpers, 'handleUserSRPAuthFlow').mockImplementationOnce( async (): Promise => ({ ChallengeName: 'CUSTOM_CHALLENGE', @@ -769,9 +735,6 @@ describe('confirmSignIn MFA_SETUP challenge happy path cases', () => { const { username, password } = authAPITestParams.user1; test('confirmSignIn with multiple MFA_SETUP options using SOFTWARE_TOKEN_MFA', async () => { - Amplify.configure({ - Auth: authConfig, - }); jest .spyOn(signInHelpers, 'handleUserSRPAuthFlow') .mockImplementationOnce( @@ -849,10 +812,6 @@ describe('confirmSignIn MFA_SETUP challenge happy path cases', () => { }); test('confirmSignIn with multiple MFA_SETUP options using EMAIL_OTP', async () => { - Amplify.configure({ - Auth: authConfig, - }); - jest .spyOn(signInHelpers, 'handleUserSRPAuthFlow') .mockImplementationOnce( @@ -916,10 +875,6 @@ describe('confirmSignIn MFA_SETUP challenge happy path cases', () => { }); test('confirmSignIn with single MFA_SETUP option using EMAIL_OTP', async () => { - Amplify.configure({ - Auth: authConfig, - }); - jest .spyOn(signInHelpers, 'handleUserSRPAuthFlow') .mockImplementationOnce( @@ -970,9 +925,6 @@ describe('confirmSignIn MFA_SETUP challenge happy path cases', () => { }); test('confirmSignIn with single MFA_SETUP option using SOFTWARE_TOKEN_MFA', async () => { - Amplify.configure({ - Auth: authConfig, - }); jest .spyOn(signInHelpers, 'handleUserSRPAuthFlow') .mockImplementationOnce( @@ -1036,3 +988,7 @@ describe('confirmSignIn MFA_SETUP challenge happy path cases', () => { expect(confirmSignInResult.nextStep.signInStep).toBe('DONE'); }); }); + +afterAll(() => { + clearGlobalContext(); +}); diff --git a/packages/auth/__tests__/providers/cognito/confirmSignUp.test.ts b/packages/auth/__tests__/providers/cognito/confirmSignUp.test.ts index 3523f9495aa..2fc7019d292 100644 --- a/packages/auth/__tests__/providers/cognito/confirmSignUp.test.ts +++ b/packages/auth/__tests__/providers/cognito/confirmSignUp.test.ts @@ -1,8 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { Amplify } from '@aws-amplify/core'; - import { confirmSignUp } from '../../../src/providers/cognito'; import { AuthValidationErrorCode } from '../../../src/errors/types/validation'; import { AuthError } from '../../../src/errors/AuthError'; @@ -10,20 +8,16 @@ import { ConfirmSignUpException } from '../../../src/providers/cognito/types/err import { createConfirmSignUpClient } from '../../../src/foundation/factories/serviceClients/cognitoIdentityProvider'; import { createCognitoUserPoolEndpointResolver } from '../../../src/providers/cognito/factories'; import { ConfirmSignUpCommandOutput } from '../../../src/foundation/factories/serviceClients/cognitoIdentityProvider/types'; +import { createMockAmplifyContext } from '../../testUtils/mockAmplifyContext'; import { authAPITestParams } from './testUtils/authApiTestParams'; import { getMockError } from './testUtils/data'; -import { setUpGetConfig } from './testUtils/setUpGetConfig'; jest.mock( '../../../src/foundation/factories/serviceClients/cognitoIdentityProvider', ); jest.mock('../../../src/providers/cognito/factories'); -jest.mock('@aws-amplify/core', () => ({ - ...(jest.createMockFromModule('@aws-amplify/core') as object), - Amplify: { getConfig: jest.fn(() => ({})) }, -})); jest.mock('@aws-amplify/core/internals/utils', () => ({ ...jest.requireActual('@aws-amplify/core/internals/utils'), isBrowser: jest.fn(() => false), @@ -39,8 +33,14 @@ describe('confirmSignUp', () => { createCognitoUserPoolEndpointResolver, ); - beforeAll(() => { - setUpGetConfig(Amplify); + const mockCtx = createMockAmplifyContext({ + Auth: { + Cognito: { + userPoolClientId: '111111-aaaaa-42d8-891d-ee81a1549398', + userPoolId: 'us-west-2_zzzzz', + identityPoolId: 'us-west-2:xxxxxx', + }, + }, }); beforeEach(() => { @@ -55,7 +55,7 @@ describe('confirmSignUp', () => { }); it('should call confirmSignUp and return a SignUpResult', async () => { - const result = await confirmSignUp({ + const result = await confirmSignUp(mockCtx, { username: user1.username, confirmationCode, }); @@ -80,7 +80,7 @@ describe('confirmSignUp', () => { it('invokes mockCreateCognitoUserPoolEndpointResolver with expected endpointOverride', async () => { const expectedUserPoolEndpoint = 'https://my-custom-endpoint.com'; - jest.mocked(Amplify.getConfig).mockReturnValueOnce({ + const endpointCtx = createMockAmplifyContext({ Auth: { Cognito: { userPoolClientId: '111111-aaaaa-42d8-891d-ee81a1549398', @@ -91,7 +91,7 @@ describe('confirmSignUp', () => { }, }); - await confirmSignUp({ + await confirmSignUp(endpointCtx, { username: user1.username, confirmationCode, }); @@ -102,7 +102,7 @@ describe('confirmSignUp', () => { }); it('should contain force alias creation', async () => { - await confirmSignUp({ + await confirmSignUp(mockCtx, { username: user1.username, confirmationCode, options: { @@ -122,7 +122,7 @@ describe('confirmSignUp', () => { it('should contain clientMetadata from request', async () => { const clientMetadata = { data: 'abcd' }; - await confirmSignUp({ + await confirmSignUp(mockCtx, { username: user1.username, confirmationCode, options: { @@ -144,7 +144,7 @@ describe('confirmSignUp', () => { it('should throw an error when username is empty', async () => { expect.assertions(2); try { - await confirmSignUp({ username: '', confirmationCode }); + await confirmSignUp(mockCtx, { username: '', confirmationCode }); } catch (error: any) { expect(error).toBeInstanceOf(AuthError); expect(error.name).toBe( @@ -156,7 +156,10 @@ describe('confirmSignUp', () => { it('should throw an error when confirmation code is empty', async () => { expect.assertions(2); try { - await confirmSignUp({ username: user1.username, confirmationCode: '' }); + await confirmSignUp(mockCtx, { + username: user1.username, + confirmationCode: '', + }); } catch (error: any) { expect(error).toBeInstanceOf(AuthError); expect(error.name).toBe(AuthValidationErrorCode.EmptyConfirmSignUpCode); @@ -169,7 +172,10 @@ describe('confirmSignUp', () => { throw getMockError(ConfirmSignUpException.InvalidParameterException); }); try { - await confirmSignUp({ username: user1.username, confirmationCode }); + await confirmSignUp(mockCtx, { + username: user1.username, + confirmationCode, + }); } catch (error: any) { expect(error).toBeInstanceOf(AuthError); expect(error.name).toBe(ConfirmSignUpException.InvalidParameterException); @@ -182,7 +188,7 @@ describe('confirmSignUp', () => { return 'abcd'; }, }; - const result = await confirmSignUp({ + const result = await confirmSignUp(mockCtx, { username: user1.username, confirmationCode, }); diff --git a/packages/auth/__tests__/providers/cognito/confirmUserAttribute.test.ts b/packages/auth/__tests__/providers/cognito/confirmUserAttribute.test.ts index 56608241897..9d23b5379f9 100644 --- a/packages/auth/__tests__/providers/cognito/confirmUserAttribute.test.ts +++ b/packages/auth/__tests__/providers/cognito/confirmUserAttribute.test.ts @@ -1,7 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { Amplify, fetchAuthSession } from '@aws-amplify/core'; import { decodeJWT } from '@aws-amplify/core/internals/utils'; import { AuthError } from '../../../src/errors/AuthError'; @@ -10,14 +9,10 @@ import { VerifyUserAttributeException } from '../../../src/providers/cognito/typ import { AuthValidationErrorCode } from '../../../src/errors/types/validation'; import { createVerifyUserAttributeClient } from '../../../src/foundation/factories/serviceClients/cognitoIdentityProvider'; import { createCognitoUserPoolEndpointResolver } from '../../../src/providers/cognito/factories'; +import { createMockAmplifyContext } from '../../testUtils/mockAmplifyContext'; import { getMockError, mockAccessToken } from './testUtils/data'; -import { setUpGetConfig } from './testUtils/setUpGetConfig'; -jest.mock('@aws-amplify/core', () => ({ - ...(jest.createMockFromModule('@aws-amplify/core') as object), - Amplify: { getConfig: jest.fn(() => ({})) }, -})); jest.mock('@aws-amplify/core/internals/utils', () => ({ ...jest.requireActual('@aws-amplify/core/internals/utils'), isBrowser: jest.fn(() => false), @@ -30,7 +25,6 @@ jest.mock('../../../src/providers/cognito/factories'); describe('confirmUserAttribute', () => { const confirmationCode = '123456'; // assert mocks - const mockFetchAuthSession = fetchAuthSession as jest.Mock; const mockVerifyUserAttribute = jest.fn(); const mockCreateVerifyUserAttributeClient = jest.mocked( createVerifyUserAttributeClient, @@ -39,9 +33,18 @@ describe('confirmUserAttribute', () => { createCognitoUserPoolEndpointResolver, ); + const mockCtx = createMockAmplifyContext({ + Auth: { + Cognito: { + userPoolClientId: '111111-aaaaa-42d8-891d-ee81a1549398', + userPoolId: 'us-west-2_zzzzz', + identityPoolId: 'us-west-2:xxxxxx', + }, + }, + }); + beforeAll(() => { - setUpGetConfig(Amplify); - mockFetchAuthSession.mockResolvedValue({ + (mockCtx.fetchAuthSession as jest.Mock).mockResolvedValue({ tokens: { accessToken: decodeJWT(mockAccessToken) }, }); }); @@ -55,12 +58,12 @@ describe('confirmUserAttribute', () => { afterEach(() => { mockVerifyUserAttribute.mockReset(); - mockFetchAuthSession.mockClear(); + (mockCtx.fetchAuthSession as jest.Mock).mockClear(); mockCreateVerifyUserAttributeClient.mockClear(); }); it('should call the service', async () => { - await confirmUserAttribute({ + await confirmUserAttribute(mockCtx, { userAttributeKey: 'email', confirmationCode, }); @@ -77,7 +80,7 @@ describe('confirmUserAttribute', () => { it('invokes mockCreateCognitoUserPoolEndpointResolver with expected endpointOverride', async () => { const expectedUserPoolEndpoint = 'https://my-custom-endpoint.com'; - jest.mocked(Amplify.getConfig).mockReturnValueOnce({ + const endpointCtx = createMockAmplifyContext({ Auth: { Cognito: { userPoolClientId: '111111-aaaaa-42d8-891d-ee81a1549398', @@ -87,7 +90,10 @@ describe('confirmUserAttribute', () => { }, }, }); - await confirmUserAttribute({ + (endpointCtx.fetchAuthSession as jest.Mock).mockResolvedValue({ + tokens: { accessToken: decodeJWT(mockAccessToken) }, + }); + await confirmUserAttribute(endpointCtx, { userAttributeKey: 'email', confirmationCode, }); @@ -99,7 +105,7 @@ describe('confirmUserAttribute', () => { it('should throw an error when confirmationCode is not defined', async () => { try { - await confirmUserAttribute({ + await confirmUserAttribute(mockCtx, { userAttributeKey: 'email', confirmationCode: '', }); @@ -119,7 +125,7 @@ describe('confirmUserAttribute', () => { ); }); try { - await confirmUserAttribute({ + await confirmUserAttribute(mockCtx, { userAttributeKey: 'email', confirmationCode, }); diff --git a/packages/auth/__tests__/providers/cognito/deleteUser.test.ts b/packages/auth/__tests__/providers/cognito/deleteUser.test.ts index b56e9736e12..89738c9d1f6 100644 --- a/packages/auth/__tests__/providers/cognito/deleteUser.test.ts +++ b/packages/auth/__tests__/providers/cognito/deleteUser.test.ts @@ -1,7 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { Amplify, fetchAuthSession } from '@aws-amplify/core'; import { decodeJWT } from '@aws-amplify/core/internals/utils'; import { AuthError } from '../../../src/errors/AuthError'; @@ -11,14 +10,10 @@ import { DeleteUserException } from '../../../src/providers/cognito/types/errors import { signOut } from '../../../src/providers/cognito/apis/signOut'; import { createDeleteUserClient } from '../../../src/foundation/factories/serviceClients/cognitoIdentityProvider'; import { createCognitoUserPoolEndpointResolver } from '../../../src/providers/cognito/factories'; +import { createMockAmplifyContext } from '../../testUtils/mockAmplifyContext'; import { getMockError, mockAccessToken } from './testUtils/data'; -import { setUpGetConfig } from './testUtils/setUpGetConfig'; -jest.mock('@aws-amplify/core', () => ({ - ...(jest.createMockFromModule('@aws-amplify/core') as object), - Amplify: { getConfig: jest.fn(() => ({})) }, -})); jest.mock('@aws-amplify/core/internals/utils', () => ({ ...jest.requireActual('@aws-amplify/core/internals/utils'), isBrowser: jest.fn(() => false), @@ -32,7 +27,6 @@ jest.mock('../../../src/providers/cognito/factories'); describe('deleteUser', () => { // assert mocks - const mockFetchAuthSession = fetchAuthSession as jest.Mock; const mockDeleteUser = jest.fn(); const mockCreateDeleteUserClient = jest.mocked(createDeleteUserClient); const mockSignOut = signOut as jest.Mock; @@ -42,9 +36,18 @@ describe('deleteUser', () => { createCognitoUserPoolEndpointResolver, ); + const mockCtx = createMockAmplifyContext({ + Auth: { + Cognito: { + userPoolClientId: '111111-aaaaa-42d8-891d-ee81a1549398', + userPoolId: 'us-west-2_zzzzz', + identityPoolId: 'us-west-2:xxxxxx', + }, + }, + }); + beforeAll(() => { - setUpGetConfig(Amplify); - mockFetchAuthSession.mockResolvedValue({ + (mockCtx.fetchAuthSession as jest.Mock).mockResolvedValue({ tokens: { accessToken: decodeJWT(mockAccessToken) }, }); }); @@ -57,12 +60,12 @@ describe('deleteUser', () => { afterEach(() => { mockDeleteUser.mockReset(); mockClearDeviceMetadata.mockClear(); - mockFetchAuthSession.mockClear(); + (mockCtx.fetchAuthSession as jest.Mock).mockClear(); mockCreateDeleteUserClient.mockClear(); }); it('should delete user, sign out and clear device tokens', async () => { - await deleteUser(); + await deleteUser(mockCtx); expect(mockDeleteUser).toHaveBeenCalledWith( expect.objectContaining({ region: 'us-west-2' }), @@ -82,7 +85,7 @@ describe('deleteUser', () => { it('invokes mockCreateCognitoUserPoolEndpointResolver with expected endpointOverride', async () => { const expectedUserPoolEndpoint = 'https://my-custom-endpoint.com'; - jest.mocked(Amplify.getConfig).mockReturnValueOnce({ + const endpointCtx = createMockAmplifyContext({ Auth: { Cognito: { userPoolClientId: '111111-aaaaa-42d8-891d-ee81a1549398', @@ -92,7 +95,10 @@ describe('deleteUser', () => { }, }, }); - await deleteUser(); + (endpointCtx.fetchAuthSession as jest.Mock).mockResolvedValue({ + tokens: { accessToken: decodeJWT(mockAccessToken) }, + }); + await deleteUser(endpointCtx); expect(mockCreateCognitoUserPoolEndpointResolver).toHaveBeenCalledWith({ endpointOverride: expectedUserPoolEndpoint, @@ -105,7 +111,7 @@ describe('deleteUser', () => { throw getMockError(DeleteUserException.InvalidParameterException); }); try { - await deleteUser(); + await deleteUser(mockCtx); } catch (error: any) { expect(error).toBeInstanceOf(AuthError); expect(error.name).toBe(DeleteUserException.InvalidParameterException); diff --git a/packages/auth/__tests__/providers/cognito/deleteUserAttributes.test.ts b/packages/auth/__tests__/providers/cognito/deleteUserAttributes.test.ts index c791b224fdb..f6c4d035a2a 100644 --- a/packages/auth/__tests__/providers/cognito/deleteUserAttributes.test.ts +++ b/packages/auth/__tests__/providers/cognito/deleteUserAttributes.test.ts @@ -1,7 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { Amplify, fetchAuthSession } from '@aws-amplify/core'; import { decodeJWT } from '@aws-amplify/core/internals/utils'; import { AuthError } from '../../../src/errors/AuthError'; @@ -9,14 +8,10 @@ import { deleteUserAttributes } from '../../../src/providers/cognito'; import { DeleteUserAttributesException } from '../../../src/providers/cognito/types/errors'; import { createDeleteUserAttributesClient } from '../../../src/foundation/factories/serviceClients/cognitoIdentityProvider'; import { createCognitoUserPoolEndpointResolver } from '../../../src/providers/cognito/factories'; +import { createMockAmplifyContext } from '../../testUtils/mockAmplifyContext'; import { getMockError, mockAccessToken } from './testUtils/data'; -import { setUpGetConfig } from './testUtils/setUpGetConfig'; -jest.mock('@aws-amplify/core', () => ({ - ...(jest.createMockFromModule('@aws-amplify/core') as object), - Amplify: { getConfig: jest.fn(() => ({})) }, -})); jest.mock('@aws-amplify/core/internals/utils', () => ({ ...jest.requireActual('@aws-amplify/core/internals/utils'), isBrowser: jest.fn(() => false), @@ -28,7 +23,6 @@ jest.mock('../../../src/providers/cognito/factories'); describe('deleteUserAttributes', () => { // assert mocks - const mockFetchAuthSession = fetchAuthSession as jest.Mock; const mockDeleteUserAttributes = jest.fn(); const mockCreateDeleteUserAttributesClient = jest.mocked( createDeleteUserAttributesClient, @@ -37,9 +31,18 @@ describe('deleteUserAttributes', () => { createCognitoUserPoolEndpointResolver, ); + const mockCtx = createMockAmplifyContext({ + Auth: { + Cognito: { + userPoolClientId: '111111-aaaaa-42d8-891d-ee81a1549398', + userPoolId: 'us-west-2_zzzzz', + identityPoolId: 'us-west-2:xxxxxx', + }, + }, + }); + beforeAll(() => { - setUpGetConfig(Amplify); - mockFetchAuthSession.mockResolvedValue({ + (mockCtx.fetchAuthSession as jest.Mock).mockResolvedValue({ tokens: { accessToken: decodeJWT(mockAccessToken) }, }); }); @@ -53,13 +56,13 @@ describe('deleteUserAttributes', () => { afterEach(() => { mockDeleteUserAttributes.mockReset(); - mockFetchAuthSession.mockClear(); + (mockCtx.fetchAuthSession as jest.Mock).mockClear(); mockCreateDeleteUserAttributesClient.mockClear(); }); it('should delete user attributes', async () => { expect.assertions(2); - await deleteUserAttributes({ + await deleteUserAttributes(mockCtx, { userAttributeKeys: ['given_name', 'address'], }); expect(mockDeleteUserAttributes).toHaveBeenCalledWith( @@ -74,7 +77,7 @@ describe('deleteUserAttributes', () => { it('invokes mockCreateCognitoUserPoolEndpointResolver with expected endpointOverride', async () => { const expectedUserPoolEndpoint = 'https://my-custom-endpoint.com'; - jest.mocked(Amplify.getConfig).mockReturnValueOnce({ + const endpointCtx = createMockAmplifyContext({ Auth: { Cognito: { userPoolClientId: '111111-aaaaa-42d8-891d-ee81a1549398', @@ -84,7 +87,10 @@ describe('deleteUserAttributes', () => { }, }, }); - await deleteUserAttributes({ + (endpointCtx.fetchAuthSession as jest.Mock).mockResolvedValue({ + tokens: { accessToken: decodeJWT(mockAccessToken) }, + }); + await deleteUserAttributes(endpointCtx, { userAttributeKeys: ['given_name', 'address'], }); @@ -101,7 +107,7 @@ describe('deleteUserAttributes', () => { ); }); try { - await deleteUserAttributes({ + await deleteUserAttributes(mockCtx, { userAttributeKeys: ['address', 'given_name'], }); } catch (error: any) { diff --git a/packages/auth/__tests__/providers/cognito/fetchDevices.test.ts b/packages/auth/__tests__/providers/cognito/fetchDevices.test.ts index 08394a09aa9..253180f0166 100644 --- a/packages/auth/__tests__/providers/cognito/fetchDevices.test.ts +++ b/packages/auth/__tests__/providers/cognito/fetchDevices.test.ts @@ -1,7 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { Amplify, fetchAuthSession } from '@aws-amplify/core'; import { decodeJWT } from '@aws-amplify/core/internals/utils'; import { AuthError } from '../../../src/errors/AuthError'; @@ -9,14 +8,10 @@ import { fetchDevices } from '../../../src/providers/cognito'; import { ListDevicesException } from '../../../src/providers/cognito/types/errors'; import { createListDevicesClient } from '../../../src/foundation/factories/serviceClients/cognitoIdentityProvider'; import { createCognitoUserPoolEndpointResolver } from '../../../src/providers/cognito/factories'; +import { createMockAmplifyContext } from '../../testUtils/mockAmplifyContext'; import { getMockError, mockAccessToken } from './testUtils/data'; -import { setUpGetConfig } from './testUtils/setUpGetConfig'; -jest.mock('@aws-amplify/core', () => ({ - ...(jest.createMockFromModule('@aws-amplify/core') as object), - Amplify: { getConfig: jest.fn(() => ({})) }, -})); jest.mock('@aws-amplify/core/internals/utils', () => ({ ...jest.requireActual('@aws-amplify/core/internals/utils'), isBrowser: jest.fn(() => false), @@ -52,16 +47,24 @@ describe('fetchDevices', () => { lastAuthenticatedDate: date, }; // assert mocks - const mockFetchAuthSession = fetchAuthSession as jest.Mock; const mockListDevices = jest.fn(); const mockCreateListDevicesClient = jest.mocked(createListDevicesClient); const mockCreateCognitoUserPoolEndpointResolver = jest.mocked( createCognitoUserPoolEndpointResolver, ); + const mockCtx = createMockAmplifyContext({ + Auth: { + Cognito: { + userPoolClientId: '111111-aaaaa-42d8-891d-ee81a1549398', + userPoolId: 'us-west-2_zzzzz', + identityPoolId: 'us-west-2:xxxxxx', + }, + }, + }); + beforeAll(() => { - setUpGetConfig(Amplify); - mockFetchAuthSession.mockResolvedValue({ + (mockCtx.fetchAuthSession as jest.Mock).mockResolvedValue({ tokens: { accessToken: decodeJWT(mockAccessToken) }, }); }); @@ -76,7 +79,7 @@ describe('fetchDevices', () => { afterEach(() => { mockListDevices.mockReset(); - mockFetchAuthSession.mockClear(); + (mockCtx.fetchAuthSession as jest.Mock).mockClear(); mockCreateListDevicesClient.mockClear(); }); @@ -88,7 +91,7 @@ describe('fetchDevices', () => { createDate, lastAuthenticatedDate, lastModifiedDate, - } = (await fetchDevices())[0]; + } = (await fetchDevices(mockCtx))[0]; expect(id).toEqual(apiOutputDevice.id); expect(name).toEqual(apiOutputDevice.name); expect(attributes).toEqual(apiOutputDevice.attributes); @@ -110,7 +113,7 @@ describe('fetchDevices', () => { it('invokes mockCreateCognitoUserPoolEndpointResolver with expected endpointOverride', async () => { const expectedUserPoolEndpoint = 'https://my-custom-endpoint.com'; - jest.mocked(Amplify.getConfig).mockReturnValueOnce({ + const endpointCtx = createMockAmplifyContext({ Auth: { Cognito: { userPoolClientId: '111111-aaaaa-42d8-891d-ee81a1549398', @@ -120,7 +123,10 @@ describe('fetchDevices', () => { }, }, }); - await fetchDevices(); + (endpointCtx.fetchAuthSession as jest.Mock).mockResolvedValue({ + tokens: { accessToken: decodeJWT(mockAccessToken) }, + }); + await fetchDevices(endpointCtx); expect(mockCreateCognitoUserPoolEndpointResolver).toHaveBeenCalledWith({ endpointOverride: expectedUserPoolEndpoint, @@ -133,7 +139,7 @@ describe('fetchDevices', () => { throw getMockError(ListDevicesException.InvalidParameterException); }); try { - await fetchDevices(); + await fetchDevices(mockCtx); } catch (error: any) { expect(error).toBeInstanceOf(AuthError); expect(error.name).toBe(ListDevicesException.InvalidParameterException); diff --git a/packages/auth/__tests__/providers/cognito/fetchMFAPreference.test.ts b/packages/auth/__tests__/providers/cognito/fetchMFAPreference.test.ts index 18dba7d80f0..bdfc39c88f9 100644 --- a/packages/auth/__tests__/providers/cognito/fetchMFAPreference.test.ts +++ b/packages/auth/__tests__/providers/cognito/fetchMFAPreference.test.ts @@ -1,7 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { Amplify, fetchAuthSession } from '@aws-amplify/core'; import { decodeJWT } from '@aws-amplify/core/internals/utils'; import { AuthError } from '../../../src/errors/AuthError'; @@ -9,14 +8,10 @@ import { fetchMFAPreference } from '../../../src/providers/cognito/apis/fetchMFA import { GetUserException } from '../../../src/providers/cognito/types/errors'; import { createGetUserClient } from '../../../src/foundation/factories/serviceClients/cognitoIdentityProvider'; import { createCognitoUserPoolEndpointResolver } from '../../../src/providers/cognito/factories'; +import { createMockAmplifyContext } from '../../testUtils/mockAmplifyContext'; import { getMockError, mockAccessToken } from './testUtils/data'; -import { setUpGetConfig } from './testUtils/setUpGetConfig'; -jest.mock('@aws-amplify/core', () => ({ - ...(jest.createMockFromModule('@aws-amplify/core') as object), - Amplify: { getConfig: jest.fn(() => ({})) }, -})); jest.mock( '../../../src/foundation/factories/serviceClients/cognitoIdentityProvider', ); @@ -24,16 +19,24 @@ jest.mock('../../../src/providers/cognito/factories'); describe('fetchMFAPreference', () => { // assert mocks - const mockFetchAuthSession = jest.mocked(fetchAuthSession); const mockGetUser = jest.fn(); const mockCreateGetUserClient = jest.mocked(createGetUserClient); const mockCreateCognitoUserPoolEndpointResolver = jest.mocked( createCognitoUserPoolEndpointResolver, ); + const mockCtx = createMockAmplifyContext({ + Auth: { + Cognito: { + userPoolClientId: '111111-aaaaa-42d8-891d-ee81a1549398', + userPoolId: 'us-west-2_zzzzz', + identityPoolId: 'us-west-2:xxxxxx', + }, + }, + }); + beforeAll(() => { - setUpGetConfig(Amplify); - mockFetchAuthSession.mockResolvedValue({ + (mockCtx.fetchAuthSession as jest.Mock).mockResolvedValue({ tokens: { accessToken: decodeJWT(mockAccessToken) }, }); mockCreateGetUserClient.mockReturnValue(mockGetUser); @@ -41,7 +44,7 @@ describe('fetchMFAPreference', () => { afterEach(() => { mockGetUser.mockReset(); - mockFetchAuthSession.mockClear(); + (mockCtx.fetchAuthSession as jest.Mock).mockClear(); }); it('should return correct MFA preferences when SMS is preferred', async () => { @@ -52,7 +55,7 @@ describe('fetchMFAPreference', () => { UserMFASettingList: ['SMS_MFA', 'SOFTWARE_TOKEN_MFA', 'EMAIL_OTP'], $metadata: {}, }); - const resp = await fetchMFAPreference(); + const resp = await fetchMFAPreference(mockCtx); expect(resp).toEqual({ preferred: 'SMS', enabled: ['SMS', 'TOTP', 'EMAIL'], @@ -67,7 +70,7 @@ describe('fetchMFAPreference', () => { UserMFASettingList: ['SMS_MFA', 'SOFTWARE_TOKEN_MFA', 'EMAIL_OTP'], $metadata: {}, }); - const resp = await fetchMFAPreference(); + const resp = await fetchMFAPreference(mockCtx); expect(resp).toEqual({ preferred: 'EMAIL', enabled: ['SMS', 'TOTP', 'EMAIL'], @@ -81,7 +84,7 @@ describe('fetchMFAPreference', () => { UserMFASettingList: ['SMS_MFA', 'SOFTWARE_TOKEN_MFA', 'EMAIL_OTP'], $metadata: {}, }); - const resp = await fetchMFAPreference(); + const resp = await fetchMFAPreference(mockCtx); expect(resp).toEqual({ preferred: 'TOTP', enabled: ['SMS', 'TOTP', 'EMAIL'], @@ -94,7 +97,7 @@ describe('fetchMFAPreference', () => { UserMFASettingList: ['SMS_MFA', 'SOFTWARE_TOKEN_MFA', 'EMAIL_OTP'], $metadata: {}, }); - const resp = await fetchMFAPreference(); + const resp = await fetchMFAPreference(mockCtx); expect(resp).toEqual({ enabled: ['SMS', 'TOTP', 'EMAIL'], }); @@ -105,13 +108,13 @@ describe('fetchMFAPreference', () => { Username: 'XXXXXXXX', $metadata: {}, }); - const resp = await fetchMFAPreference(); + const resp = await fetchMFAPreference(mockCtx); expect(resp).toEqual({}); }); it('invokes mockCreateCognitoUserPoolEndpointResolver with expected endpointOverride', async () => { const expectedUserPoolEndpoint = 'https://my-custom-endpoint.com'; - jest.mocked(Amplify.getConfig).mockReturnValueOnce({ + const endpointCtx = createMockAmplifyContext({ Auth: { Cognito: { userPoolClientId: '111111-aaaaa-42d8-891d-ee81a1549398', @@ -121,6 +124,9 @@ describe('fetchMFAPreference', () => { }, }, }); + (endpointCtx.fetchAuthSession as jest.Mock).mockResolvedValue({ + tokens: { accessToken: decodeJWT(mockAccessToken) }, + }); mockGetUser.mockResolvedValueOnce({ UserAttributes: [], @@ -130,7 +136,7 @@ describe('fetchMFAPreference', () => { $metadata: {}, }); - await fetchMFAPreference(); + await fetchMFAPreference(endpointCtx); expect(mockCreateCognitoUserPoolEndpointResolver).toHaveBeenCalledWith({ endpointOverride: expectedUserPoolEndpoint, @@ -143,7 +149,7 @@ describe('fetchMFAPreference', () => { throw getMockError(GetUserException.InvalidParameterException); }); try { - await fetchMFAPreference(); + await fetchMFAPreference(mockCtx); } catch (error: any) { expect(error).toBeInstanceOf(AuthError); expect(error.name).toBe(GetUserException.InvalidParameterException); diff --git a/packages/auth/__tests__/providers/cognito/fetchUserAttributes.test.ts b/packages/auth/__tests__/providers/cognito/fetchUserAttributes.test.ts index 87cf79e715d..fd2b7a37559 100644 --- a/packages/auth/__tests__/providers/cognito/fetchUserAttributes.test.ts +++ b/packages/auth/__tests__/providers/cognito/fetchUserAttributes.test.ts @@ -1,26 +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 { decodeJWT, fetchAuthSession } from '@aws-amplify/core/internals/utils'; +import { decodeJWT } from '@aws-amplify/core/internals/utils'; import { AuthError } from '../../../src/errors/AuthError'; import { GetUserException } from '../../../src/providers/cognito/types/errors'; import { fetchUserAttributes } from '../../../src/providers/cognito/apis/fetchUserAttributes'; import { createGetUserClient } from '../../../src/foundation/factories/serviceClients/cognitoIdentityProvider'; import { createCognitoUserPoolEndpointResolver } from '../../../src/providers/cognito/factories'; +import { createMockAmplifyContext } from '../../testUtils/mockAmplifyContext'; import { getMockError, mockAccessToken } from './testUtils/data'; -import { setUpGetConfig } from './testUtils/setUpGetConfig'; -jest.mock('@aws-amplify/core', () => ({ - ...(jest.createMockFromModule('@aws-amplify/core') as object), - Amplify: { getConfig: jest.fn(() => ({})) }, -})); -jest.mock('@aws-amplify/core/internals/utils', () => ({ - ...jest.requireActual('@aws-amplify/core/internals/utils'), - fetchAuthSession: jest.fn(), -})); jest.mock( '../../../src/foundation/factories/serviceClients/cognitoIdentityProvider', ); @@ -28,16 +19,24 @@ jest.mock('../../../src/providers/cognito/factories'); describe('fetchUserAttributes', () => { // assert mocks - const mockFetchAuthSession = fetchAuthSession as jest.Mock; const mockGetUser = jest.fn(); const mockCreateGetUserClient = jest.mocked(createGetUserClient); const mockCreateCognitoUserPoolEndpointResolver = jest.mocked( createCognitoUserPoolEndpointResolver, ); + const mockCtx = createMockAmplifyContext({ + Auth: { + Cognito: { + userPoolClientId: '111111-aaaaa-42d8-891d-ee81a1549398', + userPoolId: 'us-west-2_zzzzz', + identityPoolId: 'us-west-2:xxxxxx', + }, + }, + }); + beforeAll(() => { - setUpGetConfig(Amplify); - mockFetchAuthSession.mockResolvedValue({ + (mockCtx.fetchAuthSession as jest.Mock).mockResolvedValue({ tokens: { accessToken: decodeJWT(mockAccessToken) }, }); }); @@ -58,12 +57,12 @@ describe('fetchUserAttributes', () => { afterEach(() => { mockGetUser.mockReset(); - mockFetchAuthSession.mockClear(); + (mockCtx.fetchAuthSession as jest.Mock).mockClear(); mockCreateGetUserClient.mockClear(); }); it('should return the current user attributes into a map format', async () => { - expect(await fetchUserAttributes()).toEqual({ + expect(await fetchUserAttributes(mockCtx)).toEqual({ email: 'XXXXXXXXXXXXX', phone_number: '000000000000000', }); @@ -81,7 +80,7 @@ describe('fetchUserAttributes', () => { it('invokes mockCreateCognitoUserPoolEndpointResolver with expected endpointOverride', async () => { const expectedUserPoolEndpoint = 'https://my-custom-endpoint.com'; - jest.mocked(Amplify.getConfig).mockReturnValueOnce({ + const endpointCtx = createMockAmplifyContext({ Auth: { Cognito: { userPoolClientId: '111111-aaaaa-42d8-891d-ee81a1549398', @@ -91,7 +90,10 @@ describe('fetchUserAttributes', () => { }, }, }); - await fetchUserAttributes(); + (endpointCtx.fetchAuthSession as jest.Mock).mockResolvedValue({ + tokens: { accessToken: decodeJWT(mockAccessToken) }, + }); + await fetchUserAttributes(endpointCtx); expect(mockCreateCognitoUserPoolEndpointResolver).toHaveBeenCalledWith({ endpointOverride: expectedUserPoolEndpoint, @@ -103,14 +105,14 @@ describe('fetchUserAttributes', () => { mockGetUser.mockImplementation(() => { throw getMockError(GetUserException.InvalidParameterException); }); - mockFetchAuthSession.mockResolvedValueOnce({ + (mockCtx.fetchAuthSession as jest.Mock).mockResolvedValueOnce({ tokens: { accessToken: decodeJWT(mockAccessToken), }, }); try { - await fetchUserAttributes(); + await fetchUserAttributes(mockCtx); } catch (error: any) { expect(error).toBeInstanceOf(AuthError); expect(error.name).toBe(GetUserException.InvalidParameterException); diff --git a/packages/auth/__tests__/providers/cognito/forgetDevice.test.ts b/packages/auth/__tests__/providers/cognito/forgetDevice.test.ts index cc0a2d37407..f93ba8231be 100644 --- a/packages/auth/__tests__/providers/cognito/forgetDevice.test.ts +++ b/packages/auth/__tests__/providers/cognito/forgetDevice.test.ts @@ -1,7 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { Amplify, fetchAuthSession } from '@aws-amplify/core'; import { decodeJWT } from '@aws-amplify/core/internals/utils'; import { AuthError } from '../../../src/errors/AuthError'; @@ -11,14 +10,10 @@ import { ForgetDeviceException } from '../../../src/providers/cognito/types/erro import { tokenOrchestrator } from '../../../src/providers/cognito/tokenProvider'; import { createForgetDeviceClient } from '../../../src/foundation/factories/serviceClients/cognitoIdentityProvider'; import { createCognitoUserPoolEndpointResolver } from '../../../src/providers/cognito/factories'; +import { createMockAmplifyContext } from '../../testUtils/mockAmplifyContext'; import { getMockError, mockAccessToken } from './testUtils/data'; -import { setUpGetConfig } from './testUtils/setUpGetConfig'; -jest.mock('@aws-amplify/core', () => ({ - ...(jest.createMockFromModule('@aws-amplify/core') as object), - Amplify: { getConfig: jest.fn(() => ({})) }, -})); jest.mock('@aws-amplify/core/internals/utils', () => ({ ...jest.requireActual('@aws-amplify/core/internals/utils'), isBrowser: jest.fn(() => false), @@ -36,7 +31,6 @@ describe('fetchMFAPreference', () => { randomPassword: 'randomPassword', }; // assert mocks - const mockFetchAuthSession = fetchAuthSession as jest.Mock; const mockForgetDevice = jest.fn(); const mockCreateForgetDeviceClient = jest.mocked(createForgetDeviceClient); const mockClearDeviceMetadata = @@ -47,9 +41,18 @@ describe('fetchMFAPreference', () => { createCognitoUserPoolEndpointResolver, ); + const mockCtx = createMockAmplifyContext({ + Auth: { + Cognito: { + userPoolClientId: '111111-aaaaa-42d8-891d-ee81a1549398', + userPoolId: 'us-west-2_zzzzz', + identityPoolId: 'us-west-2:xxxxxx', + }, + }, + }); + beforeAll(() => { - setUpGetConfig(Amplify); - mockFetchAuthSession.mockResolvedValue({ + (mockCtx.fetchAuthSession as jest.Mock).mockResolvedValue({ tokens: { accessToken: decodeJWT(mockAccessToken) }, }); }); @@ -63,14 +66,14 @@ describe('fetchMFAPreference', () => { afterEach(() => { mockForgetDevice.mockReset(); mockGetDeviceMetadata.mockReset(); - mockFetchAuthSession.mockClear(); + (mockCtx.fetchAuthSession as jest.Mock).mockClear(); mockClearDeviceMetadata.mockClear(); mockCreateForgetDeviceClient.mockClear(); }); it(`should forget 'external device' 'with' inputParams when tokenStore deviceMetadata 'present'`, async () => { expect.assertions(3); - await forgetDevice({ device: { id: 'externalDeviceKey' } }); + await forgetDevice(mockCtx, { device: { id: 'externalDeviceKey' } }); expect(mockForgetDevice).toHaveBeenCalledWith( expect.objectContaining({ region: 'us-west-2' }), expect.objectContaining({ @@ -84,7 +87,7 @@ describe('fetchMFAPreference', () => { it('invokes mockCreateCognitoUserPoolEndpointResolver with expected endpointOverride', async () => { const expectedUserPoolEndpoint = 'https://my-custom-endpoint.com'; - jest.mocked(Amplify.getConfig).mockReturnValueOnce({ + const endpointCtx = createMockAmplifyContext({ Auth: { Cognito: { userPoolClientId: '111111-aaaaa-42d8-891d-ee81a1549398', @@ -94,7 +97,10 @@ describe('fetchMFAPreference', () => { }, }, }); - await forgetDevice({ device: { id: 'externalDeviceKey' } }); + (endpointCtx.fetchAuthSession as jest.Mock).mockResolvedValue({ + tokens: { accessToken: decodeJWT(mockAccessToken) }, + }); + await forgetDevice(endpointCtx, { device: { id: 'externalDeviceKey' } }); expect(mockCreateCognitoUserPoolEndpointResolver).toHaveBeenCalledWith({ endpointOverride: expectedUserPoolEndpoint, @@ -103,7 +109,9 @@ describe('fetchMFAPreference', () => { it(`should forget 'current device' 'with' inputParams when tokenStore deviceMetadata 'present'`, async () => { expect.assertions(3); - await forgetDevice({ device: { id: mockDeviceMetadata.deviceKey } }); + await forgetDevice(mockCtx, { + device: { id: mockDeviceMetadata.deviceKey }, + }); expect(mockForgetDevice).toHaveBeenCalledWith( expect.objectContaining({ region: 'us-west-2' }), expect.objectContaining({ @@ -117,7 +125,7 @@ describe('fetchMFAPreference', () => { it(`should forget 'current device' 'without' inputParams when tokenStore deviceMetadata 'present'`, async () => { expect.assertions(3); - await forgetDevice(); + await forgetDevice(mockCtx); expect(mockForgetDevice).toHaveBeenCalledWith( expect.objectContaining({ region: 'us-west-2' }), expect.objectContaining({ @@ -131,7 +139,7 @@ describe('fetchMFAPreference', () => { it(`should forget 'external device' 'with' inputParams when tokenStore deviceMetadata 'not present'`, async () => { mockGetDeviceMetadata.mockResolvedValue(null); - await forgetDevice({ device: { id: 'externalDeviceKey' } }); + await forgetDevice(mockCtx, { device: { id: 'externalDeviceKey' } }); expect(mockForgetDevice).toHaveBeenCalledWith( expect.objectContaining({ region: 'us-west-2' }), expect.objectContaining({ @@ -146,7 +154,9 @@ describe('fetchMFAPreference', () => { it(`should forget 'current device' 'with' inputParams when tokenStore deviceMetadata 'not present'`, async () => { mockGetDeviceMetadata.mockResolvedValue(null); expect.assertions(3); - await forgetDevice({ device: { id: mockDeviceMetadata.deviceKey } }); + await forgetDevice(mockCtx, { + device: { id: mockDeviceMetadata.deviceKey }, + }); expect(mockForgetDevice).toHaveBeenCalledWith( expect.objectContaining({ region: 'us-west-2' }), expect.objectContaining({ @@ -162,7 +172,7 @@ describe('fetchMFAPreference', () => { mockGetDeviceMetadata.mockResolvedValue(null); expect.assertions(2); try { - await forgetDevice(); + await forgetDevice(mockCtx); } catch (error: any) { expect(error).toBeInstanceOf(AuthError); expect(error.name).toBe(DEVICE_METADATA_NOT_FOUND_EXCEPTION); @@ -176,7 +186,9 @@ describe('fetchMFAPreference', () => { throw getMockError(ForgetDeviceException.InvalidParameterException); }); try { - await forgetDevice({ device: { id: mockDeviceMetadata.deviceKey } }); + await forgetDevice(mockCtx, { + device: { id: mockDeviceMetadata.deviceKey }, + }); } catch (error: any) { expect(error).toBeInstanceOf(AuthError); expect(error.name).toBe(ForgetDeviceException.InvalidParameterException); diff --git a/packages/auth/__tests__/providers/cognito/getCurrentUser.test.ts b/packages/auth/__tests__/providers/cognito/getCurrentUser.test.ts index 1860c2dccd2..075a001e07b 100644 --- a/packages/auth/__tests__/providers/cognito/getCurrentUser.test.ts +++ b/packages/auth/__tests__/providers/cognito/getCurrentUser.test.ts @@ -1,20 +1,15 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { Amplify } from '@aws-amplify/core'; import { decodeJWT } from '@aws-amplify/core/internals/utils'; import { AuthError } from '../../../src/errors/AuthError'; import { getCurrentUser } from '../../../src/providers/cognito'; import { USER_UNAUTHENTICATED_EXCEPTION } from '../../../src/errors/constants'; +import { createMockAmplifyContext } from '../../testUtils/mockAmplifyContext'; import { mockAccessToken } from './testUtils/data'; -import { setUpGetConfig } from './testUtils/setUpGetConfig'; -jest.mock('@aws-amplify/core', () => ({ - ...(jest.createMockFromModule('@aws-amplify/core') as object), - Amplify: { Auth: { getTokens: jest.fn() }, getConfig: jest.fn(() => ({})) }, -})); jest.mock('@aws-amplify/core/internals/utils', () => ({ ...jest.requireActual('@aws-amplify/core/internals/utils'), isBrowser: jest.fn(() => false), @@ -23,15 +18,19 @@ jest.mock('@aws-amplify/core/internals/utils', () => ({ describe('getCurrentUser', () => { const mockedSub = 'mockedSub'; const mockedUsername = 'XXXXXXXXXXXXXX'; - // assert mocks - const mockGetTokensFunction = Amplify.Auth.getTokens as jest.Mock; - beforeAll(() => { - setUpGetConfig(Amplify); + const mockCtx = createMockAmplifyContext({ + Auth: { + Cognito: { + userPoolClientId: '111111-aaaaa-42d8-891d-ee81a1549398', + userPoolId: 'us-west-2_zzzzz', + identityPoolId: 'us-west-2:xxxxxx', + }, + }, }); beforeEach(() => { - mockGetTokensFunction.mockResolvedValue({ + (mockCtx.getTokens as jest.Mock).mockResolvedValue({ accessToken: decodeJWT(mockAccessToken), idToken: { payload: { @@ -47,11 +46,11 @@ describe('getCurrentUser', () => { }); afterEach(() => { - mockGetTokensFunction.mockReset(); + (mockCtx.getTokens as jest.Mock).mockReset(); }); it('should get current user', async () => { - const result = await getCurrentUser(); + const result = await getCurrentUser(mockCtx); expect(result).toEqual({ username: mockedUsername, userId: mockedSub, @@ -63,9 +62,9 @@ describe('getCurrentUser', () => { }); it('should throw an error when tokens are not found', async () => { - mockGetTokensFunction.mockResolvedValue(undefined); + (mockCtx.getTokens as jest.Mock).mockResolvedValue(undefined); try { - await getCurrentUser(); + await getCurrentUser(mockCtx); } catch (error: any) { expect(error).toBeInstanceOf(AuthError); expect(error.name).toBe(USER_UNAUTHENTICATED_EXCEPTION); diff --git a/packages/auth/__tests__/providers/cognito/getNewDeviceMetadata.test.ts b/packages/auth/__tests__/providers/cognito/getNewDeviceMetadata.test.ts index f05c4f8c603..c585119177f 100644 --- a/packages/auth/__tests__/providers/cognito/getNewDeviceMetadata.test.ts +++ b/packages/auth/__tests__/providers/cognito/getNewDeviceMetadata.test.ts @@ -1,13 +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 { + clearGlobalContext, + setGlobalContext, +} from '@aws-amplify/core/internals/utils'; import { AuthError } from '../../../src/errors/AuthError'; import { ConfirmDeviceException } from '../../../src/providers/cognito/types/errors'; import { getNewDeviceMetadata } from '../../../src/providers/cognito/utils/getNewDeviceMetadata'; import { createCognitoUserPoolEndpointResolver } from '../../../src/providers/cognito/factories'; import { createConfirmDeviceClient } from '../../../src/foundation/factories/serviceClients/cognitoIdentityProvider'; +import { createMockAmplifyContext } from '../../testUtils/mockAmplifyContext'; jest.mock('../../../src/providers/cognito/factories'); jest.mock( @@ -16,15 +20,15 @@ jest.mock( const userPoolId = 'us-west-2_zzzzz'; -Amplify.configure({ - Auth: { - Cognito: { - userPoolClientId: '111111-aaaaa-42d8-891d-ee81a1549398', - userPoolId, - identityPoolId: 'us-west-2:xxxxxx', - }, +const authConfig = { + Cognito: { + userPoolClientId: '111111-aaaaa-42d8-891d-ee81a1549398', + userPoolId, + identityPoolId: 'us-west-2:xxxxxx', }, -}); +}; + +setGlobalContext(createMockAmplifyContext({ Auth: authConfig })); const mockedAccessToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c'; @@ -120,3 +124,7 @@ describe('test getNewDeviceMetadata API', () => { }); }); }); + +afterAll(() => { + clearGlobalContext(); +}); diff --git a/packages/auth/__tests__/providers/cognito/rememberDevice.test.ts b/packages/auth/__tests__/providers/cognito/rememberDevice.test.ts index 0521e928654..1941c1ef14e 100644 --- a/packages/auth/__tests__/providers/cognito/rememberDevice.test.ts +++ b/packages/auth/__tests__/providers/cognito/rememberDevice.test.ts @@ -2,7 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 import { decodeJWT } from '@aws-amplify/core/internals/utils'; -import { Amplify, fetchAuthSession } from '@aws-amplify/core'; import { AuthError } from '../../../src/errors/AuthError'; import { rememberDevice } from '../../../src/providers/cognito'; @@ -11,14 +10,10 @@ import { tokenOrchestrator } from '../../../src/providers/cognito/tokenProvider' import { DeviceMetadata } from '../../../src/providers/cognito/tokenProvider/types'; import { createUpdateDeviceStatusClient } from '../../../src/foundation/factories/serviceClients/cognitoIdentityProvider'; import { createCognitoUserPoolEndpointResolver } from '../../../src/providers/cognito/factories'; +import { createMockAmplifyContext } from '../../testUtils/mockAmplifyContext'; import { getMockError, mockAccessToken } from './testUtils/data'; -import { setUpGetConfig } from './testUtils/setUpGetConfig'; -jest.mock('@aws-amplify/core', () => ({ - ...(jest.createMockFromModule('@aws-amplify/core') as object), - Amplify: { getConfig: jest.fn(() => ({})) }, -})); jest.mock('@aws-amplify/core/internals/utils', () => ({ ...jest.requireActual('@aws-amplify/core/internals/utils'), isBrowser: jest.fn(() => false), @@ -36,7 +31,6 @@ describe('rememberDevice', () => { randomPassword: 'randomPassword', }; // assert mocks - const mockFetchAuthSession = fetchAuthSession as jest.Mock; const mockUpdateDeviceStatus = jest.fn(); const mockCreateUpdateDeviceStatusClient = jest.mocked( createUpdateDeviceStatusClient, @@ -47,9 +41,18 @@ describe('rememberDevice', () => { const mockGetDeviceMetadata = tokenOrchestrator.getDeviceMetadata as jest.Mock; + const mockCtx = createMockAmplifyContext({ + Auth: { + Cognito: { + userPoolClientId: '111111-aaaaa-42d8-891d-ee81a1549398', + userPoolId: 'us-west-2_zzzzz', + identityPoolId: 'us-west-2:xxxxxx', + }, + }, + }); + beforeAll(() => { - setUpGetConfig(Amplify); - mockFetchAuthSession.mockResolvedValue({ + (mockCtx.fetchAuthSession as jest.Mock).mockResolvedValue({ tokens: { accessToken: decodeJWT(mockAccessToken) }, }); }); @@ -65,13 +68,13 @@ describe('rememberDevice', () => { afterEach(() => { mockGetDeviceMetadata.mockReset(); mockUpdateDeviceStatus.mockReset(); - mockFetchAuthSession.mockClear(); + (mockCtx.fetchAuthSession as jest.Mock).mockClear(); mockCreateUpdateDeviceStatusClient.mockClear(); }); it('should call updateDeviceStatus client with correct request', async () => { expect.assertions(2); - await rememberDevice(); + await rememberDevice(mockCtx); expect(mockUpdateDeviceStatus).toHaveBeenCalledWith( expect.objectContaining({ region: 'us-west-2' }), expect.objectContaining({ @@ -85,7 +88,7 @@ describe('rememberDevice', () => { it('invokes mockCreateCognitoUserPoolEndpointResolver with expected endpointOverride', async () => { const expectedUserPoolEndpoint = 'https://my-custom-endpoint.com'; - jest.mocked(Amplify.getConfig).mockReturnValueOnce({ + const endpointCtx = createMockAmplifyContext({ Auth: { Cognito: { userPoolClientId: '111111-aaaaa-42d8-891d-ee81a1549398', @@ -95,7 +98,10 @@ describe('rememberDevice', () => { }, }, }); - await rememberDevice(); + (endpointCtx.fetchAuthSession as jest.Mock).mockResolvedValue({ + tokens: { accessToken: decodeJWT(mockAccessToken) }, + }); + await rememberDevice(endpointCtx); expect(mockCreateCognitoUserPoolEndpointResolver).toHaveBeenCalledWith({ endpointOverride: expectedUserPoolEndpoint, @@ -108,7 +114,7 @@ describe('rememberDevice', () => { throw getMockError(UpdateDeviceStatusException.InvalidParameterException); }); try { - await rememberDevice(); + await rememberDevice(mockCtx); } catch (error: any) { expect(error).toBeInstanceOf(AuthError); expect(error.name).toBe( diff --git a/packages/auth/__tests__/providers/cognito/resendSignUpCode.test.ts b/packages/auth/__tests__/providers/cognito/resendSignUpCode.test.ts index d351a950484..abd1a30756f 100644 --- a/packages/auth/__tests__/providers/cognito/resendSignUpCode.test.ts +++ b/packages/auth/__tests__/providers/cognito/resendSignUpCode.test.ts @@ -1,23 +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 { resendSignUpCode } from '../../../src/providers/cognito'; import { AuthValidationErrorCode } from '../../../src/errors/types/validation'; import { AuthError } from '../../../src/errors/AuthError'; import { ResendConfirmationException } from '../../../src/providers/cognito/types/errors'; import { createResendConfirmationCodeClient } from '../../../src/foundation/factories/serviceClients/cognitoIdentityProvider'; import { createCognitoUserPoolEndpointResolver } from '../../../src/providers/cognito/factories'; +import { createMockAmplifyContext } from '../../testUtils/mockAmplifyContext'; import { authAPITestParams } from './testUtils/authApiTestParams'; import { getMockError } from './testUtils/data'; -import { setUpGetConfig } from './testUtils/setUpGetConfig'; -jest.mock('@aws-amplify/core', () => ({ - ...(jest.createMockFromModule('@aws-amplify/core') as object), - Amplify: { getConfig: jest.fn(() => ({})) }, -})); jest.mock('@aws-amplify/core/internals/utils', () => ({ ...jest.requireActual('@aws-amplify/core/internals/utils'), isBrowser: jest.fn(() => false), @@ -38,8 +32,14 @@ describe('resendSignUpCode', () => { createCognitoUserPoolEndpointResolver, ); - beforeAll(() => { - setUpGetConfig(Amplify); + const mockCtx = createMockAmplifyContext({ + Auth: { + Cognito: { + userPoolClientId: '111111-aaaaa-42d8-891d-ee81a1549398', + userPoolId: 'us-west-2_zzzzz', + identityPoolId: 'us-west-2:xxxxxx', + }, + }, }); beforeEach(() => { @@ -56,7 +56,7 @@ describe('resendSignUpCode', () => { }); it('should call resendConfirmationCode and return a result', async () => { - const result = await resendSignUpCode({ + const result = await resendSignUpCode(mockCtx, { username: user1.username, }); expect(result).toEqual(authAPITestParams.resendSignUpAPIResult); @@ -76,7 +76,7 @@ describe('resendSignUpCode', () => { it('invokes createCognitoUserPoolEndpointResolver with expected endpointOverride', async () => { const expectedUserPoolEndpoint = 'https://my-custom-endpoint.com'; - jest.mocked(Amplify.getConfig).mockReturnValueOnce({ + const endpointCtx = createMockAmplifyContext({ Auth: { Cognito: { userPoolClientId: '111111-aaaaa-42d8-891d-ee81a1549398', @@ -86,7 +86,7 @@ describe('resendSignUpCode', () => { }, }, }); - await resendSignUpCode({ + await resendSignUpCode(endpointCtx, { username: user1.username, }); expect(mockCreateCognitoUserPoolEndpointResolver).toHaveBeenCalledWith({ @@ -97,7 +97,7 @@ describe('resendSignUpCode', () => { it('should throw an error when username is empty', async () => { expect.assertions(2); try { - await resendSignUpCode({ username: '' }); + await resendSignUpCode(mockCtx, { username: '' }); } catch (error: any) { expect(error).toBeInstanceOf(AuthError); expect(error.name).toBe(AuthValidationErrorCode.EmptySignUpUsername); @@ -110,7 +110,7 @@ describe('resendSignUpCode', () => { throw getMockError(ResendConfirmationException.InvalidParameterException); }); try { - await resendSignUpCode({ username: user1.username }); + await resendSignUpCode(mockCtx, { username: user1.username }); } catch (error: any) { expect(error).toBeInstanceOf(AuthError); expect(error.name).toBe( @@ -125,7 +125,7 @@ describe('resendSignUpCode', () => { return 'abcd'; }, }; - const result = await resendSignUpCode({ + const result = await resendSignUpCode(mockCtx, { username: user1.username, }); expect(result).toEqual(authAPITestParams.resendSignUpAPIResult); diff --git a/packages/auth/__tests__/providers/cognito/resetPassword.test.ts b/packages/auth/__tests__/providers/cognito/resetPassword.test.ts index 41deeeb170a..7e87b07e724 100644 --- a/packages/auth/__tests__/providers/cognito/resetPassword.test.ts +++ b/packages/auth/__tests__/providers/cognito/resetPassword.test.ts @@ -1,6 +1,5 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { Amplify } from '@aws-amplify/core'; import { AuthError } from '../../../src/errors/AuthError'; import { AuthValidationErrorCode } from '../../../src/errors/types/validation'; @@ -8,15 +7,11 @@ import { resetPassword } from '../../../src/providers/cognito'; import { ForgotPasswordException } from '../../../src/providers/cognito/types/errors'; import { createForgotPasswordClient } from '../../../src/foundation/factories/serviceClients/cognitoIdentityProvider'; import { createCognitoUserPoolEndpointResolver } from '../../../src/providers/cognito/factories'; +import { createMockAmplifyContext } from '../../testUtils/mockAmplifyContext'; import { authAPITestParams } from './testUtils/authApiTestParams'; import { getMockError } from './testUtils/data'; -import { setUpGetConfig } from './testUtils/setUpGetConfig'; -jest.mock('@aws-amplify/core', () => ({ - ...(jest.createMockFromModule('@aws-amplify/core') as object), - Amplify: { getConfig: jest.fn(() => ({})) }, -})); jest.mock('@aws-amplify/core/internals/utils', () => ({ ...jest.requireActual('@aws-amplify/core/internals/utils'), isBrowser: jest.fn(() => false), @@ -36,8 +31,14 @@ describe('resetPassword', () => { createCognitoUserPoolEndpointResolver, ); - beforeAll(() => { - setUpGetConfig(Amplify); + const mockCtx = createMockAmplifyContext({ + Auth: { + Cognito: { + userPoolClientId: '111111-aaaaa-42d8-891d-ee81a1549398', + userPoolId: 'us-west-2_zzzzz', + identityPoolId: 'us-west-2:xxxxxx', + }, + }, }); beforeEach(() => { @@ -54,13 +55,16 @@ describe('resetPassword', () => { }); it('should call forgotPassword and return a result', async () => { - const result = await resetPassword(authAPITestParams.resetPasswordRequest); + const result = await resetPassword( + mockCtx, + authAPITestParams.resetPasswordRequest, + ); expect(result).toEqual(authAPITestParams.resetPasswordResult); }); it('invokes createCognitoUserPoolEndpointResolver with expected endpointOverride', async () => { const expectedUserPoolEndpoint = 'https://my-custom-endpoint.com'; - jest.mocked(Amplify.getConfig).mockReturnValueOnce({ + const endpointCtx = createMockAmplifyContext({ Auth: { Cognito: { userPoolClientId: '111111-aaaaa-42d8-891d-ee81a1549398', @@ -71,7 +75,7 @@ describe('resetPassword', () => { }, }); - await resetPassword(authAPITestParams.resetPasswordRequest); + await resetPassword(endpointCtx, authAPITestParams.resetPasswordRequest); expect(mockCreateCognitoUserPoolEndpointResolver).toHaveBeenCalledWith({ endpointOverride: expectedUserPoolEndpoint, @@ -79,7 +83,7 @@ describe('resetPassword', () => { }); it('should contain clientMetadata from request', async () => { - await resetPassword({ + await resetPassword(mockCtx, { username: 'username', options: { clientMetadata: { foo: 'foo' }, @@ -98,7 +102,7 @@ describe('resetPassword', () => { it('should throw an error when username is empty', async () => { expect.assertions(2); try { - await resetPassword({ username: '' }); + await resetPassword(mockCtx, { username: '' }); } catch (error: any) { expect(error).toBeInstanceOf(AuthError); expect(error.name).toBe( @@ -113,7 +117,7 @@ describe('resetPassword', () => { throw getMockError(ForgotPasswordException.InvalidParameterException); }); try { - await resetPassword(authAPITestParams.resetPasswordRequest); + await resetPassword(mockCtx, authAPITestParams.resetPasswordRequest); } catch (error: any) { expect(error).toBeInstanceOf(AuthError); expect(error.name).toBe( @@ -128,7 +132,7 @@ describe('resetPassword', () => { return 'abcd'; }, }; - await resetPassword({ + await resetPassword(mockCtx, { username: 'username', options: { clientMetadata: { foo: 'foo' }, diff --git a/packages/auth/__tests__/providers/cognito/sendUserAttributeVerificationCode.test.ts b/packages/auth/__tests__/providers/cognito/sendUserAttributeVerificationCode.test.ts index 31376edf642..b423e4abd33 100644 --- a/packages/auth/__tests__/providers/cognito/sendUserAttributeVerificationCode.test.ts +++ b/packages/auth/__tests__/providers/cognito/sendUserAttributeVerificationCode.test.ts @@ -1,7 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { Amplify, fetchAuthSession } from '@aws-amplify/core'; import { decodeJWT } from '@aws-amplify/core/internals/utils'; import { AuthError } from '../../../src/errors/AuthError'; @@ -9,15 +8,11 @@ import { sendUserAttributeVerificationCode } from '../../../src/providers/cognit import { GetUserAttributeVerificationException } from '../../../src/providers/cognito/types/errors'; import { createGetUserAttributeVerificationCodeClient } from '../../../src/foundation/factories/serviceClients/cognitoIdentityProvider'; import { createCognitoUserPoolEndpointResolver } from '../../../src/providers/cognito/factories'; +import { createMockAmplifyContext } from '../../testUtils/mockAmplifyContext'; import { authAPITestParams } from './testUtils/authApiTestParams'; import { getMockError, mockAccessToken } from './testUtils/data'; -import { setUpGetConfig } from './testUtils/setUpGetConfig'; -jest.mock('@aws-amplify/core', () => ({ - ...(jest.createMockFromModule('@aws-amplify/core') as object), - Amplify: { getConfig: jest.fn(() => ({})) }, -})); jest.mock('@aws-amplify/core/internals/utils', () => ({ ...jest.requireActual('@aws-amplify/core/internals/utils'), isBrowser: jest.fn(() => false), @@ -29,7 +24,6 @@ jest.mock('../../../src/providers/cognito/factories'); describe('sendUserAttributeVerificationCode', () => { // assert mocks - const mockFetchAuthSession = fetchAuthSession as jest.Mock; const mockGetUserAttributeVerificationCode = jest.fn(); const mockCreateGetUserAttributeVerificationCodeClient = jest.mocked( createGetUserAttributeVerificationCodeClient, @@ -38,9 +32,18 @@ describe('sendUserAttributeVerificationCode', () => { createCognitoUserPoolEndpointResolver, ); + const mockCtx = createMockAmplifyContext({ + Auth: { + Cognito: { + userPoolClientId: '111111-aaaaa-42d8-891d-ee81a1549398', + userPoolId: 'us-west-2_zzzzz', + identityPoolId: 'us-west-2:xxxxxx', + }, + }, + }); + beforeAll(() => { - setUpGetConfig(Amplify); - mockFetchAuthSession.mockResolvedValue({ + (mockCtx.fetchAuthSession as jest.Mock).mockResolvedValue({ tokens: { accessToken: decodeJWT(mockAccessToken) }, }); }); @@ -56,12 +59,12 @@ describe('sendUserAttributeVerificationCode', () => { afterEach(() => { mockGetUserAttributeVerificationCode.mockReset(); - mockFetchAuthSession.mockClear(); + (mockCtx.fetchAuthSession as jest.Mock).mockClear(); mockCreateGetUserAttributeVerificationCodeClient.mockClear(); }); it('should return a result', async () => { - const result = await sendUserAttributeVerificationCode({ + const result = await sendUserAttributeVerificationCode(mockCtx, { userAttributeKey: 'email', options: { clientMetadata: { foo: 'bar' }, @@ -82,7 +85,7 @@ describe('sendUserAttributeVerificationCode', () => { it('invokes mockCreateCognitoUserPoolEndpointResolver with expected endpointOverride', async () => { const expectedUserPoolEndpoint = 'https://my-custom-endpoint.com'; - jest.mocked(Amplify.getConfig).mockReturnValueOnce({ + const endpointCtx = createMockAmplifyContext({ Auth: { Cognito: { userPoolClientId: '111111-aaaaa-42d8-891d-ee81a1549398', @@ -92,7 +95,10 @@ describe('sendUserAttributeVerificationCode', () => { }, }, }); - await sendUserAttributeVerificationCode({ + (endpointCtx.fetchAuthSession as jest.Mock).mockResolvedValue({ + tokens: { accessToken: decodeJWT(mockAccessToken) }, + }); + await sendUserAttributeVerificationCode(endpointCtx, { userAttributeKey: 'email', options: { clientMetadata: { foo: 'bar' }, @@ -112,7 +118,7 @@ describe('sendUserAttributeVerificationCode', () => { ); }); try { - await sendUserAttributeVerificationCode({ + await sendUserAttributeVerificationCode(mockCtx, { userAttributeKey: 'email', options: { clientMetadata: { foo: 'bar' }, diff --git a/packages/auth/__tests__/providers/cognito/setUpTOTP.test.ts b/packages/auth/__tests__/providers/cognito/setUpTOTP.test.ts index 1a7d0cfbc4b..e54852b48dc 100644 --- a/packages/auth/__tests__/providers/cognito/setUpTOTP.test.ts +++ b/packages/auth/__tests__/providers/cognito/setUpTOTP.test.ts @@ -1,7 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { Amplify, fetchAuthSession } from '@aws-amplify/core'; import { decodeJWT } from '@aws-amplify/core/internals/utils'; import { AuthError } from '../../../src/errors/AuthError'; @@ -9,14 +8,10 @@ import { AssociateSoftwareTokenException } from '../../../src/providers/cognito/ import { setUpTOTP } from '../../../src/providers/cognito'; import { createAssociateSoftwareTokenClient } from '../../../src/foundation/factories/serviceClients/cognitoIdentityProvider'; import { createCognitoUserPoolEndpointResolver } from '../../../src/providers/cognito/factories'; +import { createMockAmplifyContext } from '../../testUtils/mockAmplifyContext'; import { getMockError, mockAccessToken } from './testUtils/data'; -import { setUpGetConfig } from './testUtils/setUpGetConfig'; -jest.mock('@aws-amplify/core', () => ({ - ...(jest.createMockFromModule('@aws-amplify/core') as object), - Amplify: { getConfig: jest.fn(() => ({})) }, -})); jest.mock('@aws-amplify/core/internals/utils', () => ({ ...jest.requireActual('@aws-amplify/core/internals/utils'), isBrowser: jest.fn(() => false), @@ -29,7 +24,6 @@ jest.mock('../../../src/providers/cognito/factories'); describe('setUpTOTP', () => { const secretCode = 'secret-code'; // assert mocks - const mockFetchAuthSession = fetchAuthSession as jest.Mock; const mockAssociateSoftwareToken = jest.fn(); const mockCreateAssociateSoftwareTokenClient = jest.mocked( createAssociateSoftwareTokenClient, @@ -38,9 +32,18 @@ describe('setUpTOTP', () => { createCognitoUserPoolEndpointResolver, ); + const mockCtx = createMockAmplifyContext({ + Auth: { + Cognito: { + userPoolClientId: '111111-aaaaa-42d8-891d-ee81a1549398', + userPoolId: 'us-west-2_zzzzz', + identityPoolId: 'us-west-2:xxxxxx', + }, + }, + }); + beforeAll(() => { - setUpGetConfig(Amplify); - mockFetchAuthSession.mockResolvedValue({ + (mockCtx.fetchAuthSession as jest.Mock).mockResolvedValue({ tokens: { accessToken: decodeJWT(mockAccessToken) }, }); }); @@ -57,12 +60,12 @@ describe('setUpTOTP', () => { afterEach(() => { mockAssociateSoftwareToken.mockReset(); - mockFetchAuthSession.mockClear(); + (mockCtx.fetchAuthSession as jest.Mock).mockClear(); mockCreateAssociateSoftwareTokenClient.mockClear(); }); it('setUpTOTP API should call the UserPoolClient and should return a TOTPSetupDetails', async () => { - const result = await setUpTOTP(); + const result = await setUpTOTP(mockCtx); expect(mockAssociateSoftwareToken).toHaveBeenCalledWith( { region: 'us-west-2', @@ -78,7 +81,7 @@ describe('setUpTOTP', () => { it('invokes mockCreateCognitoUserPoolEndpointResolver with expected endpointOverride', async () => { const expectedUserPoolEndpoint = 'https://my-custom-endpoint.com'; - jest.mocked(Amplify.getConfig).mockReturnValueOnce({ + const endpointCtx = createMockAmplifyContext({ Auth: { Cognito: { userPoolClientId: '111111-aaaaa-42d8-891d-ee81a1549398', @@ -88,8 +91,11 @@ describe('setUpTOTP', () => { }, }, }); + (endpointCtx.fetchAuthSession as jest.Mock).mockResolvedValue({ + tokens: { accessToken: decodeJWT(mockAccessToken) }, + }); - await setUpTOTP(); + await setUpTOTP(endpointCtx); expect(mockCreateCognitoUserPoolEndpointResolver).toHaveBeenCalledWith({ endpointOverride: expectedUserPoolEndpoint, @@ -104,7 +110,7 @@ describe('setUpTOTP', () => { ); }); try { - await setUpTOTP(); + await setUpTOTP(mockCtx); } catch (error: any) { expect(error).toBeInstanceOf(AuthError); expect(error.name).toBe( diff --git a/packages/auth/__tests__/providers/cognito/signInErrorCases.test.ts b/packages/auth/__tests__/providers/cognito/signInErrorCases.test.ts index 94b4029418b..5cd673a8cd2 100644 --- a/packages/auth/__tests__/providers/cognito/signInErrorCases.test.ts +++ b/packages/auth/__tests__/providers/cognito/signInErrorCases.test.ts @@ -1,8 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { Amplify } from '@aws-amplify/core'; - import { AuthError } from '../../../src/errors/AuthError'; import { AuthValidationErrorCode } from '../../../src/errors/types/validation'; import { getCurrentUser, signIn } from '../../../src/providers/cognito'; @@ -10,15 +8,11 @@ import { InitiateAuthException } from '../../../src/providers/cognito/types/erro import { USER_ALREADY_AUTHENTICATED_EXCEPTION } from '../../../src/errors/constants'; import { createInitiateAuthClient } from '../../../src/foundation/factories/serviceClients/cognitoIdentityProvider'; import { AuthErrorCodes } from '../../../src/common/AuthErrorStrings'; +import { createMockAmplifyContext } from '../../testUtils/mockAmplifyContext'; import { authAPITestParams } from './testUtils/authApiTestParams'; import { getMockError } from './testUtils/data'; -import { setUpGetConfig } from './testUtils/setUpGetConfig'; -jest.mock('@aws-amplify/core', () => ({ - ...(jest.createMockFromModule('@aws-amplify/core') as object), - Amplify: { getConfig: jest.fn(() => ({})) }, -})); jest.mock('@aws-amplify/core/internals/utils', () => ({ ...jest.requireActual('@aws-amplify/core/internals/utils'), isBrowser: jest.fn(() => false), @@ -36,8 +30,14 @@ describe('signIn API error path cases:', () => { const mockedGetCurrentUser = getCurrentUser as jest.Mock; - beforeAll(() => { - setUpGetConfig(Amplify); + const mockCtx = createMockAmplifyContext({ + Auth: { + Cognito: { + userPoolClientId: '111111-aaaaa-42d8-891d-ee81a1549398', + userPoolId: 'us-west-2_zzzzz', + identityPoolId: 'us-west-2:xxxxxx', + }, + }, }); beforeEach(() => { @@ -56,7 +56,7 @@ describe('signIn API error path cases:', () => { }); try { - await signIn({ username: 'username', password: 'password' }); + await signIn(mockCtx, { username: 'username', password: 'password' }); } catch (error: any) { expect(error).toBeInstanceOf(AuthError); expect(error.name).toBe(USER_ALREADY_AUTHENTICATED_EXCEPTION); @@ -67,7 +67,7 @@ describe('signIn API error path cases:', () => { it('should throw an error when username is empty', async () => { expect.assertions(2); try { - await signIn({ username: '' }); + await signIn(mockCtx, { username: '' }); } catch (error: any) { expect(error).toBeInstanceOf(AuthError); expect(error.name).toBe(AuthValidationErrorCode.EmptySignInUsername); @@ -77,7 +77,7 @@ describe('signIn API error path cases:', () => { it('should throw an error when password is not empty and authFlow is CUSTOM_WITHOUT_SRP', async () => { expect.assertions(2); try { - await signIn({ + await signIn(mockCtx, { username: authAPITestParams.user1.username, password: authAPITestParams.user1.password, options: { @@ -95,7 +95,7 @@ describe('signIn API error path cases:', () => { throw getMockError(InitiateAuthException.InvalidParameterException); }); - const signInResultPromise = signIn({ + const signInResultPromise = signIn(mockCtx, { username: authAPITestParams.user1.username, password: authAPITestParams.user1.password, }); @@ -116,7 +116,7 @@ describe('signIn API error path cases:', () => { $metadata: {}, })); - const signInResultPromise = signIn({ + const signInResultPromise = signIn(mockCtx, { username: authAPITestParams.user1.username, password: authAPITestParams.user1.password, options: { diff --git a/packages/auth/__tests__/providers/cognito/signInResumable.test.ts b/packages/auth/__tests__/providers/cognito/signInResumable.test.ts index 7bc3a8d324a..a9ffe94e797 100644 --- a/packages/auth/__tests__/providers/cognito/signInResumable.test.ts +++ b/packages/auth/__tests__/providers/cognito/signInResumable.test.ts @@ -1,6 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { Amplify, syncSessionStorage } from '@aws-amplify/core'; +import { syncSessionStorage } from '@aws-amplify/core'; import { resetActiveSignInState, @@ -14,13 +14,15 @@ import { } from '../../../src/foundation/factories/serviceClients/cognitoIdentityProvider/types'; import * as signInHelpers from '../../../src/providers/cognito/utils/signInHelpers'; import { signIn } from '../../../src/providers/cognito'; +import { createMockAmplifyContext } from '../../testUtils/mockAmplifyContext'; -import { setUpGetConfig } from './testUtils/setUpGetConfig'; import { authAPITestParams } from './testUtils/authApiTestParams'; const signInStoreImplementation = require('../../../src/client/utils/store/signInStore'); -jest.mock('@aws-amplify/core/internals/utils'); +jest.mock('@aws-amplify/core/internals/utils', () => ({ + ...jest.requireActual('@aws-amplify/core/internals/utils'), +})); jest.mock('../../../src/providers/cognito/apis/getCurrentUser'); jest.mock('@aws-amplify/core', () => ({ ...(jest.createMockFromModule('@aws-amplify/core') as object), @@ -108,12 +110,18 @@ describe('signInStore', () => { const { username } = authAPITestParams.user1; const { password } = authAPITestParams.user1; - beforeEach(() => { - cognitoUserPoolsTokenProvider.setAuthConfig(authConfig); + const mockCtx = createMockAmplifyContext({ + Auth: { + Cognito: { + userPoolClientId: '111111-aaaaa-42d8-891d-ee81a1549398', + userPoolId: 'us-west-2_zzzzz', + identityPoolId: 'us-west-2:xxxxxx', + }, + }, }); - beforeAll(() => { - setUpGetConfig(Amplify); + beforeEach(() => { + cognitoUserPoolsTokenProvider.setAuthConfig(authConfig); }); afterEach(() => { @@ -164,7 +172,7 @@ describe('signInStore', () => { }), ); - await signIn({ + await signIn(mockCtx, { username, password, }); diff --git a/packages/auth/__tests__/providers/cognito/signInStateManagement.test.ts b/packages/auth/__tests__/providers/cognito/signInStateManagement.test.ts index bf0735f8f07..b6be314e665 100644 --- a/packages/auth/__tests__/providers/cognito/signInStateManagement.test.ts +++ b/packages/auth/__tests__/providers/cognito/signInStateManagement.test.ts @@ -1,13 +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 { + clearGlobalContext, + setGlobalContext, +} from '@aws-amplify/core/internals/utils'; import { getCurrentUser, signIn } from '../../../src/providers/cognito'; import * as signInHelpers from '../../../src/providers/cognito/utils/signInHelpers'; import { signInStore } from '../../../src/client/utils/store/signInStore'; import { cognitoUserPoolsTokenProvider } from '../../../src/providers/cognito/tokenProvider'; import { RespondToAuthChallengeCommandOutput } from '../../../src/foundation/factories/serviceClients/cognitoIdentityProvider/types'; +import { createMockAmplifyContext } from '../../testUtils/mockAmplifyContext'; import { authAPITestParams } from './testUtils/authApiTestParams'; @@ -30,9 +34,14 @@ describe('local sign-in state management tests', () => { beforeEach(() => { cognitoUserPoolsTokenProvider.setAuthConfig(authConfig); + setGlobalContext(createMockAmplifyContext({ Auth: authConfig })); signInStore.dispatch({ type: 'RESET_STATE' }); }); + afterAll(() => { + clearGlobalContext(); + }); + test('local state management should return state after signIn returns a ChallengeName', async () => { const handleUserSRPAuthflowSpy = jest .spyOn(signInHelpers, 'handleUserSRPAuthFlow') @@ -47,10 +56,6 @@ describe('local sign-in state management tests', () => { }, }), ); - - Amplify.configure({ - Auth: authConfig, - }); await signIn({ username, password, @@ -79,10 +84,6 @@ describe('local sign-in state management tests', () => { async (): Promise => authAPITestParams.RespondToAuthChallengeCommandOutput, ); - - Amplify.configure({ - Auth: authConfig, - }); await signIn({ username, password, diff --git a/packages/auth/__tests__/providers/cognito/signInWithCustomAuth.test.ts b/packages/auth/__tests__/providers/cognito/signInWithCustomAuth.test.ts index c9e5ec7ab68..eb4219a08d1 100644 --- a/packages/auth/__tests__/providers/cognito/signInWithCustomAuth.test.ts +++ b/packages/auth/__tests__/providers/cognito/signInWithCustomAuth.test.ts @@ -1,11 +1,15 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { Amplify } from 'aws-amplify'; +import { + clearGlobalContext, + setGlobalContext, +} from '@aws-amplify/core/internals/utils'; import { signIn } from '../../../src/providers/cognito'; import { signInWithCustomAuth } from '../../../src/providers/cognito/apis/signInWithCustomAuth'; import * as initiateAuthHelpers from '../../../src/providers/cognito/utils/signInHelpers'; +import { createMockAmplifyContext } from '../../testUtils/mockAmplifyContext'; import { cognitoUserPoolsTokenProvider, tokenOrchestrator, @@ -30,9 +34,8 @@ const authConfig = { }, }; -Amplify.configure({ - Auth: authConfig, -}); +const mockCtx = createMockAmplifyContext({ Auth: authConfig }); +setGlobalContext(mockCtx); cognitoUserPoolsTokenProvider.setAuthConfig(authConfig); describe('signIn API happy path cases', () => { let handleCustomAuthFlowWithoutSRPSpy: jest.SpyInstance; @@ -65,16 +68,18 @@ describe('signIn API happy path cases', () => { }); test('signInWithCustomAuth API should return a SignInResult', async () => { - const result = await signInWithCustomAuth({ + const ctx = createMockAmplifyContext({ Auth: authConfig }); + const result = await signInWithCustomAuth(ctx, { username: authAPITestParams.user1.username, }); expect(result).toEqual(authAPITestParams.signInResultWithCustomAuth()); expect(handleCustomAuthFlowWithoutSRPSpy).toHaveBeenCalledTimes(1); }); test('handleCustomAuthFlowWithoutSRP should be called with clientMetada from request', async () => { + const ctx = createMockAmplifyContext({ Auth: authConfig }); const { username } = authAPITestParams.user1; - await signInWithCustomAuth({ + await signInWithCustomAuth(ctx, { username, options: authAPITestParams.configWithClientMetadata, }); @@ -139,3 +144,7 @@ describe('Cognito ASF', () => { ); }); }); + +afterAll(() => { + clearGlobalContext(); +}); diff --git a/packages/auth/__tests__/providers/cognito/signInWithCustomSRPAuth.test.ts b/packages/auth/__tests__/providers/cognito/signInWithCustomSRPAuth.test.ts index 5d6aa8a1740..e498f3f345b 100644 --- a/packages/auth/__tests__/providers/cognito/signInWithCustomSRPAuth.test.ts +++ b/packages/auth/__tests__/providers/cognito/signInWithCustomSRPAuth.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'; +import { + clearGlobalContext, + setGlobalContext, +} from '@aws-amplify/core/internals/utils'; import { signIn } from '../../../src/providers/cognito'; import * as initiateAuthHelpers from '../../../src/providers/cognito/utils/signInHelpers'; @@ -10,6 +13,7 @@ import { cognitoUserPoolsTokenProvider, tokenOrchestrator, } from '../../../src/providers/cognito/tokenProvider'; +import { createMockAmplifyContext } from '../../testUtils/mockAmplifyContext'; import { createInitiateAuthClient } from '../../../src/foundation/factories/serviceClients/cognitoIdentityProvider'; import { RespondToAuthChallengeCommandOutput } from '../../../src/foundation/factories/serviceClients/cognitoIdentityProvider/types'; @@ -31,9 +35,8 @@ const authConfig = { }, }; cognitoUserPoolsTokenProvider.setAuthConfig(authConfig); -Amplify.configure({ - Auth: authConfig, -}); +const mockCtx = createMockAmplifyContext({ Auth: authConfig }); +setGlobalContext(mockCtx); describe('signIn API happy path cases', () => { let handleCustomSRPAuthFlowSpy: jest.SpyInstance; @@ -69,7 +72,8 @@ describe('signIn API happy path cases', () => { }); test('signInWithCustomSRPAuth API should return a SignInResult', async () => { - const result = await signInWithCustomSRPAuth({ + const ctx = createMockAmplifyContext({ Auth: authConfig }); + const result = await signInWithCustomSRPAuth(ctx, { username: authAPITestParams.user1.username, password: authAPITestParams.user1.password, }); @@ -78,9 +82,10 @@ describe('signIn API happy path cases', () => { }); test('handleCustomSRPAuthFlow should be called with clientMetada from request', async () => { + const ctx = createMockAmplifyContext({ Auth: authConfig }); const { username } = authAPITestParams.user1; const { password } = authAPITestParams.user1; - await signInWithCustomSRPAuth({ + await signInWithCustomSRPAuth(ctx, { username, password, options: authAPITestParams.configWithClientMetadata, @@ -151,3 +156,7 @@ describe('Cognito ASF', () => { ); }); }); + +afterAll(() => { + clearGlobalContext(); +}); diff --git a/packages/auth/__tests__/providers/cognito/signInWithRedirect.test.ts b/packages/auth/__tests__/providers/cognito/signInWithRedirect.test.ts index f1a7b596f7d..4f5809eaeb5 100644 --- a/packages/auth/__tests__/providers/cognito/signInWithRedirect.test.ts +++ b/packages/auth/__tests__/providers/cognito/signInWithRedirect.test.ts @@ -1,9 +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 { Hub } from '@aws-amplify/core'; import { - ADD_OAUTH_LISTENER, assertOAuthConfig, assertTokenProviderConfig, isBrowser, @@ -19,7 +18,6 @@ import { oAuthStore, } from '../../../src/providers/cognito/utils/oauth'; import { getAuthUserAgentValue, openAuthSession } from '../../../src/utils'; -import { attemptCompleteOAuthFlow } from '../../../src/providers/cognito/utils/oauth/attemptCompleteOAuthFlow'; import { createOAuthError } from '../../../src/providers/cognito/utils/oauth/createOAuthError'; import { signInWithRedirect } from '../../../src/providers/cognito/apis/signInWithRedirect'; import type { OAuthStore } from '../../../src/providers/cognito/utils/types'; @@ -32,20 +30,35 @@ jest.mock('@aws-amplify/core/internals/utils', () => ({ assertTokenProviderConfig: jest.fn(), urlSafeEncode: jest.fn(), isBrowser: jest.fn(() => true), + resolveCtxArgs: jest.fn((...args: any[]) => { + // Return a mock context with the test's auth config, and the input + const input = args[0]?.[0]; + + return [ + { + resourcesConfig: mockAuthConfigWithOAuth, + libraryOptions: {}, + fetchAuthSession: jest.fn(), + clearCredentials: jest.fn(), + getTokens: jest.fn(), + }, + input, + ]; + }), })); jest.mock('@aws-amplify/core', () => { - const { ADD_OAUTH_LISTENER: ACTUAL_ADD_OAUTH_LISTENER } = jest.requireActual( - '@aws-amplify/core/internals/utils', - ); - return { Amplify: { getConfig: jest.fn(() => mockAuthConfigWithOAuth), - [ACTUAL_ADD_OAUTH_LISTENER]: jest.fn(), + }, + Hub: { + listen: jest.fn(), }, ConsoleLogger: jest.fn().mockImplementation(() => { return { warn: jest.fn() }; }), + hasGlobalContext: jest.fn(() => false), + getActiveContext: jest.fn(), syncSessionStorage: { setItem: jest.fn((key, value) => { window.sessionStorage.setItem(key, value); @@ -270,14 +283,14 @@ describe('signInWithRedirect', () => { describe('specifications on Web', () => { describe('side effect', () => { - it('attaches oauth listener to the Amplify singleton', async () => { + it('registers a Hub listener for OAuth flow completion on configure events', async () => { (oAuthStore.loadOAuthInFlight as jest.Mock).mockResolvedValueOnce( false, ); - expect(Amplify[ADD_OAUTH_LISTENER]).toHaveBeenCalledWith( - attemptCompleteOAuthFlow, - ); + // The enableOAuthListener module sets up a Hub.listen('core', ...) side effect + // when imported. Verify Hub.listen was called. + expect(Hub.listen).toHaveBeenCalledWith('core', expect.any(Function)); }); }); @@ -319,6 +332,7 @@ describe('signInWithRedirect', () => { }); expect(mockCompleteOAuthFlow).toHaveBeenCalledWith( + expect.any(Object), expect.objectContaining({ currentUrl: mockOpenAuthSessionResult.url, preferPrivateSession: true, @@ -368,6 +382,7 @@ describe('signInWithRedirect', () => { ).rejects.toThrow(expectedError); expect(mockCompleteOAuthFlow).toHaveBeenCalledWith( + expect.any(Object), expect.objectContaining({ currentUrl: mockOpenAuthSessionResult.url, }), diff --git a/packages/auth/__tests__/providers/cognito/signInWithSRP.test.ts b/packages/auth/__tests__/providers/cognito/signInWithSRP.test.ts index 9dd1b2dd606..cf8e80d97a7 100644 --- a/packages/auth/__tests__/providers/cognito/signInWithSRP.test.ts +++ b/packages/auth/__tests__/providers/cognito/signInWithSRP.test.ts @@ -1,11 +1,15 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { Amplify } from 'aws-amplify'; +import { + clearGlobalContext, + setGlobalContext, +} from '@aws-amplify/core/internals/utils'; import { signIn } from '../../../src/providers/cognito'; import { signInWithSRP } from '../../../src/providers/cognito/apis/signInWithSRP'; import * as initiateAuthHelpers from '../../../src/providers/cognito/utils/signInHelpers'; +import { createMockAmplifyContext } from '../../testUtils/mockAmplifyContext'; import { cognitoUserPoolsTokenProvider, tokenOrchestrator, @@ -48,9 +52,8 @@ const authConfig = { }; cognitoUserPoolsTokenProvider.setAuthConfig(authConfig); -Amplify.configure({ - Auth: authConfig, -}); +const mockCtx = createMockAmplifyContext({ Auth: authConfig }); +setGlobalContext(mockCtx); const mockedDeviceMetadata = { deviceKey: 'mockedKey', @@ -173,7 +176,8 @@ describe('signIn API happy path cases', () => { }); test('signInWithSRP API should return a SignInResult', async () => { - const result = await signInWithSRP({ + const ctx = createMockAmplifyContext({ Auth: authConfig }); + const result = await signInWithSRP(ctx, { username: authAPITestParams.user1.username, password: authAPITestParams.user1.password, }); @@ -182,9 +186,10 @@ describe('signIn API happy path cases', () => { }); test('handleUserSRPFlow should be called with clientMetada from request', async () => { + const ctx = createMockAmplifyContext({ Auth: authConfig }); const { username } = authAPITestParams.user1; const { password } = authAPITestParams.user1; - await signInWithSRP({ + await signInWithSRP(ctx, { username, password, options: authAPITestParams.configWithClientMetadata, @@ -322,3 +327,7 @@ describe('Cognito ASF', () => { ); }); }); + +afterAll(() => { + clearGlobalContext(); +}); diff --git a/packages/auth/__tests__/providers/cognito/signInWithUserAuth.test.ts b/packages/auth/__tests__/providers/cognito/signInWithUserAuth.test.ts index ea7b0239e1a..ea93a2812c2 100644 --- a/packages/auth/__tests__/providers/cognito/signInWithUserAuth.test.ts +++ b/packages/auth/__tests__/providers/cognito/signInWithUserAuth.test.ts @@ -1,11 +1,15 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { Amplify } from '@aws-amplify/core'; -import { AmplifyErrorCode } from '@aws-amplify/core/internals/utils'; +import { + AmplifyErrorCode, + clearGlobalContext, + setGlobalContext, +} from '@aws-amplify/core/internals/utils'; import { signInWithUserAuth } from '../../../src/providers/cognito/apis/signInWithUserAuth'; import { cognitoUserPoolsTokenProvider } from '../../../src/providers/cognito/tokenProvider'; import { InitiateAuthCommandOutput } from '../../../src/foundation/factories/serviceClients/cognitoIdentityProvider/types'; +import { createMockAmplifyContext } from '../../testUtils/mockAmplifyContext'; jest.mock('../../../src/providers/cognito/utils/signInHelpers', () => ({ ...jest.requireActual('../../../src/providers/cognito/utils/signInHelpers'), @@ -45,15 +49,14 @@ const authConfig = { }; cognitoUserPoolsTokenProvider.setAuthConfig(authConfig); -Amplify.configure({ - Auth: authConfig, -}); +setGlobalContext(createMockAmplifyContext({ Auth: authConfig })); describe('signInWithUserAuth API tests', () => { // Update how we get the mock const { handleUserAuthFlow } = jest.requireMock( '../../../src/client/flows/userAuth/handleUserAuthFlow', ); + const mockCtx = createMockAmplifyContext({ Auth: authConfig }); beforeEach(() => { jest.clearAllMocks(); @@ -69,7 +72,7 @@ describe('signInWithUserAuth API tests', () => { }; handleUserAuthFlow.mockResolvedValue(mockResponse); - const result = await signInWithUserAuth({ + const result = await signInWithUserAuth(mockCtx, { username: 'testuser', }); @@ -102,7 +105,7 @@ describe('signInWithUserAuth API tests', () => { }; handleUserAuthFlow.mockResolvedValue(mockResponse); - const result = await signInWithUserAuth({ + const result = await signInWithUserAuth(mockCtx, { username: 'testuser', options: { preferredChallenge: 'EMAIL_OTP' }, }); @@ -129,7 +132,7 @@ describe('signInWithUserAuth API tests', () => { test('should throw validation error for empty username', async () => { await expect( - signInWithUserAuth({ + signInWithUserAuth(mockCtx, { username: '', // empty username }), ).rejects.toThrow('username is required to signIn'); @@ -150,7 +153,7 @@ describe('signInWithUserAuth API tests', () => { }; handleUserAuthFlow.mockResolvedValue(mockResponse); - const result = await signInWithUserAuth({ + const result = await signInWithUserAuth(mockCtx, { username: 'testuser', }); @@ -165,7 +168,7 @@ describe('signInWithUserAuth API tests', () => { error.name = 'PasswordResetRequiredException'; handleUserAuthFlow.mockRejectedValue(error); - const result = await signInWithUserAuth({ + const result = await signInWithUserAuth(mockCtx, { username: 'testuser', }); @@ -186,10 +189,6 @@ describe('signInWithUserAuth API tests', () => { }, }; - Amplify.configure({ - Auth: authConfigWithPasswordless, - }); - const mockResponse: InitiateAuthCommandOutput = { ChallengeName: 'EMAIL_OTP', Session: 'mockSession', @@ -198,7 +197,10 @@ describe('signInWithUserAuth API tests', () => { }; handleUserAuthFlow.mockResolvedValue(mockResponse); - await signInWithUserAuth({ + const passwordlessCtx = createMockAmplifyContext({ + Auth: authConfigWithPasswordless, + }); + await signInWithUserAuth(passwordlessCtx, { username: 'testuser', }); @@ -210,11 +212,6 @@ describe('signInWithUserAuth API tests', () => { preferredChallenge: 'EMAIL_OTP', password: undefined, }); - - // Reset config - Amplify.configure({ - Auth: authConfig, - }); }); test('should prioritize user-provided preferredChallenge over config', async () => { @@ -229,10 +226,6 @@ describe('signInWithUserAuth API tests', () => { }, }; - Amplify.configure({ - Auth: authConfigWithPasswordless, - }); - const mockResponse: InitiateAuthCommandOutput = { ChallengeName: 'SMS_OTP', Session: 'mockSession', @@ -241,7 +234,10 @@ describe('signInWithUserAuth API tests', () => { }; handleUserAuthFlow.mockResolvedValue(mockResponse); - await signInWithUserAuth({ + const passwordlessCtx2 = createMockAmplifyContext({ + Auth: authConfigWithPasswordless, + }); + await signInWithUserAuth(passwordlessCtx2, { username: 'testuser', options: { preferredChallenge: 'SMS_OTP' }, }); @@ -254,11 +250,6 @@ describe('signInWithUserAuth API tests', () => { preferredChallenge: 'SMS_OTP', password: undefined, }); - - // Reset config - Amplify.configure({ - Auth: authConfig, - }); }); test('should throw error when service error has no sign in result', async () => { @@ -267,9 +258,13 @@ describe('signInWithUserAuth API tests', () => { handleUserAuthFlow.mockRejectedValue(error); await expect( - signInWithUserAuth({ + signInWithUserAuth(mockCtx, { username: 'testuser', }), ).rejects.toThrow(AmplifyErrorCode.Unknown); }); }); + +afterAll(() => { + clearGlobalContext(); +}); diff --git a/packages/auth/__tests__/providers/cognito/signInWithUserPassword.test.ts b/packages/auth/__tests__/providers/cognito/signInWithUserPassword.test.ts index d675ace40a2..762d3f40e65 100644 --- a/packages/auth/__tests__/providers/cognito/signInWithUserPassword.test.ts +++ b/packages/auth/__tests__/providers/cognito/signInWithUserPassword.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'; +import { + clearGlobalContext, + setGlobalContext, +} from '@aws-amplify/core/internals/utils'; import { signIn } from '../../../src/providers/cognito'; import * as initiateAuthHelpers from '../../../src/providers/cognito/utils/signInHelpers'; @@ -10,6 +13,7 @@ import { cognitoUserPoolsTokenProvider, tokenOrchestrator, } from '../../../src/providers/cognito/tokenProvider'; +import { createMockAmplifyContext } from '../../testUtils/mockAmplifyContext'; import { createInitiateAuthClient } from '../../../src/foundation/factories/serviceClients/cognitoIdentityProvider'; import { RespondToAuthChallengeCommandOutput } from '../../../src/foundation/factories/serviceClients/cognitoIdentityProvider/types'; @@ -36,9 +40,8 @@ describe('signIn API happy path cases', () => { let handleUserPasswordFlowSpy: jest.SpyInstance; beforeAll(() => { - Amplify.configure({ - Auth: authConfig, - }); + const mockCtx = createMockAmplifyContext({ Auth: authConfig }); + setGlobalContext(mockCtx); cognitoUserPoolsTokenProvider.setAuthConfig(authConfig); }); @@ -68,9 +71,10 @@ describe('signIn API happy path cases', () => { }); test('handleUserPasswordAuthFlow should be called with clientMetadata from request', async () => { + const mockCtx = createMockAmplifyContext({ Auth: authConfig }); const { username } = authAPITestParams.user1; const { password } = authAPITestParams.user1; - await signInWithUserPassword({ + await signInWithUserPassword(mockCtx, { username, password, options: authAPITestParams.configWithClientMetadata, @@ -137,3 +141,7 @@ describe('Cognito ASF', () => { ); }); }); + +afterAll(() => { + clearGlobalContext(); +}); diff --git a/packages/auth/__tests__/providers/cognito/signOut.test.ts b/packages/auth/__tests__/providers/cognito/signOut.test.ts index 49779a748ca..e232060a2e9 100644 --- a/packages/auth/__tests__/providers/cognito/signOut.test.ts +++ b/packages/auth/__tests__/providers/cognito/signOut.test.ts @@ -1,13 +1,12 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +import { ConsoleLogger, Hub } from '@aws-amplify/core'; import { - Amplify, - ConsoleLogger, - Hub, - clearCredentials, -} from '@aws-amplify/core'; -import { AMPLIFY_SYMBOL } from '@aws-amplify/core/internals/utils'; + AMPLIFY_SYMBOL, + clearGlobalContext, + setGlobalContext, +} from '@aws-amplify/core/internals/utils'; import { signOut } from '../../../src/providers/cognito/apis/signOut'; import { tokenOrchestrator } from '../../../src/providers/cognito/tokenProvider'; @@ -20,8 +19,8 @@ import { } from '../../../src/foundation/factories/serviceClients/cognitoIdentityProvider'; import { getRegionFromUserPoolId } from '../../../src/foundation/parsers'; import { createCognitoUserPoolEndpointResolver } from '../../../src/providers/cognito/factories'; +import { createMockAmplifyContext } from '../../testUtils/mockAmplifyContext'; -jest.mock('@aws-amplify/core'); jest.mock('../../../src/providers/cognito/tokenProvider'); jest.mock('../../../src/providers/cognito/utils/oauth'); jest.mock('../../../src/providers/cognito/utils/signInWithRedirectStore'); @@ -52,13 +51,10 @@ describe('signOut', () => { refreshToken, }; // assert mocks - const mockAmplify = Amplify as jest.Mocked; - const mockClearCredentials = clearCredentials as jest.Mock; const mockGetRegionFromUserPoolId = jest.mocked(getRegionFromUserPoolId); const mockGlobalSignOut = jest.fn(); const mockCreateGlobalSignOutClient = jest.mocked(createGlobalSignOutClient); const mockHandleOAuthSignOut = handleOAuthSignOut as jest.Mock; - const mockHub = Hub as jest.Mocked; const mockRevokeToken = jest.fn(); const mockedRevokeTokenClient = jest.mocked(createRevokeTokenClient); const mockTokenOrchestrator = tokenOrchestrator as jest.Mocked< @@ -78,12 +74,18 @@ describe('signOut', () => { }; // create spies const loggerDebugSpy = jest.spyOn(ConsoleLogger.prototype, 'debug'); + const hubDispatchSpy = jest.spyOn(Hub, 'dispatch'); + + // mock context + let mockCtx: ReturnType; + const mockClearCredentials = () => mockCtx.clearCredentials as jest.Mock; + // create test helpers const expectSignOut = () => ({ toComplete: () => { expect(mockTokenOrchestrator.clearTokens).toHaveBeenCalledTimes(1); - expect(mockClearCredentials).toHaveBeenCalledTimes(1); - expect(mockHub.dispatch).toHaveBeenCalledWith( + expect(mockClearCredentials()).toHaveBeenCalledTimes(1); + expect(hubDispatchSpy).toHaveBeenCalledWith( 'auth', { event: 'signedOut' }, 'Auth', @@ -93,8 +95,8 @@ describe('signOut', () => { not: { toComplete: () => { expect(mockTokenOrchestrator.clearTokens).not.toHaveBeenCalled(); - expect(mockClearCredentials).not.toHaveBeenCalled(); - expect(mockHub.dispatch).not.toHaveBeenCalled(); + expect(mockClearCredentials()).not.toHaveBeenCalled(); + expect(hubDispatchSpy).not.toHaveBeenCalled(); }, }, }); @@ -107,7 +109,10 @@ describe('signOut', () => { }); beforeEach(() => { - mockAmplify.getConfig.mockReturnValue({ Auth: { Cognito: cognitoConfig } }); + mockCtx = createMockAmplifyContext({ + Auth: { Cognito: cognitoConfig }, + }); + setGlobalContext(mockCtx); mockGlobalSignOut.mockResolvedValue({ $metadata: {} }); mockCreateGlobalSignOutClient.mockReturnValueOnce(mockGlobalSignOut); mockRevokeToken.mockResolvedValue({}); @@ -117,12 +122,11 @@ describe('signOut', () => { }); afterEach(() => { - mockAmplify.getConfig.mockReset(); + clearGlobalContext(); mockGlobalSignOut.mockReset(); mockRevokeToken.mockReset(); - mockClearCredentials.mockClear(); mockGetRegionFromUserPoolId.mockClear(); - mockHub.dispatch.mockClear(); + hubDispatchSpy.mockClear(); mockTokenOrchestrator.clearTokens.mockClear(); loggerDebugSpy.mockClear(); mockCreateCognitoUserPoolEndpointResolver.mockClear(); @@ -144,7 +148,8 @@ describe('signOut', () => { it('invokes createCognitoUserPoolEndpointResolver with the userPoolEndpoint for creating the revokeToken client', async () => { const expectedUserPoolEndpoint = 'https://my-custom-endpoint.com'; const expectedEndpointResolver = jest.fn(); - mockAmplify.getConfig.mockReturnValueOnce({ + // Override context with custom endpoint config + mockCtx = createMockAmplifyContext({ Auth: { Cognito: { ...cognitoConfig, @@ -152,6 +157,7 @@ describe('signOut', () => { }, }, }); + setGlobalContext(mockCtx); mockCreateCognitoUserPoolEndpointResolver.mockReturnValueOnce( expectedEndpointResolver, ); @@ -195,7 +201,8 @@ describe('signOut', () => { it('invokes createCognitoUserPoolEndpointResolver with the userPoolEndpoint for creating the globalSignOut client', async () => { const expectedUserPoolEndpoint = 'https://my-custom-endpoint.com'; const expectedEndpointResolver = jest.fn(); - mockAmplify.getConfig.mockReturnValueOnce({ + // Override context with custom endpoint config + mockCtx = createMockAmplifyContext({ Auth: { Cognito: { ...cognitoConfig, @@ -203,6 +210,7 @@ describe('signOut', () => { }, }, }); + setGlobalContext(mockCtx); mockCreateCognitoUserPoolEndpointResolver.mockReturnValueOnce( expectedEndpointResolver, ); @@ -257,14 +265,14 @@ describe('signOut', () => { }; beforeEach(() => { - mockAmplify.getConfig.mockReturnValue({ + mockCtx = createMockAmplifyContext({ Auth: { Cognito: cognitoConfigWithOauth }, }); + setGlobalContext(mockCtx); mockHandleOAuthSignOut.mockResolvedValue({ type: 'success' }); }); afterEach(() => { - mockAmplify.getConfig.mockReset(); mockHandleOAuthSignOut.mockReset(); }); @@ -276,6 +284,9 @@ describe('signOut', () => { cognitoConfigWithOauth, ); expect(mockHandleOAuthSignOut).toHaveBeenCalledWith( + expect.objectContaining({ + resourcesConfig: expect.any(Object), + }), cognitoConfigWithOauth, mockDefaultOAuthStoreInstance, mockTokenOrchestrator, diff --git a/packages/auth/__tests__/providers/cognito/signUp.test.ts b/packages/auth/__tests__/providers/cognito/signUp.test.ts index 3b3f9bab4c5..248a4afd2f4 100644 --- a/packages/auth/__tests__/providers/cognito/signUp.test.ts +++ b/packages/auth/__tests__/providers/cognito/signUp.test.ts @@ -1,23 +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 { signUp } from '../../../src/providers/cognito'; import { AuthValidationErrorCode } from '../../../src/errors/types/validation'; import { AuthError } from '../../../src/errors/AuthError'; import { SignUpException } from '../../../src/providers/cognito/types/errors'; import { createSignUpClient } from '../../../src/foundation/factories/serviceClients/cognitoIdentityProvider'; import { createCognitoUserPoolEndpointResolver } from '../../../src/providers/cognito/factories'; +import { createMockAmplifyContext } from '../../testUtils/mockAmplifyContext'; import { authAPITestParams } from './testUtils/authApiTestParams'; import { getMockError } from './testUtils/data'; -import { setUpGetConfig } from './testUtils/setUpGetConfig'; -jest.mock('@aws-amplify/core', () => ({ - ...(jest.createMockFromModule('@aws-amplify/core') as object), - Amplify: { getConfig: jest.fn(() => ({})) }, -})); jest.mock('@aws-amplify/core/internals/utils', () => ({ ...jest.requireActual('@aws-amplify/core/internals/utils'), isBrowser: jest.fn(() => false), @@ -38,8 +32,14 @@ describe('signUp', () => { createCognitoUserPoolEndpointResolver, ); - beforeAll(() => { - setUpGetConfig(Amplify); + const mockCtx = createMockAmplifyContext({ + Auth: { + Cognito: { + userPoolClientId: '111111-aaaaa-42d8-891d-ee81a1549398', + userPoolId: 'us-west-2_zzzzz', + identityPoolId: 'us-west-2:xxxxxx', + }, + }, }); beforeEach(() => { @@ -61,7 +61,7 @@ describe('signUp', () => { }); it('should call SignUp service client with correct params', async () => { - await signUp({ + await signUp(mockCtx, { username: user1.username, password: user1.password, options: { @@ -87,7 +87,7 @@ describe('signUp', () => { it('invokes mockCreateCognitoUserPoolEndpointResolver with expected endpointOverride', async () => { const expectedUserPoolEndpoint = 'https://my-custom-endpoint.com'; - jest.mocked(Amplify.getConfig).mockReturnValueOnce({ + const endpointCtx = createMockAmplifyContext({ Auth: { Cognito: { userPoolClientId: '111111-aaaaa-42d8-891d-ee81a1549398', @@ -97,7 +97,7 @@ describe('signUp', () => { }, }, }); - await signUp({ + await signUp(endpointCtx, { username: user1.username, password: user1.password, options: { @@ -111,7 +111,7 @@ describe('signUp', () => { }); it('should return `CONFIRM_SIGN_UP` step when user isn`t confirmed yet', async () => { - const result = await signUp({ + const result = await signUp(mockCtx, { username: user1.username, password: user1.password, options: { @@ -137,7 +137,7 @@ describe('signUp', () => { UserConfirmed: true, UserSub: userId, }); - const result = await signUp({ + const result = await signUp(mockCtx, { username: user1.username, password: user1.password, options: { @@ -154,8 +154,7 @@ describe('signUp', () => { }); it('should return `COMPLETE_AUTO_SIGN_IN` step with `isSignUpComplete` false when autoSignIn is enabled and user isn`t confirmed yet', async () => { - // set up signUpVerificationMethod as link in auth config - (Amplify.getConfig as any).mockReturnValue({ + const linkCtx = createMockAmplifyContext({ Auth: { Cognito: { userPoolClientId: '111111-aaaaa-42d8-891d-ee81a1549398', @@ -166,7 +165,7 @@ describe('signUp', () => { }, }); - const result = await signUp({ + const result = await signUp(linkCtx, { username: user1.username, password: user1.password, options: { @@ -195,7 +194,7 @@ describe('signUp', () => { UserSub: userId, }); - const result = await signUp({ + const result = await signUp(mockCtx, { username: user1.username, password: user1.password, options: { @@ -219,7 +218,7 @@ describe('signUp', () => { return 'abcd'; }, }; - await signUp({ + await signUp(mockCtx, { username: user1.username, password: user1.password, options: { @@ -246,7 +245,7 @@ describe('signUp', () => { }); it('should not throw an error when password is empty', async () => { - await signUp({ username: user1.username, password: '' }); + await signUp(mockCtx, { username: user1.username, password: '' }); expect(mockSignUp).toHaveBeenCalledWith( { region: 'us-west-2', @@ -277,7 +276,7 @@ describe('signUp', () => { it('should throw an error when username is empty', async () => { expect.assertions(2); try { - await signUp({ username: '', password: user1.password }); + await signUp(mockCtx, { username: '', password: user1.password }); } catch (error: any) { expect(error).toBeInstanceOf(AuthError); expect(error.name).toBe(AuthValidationErrorCode.EmptySignUpUsername); @@ -290,7 +289,10 @@ describe('signUp', () => { throw getMockError(SignUpException.InvalidParameterException); }); try { - await signUp({ username: user1.username, password: user1.password }); + await signUp(mockCtx, { + username: user1.username, + password: user1.password, + }); } catch (error: any) { expect(error).toBeInstanceOf(AuthError); expect(error.name).toBe(SignUpException.InvalidParameterException); diff --git a/packages/auth/__tests__/providers/cognito/testUtils/setUpGetConfig.ts b/packages/auth/__tests__/providers/cognito/testUtils/setUpGetConfig.ts deleted file mode 100644 index 0f638b84e37..00000000000 --- a/packages/auth/__tests__/providers/cognito/testUtils/setUpGetConfig.ts +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -export const setUpGetConfig = (mockAmplify: any) => { - mockAmplify.getConfig.mockReturnValue({ - Auth: { - Cognito: { - userPoolClientId: '111111-aaaaa-42d8-891d-ee81a1549398', - userPoolId: 'us-west-2_zzzzz', - identityPoolId: 'us-west-2:xxxxxx', - }, - }, - }); -}; diff --git a/packages/auth/__tests__/providers/cognito/updateMFAPreference.test.ts b/packages/auth/__tests__/providers/cognito/updateMFAPreference.test.ts index 0d597b5ec9b..e2dbe3640ac 100644 --- a/packages/auth/__tests__/providers/cognito/updateMFAPreference.test.ts +++ b/packages/auth/__tests__/providers/cognito/updateMFAPreference.test.ts @@ -1,7 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { Amplify, fetchAuthSession } from '@aws-amplify/core'; import { decodeJWT } from '@aws-amplify/core/internals/utils'; import { @@ -14,16 +13,12 @@ import { getMFASettings } from '../../../src/providers/cognito/apis/updateMFAPre import { MFAPreference } from '../../../src/providers/cognito/types'; import { createSetUserMFAPreferenceClient } from '../../../src/foundation/factories/serviceClients/cognitoIdentityProvider'; import { createCognitoUserPoolEndpointResolver } from '../../../src/providers/cognito/factories'; +import { createMockAmplifyContext } from '../../testUtils/mockAmplifyContext'; import { getMockError, mockAccessToken } from './testUtils/data'; -import { setUpGetConfig } from './testUtils/setUpGetConfig'; type MfaPreferenceValue = MFAPreference | undefined; -jest.mock('@aws-amplify/core', () => ({ - ...(jest.createMockFromModule('@aws-amplify/core') as object), - Amplify: { getConfig: jest.fn(() => ({})) }, -})); jest.mock('@aws-amplify/core/internals/utils', () => ({ ...jest.requireActual('@aws-amplify/core/internals/utils'), isBrowser: jest.fn(() => false), @@ -69,7 +64,6 @@ const mfaChoices = generateUpdateMFAPreferenceOptions(); describe('updateMFAPreference', () => { // assert mocks - const mockFetchAuthSession = fetchAuthSession as jest.Mock; const mockSetUserMFAPreference = jest.fn(); const mockCreateSetUserMFAPreferenceClient = jest.mocked( createSetUserMFAPreferenceClient, @@ -78,9 +72,18 @@ describe('updateMFAPreference', () => { createCognitoUserPoolEndpointResolver, ); + const mockCtx = createMockAmplifyContext({ + Auth: { + Cognito: { + userPoolClientId: '111111-aaaaa-42d8-891d-ee81a1549398', + userPoolId: 'us-west-2_zzzzz', + identityPoolId: 'us-west-2:xxxxxx', + }, + }, + }); + beforeAll(() => { - setUpGetConfig(Amplify); - mockFetchAuthSession.mockResolvedValue({ + (mockCtx.fetchAuthSession as jest.Mock).mockResolvedValue({ tokens: { accessToken: decodeJWT(mockAccessToken) }, }); }); @@ -94,7 +97,7 @@ describe('updateMFAPreference', () => { afterEach(() => { mockSetUserMFAPreference.mockReset(); - mockFetchAuthSession.mockClear(); + (mockCtx.fetchAuthSession as jest.Mock).mockClear(); mockCreateSetUserMFAPreferenceClient.mockClear(); }); @@ -102,7 +105,7 @@ describe('updateMFAPreference', () => { 'should update with email $email, sms $sms, and totp $totp', async mfaChoice => { const { totp, sms, email } = mfaChoice; - await updateMFAPreference(mfaChoice); + await updateMFAPreference(mockCtx, mfaChoice); expect(mockSetUserMFAPreference).toHaveBeenCalledWith( { region: 'us-west-2', @@ -120,7 +123,7 @@ describe('updateMFAPreference', () => { it('invokes mockCreateCognitoUserPoolEndpointResolver with expected endpointOverride', async () => { const expectedUserPoolEndpoint = 'https://my-custom-endpoint.com'; - jest.mocked(Amplify.getConfig).mockReturnValueOnce({ + const endpointCtx = createMockAmplifyContext({ Auth: { Cognito: { userPoolClientId: '111111-aaaaa-42d8-891d-ee81a1549398', @@ -130,7 +133,10 @@ describe('updateMFAPreference', () => { }, }, }); - await updateMFAPreference(mfaChoices[0]); + (endpointCtx.fetchAuthSession as jest.Mock).mockResolvedValue({ + tokens: { accessToken: decodeJWT(mockAccessToken) }, + }); + await updateMFAPreference(endpointCtx, mfaChoices[0]); expect(mockCreateCognitoUserPoolEndpointResolver).toHaveBeenCalledWith({ endpointOverride: expectedUserPoolEndpoint, @@ -145,7 +151,7 @@ describe('updateMFAPreference', () => { ); }); try { - await updateMFAPreference({ sms: 'ENABLED', totp: 'PREFERRED' }); + await updateMFAPreference(mockCtx, { sms: 'ENABLED', totp: 'PREFERRED' }); } catch (error: any) { expect(error).toBeInstanceOf(AuthError); expect(error.name).toBe( diff --git a/packages/auth/__tests__/providers/cognito/updatePassword.test.ts b/packages/auth/__tests__/providers/cognito/updatePassword.test.ts index 72dfe80119e..5fed3eb8056 100644 --- a/packages/auth/__tests__/providers/cognito/updatePassword.test.ts +++ b/packages/auth/__tests__/providers/cognito/updatePassword.test.ts @@ -1,7 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { Amplify, fetchAuthSession } from '@aws-amplify/core'; import { decodeJWT } from '@aws-amplify/core/internals/utils'; import { AuthError } from '../../../src/errors/AuthError'; @@ -10,14 +9,10 @@ import { updatePassword } from '../../../src/providers/cognito'; import { ChangePasswordException } from '../../../src/providers/cognito/types/errors'; import { createChangePasswordClient } from '../../../src/foundation/factories/serviceClients/cognitoIdentityProvider'; import { createCognitoUserPoolEndpointResolver } from '../../../src/providers/cognito/factories'; +import { createMockAmplifyContext } from '../../testUtils/mockAmplifyContext'; import { getMockError, mockAccessToken } from './testUtils/data'; -import { setUpGetConfig } from './testUtils/setUpGetConfig'; -jest.mock('@aws-amplify/core', () => ({ - ...(jest.createMockFromModule('@aws-amplify/core') as object), - Amplify: { getConfig: jest.fn(() => ({})) }, -})); jest.mock('@aws-amplify/core/internals/utils', () => ({ ...jest.requireActual('@aws-amplify/core/internals/utils'), isBrowser: jest.fn(() => false), @@ -31,7 +26,6 @@ describe('updatePassword', () => { const oldPassword = 'oldPassword'; const newPassword = 'newPassword'; // assert mocks - const mockFetchAuthSession = fetchAuthSession as jest.Mock; const mockChangePassword = jest.fn(); const mockCreateChangePasswordClient = jest.mocked( createChangePasswordClient, @@ -40,9 +34,18 @@ describe('updatePassword', () => { createCognitoUserPoolEndpointResolver, ); + const mockCtx = createMockAmplifyContext({ + Auth: { + Cognito: { + userPoolClientId: '111111-aaaaa-42d8-891d-ee81a1549398', + userPoolId: 'us-west-2_zzzzz', + identityPoolId: 'us-west-2:xxxxxx', + }, + }, + }); + beforeAll(() => { - setUpGetConfig(Amplify); - mockFetchAuthSession.mockResolvedValue({ + (mockCtx.fetchAuthSession as jest.Mock).mockResolvedValue({ tokens: { accessToken: decodeJWT(mockAccessToken) }, }); }); @@ -54,12 +57,12 @@ describe('updatePassword', () => { afterEach(() => { mockChangePassword.mockReset(); - mockFetchAuthSession.mockClear(); + (mockCtx.fetchAuthSession as jest.Mock).mockClear(); mockCreateChangePasswordClient.mockClear(); }); it('should call changePassword', async () => { - await updatePassword({ oldPassword, newPassword }); + await updatePassword(mockCtx, { oldPassword, newPassword }); expect(mockChangePassword).toHaveBeenCalledWith( expect.objectContaining({ region: 'us-west-2' }), @@ -73,7 +76,7 @@ describe('updatePassword', () => { it('invokes mockCreateCognitoUserPoolEndpointResolver with expected endpointOverride', async () => { const expectedUserPoolEndpoint = 'https://my-custom-endpoint.com'; - jest.mocked(Amplify.getConfig).mockReturnValueOnce({ + const endpointCtx = createMockAmplifyContext({ Auth: { Cognito: { userPoolClientId: '111111-aaaaa-42d8-891d-ee81a1549398', @@ -83,7 +86,10 @@ describe('updatePassword', () => { }, }, }); - await updatePassword({ oldPassword, newPassword }); + (endpointCtx.fetchAuthSession as jest.Mock).mockResolvedValue({ + tokens: { accessToken: decodeJWT(mockAccessToken) }, + }); + await updatePassword(endpointCtx, { oldPassword, newPassword }); expect(mockCreateCognitoUserPoolEndpointResolver).toHaveBeenCalledWith({ endpointOverride: expectedUserPoolEndpoint, @@ -93,7 +99,7 @@ describe('updatePassword', () => { it('should throw an error when oldPassword is empty', async () => { expect.assertions(2); try { - await updatePassword({ oldPassword: '', newPassword }); + await updatePassword(mockCtx, { oldPassword: '', newPassword }); } catch (error: any) { expect(error).toBeInstanceOf(AuthError); expect(error.name).toBe(AuthValidationErrorCode.EmptyUpdatePassword); @@ -103,7 +109,7 @@ describe('updatePassword', () => { it('should throw an error when newPassword is empty', async () => { expect.assertions(2); try { - await updatePassword({ oldPassword, newPassword: '' }); + await updatePassword(mockCtx, { oldPassword, newPassword: '' }); } catch (error: any) { expect(error).toBeInstanceOf(AuthError); expect(error.name).toBe(AuthValidationErrorCode.EmptyUpdatePassword); @@ -117,7 +123,7 @@ describe('updatePassword', () => { }); try { - await updatePassword({ oldPassword, newPassword }); + await updatePassword(mockCtx, { oldPassword, newPassword }); } catch (error: any) { expect(error).toBeInstanceOf(AuthError); expect(error.name).toBe( diff --git a/packages/auth/__tests__/providers/cognito/updateUserAttribute.test.ts b/packages/auth/__tests__/providers/cognito/updateUserAttribute.test.ts index 4fa6ac086d7..c4984393c51 100644 --- a/packages/auth/__tests__/providers/cognito/updateUserAttribute.test.ts +++ b/packages/auth/__tests__/providers/cognito/updateUserAttribute.test.ts @@ -1,19 +1,14 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { Amplify, fetchAuthSession } from '@aws-amplify/core'; import { decodeJWT } from '@aws-amplify/core/internals/utils'; import { updateUserAttribute } from '../../../src/providers/cognito'; import { updateUserAttributes } from '../../../src/providers/cognito/apis/updateUserAttributes'; +import { createMockAmplifyContext } from '../../testUtils/mockAmplifyContext'; import { mockAccessToken } from './testUtils/data'; -import { setUpGetConfig } from './testUtils/setUpGetConfig'; -jest.mock('@aws-amplify/core', () => ({ - ...(jest.createMockFromModule('@aws-amplify/core') as object), - Amplify: { getConfig: jest.fn(() => ({})) }, -})); jest.mock('@aws-amplify/core/internals/utils', () => ({ ...jest.requireActual('@aws-amplify/core/internals/utils'), isBrowser: jest.fn(() => false), @@ -21,19 +16,27 @@ jest.mock('@aws-amplify/core/internals/utils', () => ({ jest.mock('../../../src/providers/cognito/apis/updateUserAttributes'); describe('updateUserAttribute API happy path cases', () => { - const mockFetchAuthSession = fetchAuthSession as jest.Mock; const mockUpdateUserAttributes = updateUserAttributes as jest.Mock; + const mockCtx = createMockAmplifyContext({ + Auth: { + Cognito: { + userPoolClientId: '111111-aaaaa-42d8-891d-ee81a1549398', + userPoolId: 'us-west-2_zzzzz', + identityPoolId: 'us-west-2:xxxxxx', + }, + }, + }); + beforeAll(() => { - setUpGetConfig(Amplify); - mockFetchAuthSession.mockResolvedValue({ + (mockCtx.fetchAuthSession as jest.Mock).mockResolvedValue({ tokens: { accessToken: decodeJWT(mockAccessToken) }, }); }); afterEach(() => { mockUpdateUserAttributes.mockReset(); - mockFetchAuthSession.mockClear(); + (mockCtx.fetchAuthSession as jest.Mock).mockClear(); }); it('should return correct output', async () => { @@ -58,14 +61,19 @@ describe('updateUserAttribute API happy path cases', () => { }, }; mockUpdateUserAttributes.mockResolvedValue({ email: mockOutput }); - const result = await updateUserAttribute(mockInput); + const result = await updateUserAttribute(mockCtx, mockInput); expect(result).toEqual(mockOutput); expect(mockUpdateUserAttributes).toHaveBeenCalledTimes(1); - expect(mockUpdateUserAttributes).toHaveBeenCalledWith({ - userAttributes: { - [mockInput.userAttribute.attributeKey]: mockInput.userAttribute.value, + expect(mockUpdateUserAttributes).toHaveBeenCalledWith( + expect.objectContaining({ + resourcesConfig: expect.any(Object), + }), + { + userAttributes: { + [mockInput.userAttribute.attributeKey]: mockInput.userAttribute.value, + }, + options: mockInput.options, }, - options: mockInput.options, - }); + ); }); }); diff --git a/packages/auth/__tests__/providers/cognito/updateUserAttributes.test.ts b/packages/auth/__tests__/providers/cognito/updateUserAttributes.test.ts index bfa9643b76d..16486e9ea67 100644 --- a/packages/auth/__tests__/providers/cognito/updateUserAttributes.test.ts +++ b/packages/auth/__tests__/providers/cognito/updateUserAttributes.test.ts @@ -1,7 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { Amplify, fetchAuthSession } from '@aws-amplify/core'; import { decodeJWT } from '@aws-amplify/core/internals/utils'; import { AuthError } from '../../../src/errors/AuthError'; @@ -10,14 +9,10 @@ import { UpdateUserAttributesException } from '../../../src/providers/cognito/ty import { toAttributeType } from '../../../src/providers/cognito/utils/apiHelpers'; import { createUpdateUserAttributesClient } from '../../../src/foundation/factories/serviceClients/cognitoIdentityProvider'; import { createCognitoUserPoolEndpointResolver } from '../../../src/providers/cognito/factories'; +import { createMockAmplifyContext } from '../../testUtils/mockAmplifyContext'; import { getMockError, mockAccessToken } from './testUtils/data'; -import { setUpGetConfig } from './testUtils/setUpGetConfig'; -jest.mock('@aws-amplify/core', () => ({ - ...(jest.createMockFromModule('@aws-amplify/core') as object), - Amplify: { getConfig: jest.fn(() => ({})) }, -})); jest.mock('@aws-amplify/core/internals/utils', () => ({ ...jest.requireActual('@aws-amplify/core/internals/utils'), isBrowser: jest.fn(() => false), @@ -29,7 +24,6 @@ jest.mock('../../../src/providers/cognito/factories'); describe('updateUserAttributes', () => { // assert mocks - const mockFetchAuthSession = fetchAuthSession as jest.Mock; const mockUpdateUserAttributes = jest.fn(); const mockCreateUpdateUserAttributesClient = jest.mocked( createUpdateUserAttributesClient, @@ -38,9 +32,18 @@ describe('updateUserAttributes', () => { createCognitoUserPoolEndpointResolver, ); + const mockCtx = createMockAmplifyContext({ + Auth: { + Cognito: { + userPoolClientId: '111111-aaaaa-42d8-891d-ee81a1549398', + userPoolId: 'us-west-2_zzzzz', + identityPoolId: 'us-west-2:xxxxxx', + }, + }, + }); + beforeAll(() => { - setUpGetConfig(Amplify); - mockFetchAuthSession.mockResolvedValue({ + (mockCtx.fetchAuthSession as jest.Mock).mockResolvedValue({ tokens: { accessToken: decodeJWT(mockAccessToken) }, }); }); @@ -67,7 +70,7 @@ describe('updateUserAttributes', () => { afterEach(() => { mockUpdateUserAttributes.mockReset(); - mockFetchAuthSession.mockClear(); + (mockCtx.fetchAuthSession as jest.Mock).mockClear(); mockCreateUpdateUserAttributesClient.mockClear(); }); @@ -78,7 +81,7 @@ describe('updateUserAttributes', () => { email: 'mockedEmail', phone_number: 'mockedPhoneNumber', }; - const result = await updateUserAttributes({ + const result = await updateUserAttributes(mockCtx, { userAttributes, options: { clientMetadata: { foo: 'bar' }, @@ -135,7 +138,7 @@ describe('updateUserAttributes', () => { it('invokes mockCreateCognitoUserPoolEndpointResolver with expected endpointOverride', async () => { const expectedUserPoolEndpoint = 'https://my-custom-endpoint.com'; - jest.mocked(Amplify.getConfig).mockReturnValueOnce({ + const endpointCtx = createMockAmplifyContext({ Auth: { Cognito: { userPoolClientId: '111111-aaaaa-42d8-891d-ee81a1549398', @@ -145,7 +148,10 @@ describe('updateUserAttributes', () => { }, }, }); - await updateUserAttributes({ + (endpointCtx.fetchAuthSession as jest.Mock).mockResolvedValue({ + tokens: { accessToken: decodeJWT(mockAccessToken) }, + }); + await updateUserAttributes(endpointCtx, { userAttributes: {}, options: { clientMetadata: { foo: 'bar' }, @@ -163,7 +169,7 @@ describe('updateUserAttributes', () => { address: 'mockedAddress', name: 'mockedName', }; - const result = await updateUserAttributes({ + const result = await updateUserAttributes(mockCtx, { userAttributes, options: { clientMetadata: { foo: 'bar' }, @@ -213,7 +219,7 @@ describe('updateUserAttributes', () => { email: 'mockedEmail', phone_number: 'mockedPhoneNumber', }; - const result = await updateUserAttributes({ + const result = await updateUserAttributes(mockCtx, { userAttributes, options: { clientMetadata: { foo: 'bar' }, @@ -262,7 +268,7 @@ describe('updateUserAttributes', () => { ); }); try { - await updateUserAttributes({ + await updateUserAttributes(mockCtx, { userAttributes: { email: 'mockedEmail', }, diff --git a/packages/auth/__tests__/providers/cognito/utils/dispatchSignedInHubEvent.test.ts b/packages/auth/__tests__/providers/cognito/utils/dispatchSignedInHubEvent.test.ts index cda090b166b..f37fb61fe63 100644 --- a/packages/auth/__tests__/providers/cognito/utils/dispatchSignedInHubEvent.test.ts +++ b/packages/auth/__tests__/providers/cognito/utils/dispatchSignedInHubEvent.test.ts @@ -10,6 +10,7 @@ import { } from '../../../../src/providers/cognito/utils/dispatchSignedInHubEvent'; import { getCurrentUser } from '../../../../src/providers/cognito/apis/getCurrentUser'; import { assertAuthTokens } from '../../../../src/providers/cognito/utils/types'; +import { createMockAmplifyContext } from '../../../testUtils/mockAmplifyContext'; jest.mock('../../../../src/providers/cognito/apis/getCurrentUser', () => ({ getCurrentUser: jest.fn(), @@ -28,6 +29,8 @@ const mockGetCurrentUser = getCurrentUser as jest.Mock; const mockDispatch = Hub.dispatch as jest.Mock; describe('dispatchSignedInHubEvent()', () => { + const mockCtx = createMockAmplifyContext(); + it('dispatches Hub event `signedIn` with `getCurrentUser()` returned data', async () => { const mockGetCurrentUserPayload = { username: 'hello', @@ -35,7 +38,7 @@ describe('dispatchSignedInHubEvent()', () => { }; mockGetCurrentUser.mockResolvedValueOnce(mockGetCurrentUserPayload); - await dispatchSignedInHubEvent(); + await dispatchSignedInHubEvent(mockCtx); expect(mockDispatch).toHaveBeenCalledWith( 'auth', @@ -53,7 +56,9 @@ describe('dispatchSignedInHubEvent()', () => { assertAuthTokens(null); }); - expect(() => dispatchSignedInHubEvent()).rejects.toThrow(ERROR_MESSAGE); + expect(() => dispatchSignedInHubEvent(mockCtx)).rejects.toThrow( + ERROR_MESSAGE, + ); }); it('rethrows error if the error is not handled by itself', () => { @@ -63,6 +68,6 @@ describe('dispatchSignedInHubEvent()', () => { throw mockError; }); - expect(() => dispatchSignedInHubEvent()).rejects.toThrow(mockError); + expect(() => dispatchSignedInHubEvent(mockCtx)).rejects.toThrow(mockError); }); }); diff --git a/packages/auth/__tests__/providers/cognito/utils/oauth/attemptCompleteOAuthFlow.test.ts b/packages/auth/__tests__/providers/cognito/utils/oauth/attemptCompleteOAuthFlow.test.ts index 589396dc848..45a042f33a3 100644 --- a/packages/auth/__tests__/providers/cognito/utils/oauth/attemptCompleteOAuthFlow.test.ts +++ b/packages/auth/__tests__/providers/cognito/utils/oauth/attemptCompleteOAuthFlow.test.ts @@ -12,6 +12,7 @@ import { getRedirectUrl } from '../../../../../src/providers/cognito/utils/oauth import { oAuthStore } from '../../../../../src/providers/cognito/utils/oauth/oAuthStore'; import { mockAuthConfigWithOAuth } from '../../../../mockData'; import type { OAuthStore } from '../../../../../src/providers/cognito/utils/types'; +import { createMockAmplifyContext } from '../../../../testUtils/mockAmplifyContext'; jest.mock('@aws-amplify/core/internals/utils'); jest.mock('../../../../../src/providers/cognito/utils/oauth/completeOAuthFlow'); @@ -50,6 +51,9 @@ const mockGetRedirectUrl = getRedirectUrl as jest.Mock; describe('attemptCompleteOAuthFlow', () => { const windowSpy = jest.spyOn(window, 'window', 'get'); const mockRedirectUrl = 'http://localhost:3000/'; + const mockCtx = createMockAmplifyContext({ + Auth: mockAuthConfigWithOAuth.Auth, + }); beforeAll(() => { (oAuthStore.loadOAuthInFlight as jest.Mock).mockResolvedValue(false); @@ -82,7 +86,7 @@ describe('attemptCompleteOAuthFlow', () => { it('invokes config asserters', async () => { const cognitoConfig = mockAuthConfigWithOAuth.Auth.Cognito; - await attemptCompleteOAuthFlow(cognitoConfig); + await attemptCompleteOAuthFlow(mockCtx, cognitoConfig); expect(mockAssertTokenProviderConfig).toHaveBeenCalledWith(cognitoConfig); expect(mockAssertOAuthConfig).toHaveBeenCalledWith(cognitoConfig); @@ -90,7 +94,10 @@ describe('attemptCompleteOAuthFlow', () => { }); it('does nothing when `await oAuthStore.loadOAuthInFlight()` resolves `false` (there is no inflight oauth process)', async () => { - await attemptCompleteOAuthFlow(mockAuthConfigWithOAuth.Auth.Cognito); + await attemptCompleteOAuthFlow( + mockCtx, + mockAuthConfigWithOAuth.Auth.Cognito, + ); expect(oAuthStore.loadOAuthInFlight).toHaveBeenCalledTimes(1); expect(mockCompleteOAuthFlow).not.toHaveBeenCalled(); @@ -99,9 +106,13 @@ describe('attemptCompleteOAuthFlow', () => { it('invokes `completeOAuthFlow` to complete an inflight oauth process', async () => { (oAuthStore.loadOAuthInFlight as jest.Mock).mockResolvedValueOnce(true); - await attemptCompleteOAuthFlow(mockAuthConfigWithOAuth.Auth.Cognito); + await attemptCompleteOAuthFlow( + mockCtx, + mockAuthConfigWithOAuth.Auth.Cognito, + ); expect(mockCompleteOAuthFlow).toHaveBeenCalledWith( + mockCtx, expect.objectContaining({ currentUrl: 'http://localhost:3000/', redirectUri: 'http://localhost:3000/', @@ -118,7 +129,7 @@ describe('attemptCompleteOAuthFlow', () => { throw new Error('some error'); }); expect( - attemptCompleteOAuthFlow(mockAuthConfigWithOAuth.Auth.Cognito), + attemptCompleteOAuthFlow(mockCtx, mockAuthConfigWithOAuth.Auth.Cognito), ).resolves.toBeUndefined(); }); }); diff --git a/packages/auth/__tests__/providers/cognito/utils/oauth/completeOAuthFlow.test.ts b/packages/auth/__tests__/providers/cognito/utils/oauth/completeOAuthFlow.test.ts index 5478a230394..be4600e79f9 100644 --- a/packages/auth/__tests__/providers/cognito/utils/oauth/completeOAuthFlow.test.ts +++ b/packages/auth/__tests__/providers/cognito/utils/oauth/completeOAuthFlow.test.ts @@ -12,6 +12,7 @@ import { AuthError } from '../../../../../src/errors/AuthError'; import { AuthErrorTypes } from '../../../../../src/types/Auth'; import { OAuthStore } from '../../../../../src/providers/cognito/utils/types'; import { completeOAuthFlow } from '../../../../../src/providers/cognito/utils/oauth/completeOAuthFlow'; +import { createMockAmplifyContext } from '../../../../testUtils/mockAmplifyContext'; jest.mock('../../../../../src/providers/cognito/tokenProvider'); jest.mock('@aws-amplify/core', () => ({ @@ -57,6 +58,7 @@ describe('completeOAuthFlow', () => { const windowSpy = jest.spyOn(window, 'window', 'get'); const mockFetch = jest.fn(); const mockReplaceState = jest.fn(); + const mockCtx = createMockAmplifyContext(); beforeAll(() => { (global as any).fetch = mockFetch; @@ -88,7 +90,7 @@ describe('completeOAuthFlow', () => { const expectedErrorMessage = 'some error message'; expect( - completeOAuthFlow({ + completeOAuthFlow(mockCtx, { currentUrl: `http://localhost:3000?error=true&error_description=${expectedErrorMessage}`, userAgentValue: 'UserAgent', clientId: 'clientId', @@ -112,7 +114,7 @@ describe('completeOAuthFlow', () => { it('throws when `code` is not presented in the redirect url', () => { expect( - completeOAuthFlow({ + completeOAuthFlow(mockCtx, { ...testInput, currentUrl: `http://localhost:3000?state=someState123`, }), @@ -121,7 +123,7 @@ describe('completeOAuthFlow', () => { it('throws when `state` is not presented in the redirect url', async () => { expect( - completeOAuthFlow({ + completeOAuthFlow(mockCtx, { ...testInput, currentUrl: `http://localhost:3000?code=123`, }), @@ -137,7 +139,7 @@ describe('completeOAuthFlow', () => { }); }); - await expect(completeOAuthFlow(testInput)).rejects.toThrow( + await expect(completeOAuthFlow(mockCtx, testInput)).rejects.toThrow( expectedErrorMessage, ); expect(mockValidateState).toHaveBeenCalledWith(expectedState); @@ -171,7 +173,7 @@ describe('completeOAuthFlow', () => { executionOrder.push('hubDispatch'), ); - await completeOAuthFlow(testInput); + await completeOAuthFlow(mockCtx, testInput); expect(mockFetch).toHaveBeenCalledWith( 'https://oauth.domain.com/oauth2/token', @@ -221,7 +223,7 @@ describe('completeOAuthFlow', () => { json: mockJsonMethod, }); - expect(completeOAuthFlow(testInput)).rejects.toThrow( + expect(completeOAuthFlow(mockCtx, testInput)).rejects.toThrow( mockError.error_message, ); }); @@ -240,7 +242,7 @@ describe('completeOAuthFlow', () => { it('throws when error and error_description are presented in the redirect url', () => { const expectedErrorMessage = 'invalid_scope'; expect( - completeOAuthFlow({ + completeOAuthFlow(mockCtx, { ...testInput, currentUrl: `http://localhost:3000#error_description=${expectedErrorMessage}&error=invalid_request`, }), @@ -249,7 +251,7 @@ describe('completeOAuthFlow', () => { it('throws when access_token is not presented in the redirect url', () => { expect( - completeOAuthFlow({ + completeOAuthFlow(mockCtx, { ...testInput, currentUrl: `http://localhost:3000#`, }), @@ -265,7 +267,7 @@ describe('completeOAuthFlow', () => { }); }); - await expect(completeOAuthFlow(testInput)).rejects.toThrow( + await expect(completeOAuthFlow(mockCtx, testInput)).rejects.toThrow( expectedErrorMessage, ); }); @@ -282,7 +284,7 @@ describe('completeOAuthFlow', () => { }, }); - await completeOAuthFlow({ + await completeOAuthFlow(mockCtx, { ...testInput, currentUrl: `http://localhost:3000#access_token=${expectedAccessToken}&id_token=${expectedIdToken}&token_type=${expectedTokenType}&expires_in=${expectedExpiresIn}`, }); diff --git a/packages/auth/__tests__/providers/cognito/utils/oauth/completeOAuthSignOut.test.ts b/packages/auth/__tests__/providers/cognito/utils/oauth/completeOAuthSignOut.test.ts index 1fa56a926ca..93590834685 100644 --- a/packages/auth/__tests__/providers/cognito/utils/oauth/completeOAuthSignOut.test.ts +++ b/packages/auth/__tests__/providers/cognito/utils/oauth/completeOAuthSignOut.test.ts @@ -7,6 +7,7 @@ import { AMPLIFY_SYMBOL } from '@aws-amplify/core/internals/utils'; import { tokenOrchestrator } from '../../../../../src/providers/cognito/tokenProvider/tokenProvider'; import { completeOAuthSignOut } from '../../../../../src/providers/cognito/utils/oauth/completeOAuthSignOut'; import { DefaultOAuthStore } from '../../../../../src/providers/cognito/utils/signInWithRedirectStore'; +import { createMockAmplifyContext } from '../../../../testUtils/mockAmplifyContext'; jest.mock('@aws-amplify/core', () => { return { @@ -40,11 +41,12 @@ describe('completeOAuthSignOut', () => { }); it('should complete OAuth sign out', async () => { - await completeOAuthSignOut(mockStore); + const mockCtx = createMockAmplifyContext(); + await completeOAuthSignOut(mockCtx, mockStore); expect(mockStore.clearOAuthData).toHaveBeenCalledTimes(1); expect(mockTokenOrchestrator.clearTokens).toHaveBeenCalledTimes(1); - expect(mockClearCredentials).toHaveBeenCalledTimes(1); + expect(mockCtx.clearCredentials).toHaveBeenCalledTimes(1); expect(mockHub.dispatch).toHaveBeenCalledWith( 'auth', { event: 'signedOut' }, diff --git a/packages/auth/__tests__/providers/cognito/utils/oauth/handleOAuthSignOut.native.test.ts b/packages/auth/__tests__/providers/cognito/utils/oauth/handleOAuthSignOut.native.test.ts index bd056ccdf23..f6991cff30a 100644 --- a/packages/auth/__tests__/providers/cognito/utils/oauth/handleOAuthSignOut.native.test.ts +++ b/packages/auth/__tests__/providers/cognito/utils/oauth/handleOAuthSignOut.native.test.ts @@ -6,6 +6,7 @@ import { completeOAuthSignOut } from '../../../../../src/providers/cognito/utils import { handleOAuthSignOut } from '../../../../../src/providers/cognito/utils/oauth/handleOAuthSignOut.native'; import { oAuthSignOutRedirect } from '../../../../../src/providers/cognito/utils/oauth/oAuthSignOutRedirect'; import { DefaultOAuthStore } from '../../../../../src/providers/cognito/utils/signInWithRedirectStore'; +import { createMockAmplifyContext } from '../../../../testUtils/mockAmplifyContext'; jest.mock( '../../../../../src/providers/cognito/utils/oauth/completeOAuthSignOut', @@ -21,6 +22,9 @@ describe('handleOAuthSignOut (native)', () => { userPoolId: `${region}_zzzzz`, identityPoolId: `${region}:xxxxxx`, }; + const mockCtx = createMockAmplifyContext({ + Auth: { Cognito: cognitoConfig }, + }); // assert mocks const mockCompleteOAuthSignOut = completeOAuthSignOut as jest.Mock; const mockOAuthSignOutRedirect = oAuthSignOutRedirect as jest.Mock; @@ -48,6 +52,7 @@ describe('handleOAuthSignOut (native)', () => { it('should complete OAuth sign out and redirect', async () => { mockOAuthSignOutRedirect.mockResolvedValue({ type: 'success' }); await handleOAuthSignOut( + mockCtx, cognitoConfig, mockStore, mockTokenOrchestrator, @@ -59,12 +64,13 @@ describe('handleOAuthSignOut (native)', () => { false, undefined, ); - expect(mockCompleteOAuthSignOut).toHaveBeenCalledWith(mockStore); + expect(mockCompleteOAuthSignOut).toHaveBeenCalledWith(mockCtx, mockStore); }); it('should not complete OAuth sign out if redirect is canceled', async () => { mockOAuthSignOutRedirect.mockResolvedValue({ type: 'canceled' }); await handleOAuthSignOut( + mockCtx, cognitoConfig, mockStore, mockTokenOrchestrator, @@ -82,6 +88,7 @@ describe('handleOAuthSignOut (native)', () => { it('should not complete OAuth sign out if redirect failed', async () => { mockOAuthSignOutRedirect.mockResolvedValue({ type: 'error' }); await handleOAuthSignOut( + mockCtx, cognitoConfig, mockStore, mockTokenOrchestrator, @@ -104,6 +111,7 @@ describe('handleOAuthSignOut (native)', () => { }); mockOAuthSignOutRedirect.mockResolvedValue({ type: 'error' }); await handleOAuthSignOut( + mockCtx, cognitoConfig, mockStore, mockTokenOrchestrator, @@ -115,7 +123,7 @@ describe('handleOAuthSignOut (native)', () => { true, undefined, ); - expect(mockCompleteOAuthSignOut).toHaveBeenCalledWith(mockStore); + expect(mockCompleteOAuthSignOut).toHaveBeenCalledWith(mockCtx, mockStore); }); it('should complete OAuth sign out but not redirect', async () => { @@ -124,6 +132,7 @@ describe('handleOAuthSignOut (native)', () => { preferPrivateSession: false, }); await handleOAuthSignOut( + mockCtx, cognitoConfig, mockStore, mockTokenOrchestrator, @@ -131,6 +140,6 @@ describe('handleOAuthSignOut (native)', () => { ); expect(mockOAuthSignOutRedirect).not.toHaveBeenCalled(); - expect(mockCompleteOAuthSignOut).toHaveBeenCalledWith(mockStore); + expect(mockCompleteOAuthSignOut).toHaveBeenCalledWith(mockCtx, mockStore); }); }); diff --git a/packages/auth/__tests__/providers/cognito/utils/oauth/handleOAuthSignOut.test.ts b/packages/auth/__tests__/providers/cognito/utils/oauth/handleOAuthSignOut.test.ts index 6109b2e68e7..d749838795d 100644 --- a/packages/auth/__tests__/providers/cognito/utils/oauth/handleOAuthSignOut.test.ts +++ b/packages/auth/__tests__/providers/cognito/utils/oauth/handleOAuthSignOut.test.ts @@ -6,6 +6,7 @@ import { completeOAuthSignOut } from '../../../../../src/providers/cognito/utils import { handleOAuthSignOut } from '../../../../../src/providers/cognito/utils/oauth/handleOAuthSignOut'; import { oAuthSignOutRedirect } from '../../../../../src/providers/cognito/utils/oauth/oAuthSignOutRedirect'; import { DefaultOAuthStore } from '../../../../../src/providers/cognito/utils/signInWithRedirectStore'; +import { createMockAmplifyContext } from '../../../../testUtils/mockAmplifyContext'; jest.mock( '../../../../../src/providers/cognito/utils/oauth/completeOAuthSignOut', @@ -22,6 +23,9 @@ describe('handleOAuthSignOut', () => { userPoolId: `${region}_zzzzz`, identityPoolId: `${region}:xxxxxx`, }; + const mockCtx = createMockAmplifyContext({ + Auth: { Cognito: cognitoConfig }, + }); // assert mocks const mockCompleteOAuthSignOut = completeOAuthSignOut as jest.Mock; const mockOAuthSignOutRedirect = oAuthSignOutRedirect as jest.Mock; @@ -46,13 +50,14 @@ describe('handleOAuthSignOut', () => { preferPrivateSession: false, }); await handleOAuthSignOut( + mockCtx, cognitoConfig, mockStore, mockTokenOrchestrator, undefined, ); - expect(mockCompleteOAuthSignOut).toHaveBeenCalledWith(mockStore); + expect(mockCompleteOAuthSignOut).toHaveBeenCalledWith(mockCtx, mockStore); expect(mockOAuthSignOutRedirect).toHaveBeenCalledWith( cognitoConfig, false, @@ -69,13 +74,14 @@ describe('handleOAuthSignOut', () => { preferPrivateSession: false, }); await handleOAuthSignOut( + mockCtx, cognitoConfig, mockStore, mockTokenOrchestrator, undefined, ); - expect(mockCompleteOAuthSignOut).toHaveBeenCalledWith(mockStore); + expect(mockCompleteOAuthSignOut).toHaveBeenCalledWith(mockCtx, mockStore); expect(mockOAuthSignOutRedirect).toHaveBeenCalledWith( cognitoConfig, false, @@ -89,13 +95,14 @@ describe('handleOAuthSignOut', () => { preferPrivateSession: false, }); await handleOAuthSignOut( + mockCtx, cognitoConfig, mockStore, mockTokenOrchestrator, undefined, ); - expect(mockCompleteOAuthSignOut).toHaveBeenCalledWith(mockStore); + expect(mockCompleteOAuthSignOut).toHaveBeenCalledWith(mockCtx, mockStore); expect(mockOAuthSignOutRedirect).not.toHaveBeenCalled(); }); }); diff --git a/packages/auth/__tests__/providers/cognito/utils/signInHelpers/getSignInResult.test.ts b/packages/auth/__tests__/providers/cognito/utils/signInHelpers/getSignInResult.test.ts index 366b925bffd..0ae139cfa35 100644 --- a/packages/auth/__tests__/providers/cognito/utils/signInHelpers/getSignInResult.test.ts +++ b/packages/auth/__tests__/providers/cognito/utils/signInHelpers/getSignInResult.test.ts @@ -1,18 +1,12 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { Amplify } from '@aws-amplify/core'; - import { ChallengeName } from '../../../../../src/foundation/factories/serviceClients/cognitoIdentityProvider/types'; import { getSignInResult } from '../../../../../src/providers/cognito/utils/signInHelpers'; import { AuthSignInOutput } from '../../../../../src/types'; -import { setUpGetConfig } from '../../testUtils/setUpGetConfig'; import { createAssociateSoftwareTokenClient } from '../../../../../src/foundation/factories/serviceClients/cognitoIdentityProvider'; +import { createMockAmplifyContext } from '../../../../testUtils/mockAmplifyContext'; -jest.mock('@aws-amplify/core', () => ({ - ...(jest.createMockFromModule('@aws-amplify/core') as object), - Amplify: { getConfig: jest.fn(() => ({})) }, -})); jest.mock( '../../../../../src/foundation/factories/serviceClients/cognitoIdentityProvider', ); @@ -39,8 +33,17 @@ describe('getSignInResult', () => { Promise.resolve({ Session: '123456', SecretCode: 'TEST', $metadata: {} }), ); + const mockCtx = createMockAmplifyContext({ + Auth: { + Cognito: { + userPoolClientId: '111111-aaaaa-42d8-891d-ee81a1549398', + userPoolId: 'us-west-2_zzzzz', + identityPoolId: 'us-west-2:xxxxxx', + }, + }, + }); + beforeAll(() => { - setUpGetConfig(Amplify); mockCreateAssociateSoftwareTokenClient.mockReturnValue( mockAssociateSoftwareToken, ); @@ -49,7 +52,7 @@ describe('getSignInResult', () => { it.each(basicGetSignInResultTestCases)( 'should return the correct sign in step for challenge %s', async (challengeName, signInStep) => { - const { nextStep } = await getSignInResult({ + const { nextStep } = await getSignInResult(mockCtx, { challengeName, challengeParameters: {}, }); @@ -59,7 +62,7 @@ describe('getSignInResult', () => { ); it('should return the correct sign in step for challenge MFA_SETUP when multiple available', async () => { - const { nextStep } = await getSignInResult({ + const { nextStep } = await getSignInResult(mockCtx, { challengeName: 'MFA_SETUP', challengeParameters: { MFAS_CAN_SETUP: '["SOFTWARE_TOKEN_MFA", "EMAIL_OTP"]', @@ -71,7 +74,7 @@ describe('getSignInResult', () => { }); it('should return the correct sign in step for challenge MFA_SETUP when only totp available', async () => { - const { nextStep } = await getSignInResult({ + const { nextStep } = await getSignInResult(mockCtx, { challengeName: 'MFA_SETUP', challengeParameters: { MFAS_CAN_SETUP: '["SOFTWARE_TOKEN_MFA"]', @@ -81,7 +84,7 @@ describe('getSignInResult', () => { }); it('should return the correct sign in step for challenge MFA_SETUP when only email available', async () => { - const { nextStep } = await getSignInResult({ + const { nextStep } = await getSignInResult(mockCtx, { challengeName: 'MFA_SETUP', challengeParameters: { MFAS_CAN_SETUP: '["EMAIL_OTP"]', diff --git a/packages/auth/__tests__/providers/cognito/utils/signInHelpers/handleWebAuthnSignInResult.test.ts b/packages/auth/__tests__/providers/cognito/utils/signInHelpers/handleWebAuthnSignInResult.test.ts index bf5773ac0bd..4f0a0118731 100644 --- a/packages/auth/__tests__/providers/cognito/utils/signInHelpers/handleWebAuthnSignInResult.test.ts +++ b/packages/auth/__tests__/providers/cognito/utils/signInHelpers/handleWebAuthnSignInResult.test.ts @@ -1,8 +1,5 @@ -import { Amplify } from '@aws-amplify/core'; - import { signInStore } from '../../../../../src/client/utils/store'; import { authAPITestParams } from '../../testUtils/authApiTestParams'; -import { setUpGetConfig } from '../../testUtils/setUpGetConfig'; import { createRespondToAuthChallengeClient } from '../../../../../src/foundation/factories/serviceClients/cognitoIdentityProvider'; import { handleWebAuthnSignInResult } from '../../../../../src/client/flows/userAuth/handleWebAuthnSignInResult'; import { @@ -14,6 +11,7 @@ import { AuthError } from '../../../../../src/errors/AuthError'; import { AuthErrorCodes } from '../../../../../src/common/AuthErrorStrings'; import { cacheCognitoTokens } from '../../../../../src/providers/cognito/tokenProvider/cacheTokens'; import { dispatchSignedInHubEvent } from '../../../../../src/providers/cognito/utils/dispatchSignedInHubEvent'; +import { createMockAmplifyContext } from '../../../../testUtils/mockAmplifyContext'; import { getIsPasskeySupported } from '../../../../../src/client/utils/passkey/getIsPasskeySupported'; import { assertCredentialIsPkcWithAuthenticatorAssertionResponse, @@ -25,10 +23,6 @@ import { ChallengeParameters, } from '../../../../../src/foundation/factories/serviceClients/cognitoIdentityProvider/types'; -jest.mock('@aws-amplify/core', () => ({ - ...(jest.createMockFromModule('@aws-amplify/core') as object), - Amplify: { getConfig: jest.fn(() => ({})) }, -})); jest.mock('../../../../../src/client/utils/store'); jest.mock( '../../../../../src/foundation/factories/serviceClients/cognitoIdentityProvider', @@ -70,8 +64,17 @@ describe('handleWebAuthnSignInResult', () => { const mockAssertCredentialIsPkcWithAuthenticatorAttestationResponse = jest.mocked(assertCredentialIsPkcWithAuthenticatorAttestationResponse); + const mockCtx = createMockAmplifyContext({ + Auth: { + Cognito: { + userPoolClientId: '111111-aaaaa-42d8-891d-ee81a1549398', + userPoolId: 'us-west-2_zzzzz', + identityPoolId: 'us-west-2:xxxxxx', + }, + }, + }); + beforeAll(() => { - setUpGetConfig(Amplify); mockGetIsPasskeySupported.mockReturnValue(true); mockAssertCredentialIsPkcWithAuthenticatorAssertionResponse.mockImplementation( () => undefined, @@ -100,7 +103,7 @@ describe('handleWebAuthnSignInResult', () => { }); expect.assertions(2); try { - await handleWebAuthnSignInResult(challengeParameters); + await handleWebAuthnSignInResult(mockCtx, challengeParameters); } catch (error: any) { expect(error).toBeInstanceOf(AuthError); expect(error.name).toBe(AuthErrorCodes.SignInException); @@ -110,7 +113,7 @@ describe('handleWebAuthnSignInResult', () => { it('should throw an error when CREDENTIAL_REQUEST_OPTIONS is empty', async () => { expect.assertions(2); try { - await handleWebAuthnSignInResult({}); + await handleWebAuthnSignInResult(mockCtx, {}); } catch (error: any) { expect(error).toBeInstanceOf(AuthError); expect(error.name).toBe(AuthErrorCodes.SignInException); @@ -125,7 +128,7 @@ describe('handleWebAuthnSignInResult', () => { }); expect.assertions(2); try { - await handleWebAuthnSignInResult(challengeParameters); + await handleWebAuthnSignInResult(mockCtx, challengeParameters); } catch (error: any) { expect(error).toBeInstanceOf(AuthError); expect(error.name).toBe(AuthErrorCodes.SignInException); @@ -139,7 +142,7 @@ describe('handleWebAuthnSignInResult', () => { signInSession, }); try { - await handleWebAuthnSignInResult(challengeParameters); + await handleWebAuthnSignInResult(mockCtx, challengeParameters); } catch (error: any) { // __ we don't care about this error } @@ -173,6 +176,7 @@ describe('handleWebAuthnSignInResult', () => { mockDispatchSignedInHubEvent.mockResolvedValue(undefined); const result = (await handleWebAuthnSignInResult( + mockCtx, challengeParameters, )) as AuthSignInOutput; @@ -192,7 +196,10 @@ describe('handleWebAuthnSignInResult', () => { mockCacheCognitoTokens.mockResolvedValue(undefined); mockDispatchSignedInHubEvent.mockResolvedValue(undefined); - const result = (await handleWebAuthnSignInResult(challengeParameters)) as { + const result = (await handleWebAuthnSignInResult( + mockCtx, + challengeParameters, + )) as { challengeName: ChallengeName; challengeParameters: ChallengeParameters; }; @@ -216,7 +223,7 @@ describe('handleWebAuthnSignInResult', () => { mockDispatchSignedInHubEvent.mockResolvedValue(undefined); await expect( - handleWebAuthnSignInResult(challengeParameters), + handleWebAuthnSignInResult(mockCtx, challengeParameters), ).rejects.toThrow('Sequential WEB_AUTHN challenges returned'); }); }); diff --git a/packages/auth/__tests__/providers/cognito/utils/signUpHelpers/autoSignInUserConfirmed.test.ts b/packages/auth/__tests__/providers/cognito/utils/signUpHelpers/autoSignInUserConfirmed.test.ts index 98c02e16e5f..bd848f254b2 100644 --- a/packages/auth/__tests__/providers/cognito/utils/signUpHelpers/autoSignInUserConfirmed.test.ts +++ b/packages/auth/__tests__/providers/cognito/utils/signUpHelpers/autoSignInUserConfirmed.test.ts @@ -3,6 +3,7 @@ import { authAPITestParams } from '../../testUtils/authApiTestParams'; import { signInWithUserAuth } from '../../../../../src/providers/cognito/apis/signInWithUserAuth'; import { signIn } from '../../../../../src/providers/cognito/apis/signIn'; import { SignInInput } from '../../../../../src/providers/cognito/types/inputs'; +import { createMockAmplifyContext } from '../../../../testUtils/mockAmplifyContext'; jest.mock('@aws-amplify/core/internals/utils', () => ({ ...jest.requireActual('@aws-amplify/core/internals/utils'), @@ -17,6 +18,7 @@ jest.mock('../../../../../src/providers/cognito/apis/signIn'); describe('autoSignInUserConfirmed()', () => { const mockSignInWithUserAuth = jest.mocked(signInWithUserAuth); const mockSignIn = jest.mocked(signIn); + const mockCtx = createMockAmplifyContext(); jest.useFakeTimers(); @@ -42,10 +44,10 @@ describe('autoSignInUserConfirmed()', () => { }, }; - autoSignInUserConfirmed(signInInput)(); + autoSignInUserConfirmed(mockCtx, signInInput)(); expect(mockSignInWithUserAuth).toHaveBeenCalledTimes(1); - expect(mockSignInWithUserAuth).toHaveBeenCalledWith(signInInput); + expect(mockSignInWithUserAuth).toHaveBeenCalledWith(mockCtx, signInInput); expect(mockSignIn).not.toHaveBeenCalled(); }); @@ -55,11 +57,11 @@ describe('autoSignInUserConfirmed()', () => { username: user1.username, }; - autoSignInUserConfirmed(signInInput)(); + autoSignInUserConfirmed(mockCtx, signInInput)(); expect(mockSignInWithUserAuth).not.toHaveBeenCalled(); expect(mockSignIn).toHaveBeenCalledTimes(1); - expect(mockSignIn).toHaveBeenCalledWith(signInInput); + expect(mockSignIn).toHaveBeenCalledWith(mockCtx, signInInput); }); }); diff --git a/packages/auth/__tests__/providers/cognito/verifyTOTPSetup.test.ts b/packages/auth/__tests__/providers/cognito/verifyTOTPSetup.test.ts index 0f7c5bcb109..60db413c705 100644 --- a/packages/auth/__tests__/providers/cognito/verifyTOTPSetup.test.ts +++ b/packages/auth/__tests__/providers/cognito/verifyTOTPSetup.test.ts @@ -1,7 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { Amplify, fetchAuthSession } from '@aws-amplify/core'; import { decodeJWT } from '@aws-amplify/core/internals/utils'; import { AuthError } from '../../../src/errors/AuthError'; @@ -10,14 +9,10 @@ import { VerifySoftwareTokenException } from '../../../src/providers/cognito/typ import { verifyTOTPSetup } from '../../../src/providers/cognito'; import { createVerifySoftwareTokenClient } from '../../../src/foundation/factories/serviceClients/cognitoIdentityProvider'; import { createCognitoUserPoolEndpointResolver } from '../../../src/providers/cognito/factories'; +import { createMockAmplifyContext } from '../../testUtils/mockAmplifyContext'; import { getMockError, mockAccessToken } from './testUtils/data'; -import { setUpGetConfig } from './testUtils/setUpGetConfig'; -jest.mock('@aws-amplify/core', () => ({ - ...(jest.createMockFromModule('@aws-amplify/core') as object), - Amplify: { getConfig: jest.fn(() => ({})) }, -})); jest.mock('@aws-amplify/core/internals/utils', () => ({ ...jest.requireActual('@aws-amplify/core/internals/utils'), isBrowser: jest.fn(() => false), @@ -31,7 +26,6 @@ describe('verifyTOTPSetup', () => { const code = '123456'; const friendlyDeviceName = 'FriendlyDeviceName'; // assert mocks - const mockFetchAuthSession = fetchAuthSession as jest.Mock; const mockVerifySoftwareToken = jest.fn(); const mockCreateVerifySoftwareTokenClient = jest.mocked( createVerifySoftwareTokenClient, @@ -40,9 +34,18 @@ describe('verifyTOTPSetup', () => { createCognitoUserPoolEndpointResolver, ); + const mockCtx = createMockAmplifyContext({ + Auth: { + Cognito: { + userPoolClientId: '111111-aaaaa-42d8-891d-ee81a1549398', + userPoolId: 'us-west-2_zzzzz', + identityPoolId: 'us-west-2:xxxxxx', + }, + }, + }); + beforeAll(() => { - setUpGetConfig(Amplify); - mockFetchAuthSession.mockResolvedValue({ + (mockCtx.fetchAuthSession as jest.Mock).mockResolvedValue({ tokens: { accessToken: decodeJWT(mockAccessToken) }, }); }); @@ -56,12 +59,12 @@ describe('verifyTOTPSetup', () => { afterEach(() => { mockVerifySoftwareToken.mockReset(); - mockFetchAuthSession.mockClear(); + (mockCtx.fetchAuthSession as jest.Mock).mockClear(); mockCreateVerifySoftwareTokenClient.mockClear(); }); it('should return successful response', async () => { - await verifyTOTPSetup({ + await verifyTOTPSetup(mockCtx, { code, options: { friendlyDeviceName }, }); @@ -78,7 +81,7 @@ describe('verifyTOTPSetup', () => { it('invokes mockCreateCognitoUserPoolEndpointResolver with expected endpointOverride', async () => { const expectedUserPoolEndpoint = 'https://my-custom-endpoint.com'; - jest.mocked(Amplify.getConfig).mockReturnValueOnce({ + const endpointCtx = createMockAmplifyContext({ Auth: { Cognito: { userPoolClientId: '111111-aaaaa-42d8-891d-ee81a1549398', @@ -88,8 +91,11 @@ describe('verifyTOTPSetup', () => { }, }, }); + (endpointCtx.fetchAuthSession as jest.Mock).mockResolvedValue({ + tokens: { accessToken: decodeJWT(mockAccessToken) }, + }); - await verifyTOTPSetup({ + await verifyTOTPSetup(endpointCtx, { code, options: { friendlyDeviceName }, }); @@ -102,7 +108,7 @@ describe('verifyTOTPSetup', () => { it('should throw an error when code is empty', async () => { expect.assertions(2); try { - await verifyTOTPSetup({ code: '' }); + await verifyTOTPSetup(mockCtx, { code: '' }); } catch (error: any) { expect(error).toBeInstanceOf(AuthError); expect(error.name).toBe(AuthValidationErrorCode.EmptyVerifyTOTPSetupCode); @@ -117,7 +123,7 @@ describe('verifyTOTPSetup', () => { ); }); try { - await verifyTOTPSetup({ code }); + await verifyTOTPSetup(mockCtx, { code }); } catch (error: any) { expect(error).toBeInstanceOf(AuthError); expect(error.name).toBe( diff --git a/packages/auth/__tests__/testUtils/mockAmplifyContext.ts b/packages/auth/__tests__/testUtils/mockAmplifyContext.ts new file mode 100644 index 00000000000..e8e7b69b21d --- /dev/null +++ b/packages/auth/__tests__/testUtils/mockAmplifyContext.ts @@ -0,0 +1,30 @@ +// 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'; + +/** + * Creates a mock AmplifyContext for testing. + */ +export function createMockAmplifyContext( + resourcesConfig: ResourcesConfig = {}, +): AmplifyContext { + const ctx: AmplifyContext = { + 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/auth/package.json b/packages/auth/package.json index 00bf5a651a9..92a8ec0eb96 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -33,12 +33,6 @@ ">=4.2": { "cognito": [ "./dist/esm/providers/cognito/index.d.ts" - ], - "cognito/server": [ - "./dist/esm/providers/cognito/apis/server/index.d.ts" - ], - "server": [ - "./dist/esm/server.d.ts" ] } }, @@ -55,40 +49,38 @@ "import": "./dist/esm/providers/cognito/index.mjs", "require": "./dist/cjs/providers/cognito/index.js" }, - "./cognito/server": { - "types": "./dist/esm/providers/cognito/apis/server/index.d.ts", - "import": "./dist/esm/providers/cognito/apis/server/index.mjs", - "require": "./dist/cjs/providers/cognito/apis/server/index.js" + "./enable-oauth-listener": { + "types": "./dist/esm/providers/cognito/utils/oauth/enableOAuthListener.d.ts", + "import": "./dist/esm/providers/cognito/utils/oauth/enableOAuthListener.mjs", + "require": "./dist/cjs/providers/cognito/utils/oauth/enableOAuthListener.js" }, "./server": { "types": "./dist/esm/server.d.ts", "import": "./dist/esm/server.mjs", "require": "./dist/cjs/server.js" }, - "./enable-oauth-listener": { - "types": "./dist/esm/providers/cognito/utils/oauth/enableOAuthListener.d.ts", - "import": "./dist/esm/providers/cognito/utils/oauth/enableOAuthListener.mjs", - "require": "./dist/cjs/providers/cognito/utils/oauth/enableOAuthListener.js" + "./cognito/server": { + "types": "./dist/esm/providers/cognito/apis/server/index.d.ts", + "import": "./dist/esm/providers/cognito/apis/server/index.mjs", + "require": "./dist/cjs/providers/cognito/apis/server/index.js" }, "./package.json": "./package.json" }, "repository": { "type": "git", - "url": "https://github.com/aws-amplify/amplify-js.git", - "directory": "packages/auth" + "url": "https://github.com/aws-amplify/amplify-js.git" }, "author": "Amazon Web Services", "license": "Apache-2.0", "bugs": { "url": "https://github.com/aws/aws-amplify/issues" }, - "homepage": "https://docs.amplify.aws/", + "homepage": "https://aws-amplify.github.io/", "files": [ "dist/cjs", "dist/esm", "src", "cognito", - "server", "enable-oauth-listener" ], "dependencies": { @@ -97,8 +89,8 @@ "tslib": "^2.5.0" }, "peerDependencies": { - "@aws-amplify/core": "^6.16.2", - "@aws-amplify/react-native": "^1.1.10" + "@aws-amplify/core": "^6.16.3", + "@aws-amplify/react-native": "^1.3.3" }, "peerDependenciesMeta": { "@aws-amplify/react-native": { diff --git a/packages/auth/src/client/apis/associateWebAuthnCredential.ts b/packages/auth/src/client/apis/associateWebAuthnCredential.ts index caf8307f447..50ef9de8bdd 100644 --- a/packages/auth/src/client/apis/associateWebAuthnCredential.ts +++ b/packages/auth/src/client/apis/associateWebAuthnCredential.ts @@ -1,10 +1,11 @@ // 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 { AuthAction, assertTokenProviderConfig, + resolveCtxArgs, } from '@aws-amplify/core/internals/utils'; import { @@ -37,14 +38,21 @@ import { assertValidCredentialCreationOptions } from '../utils/passkey/types'; * @throws - {@link CompleteWebAuthnRegistrationException} * - Thrown due to a service error when verifying WebAuthn registration result */ -export async function associateWebAuthnCredential(): Promise { - const authConfig = Amplify.getConfig().Auth?.Cognito; +export async function associateWebAuthnCredential(): Promise; +export async function associateWebAuthnCredential( + ctx: AmplifyContext, +): Promise; +export async function associateWebAuthnCredential( + ...args: any[] +): Promise { + const [ctx] = resolveCtxArgs(args); + const authConfig = ctx.resourcesConfig.Auth?.Cognito; assertTokenProviderConfig(authConfig); const { userPoolEndpoint, userPoolId } = authConfig; - const { tokens } = await fetchAuthSession(); + const { tokens } = await ctx.fetchAuthSession(); assertAuthTokens(tokens); diff --git a/packages/auth/src/client/apis/deleteWebAuthnCredential.ts b/packages/auth/src/client/apis/deleteWebAuthnCredential.ts index 5e17d71fe38..8ed5469e3d9 100644 --- a/packages/auth/src/client/apis/deleteWebAuthnCredential.ts +++ b/packages/auth/src/client/apis/deleteWebAuthnCredential.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 { DeleteWebAuthnCredentialException } from '../../foundation/factories/serviceClients/cognitoIdentityProvider/types'; import { DeleteWebAuthnCredentialInput } from '../../foundation/types'; @@ -19,6 +20,13 @@ import { deleteWebAuthnCredential as deleteWebAuthnCredentialFoundation } from ' */ export async function deleteWebAuthnCredential( input: DeleteWebAuthnCredentialInput, -): Promise { - return deleteWebAuthnCredentialFoundation(Amplify, input); +): Promise; +export async function deleteWebAuthnCredential( + ctx: AmplifyContext, + input: DeleteWebAuthnCredentialInput, +): Promise; +export async function deleteWebAuthnCredential(...args: any[]): Promise { + const [ctx, input] = resolveCtxArgs(args); + + return deleteWebAuthnCredentialFoundation(ctx, input); } diff --git a/packages/auth/src/client/apis/listWebAuthnCredentials.ts b/packages/auth/src/client/apis/listWebAuthnCredentials.ts index 91ee2b2310f..64c3a75c4ba 100644 --- a/packages/auth/src/client/apis/listWebAuthnCredentials.ts +++ b/packages/auth/src/client/apis/listWebAuthnCredentials.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 { ListWebAuthnCredentialsException } from '../../foundation/factories/serviceClients/cognitoIdentityProvider/types'; import { @@ -23,6 +24,17 @@ import { listWebAuthnCredentials as listWebAuthnCredentialsFoundation } from '.. */ export async function listWebAuthnCredentials( input?: ListWebAuthnCredentialsInput, +): Promise; +export async function listWebAuthnCredentials( + ctx: AmplifyContext, + input?: ListWebAuthnCredentialsInput, +): Promise; +export async function listWebAuthnCredentials( + ...args: any[] ): Promise { - return listWebAuthnCredentialsFoundation(Amplify, input); + const [ctx, input] = resolveCtxArgs( + args, + ); + + return listWebAuthnCredentialsFoundation(ctx, input); } diff --git a/packages/auth/src/client/flows/userAuth/handleWebAuthnSignInResult.ts b/packages/auth/src/client/flows/userAuth/handleWebAuthnSignInResult.ts index b0105694047..8497582692e 100644 --- a/packages/auth/src/client/flows/userAuth/handleWebAuthnSignInResult.ts +++ b/packages/auth/src/client/flows/userAuth/handleWebAuthnSignInResult.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 { AuthAction, assertTokenProviderConfig, @@ -30,9 +30,10 @@ import { getNewDeviceMetadata } from '../../../providers/cognito/utils/getNewDev import { WebAuthnSignInResult } from './types'; export async function handleWebAuthnSignInResult( + ctx: AmplifyContext, challengeParameters: ChallengeParameters, ): Promise { - const authConfig = Amplify.getConfig().Auth?.Cognito; + const authConfig = ctx.resourcesConfig.Auth?.Cognito; assertTokenProviderConfig(authConfig); const { username, signInSession, signInDetails, challengeName } = signInStore.getState(); @@ -101,7 +102,7 @@ export async function handleWebAuthnSignInResult( signInDetails, }); signInStore.dispatch({ type: 'RESET_STATE' }); - await dispatchSignedInHubEvent(); + await dispatchSignedInHubEvent(ctx); return { isSignedIn: true, diff --git a/packages/auth/src/foundation/apis/deleteWebAuthnCredential.ts b/packages/auth/src/foundation/apis/deleteWebAuthnCredential.ts index c47b13ea303..a3d5c248310 100644 --- a/packages/auth/src/foundation/apis/deleteWebAuthnCredential.ts +++ b/packages/auth/src/foundation/apis/deleteWebAuthnCredential.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 { AuthAction, assertTokenProviderConfig, @@ -15,13 +15,13 @@ import { createDeleteWebAuthnCredentialClient } from '../factories/serviceClient import { DeleteWebAuthnCredentialInput } from '../types'; export async function deleteWebAuthnCredential( - amplify: AmplifyClassV6, + amplify: AmplifyContext, input: DeleteWebAuthnCredentialInput, ): Promise { - const authConfig = amplify.getConfig().Auth?.Cognito; + const authConfig = amplify.resourcesConfig.Auth?.Cognito; assertTokenProviderConfig(authConfig); const { userPoolEndpoint, userPoolId } = authConfig; - const { tokens } = await amplify.Auth.fetchAuthSession(); + const { tokens } = await amplify.fetchAuthSession(); assertAuthTokens(tokens); const deleteWebAuthnCredentialResult = createDeleteWebAuthnCredentialClient({ diff --git a/packages/auth/src/foundation/apis/listWebAuthnCredentials.ts b/packages/auth/src/foundation/apis/listWebAuthnCredentials.ts index 5016833bdc6..4ef1dd0c150 100644 --- a/packages/auth/src/foundation/apis/listWebAuthnCredentials.ts +++ b/packages/auth/src/foundation/apis/listWebAuthnCredentials.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 { AuthAction, assertTokenProviderConfig, @@ -19,14 +19,14 @@ import { } from '../types'; export async function listWebAuthnCredentials( - amplify: AmplifyClassV6, + amplify: AmplifyContext, input?: ListWebAuthnCredentialsInput, ): Promise { - const authConfig = amplify.getConfig().Auth?.Cognito; + const authConfig = amplify.resourcesConfig.Auth?.Cognito; assertTokenProviderConfig(authConfig); const { userPoolEndpoint, userPoolId } = authConfig; - const { tokens } = await amplify.Auth.fetchAuthSession(); + const { tokens } = await amplify.fetchAuthSession(); assertAuthTokens(tokens); const listWebAuthnCredentialsResult = createListWebAuthnCredentialsClient({ diff --git a/packages/auth/src/foundation/factories/serviceClients/cognitoIdentityProvider/shared/handler/cognitoUserPoolTransferHandler.ts b/packages/auth/src/foundation/factories/serviceClients/cognitoIdentityProvider/shared/handler/cognitoUserPoolTransferHandler.ts index 2ec580fe2e9..b8586c3327f 100644 --- a/packages/auth/src/foundation/factories/serviceClients/cognitoIdentityProvider/shared/handler/cognitoUserPoolTransferHandler.ts +++ b/packages/auth/src/foundation/factories/serviceClients/cognitoIdentityProvider/shared/handler/cognitoUserPoolTransferHandler.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 { getGlobalContext, hasGlobalContext } from '@aws-amplify/core'; import { composeTransferHandler } from '@aws-amplify/core/internals/aws-client-utils/composers'; import { HttpRequest, @@ -22,7 +22,8 @@ const disableCacheMiddlewareFactory: Middleware< request.headers = { ...request.headers, 'cache-control': 'no-store', - ...(await Amplify.libraryOptions?.Auth?.headers?.()), + ...(hasGlobalContext() && + (await getGlobalContext().libraryOptions?.Auth?.headers?.())), }; return next(request); diff --git a/packages/auth/src/index.ts b/packages/auth/src/index.ts index b4ba2de0c29..b9a7808f2d6 100644 --- a/packages/auth/src/index.ts +++ b/packages/auth/src/index.ts @@ -76,7 +76,6 @@ export { export { AuthError } from './errors/AuthError'; export { - fetchAuthSession, FetchAuthSessionOptions, AuthSession, decodeJWT, diff --git a/packages/auth/src/providers/cognito/apis/confirmResetPassword.ts b/packages/auth/src/providers/cognito/apis/confirmResetPassword.ts index 5c4edc100cf..89bdec6230f 100644 --- a/packages/auth/src/providers/cognito/apis/confirmResetPassword.ts +++ b/packages/auth/src/providers/cognito/apis/confirmResetPassword.ts @@ -1,10 +1,11 @@ // 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 { AuthAction, assertTokenProviderConfig, + resolveCtxArgs, } from '@aws-amplify/core/internals/utils'; import { AuthValidationErrorCode } from '../../../errors/types/validation'; @@ -28,8 +29,14 @@ import { getRegionFromUserPoolId } from '../../../foundation/parsers'; */ export async function confirmResetPassword( input: ConfirmResetPasswordInput, -): Promise { - const authConfig = Amplify.getConfig().Auth?.Cognito; +): Promise; +export async function confirmResetPassword( + ctx: AmplifyContext, + input: ConfirmResetPasswordInput, +): Promise; +export async function confirmResetPassword(...args: any[]): Promise { + const [ctx, input] = resolveCtxArgs(args); + const authConfig = ctx.resourcesConfig.Auth?.Cognito; assertTokenProviderConfig(authConfig); const { userPoolClientId, userPoolId, userPoolEndpoint } = authConfig; const { username, newPassword } = input; diff --git a/packages/auth/src/providers/cognito/apis/confirmSignIn.ts b/packages/auth/src/providers/cognito/apis/confirmSignIn.ts index 48c904d5897..c12ef684378 100644 --- a/packages/auth/src/providers/cognito/apis/confirmSignIn.ts +++ b/packages/auth/src/providers/cognito/apis/confirmSignIn.ts @@ -1,8 +1,11 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { Amplify } from '@aws-amplify/core'; -import { assertTokenProviderConfig } from '@aws-amplify/core/internals/utils'; +import { AmplifyContext } from '@aws-amplify/core'; +import { + assertTokenProviderConfig, + resolveCtxArgs, +} from '@aws-amplify/core/internals/utils'; import { AssociateSoftwareTokenException, @@ -51,12 +54,20 @@ import { getNewDeviceMetadata } from '../utils/getNewDeviceMetadata'; */ export async function confirmSignIn( input: ConfirmSignInInput, +): Promise; +export async function confirmSignIn( + ctx: AmplifyContext, + input: ConfirmSignInInput, +): Promise; +export async function confirmSignIn( + ...args: any[] ): Promise { + const [ctx, input] = resolveCtxArgs(args); const { challengeResponse, options } = input; const { username, challengeName, signInSession, signInDetails } = signInStore.getState(); - const authConfig = Amplify.getConfig().Auth?.Cognito; + const authConfig = ctx.resourcesConfig.Auth?.Cognito; assertTokenProviderConfig(authConfig); const clientMetaData = options?.clientMetadata; @@ -122,7 +133,7 @@ export async function confirmSignIn( }); resetActiveSignInState(); - await dispatchSignedInHubEvent(); + await dispatchSignedInHubEvent(ctx); return { isSignedIn: true, @@ -130,7 +141,7 @@ export async function confirmSignIn( }; } - return getSignInResult({ + return getSignInResult(ctx, { challengeName: handledChallengeName as ChallengeName, challengeParameters: handledChallengeParameters as ChallengeParameters, }); diff --git a/packages/auth/src/providers/cognito/apis/confirmSignUp.ts b/packages/auth/src/providers/cognito/apis/confirmSignUp.ts index c9633531908..31dfdc3d215 100644 --- a/packages/auth/src/providers/cognito/apis/confirmSignUp.ts +++ b/packages/auth/src/providers/cognito/apis/confirmSignUp.ts @@ -1,11 +1,12 @@ // 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 { AuthAction, HubInternal, assertTokenProviderConfig, + resolveCtxArgs, } from '@aws-amplify/core/internals/utils'; import { ConfirmSignUpInput, ConfirmSignUpOutput } from '../types'; @@ -35,10 +36,18 @@ import { resetAutoSignIn } from './autoSignIn'; */ export async function confirmSignUp( input: ConfirmSignUpInput, +): Promise; +export async function confirmSignUp( + ctx: AmplifyContext, + input: ConfirmSignUpInput, +): Promise; +export async function confirmSignUp( + ...args: any[] ): Promise { + const [ctx, input] = resolveCtxArgs(args); const { username, confirmationCode, options } = input; - const authConfig = Amplify.getConfig().Auth?.Cognito; + const authConfig = ctx.resourcesConfig.Auth?.Cognito; assertTokenProviderConfig(authConfig); const { userPoolId, userPoolClientId, userPoolEndpoint } = authConfig; const clientMetadata = options?.clientMetadata; diff --git a/packages/auth/src/providers/cognito/apis/confirmUserAttribute.ts b/packages/auth/src/providers/cognito/apis/confirmUserAttribute.ts index 8c0c4dba1ad..53781efeba6 100644 --- a/packages/auth/src/providers/cognito/apis/confirmUserAttribute.ts +++ b/packages/auth/src/providers/cognito/apis/confirmUserAttribute.ts @@ -1,10 +1,11 @@ // 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 { AuthAction, assertTokenProviderConfig, + resolveCtxArgs, } from '@aws-amplify/core/internals/utils'; import { AuthValidationErrorCode } from '../../../errors/types/validation'; @@ -28,8 +29,14 @@ import { createCognitoUserPoolEndpointResolver } from '../factories'; */ export async function confirmUserAttribute( input: ConfirmUserAttributeInput, -): Promise { - const authConfig = Amplify.getConfig().Auth?.Cognito; +): Promise; +export async function confirmUserAttribute( + ctx: AmplifyContext, + input: ConfirmUserAttributeInput, +): Promise; +export async function confirmUserAttribute(...args: any[]): Promise { + const [ctx, input] = resolveCtxArgs(args); + const authConfig = ctx.resourcesConfig.Auth?.Cognito; assertTokenProviderConfig(authConfig); const { userPoolEndpoint, userPoolId } = authConfig; const { confirmationCode, userAttributeKey } = input; @@ -37,7 +44,7 @@ export async function confirmUserAttribute( !!confirmationCode, AuthValidationErrorCode.EmptyConfirmUserAttributeCode, ); - const { tokens } = await fetchAuthSession({ forceRefresh: false }); + const { tokens } = await ctx.fetchAuthSession({ forceRefresh: false }); assertAuthTokens(tokens); const verifyUserAttribute = createVerifyUserAttributeClient({ endpointResolver: createCognitoUserPoolEndpointResolver({ diff --git a/packages/auth/src/providers/cognito/apis/deleteUser.ts b/packages/auth/src/providers/cognito/apis/deleteUser.ts index 53c0c18c6dd..48e75e3d9fd 100644 --- a/packages/auth/src/providers/cognito/apis/deleteUser.ts +++ b/packages/auth/src/providers/cognito/apis/deleteUser.ts @@ -1,10 +1,11 @@ // 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 { AuthAction, assertTokenProviderConfig, + resolveCtxArgs, } from '@aws-amplify/core/internals/utils'; import { getRegionFromUserPoolId } from '../../../foundation/parsers'; @@ -23,11 +24,14 @@ import { signOut } from './signOut'; * @throws - {@link DeleteUserException} * @throws AuthTokenConfigException - Thrown when the token provider config is invalid. */ -export async function deleteUser(): Promise { - const authConfig = Amplify.getConfig().Auth?.Cognito; +export async function deleteUser(): Promise; +export async function deleteUser(ctx: AmplifyContext): Promise; +export async function deleteUser(...args: any[]): Promise { + const [ctx] = resolveCtxArgs(args); + const authConfig = ctx.resourcesConfig.Auth?.Cognito; assertTokenProviderConfig(authConfig); const { userPoolEndpoint, userPoolId } = authConfig; - const { tokens } = await fetchAuthSession(); + const { tokens } = await ctx.fetchAuthSession(); assertAuthTokens(tokens); const serviceDeleteUser = createDeleteUserClient({ endpointResolver: createCognitoUserPoolEndpointResolver({ @@ -44,5 +48,5 @@ export async function deleteUser(): Promise { }, ); await tokenOrchestrator.clearDeviceMetadata(); - await signOut(); + await signOut(ctx); } diff --git a/packages/auth/src/providers/cognito/apis/deleteUserAttributes.ts b/packages/auth/src/providers/cognito/apis/deleteUserAttributes.ts index b958dfacc1f..cb9904dff0c 100644 --- a/packages/auth/src/providers/cognito/apis/deleteUserAttributes.ts +++ b/packages/auth/src/providers/cognito/apis/deleteUserAttributes.ts @@ -1,10 +1,11 @@ // 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 { AuthAction, assertTokenProviderConfig, + resolveCtxArgs, } from '@aws-amplify/core/internals/utils'; import { getRegionFromUserPoolId } from '../../../foundation/parsers'; @@ -24,12 +25,18 @@ import { createCognitoUserPoolEndpointResolver } from '../factories'; */ export async function deleteUserAttributes( input: DeleteUserAttributesInput, -): Promise { - const authConfig = Amplify.getConfig().Auth?.Cognito; +): Promise; +export async function deleteUserAttributes( + ctx: AmplifyContext, + input: DeleteUserAttributesInput, +): Promise; +export async function deleteUserAttributes(...args: any[]): Promise { + const [ctx, input] = resolveCtxArgs(args); + const authConfig = ctx.resourcesConfig.Auth?.Cognito; assertTokenProviderConfig(authConfig); const { userAttributeKeys } = input; const { userPoolEndpoint, userPoolId } = authConfig; - const { tokens } = await fetchAuthSession({ forceRefresh: false }); + const { tokens } = await ctx.fetchAuthSession({ forceRefresh: false }); assertAuthTokens(tokens); const deleteUserAttributesClient = createDeleteUserAttributesClient({ endpointResolver: createCognitoUserPoolEndpointResolver({ diff --git a/packages/auth/src/providers/cognito/apis/fetchDevices.ts b/packages/auth/src/providers/cognito/apis/fetchDevices.ts index 5fda1b8fefc..887191ba03a 100644 --- a/packages/auth/src/providers/cognito/apis/fetchDevices.ts +++ b/packages/auth/src/providers/cognito/apis/fetchDevices.ts @@ -1,10 +1,11 @@ // 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 { AuthAction, assertTokenProviderConfig, + resolveCtxArgs, } from '@aws-amplify/core/internals/utils'; import { AWSAuthDevice, FetchDevicesOutput } from '../types'; @@ -28,11 +29,18 @@ const MAX_DEVICES = 60; * @throws {@link ListDevicesException} * @throws AuthTokenConfigException - Thrown when the token provider config is invalid. */ -export async function fetchDevices(): Promise { - const authConfig = Amplify.getConfig().Auth?.Cognito; +export async function fetchDevices(): Promise; +export async function fetchDevices( + ctx: AmplifyContext, +): Promise; +export async function fetchDevices( + ...args: any[] +): Promise { + const [ctx] = resolveCtxArgs(args); + const authConfig = ctx.resourcesConfig.Auth?.Cognito; assertTokenProviderConfig(authConfig); const { userPoolEndpoint, userPoolId } = authConfig; - const { tokens } = await fetchAuthSession(); + const { tokens } = await ctx.fetchAuthSession(); assertAuthTokens(tokens); const listDevices = createListDevicesClient({ endpointResolver: createCognitoUserPoolEndpointResolver({ diff --git a/packages/auth/src/providers/cognito/apis/fetchMFAPreference.ts b/packages/auth/src/providers/cognito/apis/fetchMFAPreference.ts index e6da216ba81..e896a9d8cc7 100644 --- a/packages/auth/src/providers/cognito/apis/fetchMFAPreference.ts +++ b/packages/auth/src/providers/cognito/apis/fetchMFAPreference.ts @@ -1,10 +1,11 @@ // 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 { AuthAction, assertTokenProviderConfig, + resolveCtxArgs, } from '@aws-amplify/core/internals/utils'; import { FetchMFAPreferenceOutput } from '../types'; @@ -24,11 +25,18 @@ import { createCognitoUserPoolEndpointResolver } from '../factories'; * and settings. * @throws AuthTokenConfigException - Thrown when the token provider config is invalid. */ -export async function fetchMFAPreference(): Promise { - const authConfig = Amplify.getConfig().Auth?.Cognito; +export async function fetchMFAPreference(): Promise; +export async function fetchMFAPreference( + ctx: AmplifyContext, +): Promise; +export async function fetchMFAPreference( + ...args: any[] +): Promise { + const [ctx] = resolveCtxArgs(args); + const authConfig = ctx.resourcesConfig.Auth?.Cognito; assertTokenProviderConfig(authConfig); const { userPoolEndpoint, userPoolId } = authConfig; - const { tokens } = await fetchAuthSession({ forceRefresh: false }); + const { tokens } = await ctx.fetchAuthSession({ forceRefresh: false }); assertAuthTokens(tokens); const getUser = createGetUserClient({ endpointResolver: createCognitoUserPoolEndpointResolver({ diff --git a/packages/auth/src/providers/cognito/apis/fetchUserAttributes.ts b/packages/auth/src/providers/cognito/apis/fetchUserAttributes.ts index 0a3673fd6e4..00e593dadee 100644 --- a/packages/auth/src/providers/cognito/apis/fetchUserAttributes.ts +++ b/packages/auth/src/providers/cognito/apis/fetchUserAttributes.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 { FetchUserAttributesOutput } from '../types'; import { GetUserException } from '../types/errors'; @@ -14,6 +15,14 @@ import { fetchUserAttributes as fetchUserAttributesInternal } from './internal/f * @throws - {@link GetUserException} - Cognito service errors thrown when the service is not able to get the user. * @throws AuthTokenConfigException - Thrown when the token provider config is invalid. */ -export const fetchUserAttributes = (): Promise => { - return fetchUserAttributesInternal(Amplify); -}; +export async function fetchUserAttributes(): Promise; +export async function fetchUserAttributes( + ctx: AmplifyContext, +): Promise; +export async function fetchUserAttributes( + ...args: any[] +): Promise { + const [ctx] = resolveCtxArgs(args); + + return fetchUserAttributesInternal(ctx); +} diff --git a/packages/auth/src/providers/cognito/apis/forgetDevice.ts b/packages/auth/src/providers/cognito/apis/forgetDevice.ts index b1ca574e1e4..85a79a6714b 100644 --- a/packages/auth/src/providers/cognito/apis/forgetDevice.ts +++ b/packages/auth/src/providers/cognito/apis/forgetDevice.ts @@ -1,10 +1,11 @@ // 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 { AuthAction, assertTokenProviderConfig, + resolveCtxArgs, } from '@aws-amplify/core/internals/utils'; import { assertAuthTokens, assertDeviceMetadata } from '../utils/types'; @@ -24,12 +25,18 @@ import { createCognitoUserPoolEndpointResolver } from '../factories'; * forgetting device with invalid device key * @throws AuthTokenConfigException - Thrown when the token provider config is invalid. */ -export async function forgetDevice(input?: ForgetDeviceInput): Promise { +export async function forgetDevice(input?: ForgetDeviceInput): Promise; +export async function forgetDevice( + ctx: AmplifyContext, + input?: ForgetDeviceInput, +): Promise; +export async function forgetDevice(...args: any[]): Promise { + const [ctx, input] = resolveCtxArgs(args); const { device: { id: externalDeviceKey } = { id: undefined } } = input ?? {}; - const authConfig = Amplify.getConfig().Auth?.Cognito; + const authConfig = ctx.resourcesConfig.Auth?.Cognito; assertTokenProviderConfig(authConfig); const { userPoolEndpoint, userPoolId } = authConfig; - const { tokens } = await fetchAuthSession(); + const { tokens } = await ctx.fetchAuthSession(); assertAuthTokens(tokens); const deviceMetadata = await tokenOrchestrator.getDeviceMetadata(); diff --git a/packages/auth/src/providers/cognito/apis/getCurrentUser.ts b/packages/auth/src/providers/cognito/apis/getCurrentUser.ts index 2c35937b8ba..7672117c108 100644 --- a/packages/auth/src/providers/cognito/apis/getCurrentUser.ts +++ b/packages/auth/src/providers/cognito/apis/getCurrentUser.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 { GetCurrentUserOutput } from '../types'; import { InitiateAuthException } from '../types/errors'; @@ -16,6 +17,14 @@ import { getCurrentUser as getCurrentUserInternal } from './internal/getCurrentU * @throws - {@link InitiateAuthException} - Thrown when the service fails to refresh the tokens. * @throws AuthTokenConfigException - Thrown when the token provider config is invalid. */ -export const getCurrentUser = async (): Promise => { - return getCurrentUserInternal(Amplify); -}; +export async function getCurrentUser(): Promise; +export async function getCurrentUser( + ctx: AmplifyContext, +): Promise; +export async function getCurrentUser( + ...args: any[] +): Promise { + const [ctx] = resolveCtxArgs(args); + + return getCurrentUserInternal(ctx); +} diff --git a/packages/auth/src/providers/cognito/apis/internal/fetchUserAttributes.ts b/packages/auth/src/providers/cognito/apis/internal/fetchUserAttributes.ts index 01230bf5153..fee04dc6090 100644 --- a/packages/auth/src/providers/cognito/apis/internal/fetchUserAttributes.ts +++ b/packages/auth/src/providers/cognito/apis/internal/fetchUserAttributes.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 { AuthAction, assertTokenProviderConfig, @@ -17,9 +17,9 @@ import { createGetUserClient } from '../../../../foundation/factories/serviceCli import { createCognitoUserPoolEndpointResolver } from '../../factories'; export const fetchUserAttributes = async ( - amplify: AmplifyClassV6, + amplify: AmplifyContext, ): Promise => { - const authConfig = amplify.getConfig().Auth?.Cognito; + const authConfig = amplify.resourcesConfig.Auth?.Cognito; assertTokenProviderConfig(authConfig); const { userPoolEndpoint, userPoolId } = authConfig; const { tokens } = await fetchAuthSession(amplify, { diff --git a/packages/auth/src/providers/cognito/apis/internal/getCurrentUser.ts b/packages/auth/src/providers/cognito/apis/internal/getCurrentUser.ts index 350651dd4bf..4dcdcc469e7 100644 --- a/packages/auth/src/providers/cognito/apis/internal/getCurrentUser.ts +++ b/packages/auth/src/providers/cognito/apis/internal/getCurrentUser.ts @@ -1,7 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { AmplifyClassV6, AuthTokens } from '@aws-amplify/core'; +import { AmplifyContext, AuthTokens } from '@aws-amplify/core'; import { assertTokenProviderConfig } from '@aws-amplify/core/internals/utils'; import { assertAuthTokens } from '../../utils/types'; @@ -12,12 +12,12 @@ import { } from '../../types'; export const getCurrentUser = async ( - amplify: AmplifyClassV6, + amplify: AmplifyContext, ): Promise => { - const authConfig = amplify.getConfig().Auth?.Cognito; + const authConfig = amplify.resourcesConfig.Auth?.Cognito; assertTokenProviderConfig(authConfig); - const tokens = await amplify.Auth.getTokens({ forceRefresh: false }); + const tokens = await amplify.getTokens({ forceRefresh: false }); assertAuthTokens(tokens); const { 'cognito:username': username, sub } = tokens.idToken?.payload ?? {}; diff --git a/packages/auth/src/providers/cognito/apis/rememberDevice.ts b/packages/auth/src/providers/cognito/apis/rememberDevice.ts index eb24022096e..e1ebfcc1a53 100644 --- a/packages/auth/src/providers/cognito/apis/rememberDevice.ts +++ b/packages/auth/src/providers/cognito/apis/rememberDevice.ts @@ -1,10 +1,11 @@ // 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 { AuthAction, assertTokenProviderConfig, + resolveCtxArgs, } from '@aws-amplify/core/internals/utils'; import { assertAuthTokens, assertDeviceMetadata } from '../utils/types'; @@ -22,11 +23,14 @@ import { createCognitoUserPoolEndpointResolver } from '../factories'; * setting device status to remembered using an invalid device key. * @throws AuthTokenConfigException - Thrown when the token provider config is invalid. */ -export async function rememberDevice(): Promise { - const authConfig = Amplify.getConfig().Auth?.Cognito; +export async function rememberDevice(): Promise; +export async function rememberDevice(ctx: AmplifyContext): Promise; +export async function rememberDevice(...args: any[]): Promise { + const [ctx] = resolveCtxArgs(args); + const authConfig = ctx.resourcesConfig.Auth?.Cognito; assertTokenProviderConfig(authConfig); const { userPoolEndpoint, userPoolId } = authConfig; - const { tokens } = await fetchAuthSession(); + const { tokens } = await ctx.fetchAuthSession(); assertAuthTokens(tokens); const deviceMetadata = await tokenOrchestrator?.getDeviceMetadata(); diff --git a/packages/auth/src/providers/cognito/apis/resendSignUpCode.ts b/packages/auth/src/providers/cognito/apis/resendSignUpCode.ts index cdda7b980eb..9ba72d8b63d 100644 --- a/packages/auth/src/providers/cognito/apis/resendSignUpCode.ts +++ b/packages/auth/src/providers/cognito/apis/resendSignUpCode.ts @@ -1,11 +1,12 @@ // 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 { AuthAction, AuthVerifiableAttributeKey, assertTokenProviderConfig, + resolveCtxArgs, } from '@aws-amplify/core/internals/utils'; import { AuthDeliveryMedium } from '../../../types'; @@ -30,13 +31,21 @@ import { createCognitoUserPoolEndpointResolver } from '../factories'; */ export async function resendSignUpCode( input: ResendSignUpCodeInput, +): Promise; +export async function resendSignUpCode( + ctx: AmplifyContext, + input: ResendSignUpCodeInput, +): Promise; +export async function resendSignUpCode( + ...args: any[] ): Promise { + const [ctx, input] = resolveCtxArgs(args); const { username } = input; assertValidationError( !!username, AuthValidationErrorCode.EmptySignUpUsername, ); - const authConfig = Amplify.getConfig().Auth?.Cognito; + const authConfig = ctx.resourcesConfig.Auth?.Cognito; assertTokenProviderConfig(authConfig); const { userPoolClientId, userPoolId, userPoolEndpoint } = authConfig; const clientMetadata = input.options?.clientMetadata; diff --git a/packages/auth/src/providers/cognito/apis/resetPassword.ts b/packages/auth/src/providers/cognito/apis/resetPassword.ts index cd6d37a39ca..aa9699a580a 100644 --- a/packages/auth/src/providers/cognito/apis/resetPassword.ts +++ b/packages/auth/src/providers/cognito/apis/resetPassword.ts @@ -1,11 +1,12 @@ // 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 { AuthAction, AuthVerifiableAttributeKey, assertTokenProviderConfig, + resolveCtxArgs, } from '@aws-amplify/core/internals/utils'; import { AuthValidationErrorCode } from '../../../errors/types/validation'; @@ -32,13 +33,21 @@ import { createCognitoUserPoolEndpointResolver } from '../factories'; **/ export async function resetPassword( input: ResetPasswordInput, +): Promise; +export async function resetPassword( + ctx: AmplifyContext, + input: ResetPasswordInput, +): Promise; +export async function resetPassword( + ...args: any[] ): Promise { + const [ctx, input] = resolveCtxArgs(args); const { username } = input; assertValidationError( !!username, AuthValidationErrorCode.EmptyResetPasswordUsername, ); - const authConfig = Amplify.getConfig().Auth?.Cognito; + const authConfig = ctx.resourcesConfig.Auth?.Cognito; assertTokenProviderConfig(authConfig); const { userPoolClientId, userPoolId, userPoolEndpoint } = authConfig; const clientMetadata = input.options?.clientMetadata; diff --git a/packages/auth/src/providers/cognito/apis/sendUserAttributeVerificationCode.ts b/packages/auth/src/providers/cognito/apis/sendUserAttributeVerificationCode.ts index 4b04b2a85d1..228720c975c 100644 --- a/packages/auth/src/providers/cognito/apis/sendUserAttributeVerificationCode.ts +++ b/packages/auth/src/providers/cognito/apis/sendUserAttributeVerificationCode.ts @@ -1,11 +1,12 @@ // 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 { AuthAction, AuthVerifiableAttributeKey, assertTokenProviderConfig, + resolveCtxArgs, } from '@aws-amplify/core/internals/utils'; import { AuthDeliveryMedium } from '../../../types'; @@ -28,15 +29,24 @@ import { createCognitoUserPoolEndpointResolver } from '../factories'; * @throws - {@link GetUserAttributeVerificationException} * @throws AuthTokenConfigException - Thrown when the token provider config is invalid. */ -export const sendUserAttributeVerificationCode = async ( +export async function sendUserAttributeVerificationCode( input: SendUserAttributeVerificationCodeInput, -): Promise => { +): Promise; +export async function sendUserAttributeVerificationCode( + ctx: AmplifyContext, + input: SendUserAttributeVerificationCodeInput, +): Promise; +export async function sendUserAttributeVerificationCode( + ...args: any[] +): Promise { + const [ctx, input] = + resolveCtxArgs(args); const { userAttributeKey, options } = input; - const authConfig = Amplify.getConfig().Auth?.Cognito; + const authConfig = ctx.resourcesConfig.Auth?.Cognito; const clientMetadata = options?.clientMetadata; assertTokenProviderConfig(authConfig); const { userPoolEndpoint, userPoolId } = authConfig; - const { tokens } = await fetchAuthSession({ forceRefresh: false }); + const { tokens } = await ctx.fetchAuthSession({ forceRefresh: false }); assertAuthTokens(tokens); const getUserAttributeVerificationCode = createGetUserAttributeVerificationCodeClient({ @@ -66,4 +76,4 @@ export const sendUserAttributeVerificationCode = async ( deliveryMedium: DeliveryMedium as AuthDeliveryMedium, attributeName: AttributeName as AuthVerifiableAttributeKey, }; -}; +} diff --git a/packages/auth/src/providers/cognito/apis/server/fetchUserAttributes.ts b/packages/auth/src/providers/cognito/apis/server/fetchUserAttributes.ts deleted file mode 100644 index d30db35b787..00000000000 --- a/packages/auth/src/providers/cognito/apis/server/fetchUserAttributes.ts +++ /dev/null @@ -1,18 +0,0 @@ -// 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 { FetchUserAttributesOutput } from '../../types'; -import { fetchUserAttributes as fetchUserAttributesInternal } from '../internal/fetchUserAttributes'; - -export const fetchUserAttributes = ( - contextSpec: AmplifyServer.ContextSpec, -): Promise => { - return fetchUserAttributesInternal( - getAmplifyServerContext(contextSpec).amplify, - ); -}; diff --git a/packages/auth/src/providers/cognito/apis/server/getCurrentUser.ts b/packages/auth/src/providers/cognito/apis/server/getCurrentUser.ts deleted file mode 100644 index ea96605b16e..00000000000 --- a/packages/auth/src/providers/cognito/apis/server/getCurrentUser.ts +++ /dev/null @@ -1,24 +0,0 @@ -// 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 { GetCurrentUserOutput } from '../../types'; -import { getCurrentUser as getCurrentUserInternal } from '../internal/getCurrentUser'; -import { InitiateAuthException } from '../../types/errors'; - -/** - * Gets the current user from the idToken. - * - * @returns GetCurrentUserOutput - * @throws - {@link InitiateAuthException} - Thrown when the service fails to refresh the tokens. - * @throws AuthTokenConfigException - Thrown when the token provider config is invalid. - */ -export const getCurrentUser = async ( - contextSpec: AmplifyServer.ContextSpec, -): Promise => { - return getCurrentUserInternal(getAmplifyServerContext(contextSpec).amplify); -}; diff --git a/packages/auth/src/providers/cognito/apis/server/index.ts b/packages/auth/src/providers/cognito/apis/server/index.ts index 47388bbb72a..81f327b78b2 100644 --- a/packages/auth/src/providers/cognito/apis/server/index.ts +++ b/packages/auth/src/providers/cognito/apis/server/index.ts @@ -1,5 +1,4 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -export { fetchUserAttributes } from './fetchUserAttributes'; -export { getCurrentUser } from './getCurrentUser'; +export { fetchUserAttributes, getCurrentUser } from '../..'; diff --git a/packages/auth/src/providers/cognito/apis/setUpTOTP.ts b/packages/auth/src/providers/cognito/apis/setUpTOTP.ts index 43dac4c787b..9c5b829531e 100644 --- a/packages/auth/src/providers/cognito/apis/setUpTOTP.ts +++ b/packages/auth/src/providers/cognito/apis/setUpTOTP.ts @@ -1,10 +1,11 @@ // 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 { AuthAction, assertTokenProviderConfig, + resolveCtxArgs, } from '@aws-amplify/core/internals/utils'; import { AuthError } from '../../../errors/AuthError'; @@ -28,11 +29,14 @@ import { createCognitoUserPoolEndpointResolver } from '../factories'; * Thrown if a service occurs while setting up TOTP. * @throws AuthTokenConfigException - Thrown when the token provider config is invalid. **/ -export async function setUpTOTP(): Promise { - const authConfig = Amplify.getConfig().Auth?.Cognito; +export async function setUpTOTP(): Promise; +export async function setUpTOTP(ctx: AmplifyContext): Promise; +export async function setUpTOTP(...args: any[]): Promise { + const [ctx] = resolveCtxArgs(args); + const authConfig = ctx.resourcesConfig.Auth?.Cognito; assertTokenProviderConfig(authConfig); const { userPoolEndpoint, userPoolId } = authConfig; - const { tokens } = await fetchAuthSession({ forceRefresh: false }); + const { tokens } = await ctx.fetchAuthSession({ forceRefresh: false }); assertAuthTokens(tokens); const username = tokens.idToken?.payload['cognito:username'] ?? ''; const associateSoftwareToken = createAssociateSoftwareTokenClient({ diff --git a/packages/auth/src/providers/cognito/apis/signIn.ts b/packages/auth/src/providers/cognito/apis/signIn.ts index 7fc23cfcc67..2697486d8a6 100644 --- a/packages/auth/src/providers/cognito/apis/signIn.ts +++ b/packages/auth/src/providers/cognito/apis/signIn.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 { InitiateAuthException, RespondToAuthChallengeException, @@ -27,7 +30,13 @@ import { resetAutoSignIn } from './autoSignIn'; * are not defined. * @throws AuthTokenConfigException - Thrown when the token provider config is invalid. */ -export async function signIn(input: SignInInput): Promise { +export async function signIn(input: SignInInput): Promise; +export async function signIn( + ctx: AmplifyContext, + input: SignInInput, +): Promise; +export async function signIn(...args: any[]): Promise { + const [ctx, input] = resolveCtxArgs(args); // Here we want to reset the store but not reassign the callback. // The callback is reset when the underlying promise resolves or rejects. // With the advent of session based sign in, this guarantees that the signIn API initiates a new auth flow, @@ -35,19 +44,19 @@ export async function signIn(input: SignInInput): Promise { resetAutoSignIn(false); const authFlowType = input.options?.authFlowType; - await assertUserNotAuthenticated(); + await assertUserNotAuthenticated(ctx); switch (authFlowType) { case 'USER_SRP_AUTH': - return signInWithSRP(input); + return signInWithSRP(ctx, input); case 'USER_PASSWORD_AUTH': - return signInWithUserPassword(input); + return signInWithUserPassword(ctx, input); case 'CUSTOM_WITHOUT_SRP': - return signInWithCustomAuth(input); + return signInWithCustomAuth(ctx, input); case 'CUSTOM_WITH_SRP': - return signInWithCustomSRPAuth(input); + return signInWithCustomSRPAuth(ctx, input); case 'USER_AUTH': - return signInWithUserAuth(input); + return signInWithUserAuth(ctx, input); default: - return signInWithSRP(input); + return signInWithSRP(ctx, input); } } diff --git a/packages/auth/src/providers/cognito/apis/signInWithCustomAuth.ts b/packages/auth/src/providers/cognito/apis/signInWithCustomAuth.ts index a1260538d17..49be504ba46 100644 --- a/packages/auth/src/providers/cognito/apis/signInWithCustomAuth.ts +++ b/packages/auth/src/providers/cognito/apis/signInWithCustomAuth.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 { assertTokenProviderConfig } from '@aws-amplify/core/internals/utils'; import { AuthValidationErrorCode } from '../../../errors/types/validation'; @@ -44,9 +44,10 @@ import { getNewDeviceMetadata } from '../utils/getNewDeviceMetadata'; * @throws SignInWithCustomAuthOutput - Thrown when the token provider config is invalid. */ export async function signInWithCustomAuth( + ctx: AmplifyContext, input: SignInWithCustomAuthInput, ): Promise { - const authConfig = Amplify.getConfig().Auth?.Cognito; + const authConfig = ctx.resourcesConfig.Auth?.Cognito; assertTokenProviderConfig(authConfig); const { username, password, options } = input; const signInDetails: CognitoAuthSignInDetails = { @@ -97,7 +98,7 @@ export async function signInWithCustomAuth( }); resetActiveSignInState(); - await dispatchSignedInHubEvent(); + await dispatchSignedInHubEvent(ctx); return { isSignedIn: true, @@ -105,7 +106,7 @@ export async function signInWithCustomAuth( }; } - return getSignInResult({ + return getSignInResult(ctx, { challengeName: retriedChallengeName as ChallengeName, challengeParameters: retiredChallengeParameters as ChallengeParameters, }); diff --git a/packages/auth/src/providers/cognito/apis/signInWithCustomSRPAuth.ts b/packages/auth/src/providers/cognito/apis/signInWithCustomSRPAuth.ts index 3827699f476..4d292c1ca41 100644 --- a/packages/auth/src/providers/cognito/apis/signInWithCustomSRPAuth.ts +++ b/packages/auth/src/providers/cognito/apis/signInWithCustomSRPAuth.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 { assertTokenProviderConfig } from '@aws-amplify/core/internals/utils'; import { AuthValidationErrorCode } from '../../../errors/types/validation'; @@ -47,6 +47,7 @@ import { getNewDeviceMetadata } from '../utils/getNewDeviceMetadata'; * @throws AuthTokenConfigException - Thrown when the token provider config is invalid. */ export async function signInWithCustomSRPAuth( + ctx: AmplifyContext, input: SignInWithCustomSRPAuthInput, ): Promise { const { username, password, options } = input; @@ -54,7 +55,7 @@ export async function signInWithCustomSRPAuth( loginId: username, authFlowType: 'CUSTOM_WITH_SRP', }; - const authConfig = Amplify.getConfig().Auth?.Cognito; + const authConfig = ctx.resourcesConfig.Auth?.Cognito; assertTokenProviderConfig(authConfig); const metadata = options?.clientMetadata; assertValidationError( @@ -102,7 +103,7 @@ export async function signInWithCustomSRPAuth( }); resetActiveSignInState(); - await dispatchSignedInHubEvent(); + await dispatchSignedInHubEvent(ctx); return { isSignedIn: true, @@ -110,7 +111,7 @@ export async function signInWithCustomSRPAuth( }; } - return getSignInResult({ + return getSignInResult(ctx, { challengeName: handledChallengeName as ChallengeName, challengeParameters: handledChallengeParameters as ChallengeParameters, }); diff --git a/packages/auth/src/providers/cognito/apis/signInWithRedirect.ts b/packages/auth/src/providers/cognito/apis/signInWithRedirect.ts index c630be74298..e2bf0ec754e 100644 --- a/packages/auth/src/providers/cognito/apis/signInWithRedirect.ts +++ b/packages/auth/src/providers/cognito/apis/signInWithRedirect.ts @@ -1,12 +1,13 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { Amplify, OAuthConfig } from '@aws-amplify/core'; +import { AmplifyContext, OAuthConfig } from '@aws-amplify/core'; import { AuthAction, assertOAuthConfig, assertTokenProviderConfig, isBrowser, + resolveCtxArgs, urlSafeEncode, } from '@aws-amplify/core/internals/utils'; @@ -40,14 +41,22 @@ import { OpenAuthSession } from '../../../utils/types'; */ export async function signInWithRedirect( input?: SignInWithRedirectInput, -): Promise { - const authConfig = Amplify.getConfig().Auth?.Cognito; +): Promise; +export async function signInWithRedirect( + ctx: AmplifyContext, + input?: SignInWithRedirectInput, +): Promise; +export async function signInWithRedirect(...args: any[]): Promise { + const [ctx, input] = resolveCtxArgs( + args, + ); + const authConfig = ctx.resourcesConfig.Auth?.Cognito; assertTokenProviderConfig(authConfig); assertOAuthConfig(authConfig); oAuthStore.setAuthConfig(authConfig); if (!input?.options?.prompt) { - await assertUserNotAuthenticated(); + await assertUserNotAuthenticated(ctx); } let provider = 'COGNITO'; // Default @@ -61,7 +70,7 @@ export async function signInWithRedirect( ({ idpIdentifier } = input.provider); } - return oauthSignIn({ + return oauthSignIn(ctx, { oauthConfig: authConfig.loginWith.oauth, clientId: authConfig.userPoolClientId, provider, @@ -78,25 +87,28 @@ export async function signInWithRedirect( }); } -const oauthSignIn = async ({ - oauthConfig, - provider, - idpIdentifier, - clientId, - customState, - preferPrivateSession, - options, - authSessionOpener, -}: { - oauthConfig: OAuthConfig; - provider: string; - idpIdentifier?: string; - clientId: string; - customState?: string; - preferPrivateSession?: boolean; - options?: SignInWithRedirectInput['options']; - authSessionOpener?: OpenAuthSession; -}) => { +const oauthSignIn = async ( + ctx: AmplifyContext, + { + oauthConfig, + provider, + idpIdentifier, + clientId, + customState, + preferPrivateSession, + options, + authSessionOpener, + }: { + oauthConfig: OAuthConfig; + provider: string; + idpIdentifier?: string; + clientId: string; + customState?: string; + preferPrivateSession?: boolean; + options?: SignInWithRedirectInput['options']; + authSessionOpener?: OpenAuthSession; + }, +) => { const { domain, redirectSignIn, responseType, scopes } = oauthConfig; const { loginHint, lang, nonce, prompt } = options ?? {}; const randomState = generateState(); @@ -165,7 +177,7 @@ const oauthSignIn = async ({ throw createOAuthError(String(type)); } if (type === 'success' && url) { - await completeOAuthFlow({ + await completeOAuthFlow(ctx, { currentUrl: url, clientId, domain, diff --git a/packages/auth/src/providers/cognito/apis/signInWithSRP.ts b/packages/auth/src/providers/cognito/apis/signInWithSRP.ts index d2d9588f6bc..9e570b248ee 100644 --- a/packages/auth/src/providers/cognito/apis/signInWithSRP.ts +++ b/packages/auth/src/providers/cognito/apis/signInWithSRP.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 { assertTokenProviderConfig } from '@aws-amplify/core/internals/utils'; import { AuthValidationErrorCode } from '../../../errors/types/validation'; @@ -49,10 +49,11 @@ import { resetAutoSignIn } from './autoSignIn'; * @throws AuthTokenConfigException - Thrown when the token provider config is invalid. */ export async function signInWithSRP( + ctx: AmplifyContext, input: SignInWithSRPInput, ): Promise { const { username, password } = input; - const authConfig = Amplify.getConfig().Auth?.Cognito; + const authConfig = ctx.resourcesConfig.Auth?.Cognito; const signInDetails: CognitoAuthSignInDetails = { loginId: username, authFlowType: 'USER_SRP_AUTH', @@ -104,7 +105,7 @@ export async function signInWithSRP( }); resetActiveSignInState(); - await dispatchSignedInHubEvent(); + await dispatchSignedInHubEvent(ctx); resetAutoSignIn(); @@ -114,7 +115,7 @@ export async function signInWithSRP( }; } - return getSignInResult({ + return getSignInResult(ctx, { challengeName: handledChallengeName as ChallengeName, challengeParameters: handledChallengeParameters as ChallengeParameters, }); diff --git a/packages/auth/src/providers/cognito/apis/signInWithUserAuth.ts b/packages/auth/src/providers/cognito/apis/signInWithUserAuth.ts index ce0802866af..f3ca4084f20 100644 --- a/packages/auth/src/providers/cognito/apis/signInWithUserAuth.ts +++ b/packages/auth/src/providers/cognito/apis/signInWithUserAuth.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 { assertTokenProviderConfig } from '@aws-amplify/core/internals/utils'; import { AuthValidationErrorCode } from '../../../errors/types/validation'; @@ -53,10 +53,11 @@ import { resetAutoSignIn } from './autoSignIn'; * @throws AuthTokenConfigException - Thrown when the token provider config is invalid. */ export async function signInWithUserAuth( + ctx: AmplifyContext, input: SignInWithUserAuthInput, ): Promise { const { username, password, options } = input; - const authConfig = Amplify.getConfig().Auth?.Cognito; + const authConfig = ctx.resourcesConfig.Auth?.Cognito; const signInDetails: CognitoAuthSignInDetails = { loginId: username, authFlowType: 'USER_AUTH', @@ -114,7 +115,7 @@ export async function signInWithUserAuth( }); resetActiveSignInState(); - await dispatchSignedInHubEvent(); + await dispatchSignedInHubEvent(ctx); resetAutoSignIn(); @@ -124,7 +125,7 @@ export async function signInWithUserAuth( }; } - return getSignInResult({ + return getSignInResult(ctx, { challengeName: response.ChallengeName as ChallengeName, challengeParameters: response.ChallengeParameters as ChallengeParameters, availableChallenges: diff --git a/packages/auth/src/providers/cognito/apis/signInWithUserPassword.ts b/packages/auth/src/providers/cognito/apis/signInWithUserPassword.ts index e9280227a37..5944a074a2f 100644 --- a/packages/auth/src/providers/cognito/apis/signInWithUserPassword.ts +++ b/packages/auth/src/providers/cognito/apis/signInWithUserPassword.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 { assertTokenProviderConfig } from '@aws-amplify/core/internals/utils'; import { AuthValidationErrorCode } from '../../../errors/types/validation'; @@ -46,10 +46,11 @@ import { resetAutoSignIn } from './autoSignIn'; * @throws AuthTokenConfigException - Thrown when the token provider config is invalid. */ export async function signInWithUserPassword( + ctx: AmplifyContext, input: SignInWithUserPasswordInput, ): Promise { const { username, password, options } = input; - const authConfig = Amplify.getConfig().Auth?.Cognito; + const authConfig = ctx.resourcesConfig.Auth?.Cognito; const signInDetails: CognitoAuthSignInDetails = { loginId: username, authFlowType: 'USER_PASSWORD_AUTH', @@ -99,7 +100,7 @@ export async function signInWithUserPassword( }); resetActiveSignInState(); - await dispatchSignedInHubEvent(); + await dispatchSignedInHubEvent(ctx); resetAutoSignIn(); @@ -109,7 +110,7 @@ export async function signInWithUserPassword( }; } - return getSignInResult({ + return getSignInResult(ctx, { challengeName: retiredChallengeName as ChallengeName, challengeParameters: retriedChallengeParameters as ChallengeParameters, }); diff --git a/packages/auth/src/providers/cognito/apis/signOut.ts b/packages/auth/src/providers/cognito/apis/signOut.ts index 2fa52b73ee4..a17ae77c67b 100644 --- a/packages/auth/src/providers/cognito/apis/signOut.ts +++ b/packages/auth/src/providers/cognito/apis/signOut.ts @@ -2,11 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 import { - Amplify, + AmplifyContext, CognitoUserPoolConfig, ConsoleLogger, Hub, - clearCredentials, defaultStorage, } from '@aws-amplify/core'; import { @@ -15,6 +14,7 @@ import { JWT, assertOAuthConfig, assertTokenProviderConfig, + resolveCtxArgs, } from '@aws-amplify/core/internals/utils'; import { getAuthUserAgentValue } from '../../../utils'; @@ -43,8 +43,14 @@ const logger = new ConsoleLogger('Auth'); * @param input - The SignOutInput object * @throws AuthTokenConfigException - Thrown when the token provider config is invalid. */ -export async function signOut(input?: SignOutInput): Promise { - const cognitoConfig = Amplify.getConfig().Auth?.Cognito; +export async function signOut(input?: SignOutInput): Promise; +export async function signOut( + ctx: AmplifyContext, + input?: SignOutInput, +): Promise; +export async function signOut(...args: any[]): Promise { + const [ctx, input] = resolveCtxArgs(args); + const cognitoConfig = ctx.resourcesConfig.Auth?.Cognito; assertTokenProviderConfig(cognitoConfig); if (input?.global) { @@ -66,6 +72,7 @@ export async function signOut(input?: SignOutInput): Promise { oAuthStore.setAuthConfig(cognitoConfig); const { type } = (await handleOAuthSignOut( + ctx, cognitoConfig, oAuthStore, tokenOrchestrator, @@ -80,7 +87,7 @@ export async function signOut(input?: SignOutInput): Promise { } else { // complete sign out tokenOrchestrator.clearTokens(); - await clearCredentials(); + await ctx.clearCredentials(); Hub.dispatch('auth', { event: 'signedOut' }, 'Auth', AMPLIFY_SYMBOL); } } diff --git a/packages/auth/src/providers/cognito/apis/signUp.ts b/packages/auth/src/providers/cognito/apis/signUp.ts index 2861541243c..b2be3e24daa 100644 --- a/packages/auth/src/providers/cognito/apis/signUp.ts +++ b/packages/auth/src/providers/cognito/apis/signUp.ts @@ -1,11 +1,12 @@ // 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 { AuthAction, AuthVerifiableAttributeKey, assertTokenProviderConfig, + resolveCtxArgs, } from '@aws-amplify/core/internals/utils'; import { AuthDeliveryMedium } from '../../../types'; @@ -39,9 +40,15 @@ import { setAutoSignIn } from './autoSignIn'; * are not defined. * @throws AuthTokenConfigException - Thrown when the token provider config is invalid. */ -export async function signUp(input: SignUpInput): Promise { +export async function signUp(input: SignUpInput): Promise; +export async function signUp( + ctx: AmplifyContext, + input: SignUpInput, +): Promise; +export async function signUp(...args: any[]): Promise { + const [ctx, input] = resolveCtxArgs(args); const { username, password, options } = input; - const authConfig = Amplify.getConfig().Auth?.Cognito; + const authConfig = ctx.resourcesConfig.Auth?.Cognito; const signUpVerificationMethod = authConfig?.signUpVerificationMethod ?? 'code'; const { clientMetadata, validationData, autoSignIn } = input.options ?? {}; @@ -121,7 +128,7 @@ export async function signUp(input: SignUpInput): Promise { // No Confirm Sign In Step Required if (isSignUpComplete) { if (isAutoSignInStarted) { - setAutoSignIn(autoSignInUserConfirmed(signInInput)); + setAutoSignIn(autoSignInUserConfirmed(ctx, signInInput)); return { isSignUpComplete: true, @@ -147,7 +154,7 @@ export async function signUp(input: SignUpInput): Promise { // Confirmation Via Link Occurs In Separate Context // AutoSignIn Fn Will Initiate Polling Once Executed if (signUpVerificationMethod === 'link') { - setAutoSignIn(autoSignInWhenUserIsConfirmedWithLink(signInInput)); + setAutoSignIn(autoSignInWhenUserIsConfirmedWithLink(ctx, signInInput)); return { isSignUpComplete: false, @@ -160,7 +167,7 @@ export async function signUp(input: SignUpInput): Promise { } // Confirmation Via Code Occurs In Same Context // AutoSignIn Next Step Will Be Returned From Confirm Sign Up - handleCodeAutoSignIn(signInInput); + handleCodeAutoSignIn(ctx, signInInput); } return { diff --git a/packages/auth/src/providers/cognito/apis/updateMFAPreference.ts b/packages/auth/src/providers/cognito/apis/updateMFAPreference.ts index 200c9e59f0e..b0e7867769d 100644 --- a/packages/auth/src/providers/cognito/apis/updateMFAPreference.ts +++ b/packages/auth/src/providers/cognito/apis/updateMFAPreference.ts @@ -1,10 +1,11 @@ // 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 { AuthAction, assertTokenProviderConfig, + resolveCtxArgs, } from '@aws-amplify/core/internals/utils'; import { UpdateMFAPreferenceInput } from '../types'; @@ -26,12 +27,18 @@ import { createCognitoUserPoolEndpointResolver } from '../factories'; */ export async function updateMFAPreference( input: UpdateMFAPreferenceInput, -): Promise { +): Promise; +export async function updateMFAPreference( + ctx: AmplifyContext, + input: UpdateMFAPreferenceInput, +): Promise; +export async function updateMFAPreference(...args: any[]): Promise { + const [ctx, input] = resolveCtxArgs(args); const { sms, totp, email } = input; - const authConfig = Amplify.getConfig().Auth?.Cognito; + const authConfig = ctx.resourcesConfig.Auth?.Cognito; assertTokenProviderConfig(authConfig); const { userPoolEndpoint, userPoolId } = authConfig; - const { tokens } = await fetchAuthSession({ forceRefresh: false }); + const { tokens } = await ctx.fetchAuthSession({ forceRefresh: false }); assertAuthTokens(tokens); const setUserMFAPreference = createSetUserMFAPreferenceClient({ endpointResolver: createCognitoUserPoolEndpointResolver({ diff --git a/packages/auth/src/providers/cognito/apis/updatePassword.ts b/packages/auth/src/providers/cognito/apis/updatePassword.ts index f8c8c4bdeae..0bc2da708c3 100644 --- a/packages/auth/src/providers/cognito/apis/updatePassword.ts +++ b/packages/auth/src/providers/cognito/apis/updatePassword.ts @@ -1,10 +1,11 @@ // 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 { AuthAction, assertTokenProviderConfig, + resolveCtxArgs, } from '@aws-amplify/core/internals/utils'; import { AuthValidationErrorCode } from '../../../errors/types/validation'; @@ -25,10 +26,14 @@ import { createCognitoUserPoolEndpointResolver } from '../factories'; * @throws - {@link AuthValidationErrorCode} - Validation errors thrown when oldPassword or newPassword are empty. * @throws AuthTokenConfigException - Thrown when the token provider config is invalid. */ +export async function updatePassword(input: UpdatePasswordInput): Promise; export async function updatePassword( + ctx: AmplifyContext, input: UpdatePasswordInput, -): Promise { - const authConfig = Amplify.getConfig().Auth?.Cognito; +): Promise; +export async function updatePassword(...args: any[]): Promise { + const [ctx, input] = resolveCtxArgs(args); + const authConfig = ctx.resourcesConfig.Auth?.Cognito; assertTokenProviderConfig(authConfig); const { userPoolEndpoint, userPoolId } = authConfig; const { oldPassword, newPassword } = input; @@ -41,7 +46,7 @@ export async function updatePassword( !!newPassword, AuthValidationErrorCode.EmptyUpdatePassword, ); - const { tokens } = await fetchAuthSession({ forceRefresh: false }); + const { tokens } = await ctx.fetchAuthSession({ forceRefresh: false }); assertAuthTokens(tokens); const changePassword = createChangePasswordClient({ endpointResolver: createCognitoUserPoolEndpointResolver({ diff --git a/packages/auth/src/providers/cognito/apis/updateUserAttribute.ts b/packages/auth/src/providers/cognito/apis/updateUserAttribute.ts index 242eb7dfe7c..9bfbaee9f07 100644 --- a/packages/auth/src/providers/cognito/apis/updateUserAttribute.ts +++ b/packages/auth/src/providers/cognito/apis/updateUserAttribute.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 { UpdateUserAttributeInput, UpdateUserAttributeOutput } from '../types'; import { UpdateUserAttributesException } from '../types/errors'; @@ -14,17 +17,25 @@ import { updateUserAttributes } from './updateUserAttributes'; * @throws - {@link UpdateUserAttributesException} * @throws AuthTokenConfigException - Thrown when the token provider config is invalid. */ -export const updateUserAttribute = async ( +export async function updateUserAttribute( + input: UpdateUserAttributeInput, +): Promise; +export async function updateUserAttribute( + ctx: AmplifyContext, input: UpdateUserAttributeInput, -): Promise => { +): Promise; +export async function updateUserAttribute( + ...args: any[] +): Promise { + const [ctx, input] = resolveCtxArgs(args); const { userAttribute: { attributeKey, value }, options, } = input; - const output = await updateUserAttributes({ + const output = await updateUserAttributes(ctx, { userAttributes: { [attributeKey]: value }, options, }); return Object.values(output)[0]; -}; +} diff --git a/packages/auth/src/providers/cognito/apis/updateUserAttributes.ts b/packages/auth/src/providers/cognito/apis/updateUserAttributes.ts index 5076e3145a5..8559e25d7a6 100644 --- a/packages/auth/src/providers/cognito/apis/updateUserAttributes.ts +++ b/packages/auth/src/providers/cognito/apis/updateUserAttributes.ts @@ -1,10 +1,11 @@ // 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 { AuthAction, assertTokenProviderConfig, + resolveCtxArgs, } from '@aws-amplify/core/internals/utils'; import { @@ -33,15 +34,23 @@ import { createCognitoUserPoolEndpointResolver } from '../factories'; * @throws - {@link UpdateUserAttributesException} * @throws AuthTokenConfigException - Thrown when the token provider config is invalid. */ -export const updateUserAttributes = async ( +export async function updateUserAttributes( input: UpdateUserAttributesInput, -): Promise => { +): Promise; +export async function updateUserAttributes( + ctx: AmplifyContext, + input: UpdateUserAttributesInput, +): Promise; +export async function updateUserAttributes( + ...args: any[] +): Promise { + const [ctx, input] = resolveCtxArgs(args); const { userAttributes, options } = input; - const authConfig = Amplify.getConfig().Auth?.Cognito; + const authConfig = ctx.resourcesConfig.Auth?.Cognito; const clientMetadata = options?.clientMetadata; assertTokenProviderConfig(authConfig); const { userPoolEndpoint, userPoolId } = authConfig; - const { tokens } = await fetchAuthSession({ forceRefresh: false }); + const { tokens } = await ctx.fetchAuthSession({ forceRefresh: false }); assertAuthTokens(tokens); const updateUserAttributesClient = createUpdateUserAttributesClient({ endpointResolver: createCognitoUserPoolEndpointResolver({ @@ -64,7 +73,7 @@ export const updateUserAttributes = async ( ...getConfirmedAttributes(userAttributes), ...getUnConfirmedAttributes(CodeDeliveryDetailsList), }; -}; +} function getConfirmedAttributes( attributes: AuthUserAttributes, diff --git a/packages/auth/src/providers/cognito/apis/verifyTOTPSetup.ts b/packages/auth/src/providers/cognito/apis/verifyTOTPSetup.ts index c5c1212c194..9fab38a04ac 100644 --- a/packages/auth/src/providers/cognito/apis/verifyTOTPSetup.ts +++ b/packages/auth/src/providers/cognito/apis/verifyTOTPSetup.ts @@ -1,10 +1,11 @@ // 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 { AuthAction, assertTokenProviderConfig, + resolveCtxArgs, } from '@aws-amplify/core/internals/utils'; import { AuthValidationErrorCode } from '../../../errors/types/validation'; @@ -29,8 +30,14 @@ import { createCognitoUserPoolEndpointResolver } from '../factories'; */ export async function verifyTOTPSetup( input: VerifyTOTPSetupInput, -): Promise { - const authConfig = Amplify.getConfig().Auth?.Cognito; +): Promise; +export async function verifyTOTPSetup( + ctx: AmplifyContext, + input: VerifyTOTPSetupInput, +): Promise; +export async function verifyTOTPSetup(...args: any[]): Promise { + const [ctx, input] = resolveCtxArgs(args); + const authConfig = ctx.resourcesConfig.Auth?.Cognito; assertTokenProviderConfig(authConfig); const { userPoolEndpoint, userPoolId } = authConfig; const { code, options } = input; @@ -38,7 +45,7 @@ export async function verifyTOTPSetup( !!code, AuthValidationErrorCode.EmptyVerifyTOTPSetupCode, ); - const { tokens } = await fetchAuthSession({ forceRefresh: false }); + const { tokens } = await ctx.fetchAuthSession({ forceRefresh: false }); assertAuthTokens(tokens); const verifySoftwareToken = createVerifySoftwareTokenClient({ endpointResolver: createCognitoUserPoolEndpointResolver({ diff --git a/packages/auth/src/providers/cognito/utils/dispatchSignedInHubEvent.ts b/packages/auth/src/providers/cognito/utils/dispatchSignedInHubEvent.ts index 76fd6a4c24e..829a9ba4684 100644 --- a/packages/auth/src/providers/cognito/utils/dispatchSignedInHubEvent.ts +++ b/packages/auth/src/providers/cognito/utils/dispatchSignedInHubEvent.ts @@ -1,7 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { Hub } from '@aws-amplify/core'; +import { AmplifyContext, Hub } from '@aws-amplify/core'; import { AMPLIFY_SYMBOL } from '@aws-amplify/core/internals/utils'; import { getCurrentUser } from '../apis/getCurrentUser'; @@ -14,13 +14,13 @@ import { AuthError } from '../../../errors/AuthError'; export const ERROR_MESSAGE = 'Unable to get user session following successful sign-in.'; -export const dispatchSignedInHubEvent = async () => { +export const dispatchSignedInHubEvent = async (ctx: AmplifyContext) => { try { Hub.dispatch( 'auth', { event: 'signedIn', - data: await getCurrentUser(), + data: await getCurrentUser(ctx), }, 'Auth', AMPLIFY_SYMBOL, diff --git a/packages/auth/src/providers/cognito/utils/oauth/attemptCompleteOAuthFlow.ts b/packages/auth/src/providers/cognito/utils/oauth/attemptCompleteOAuthFlow.ts index 7b526e8e6a9..54b6f0cceb0 100644 --- a/packages/auth/src/providers/cognito/utils/oauth/attemptCompleteOAuthFlow.ts +++ b/packages/auth/src/providers/cognito/utils/oauth/attemptCompleteOAuthFlow.ts @@ -1,7 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { AuthConfig } from '@aws-amplify/core'; +import { AmplifyContext, AuthConfig } from '@aws-amplify/core'; import { AuthAction, assertOAuthConfig, @@ -16,6 +16,7 @@ import { getRedirectUrl } from './getRedirectUrl'; import { handleFailure } from './handleFailure'; export const attemptCompleteOAuthFlow = async ( + ctx: AmplifyContext, authConfig: AuthConfig['Cognito'], ): Promise => { try { @@ -40,7 +41,7 @@ export const attemptCompleteOAuthFlow = async ( const { domain, redirectSignIn, responseType } = loginWith.oauth; const redirectUri = getRedirectUrl(redirectSignIn); - await completeOAuthFlow({ + await completeOAuthFlow(ctx, { currentUrl, clientId: userPoolClientId, domain, diff --git a/packages/auth/src/providers/cognito/utils/oauth/completeOAuthFlow.ts b/packages/auth/src/providers/cognito/utils/oauth/completeOAuthFlow.ts index e82e152dab3..308fc5e668d 100644 --- a/packages/auth/src/providers/cognito/utils/oauth/completeOAuthFlow.ts +++ b/packages/auth/src/providers/cognito/utils/oauth/completeOAuthFlow.ts @@ -7,7 +7,7 @@ import { USER_AGENT_HEADER, urlSafeDecode, } from '@aws-amplify/core/internals/utils'; -import { Hub, decodeJWT } from '@aws-amplify/core'; +import { AmplifyContext, Hub, decodeJWT } from '@aws-amplify/core'; import { cacheCognitoTokens } from '../../tokenProvider/cacheTokens'; import { dispatchSignedInHubEvent } from '../dispatchSignedInHubEvent'; @@ -18,23 +18,26 @@ import { resolveAndClearInflightPromises } from './inflightPromise'; import { validateState } from './validateState'; import { oAuthStore } from './oAuthStore'; -export const completeOAuthFlow = async ({ - currentUrl, - userAgentValue, - clientId, - redirectUri, - responseType, - domain, - preferPrivateSession, -}: { - currentUrl: string; - userAgentValue: string; - clientId: string; - redirectUri: string; - responseType: string; - domain: string; - preferPrivateSession?: boolean; -}): Promise => { +export const completeOAuthFlow = async ( + ctx: AmplifyContext, + { + currentUrl, + userAgentValue, + clientId, + redirectUri, + responseType, + domain, + preferPrivateSession, + }: { + currentUrl: string; + userAgentValue: string; + clientId: string; + redirectUri: string; + responseType: string; + domain: string; + preferPrivateSession?: boolean; + }, +): Promise => { const urlParams = new AmplifyUrl(currentUrl); const error = urlParams.searchParams.get('error'); const errorMessage = urlParams.searchParams.get('error_description'); @@ -44,7 +47,7 @@ export const completeOAuthFlow = async ({ } if (responseType === 'code') { - return handleCodeFlow({ + return handleCodeFlow(ctx, { currentUrl, userAgentValue, clientId, @@ -54,28 +57,31 @@ export const completeOAuthFlow = async ({ }); } - return handleImplicitFlow({ + return handleImplicitFlow(ctx, { currentUrl, redirectUri, preferPrivateSession, }); }; -const handleCodeFlow = async ({ - currentUrl, - userAgentValue, - clientId, - redirectUri, - domain, - preferPrivateSession, -}: { - currentUrl: string; - userAgentValue: string; - clientId: string; - redirectUri: string; - domain: string; - preferPrivateSession?: boolean; -}) => { +const handleCodeFlow = async ( + ctx: AmplifyContext, + { + currentUrl, + userAgentValue, + clientId, + redirectUri, + domain, + preferPrivateSession, + }: { + currentUrl: string; + userAgentValue: string; + clientId: string; + redirectUri: string; + domain: string; + preferPrivateSession?: boolean; + }, +) => { /* Convert URL into an object with parameters as keys { redirect_uri: 'http://localhost:3000/', response_type: 'code', ...} */ const url = new AmplifyUrl(currentUrl); @@ -150,22 +156,25 @@ const handleCodeFlow = async ({ ExpiresIn: expires_in, }); - return completeFlow({ + return completeFlow(ctx, { redirectUri, state: validatedState, preferPrivateSession, }); }; -const handleImplicitFlow = async ({ - currentUrl, - redirectUri, - preferPrivateSession, -}: { - currentUrl: string; - redirectUri: string; - preferPrivateSession?: boolean; -}) => { +const handleImplicitFlow = async ( + ctx: AmplifyContext, + { + currentUrl, + redirectUri, + preferPrivateSession, + }: { + currentUrl: string; + redirectUri: string; + preferPrivateSession?: boolean; + }, +) => { // hash is `null` if `#` doesn't exist on URL const url = new AmplifyUrl(currentUrl); @@ -212,22 +221,25 @@ const handleImplicitFlow = async ({ ExpiresIn: expires_in, }); - return completeFlow({ + return completeFlow(ctx, { redirectUri, state: validatedState, preferPrivateSession, }); }; -const completeFlow = async ({ - redirectUri, - state, - preferPrivateSession, -}: { - preferPrivateSession?: boolean; - redirectUri: string; - state: string; -}) => { +const completeFlow = async ( + ctx: AmplifyContext, + { + redirectUri, + state, + preferPrivateSession, + }: { + preferPrivateSession?: boolean; + redirectUri: string; + state: string; + }, +) => { await tokenOrchestrator.setOAuthMetadata({ oauthSignIn: true, }); @@ -254,7 +266,7 @@ const completeFlow = async ({ ); } Hub.dispatch('auth', { event: 'signInWithRedirect' }, 'Auth', AMPLIFY_SYMBOL); - await dispatchSignedInHubEvent(); + await dispatchSignedInHubEvent(ctx); }; const isCustomState = (state: string): boolean => { diff --git a/packages/auth/src/providers/cognito/utils/oauth/completeOAuthSignOut.ts b/packages/auth/src/providers/cognito/utils/oauth/completeOAuthSignOut.ts index bb2a30bac7e..d61bb77fa11 100644 --- a/packages/auth/src/providers/cognito/utils/oauth/completeOAuthSignOut.ts +++ b/packages/auth/src/providers/cognito/utils/oauth/completeOAuthSignOut.ts @@ -1,15 +1,18 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { Hub, clearCredentials } from '@aws-amplify/core'; +import { AmplifyContext, Hub } from '@aws-amplify/core'; import { AMPLIFY_SYMBOL } from '@aws-amplify/core/internals/utils'; import { DefaultOAuthStore } from '../../utils/signInWithRedirectStore'; import { tokenOrchestrator } from '../../tokenProvider'; -export const completeOAuthSignOut = async (store: DefaultOAuthStore) => { +export const completeOAuthSignOut = async ( + ctx: AmplifyContext, + store: DefaultOAuthStore, +) => { await store.clearOAuthData(); tokenOrchestrator.clearTokens(); - await clearCredentials(); + await ctx.clearCredentials(); Hub.dispatch('auth', { event: 'signedOut' }, 'Auth', AMPLIFY_SYMBOL); }; diff --git a/packages/auth/src/providers/cognito/utils/oauth/enableOAuthListener.ts b/packages/auth/src/providers/cognito/utils/oauth/enableOAuthListener.ts index c0ec3df85db..118c9ac76a7 100644 --- a/packages/auth/src/providers/cognito/utils/oauth/enableOAuthListener.ts +++ b/packages/auth/src/providers/cognito/utils/oauth/enableOAuthListener.ts @@ -1,21 +1,39 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { Amplify } from '@aws-amplify/core'; import { - ADD_OAUTH_LISTENER, - isBrowser, -} from '@aws-amplify/core/internals/utils'; + Hub, + ResourcesConfig, + getActiveContext, + hasGlobalContext, +} from '@aws-amplify/core'; +import { isBrowser } from '@aws-amplify/core/internals/utils'; import { attemptCompleteOAuthFlow } from './attemptCompleteOAuthFlow'; -// attach the side effect for handling the completion of an inflight oauth flow -// this side effect works only on Web -isBrowser() && - (() => { - // add the listener to the singleton for triggering - Amplify[ADD_OAUTH_LISTENER](attemptCompleteOAuthFlow); - })(); +// Attach the side effect for handling the completion of an inflight OAuth flow. +// This side effect works only on Web. +if (isBrowser()) { + Hub.listen('core', ({ payload }) => { + if (payload.event === 'configure') { + const data = payload.data as ResourcesConfig | undefined; + if (data?.Auth?.Cognito?.loginWith?.oauth) { + attemptCompleteOAuthFlow(getActiveContext(), data.Auth.Cognito); + } + } + }); -// required to present for module loaders + // Catch-up: if Amplify.configure() was called before this module was imported + // (e.g. dynamic imports, code-splitting), the Hub event was already fired. + // Check if global context is already configured with OAuth and attempt completion. + if (hasGlobalContext()) { + const ctx = getActiveContext(); + const oauthConfig = ctx.resourcesConfig?.Auth?.Cognito?.loginWith?.oauth; + if (oauthConfig) { + attemptCompleteOAuthFlow(ctx, ctx.resourcesConfig.Auth!.Cognito!); + } + } +} + +// required to be present for module loaders export {}; diff --git a/packages/auth/src/providers/cognito/utils/oauth/handleOAuthSignOut.native.ts b/packages/auth/src/providers/cognito/utils/oauth/handleOAuthSignOut.native.ts index e67c8a255ef..9e4d2b7ba78 100644 --- a/packages/auth/src/providers/cognito/utils/oauth/handleOAuthSignOut.native.ts +++ b/packages/auth/src/providers/cognito/utils/oauth/handleOAuthSignOut.native.ts @@ -1,7 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { CognitoUserPoolConfig } from '@aws-amplify/core'; +import { AmplifyContext, CognitoUserPoolConfig } from '@aws-amplify/core'; import { OpenAuthSessionResult } from '../../../../utils/types'; import { DefaultOAuthStore } from '../../utils/signInWithRedirectStore'; @@ -11,6 +11,7 @@ import { completeOAuthSignOut } from './completeOAuthSignOut'; import { oAuthSignOutRedirect } from './oAuthSignOutRedirect'; export const handleOAuthSignOut = async ( + ctx: AmplifyContext, cognitoConfig: CognitoUserPoolConfig, store: DefaultOAuthStore, // No-op here as it's only used in the non-native implementation @@ -29,11 +30,11 @@ export const handleOAuthSignOut = async ( const shouldCompleteSignOut = preferPrivateSession || result?.type === 'success'; if (shouldCompleteSignOut) { - await completeOAuthSignOut(store); + await completeOAuthSignOut(ctx, store); } return result; } - return completeOAuthSignOut(store); + return completeOAuthSignOut(ctx, store); }; diff --git a/packages/auth/src/providers/cognito/utils/oauth/handleOAuthSignOut.ts b/packages/auth/src/providers/cognito/utils/oauth/handleOAuthSignOut.ts index da4f7eb380a..d5610ffc831 100644 --- a/packages/auth/src/providers/cognito/utils/oauth/handleOAuthSignOut.ts +++ b/packages/auth/src/providers/cognito/utils/oauth/handleOAuthSignOut.ts @@ -1,7 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { CognitoUserPoolConfig } from '@aws-amplify/core'; +import { AmplifyContext, CognitoUserPoolConfig } from '@aws-amplify/core'; import { OpenAuthSessionResult } from '../../../../utils/types'; import { DefaultOAuthStore } from '../../utils/signInWithRedirectStore'; @@ -11,6 +11,7 @@ import { completeOAuthSignOut } from './completeOAuthSignOut'; import { oAuthSignOutRedirect } from './oAuthSignOutRedirect'; export const handleOAuthSignOut = async ( + ctx: AmplifyContext, cognitoConfig: CognitoUserPoolConfig, store: DefaultOAuthStore, tokenOrchestrator: TokenOrchestrator, @@ -21,7 +22,7 @@ export const handleOAuthSignOut = async ( // Clear everything before attempting to visted logout endpoint since the current application // state could be wiped away on redirect - await completeOAuthSignOut(store); + await completeOAuthSignOut(ctx, store); // The isOAuthSignIn flag is propagated by the oAuthToken store which manages oauth keys in local storage only. // These keys are used to determine if a user is in an inflight or signedIn oauth states. diff --git a/packages/auth/src/providers/cognito/utils/signInHelpers.ts b/packages/auth/src/providers/cognito/utils/signInHelpers.ts index 0911df68889..a198e238971 100644 --- a/packages/auth/src/providers/cognito/utils/signInHelpers.ts +++ b/packages/auth/src/providers/cognito/utils/signInHelpers.ts @@ -1,7 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { Amplify, CognitoUserPoolConfig } from '@aws-amplify/core'; +import { AmplifyContext, CognitoUserPoolConfig } from '@aws-amplify/core'; import { AmplifyUrl, AuthAction, @@ -563,13 +563,16 @@ export async function handleCustomSRPAuthFlow( ); } -export async function getSignInResult(params: { - challengeName: ChallengeName; - challengeParameters: ChallengeParameters; - availableChallenges?: ChallengeName[]; -}): Promise { +export async function getSignInResult( + ctx: AmplifyContext, + params: { + challengeName: ChallengeName; + challengeParameters: ChallengeParameters; + availableChallenges?: ChallengeName[]; + }, +): Promise { const { challengeName, challengeParameters, availableChallenges } = params; - const authConfig = Amplify.getConfig().Auth?.Cognito; + const authConfig = ctx.resourcesConfig.Auth?.Cognito; assertTokenProviderConfig(authConfig); switch (challengeName) { @@ -698,12 +701,12 @@ export async function getSignInResult(params: { }; case 'WEB_AUTHN': { - const result = await handleWebAuthnSignInResult(challengeParameters); + const result = await handleWebAuthnSignInResult(ctx, challengeParameters); if (isWebAuthnResultAuthSignInOutput(result)) { return result; } - return getSignInResult(result); + return getSignInResult(ctx, result); } case 'PASSWORD': case 'PASSWORD_SRP': @@ -944,10 +947,10 @@ export function getAllowedMfaSetupTypes(availableMfaSetupTypes: AuthMFAType[]) { ); } -export async function assertUserNotAuthenticated() { +export async function assertUserNotAuthenticated(ctx: AmplifyContext) { let authUser: AWSAuthUser | undefined; try { - authUser = await getCurrentUser(); + authUser = await getCurrentUser(ctx); } catch (error) {} if (authUser && authUser.userId && authUser.username) { diff --git a/packages/auth/src/providers/cognito/utils/signUpHelpers.ts b/packages/auth/src/providers/cognito/utils/signUpHelpers.ts index 9bebcf4be82..1f61d8083b6 100644 --- a/packages/auth/src/providers/cognito/utils/signUpHelpers.ts +++ b/packages/auth/src/providers/cognito/utils/signUpHelpers.ts @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { HubInternal } from '@aws-amplify/core/internals/utils'; +import { AmplifyContext } from '@aws-amplify/core'; import { signIn } from '../apis/signIn'; import { SignInInput, SignInOutput } from '../types'; @@ -14,7 +15,10 @@ import { signInWithUserAuth } from '../apis/signInWithUserAuth'; const MAX_AUTOSIGNIN_POLLING_MS = 3 * 60 * 1000; -export function handleCodeAutoSignIn(signInInput: SignInInput) { +export function handleCodeAutoSignIn( + ctx: AmplifyContext, + signInInput: SignInInput, +) { const stopHubListener = HubInternal.listen( 'auth-internal', async ({ payload }) => { @@ -25,7 +29,7 @@ export function handleCodeAutoSignIn(signInInput: SignInInput) { HubInternal.dispatch('auth-internal', { event: 'autoSignIn', }); - setAutoSignIn(autoSignInWithCode(signInInput)); + setAutoSignIn(autoSignInWithCode(ctx, signInInput)); stopHubListener(); } } @@ -63,6 +67,7 @@ function debounce any>(fun: F, delay: number) { } function handleAutoSignInWithLink( + ctx: AmplifyContext, signInInput: SignInInput, resolve: (value: SignInOutput) => void, reject: (reason?: any) => void, @@ -84,7 +89,7 @@ function handleAutoSignInWithLink( resetAutoSignIn(); } else { try { - const signInOutput = await signIn(signInInput); + const signInOutput = await signIn(ctx, signInInput); if (signInOutput.nextStep.signInStep !== 'CONFIRM_SIGN_UP') { resolve(signInOutput); clearInterval(autoSignInPollingIntervalId); @@ -105,15 +110,17 @@ const debouncedAutoSignWithCodeOrUserConfirmed = debounce( ); export function autoSignInWhenUserIsConfirmedWithLink( + ctx: AmplifyContext, signInInput: SignInInput, ): AutoSignInCallback { return async () => { return new Promise((resolve, reject) => { - debouncedAutoSignInWithLink([signInInput, resolve, reject]); + debouncedAutoSignInWithLink([ctx, signInInput, resolve, reject]); }); }; } async function handleAutoSignInWithCodeOrUserConfirmed( + ctx: AmplifyContext, signInInput: SignInInput, resolve: (value: SignInOutput) => void, reject: (reason?: any) => void, @@ -121,8 +128,8 @@ async function handleAutoSignInWithCodeOrUserConfirmed( try { const output = signInInput?.options?.authFlowType === 'USER_AUTH' - ? await signInWithUserAuth(signInInput) - : await signIn(signInInput); + ? await signInWithUserAuth(ctx, signInInput) + : await signIn(ctx, signInInput); resolve(output); resetAutoSignIn(); @@ -132,10 +139,18 @@ async function handleAutoSignInWithCodeOrUserConfirmed( } } -function autoSignInWithCode(signInInput: SignInInput): AutoSignInCallback { +function autoSignInWithCode( + ctx: AmplifyContext, + signInInput: SignInInput, +): AutoSignInCallback { return async () => { return new Promise((resolve, reject) => { - debouncedAutoSignWithCodeOrUserConfirmed([signInInput, resolve, reject]); + debouncedAutoSignWithCodeOrUserConfirmed([ + ctx, + signInInput, + resolve, + reject, + ]); }); }; } diff --git a/packages/auth/src/server.ts b/packages/auth/src/server.ts index 295237c6895..add897f4f16 100644 --- a/packages/auth/src/server.ts +++ b/packages/auth/src/server.ts @@ -1,5 +1,5 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -export * from '@aws-amplify/core/server'; -export * from './providers/cognito/apis/server'; +export { fetchAuthSession } from '@aws-amplify/core/server'; +export { fetchUserAttributes, getCurrentUser } from './providers/cognito'; diff --git a/packages/aws-amplify/__tests__/adapterCore/runWithAmplifyServerContext.test.ts b/packages/aws-amplify/__tests__/adapterCore/runWithAmplifyServerContext.test.ts deleted file mode 100644 index 9e2655bd2d5..00000000000 --- a/packages/aws-amplify/__tests__/adapterCore/runWithAmplifyServerContext.test.ts +++ /dev/null @@ -1,112 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -import { - createAmplifyServerContext, - destroyAmplifyServerContext, -} from '@aws-amplify/core/internals/adapter-core'; - -import { runWithAmplifyServerContext } from '../../src/adapter-core'; - -// mock serverContext -jest.mock('@aws-amplify/core/internals/adapter-core'); -const mockCreateAmplifyServerContext = createAmplifyServerContext as jest.Mock; -const mockDestroyAmplifyServerContext = - destroyAmplifyServerContext as jest.Mock; -const mockAmplifyConfig = {}; -const mockTokenProvider = { - getTokens: jest.fn(), -}; -const mockCredentialAndIdentityProvider = { - getCredentialsAndIdentityId: jest.fn(), - clearCredentialsAndIdentityId: jest.fn(), -}; -const mockContextSpec = { - token: { value: Symbol('AmplifyServerContextToken') }, -}; - -describe('runWithAmplifyServerContext', () => { - beforeEach(() => { - mockCreateAmplifyServerContext.mockReturnValueOnce(mockContextSpec); - }); - - afterEach(() => { - mockDestroyAmplifyServerContext.mockReset(); - }); - - it('should run the operation with the context', () => { - const mockOperation = jest.fn(); - runWithAmplifyServerContext( - mockAmplifyConfig, - { - Auth: { - tokenProvider: mockTokenProvider, - credentialsProvider: mockCredentialAndIdentityProvider, - }, - }, - mockOperation, - ); - - expect(mockOperation).toHaveBeenCalledWith(mockContextSpec); - }); - - it('should destroy the context after the operation completed', async () => { - const mockOperation = jest.fn(); - await runWithAmplifyServerContext( - mockAmplifyConfig, - { - Auth: { - tokenProvider: mockTokenProvider, - credentialsProvider: mockCredentialAndIdentityProvider, - }, - }, - mockOperation, - ); - - expect(mockDestroyAmplifyServerContext).toHaveBeenCalledWith( - mockContextSpec, - ); - }); - - it('should destroy the context when the operation throws', async () => { - const testError = new Error('some error'); - const mockOperation = jest.fn(); - mockOperation.mockRejectedValueOnce(testError); - - await expect( - runWithAmplifyServerContext( - mockAmplifyConfig, - { - Auth: { - tokenProvider: mockTokenProvider, - credentialsProvider: mockCredentialAndIdentityProvider, - }, - }, - mockOperation, - ), - ).rejects.toThrow(testError); - - expect(mockDestroyAmplifyServerContext).toHaveBeenCalledWith( - mockContextSpec, - ); - }); - - it('should return the result returned by the operation callback function', async () => { - const mockResultValue = { - url: 'http://123.com', - }; - const mockOperation = jest.fn(() => Promise.resolve(mockResultValue)); - const result = await runWithAmplifyServerContext( - mockAmplifyConfig, - { - Auth: { - tokenProvider: mockTokenProvider, - credentialsProvider: mockCredentialAndIdentityProvider, - }, - }, - mockOperation, - ); - - expect(result).toStrictEqual(mockResultValue); - }); -}); diff --git a/packages/aws-amplify/__tests__/configure.test.ts b/packages/aws-amplify/__tests__/configure.test.ts new file mode 100644 index 00000000000..f2fa7393310 --- /dev/null +++ b/packages/aws-amplify/__tests__/configure.test.ts @@ -0,0 +1,158 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { createConfigurationBuilder } from '@aws-amplify/core'; + +import { createAmplifyContext } from '../src/configure'; + +import { amplifyOutputsFixture } from './fixtures/amplifyOutputs'; + +describe('createAmplifyContext()', () => { + it('returns a frozen AmplifyContext from amplify_outputs fixture', () => { + const ctx = createAmplifyContext(amplifyOutputsFixture); + + expect(Object.isFrozen(ctx)).toBe(true); + expect(ctx.resourcesConfig.Auth?.Cognito.userPoolId).toBe( + 'eu-north-1_Ab12CdEfG', + ); + expect(ctx.resourcesConfig.Auth?.Cognito.userPoolClientId).toBe( + '1a2b3c4d5e6f7g8h9i0jklmnop', + ); + expect(ctx.resourcesConfig.Auth?.Cognito.identityPoolId).toBe( + 'eu-north-1:aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee', + ); + expect(ctx.resourcesConfig.Storage?.S3?.bucket).toBe( + 'my-test-app-storage-bucket-abcdef123456', + ); + expect(ctx.resourcesConfig.Storage?.S3?.region).toBe('eu-north-1'); + expect(ctx.resourcesConfig.API?.GraphQL?.endpoint).toBe( + 'https://xxxxxxxxxxxxxxxxxxxxxxxxxx.appsync-api.eu-north-1.amazonaws.com/graphql', + ); + expect(ctx.resourcesConfig.API?.GraphQL?.apiKey).toBe( + 'da2-fakeapikey1234567890abcdef', + ); + }); + + it('exposes fetchAuthSession, clearCredentials, and getTokens', () => { + const ctx = createAmplifyContext(amplifyOutputsFixture); + + expect(typeof ctx.fetchAuthSession).toBe('function'); + expect(typeof ctx.clearCredentials).toBe('function'); + expect(typeof ctx.getTokens).toBe('function'); + }); + + it('supports reconfiguration by calling createAmplifyContext() again', () => { + const ctx1 = createAmplifyContext(amplifyOutputsFixture); + const ctx2 = createAmplifyContext({ + ...amplifyOutputsFixture, + auth: { + ...amplifyOutputsFixture.auth, + // eslint-disable-next-line camelcase + user_pool_id: 'eu-north-1_NewPoolId', + }, + }); + + expect(ctx1.resourcesConfig.Auth?.Cognito.userPoolId).toBe( + 'eu-north-1_Ab12CdEfG', + ); + expect(ctx2.resourcesConfig.Auth?.Cognito.userPoolId).toBe( + 'eu-north-1_NewPoolId', + ); + }); +}); + +describe('createAmplifyContext() — resolveLocalLibraryOptions branches', () => { + it('returns empty options when no Auth config', () => { + const ctx = createAmplifyContext({ + version: '1.4', + storage: amplifyOutputsFixture.storage, + }); + expect(ctx.resourcesConfig.Auth).toBeUndefined(); + expect(ctx.resourcesConfig.Storage?.S3?.bucket).toBe( + 'my-test-app-storage-bucket-abcdef123456', + ); + }); + + it('passes through custom Auth libraryOptions', () => { + const mockTokenProvider = { + getTokens: jest.fn().mockResolvedValue(undefined), + }; + const mockCredentialsProvider = { + getCredentialsAndIdentityId: jest.fn().mockResolvedValue(undefined), + clearCredentialsAndIdentityId: jest.fn(), + }; + const ctx = createAmplifyContext(amplifyOutputsFixture, { + Auth: { + tokenProvider: mockTokenProvider as any, + credentialsProvider: mockCredentialsProvider as any, + }, + }); + expect(ctx.resourcesConfig.Auth?.Cognito.userPoolId).toBe( + 'eu-north-1_Ab12CdEfG', + ); + }); + + it('uses cookie storage when ssr is true', () => { + const ctx = createAmplifyContext(amplifyOutputsFixture, { ssr: true }); + expect(ctx.resourcesConfig.Auth?.Cognito.userPoolId).toBe( + 'eu-north-1_Ab12CdEfG', + ); + }); + + it('delegates fetchAuthSession to AuthClass', () => { + const ctx = createAmplifyContext(amplifyOutputsFixture); + expect(typeof ctx.fetchAuthSession).toBe('function'); + }); + + it('delegates clearCredentials to AuthClass', () => { + const ctx = createAmplifyContext(amplifyOutputsFixture); + expect(typeof ctx.clearCredentials).toBe('function'); + }); + + it('delegates getTokens to AuthClass', () => { + const ctx = createAmplifyContext(amplifyOutputsFixture); + expect(typeof ctx.getTokens).toBe('function'); + }); +}); + +describe('createConfigurationBuilder()', () => { + it('round-trips through createAmplifyContext()', () => { + const config = createConfigurationBuilder() + .auth(amplifyOutputsFixture.auth) + .storage(amplifyOutputsFixture.storage) + .data(amplifyOutputsFixture.data) + .build(); + + expect(config.version).toBe('1.4'); + expect(Object.isFrozen(config)).toBe(true); + + const ctx = createAmplifyContext(config); + + expect(ctx.resourcesConfig.Auth?.Cognito.userPoolId).toBe( + 'eu-north-1_Ab12CdEfG', + ); + expect(ctx.resourcesConfig.Storage?.S3?.bucket).toBe( + 'my-test-app-storage-bucket-abcdef123456', + ); + expect(ctx.resourcesConfig.API?.GraphQL?.endpoint).toBe( + 'https://xxxxxxxxxxxxxxxxxxxxxxxxxx.appsync-api.eu-north-1.amazonaws.com/graphql', + ); + }); + + it('allows replacing a scope for reconfiguration', () => { + const config = createConfigurationBuilder() + .auth(amplifyOutputsFixture.auth) + .auth({ + ...amplifyOutputsFixture.auth, + // eslint-disable-next-line camelcase + user_pool_id: 'eu-north-1_Replaced', + }) + .build(); + + const ctx = createAmplifyContext(config); + + expect(ctx.resourcesConfig.Auth?.Cognito.userPoolId).toBe( + 'eu-north-1_Replaced', + ); + }); +}); diff --git a/packages/aws-amplify/__tests__/exports.test.ts b/packages/aws-amplify/__tests__/exports.test.ts index 9e0015afb9f..4d791547886 100644 --- a/packages/aws-amplify/__tests__/exports.test.ts +++ b/packages/aws-amplify/__tests__/exports.test.ts @@ -24,7 +24,13 @@ import * as storageS3Exports from '../src/storage/s3'; describe('aws-amplify Exports', () => { describe('Top-level exports', () => { it('should only export expected symbols', () => { - expect(Object.keys(topLevelExports).sort()).toEqual(['Amplify'].sort()); + expect(Object.keys(topLevelExports).sort()).toEqual( + [ + 'Amplify', + 'createAmplifyContext', + 'createConfigurationBuilder', + ].sort(), + ); }); }); @@ -178,8 +184,8 @@ describe('aws-amplify Exports', () => { 'forgetDevice', 'fetchDevices', 'autoSignIn', - 'fetchAuthSession', 'decodeJWT', + 'fetchAuthSession', 'associateWebAuthnCredential', 'listWebAuthnCredentials', 'deleteWebAuthnCredential', diff --git a/packages/aws-amplify/__tests__/fixtures/amplifyOutputs.ts b/packages/aws-amplify/__tests__/fixtures/amplifyOutputs.ts new file mode 100644 index 00000000000..807b2b4f2e4 --- /dev/null +++ b/packages/aws-amplify/__tests__/fixtures/amplifyOutputs.ts @@ -0,0 +1,110 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +/* eslint-disable camelcase */ + +export const amplifyOutputsFixture = { + auth: { + user_pool_id: 'eu-north-1_Ab12CdEfG', + aws_region: 'eu-north-1', + user_pool_client_id: '1a2b3c4d5e6f7g8h9i0jklmnop', + identity_pool_id: 'eu-north-1:aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee', + mfa_methods: [], + standard_required_attributes: ['email'], + username_attributes: ['email'], + user_verification_types: ['email'], + groups: [ + { admin: { precedence: 0 } }, + { contributor: { precedence: 1 } }, + { user: { precedence: 2 } }, + ], + mfa_configuration: 'NONE', + password_policy: { + min_length: 8, + require_lowercase: true, + require_numbers: true, + require_symbols: true, + require_uppercase: true, + }, + unauthenticated_identities_enabled: true, + }, + data: { + url: 'https://xxxxxxxxxxxxxxxxxxxxxxxxxx.appsync-api.eu-north-1.amazonaws.com/graphql', + aws_region: 'eu-north-1', + api_key: 'da2-fakeapikey1234567890abcdef', + default_authorization_type: 'AMAZON_COGNITO_USER_POOLS', + authorization_types: ['API_KEY', 'AWS_IAM'], + model_introspection: { + version: 1, + models: { + Todo: { + name: 'Todo', + fields: { + id: { + name: 'id', + isArray: false, + type: 'ID', + isRequired: true, + attributes: [], + }, + content: { + name: 'content', + isArray: false, + type: 'String', + isRequired: true, + attributes: [], + }, + }, + syncable: true, + pluralName: 'Todos', + attributes: [ + { type: 'model', properties: {} }, + { + type: 'auth', + properties: { + rules: [ + { + provider: 'userPools', + ownerField: 'owner', + allow: 'owner', + identityClaim: 'cognito:username', + operations: ['create', 'update', 'delete', 'read'], + }, + ], + }, + }, + ], + primaryKeyInfo: { + isCustomPrimaryKey: false, + primaryKeyFieldName: 'id', + sortKeyFieldNames: [], + }, + }, + }, + enums: {}, + nonModels: {}, + }, + }, + storage: { + aws_region: 'eu-north-1', + bucket_name: 'my-test-app-storage-bucket-abcdef123456', + buckets: [ + { + name: 'amplify_storage_bucket', + bucket_name: 'my-test-app-storage-bucket-abcdef123456', + aws_region: 'eu-north-1', + paths: { + 'public/*': { + guest: ['get', 'list', 'write'], + }, + 'restricted/*': { + groupsuser: ['get', 'list'], + groupscontributor: ['get', 'list', 'write'], + groupsadmin: ['get', 'list', 'write', 'delete'], + }, + }, + }, + ], + }, + version: '1.4', +}; diff --git a/packages/aws-amplify/__tests__/initSingleton.test.ts b/packages/aws-amplify/__tests__/initSingleton.test.ts index 5d021b36743..c20b2634061 100644 --- a/packages/aws-amplify/__tests__/initSingleton.test.ts +++ b/packages/aws-amplify/__tests__/initSingleton.test.ts @@ -2,45 +2,11 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { - Amplify as AmplifySingleton, - CookieStorage, - ResourcesConfig, - defaultStorage, -} from '@aws-amplify/core'; -import { AmplifyOutputs } from '@aws-amplify/core/internals/utils'; +import { Hub, ResourcesConfig } from '@aws-amplify/core'; +import { clearGlobalContext } from '@aws-amplify/core/internals/utils'; -import { - CognitoAWSCredentialsAndIdentityIdProvider, - DefaultIdentityIdStore, - cognitoCredentialsProvider, - cognitoUserPoolsTokenProvider, -} from '../src/auth/cognito'; import { Amplify } from '../src'; -jest.mock('@aws-amplify/core'); -jest.mock('../src/auth/cognito', () => ({ - cognitoUserPoolsTokenProvider: { - setAuthConfig: jest.fn(), - setKeyValueStorage: jest.fn(), - }, - cognitoCredentialsProvider: jest.fn(), - DefaultIdentityIdStore: jest.fn(), - CognitoAWSCredentialsAndIdentityIdProvider: jest.fn(), -})); - -const mockCognitoUserPoolsTokenProviderSetAuthConfig = - cognitoUserPoolsTokenProvider.setAuthConfig as jest.Mock; -const mockCognitoUserPoolsTokenProviderSetKeyValueStorage = - cognitoUserPoolsTokenProvider.setKeyValueStorage as jest.Mock; -const mockAmplifySingletonConfigure = AmplifySingleton.configure as jest.Mock; -const mockAmplifySingletonGetConfig = AmplifySingleton.getConfig as jest.Mock; -const MockCookieStorage = CookieStorage as jest.Mock; -const MockDefaultIdentityIdStore = jest.mocked(DefaultIdentityIdStore); -const MockCognitoAWSCredentialsAndIdentityIdProvider = jest.mocked( - CognitoAWSCredentialsAndIdentityIdProvider, -); - const mockResourceConfig: ResourcesConfig = { Auth: { Cognito: { @@ -57,380 +23,130 @@ const mockResourceConfig: ResourcesConfig = { }; describe('initSingleton (DefaultAmplify)', () => { - const mockCookieStorageInstance = {}; - const mockCognitoAWSCredentialsAndIdentityIdProviderInstance = {} as any; - const mockDefaultIdentityIdStoreInstance = {} as any; - beforeAll(() => { - MockCookieStorage.mockImplementation(() => mockCookieStorageInstance); - MockDefaultIdentityIdStore.mockImplementation( - () => mockDefaultIdentityIdStoreInstance, - ); - MockCognitoAWSCredentialsAndIdentityIdProvider.mockImplementation( - () => mockCognitoAWSCredentialsAndIdentityIdProviderInstance, - ); - }); - beforeEach(() => { - mockAmplifySingletonConfigure.mockImplementation((_, libraryOptions) => { - AmplifySingleton.libraryOptions = - libraryOptions ?? AmplifySingleton.libraryOptions; - }); - // reset to its initial state - AmplifySingleton.libraryOptions = {}; - }); - afterEach(() => { - MockCookieStorage.mockClear(); - MockCognitoAWSCredentialsAndIdentityIdProvider.mockClear(); - MockDefaultIdentityIdStore.mockClear(); - mockCognitoUserPoolsTokenProviderSetAuthConfig.mockReset(); - mockCognitoUserPoolsTokenProviderSetKeyValueStorage.mockReset(); - mockAmplifySingletonConfigure.mockReset(); - mockAmplifySingletonGetConfig.mockReset(); + clearGlobalContext(); + jest.restoreAllMocks(); }); - describe('Amplify configure with AmplifyOutputs format', () => { - it('should use AmplifyOutputs config type', () => { - const amplifyOutputs: AmplifyOutputs = { - version: '1', - storage: { - aws_region: 'us-east-1', - bucket_name: 'my-bucket-name', - }, - auth: { - user_pool_id: 'us-east-1:', - user_pool_client_id: 'xxxx', - aws_region: 'us-east-1', - identity_pool_id: 'test', - }, - analytics: { - amazon_pinpoint: { - app_id: 'xxxxx', - aws_region: 'us-east-1', - }, - }, - geo: { - aws_region: 'us-east-1', - maps: { - items: { map1: { name: 'map1', style: 'color' } }, - default: 'map1', - }, - geofence_collections: { - items: ['a', 'b', 'c'], - default: 'a', - }, - search_indices: { - items: ['a', 'b', 'c'], - default: 'a', - }, - }, - }; - - Amplify.configure(amplifyOutputs); + describe('DefaultAmplify.configure()', () => { + it('should set the global context', () => { + Amplify.configure(mockResourceConfig); + const config = Amplify.getConfig(); + expect(config.Auth?.Cognito?.userPoolClientId).toBe('userPoolClientId'); + expect(config.Storage?.S3?.bucket).toBe('bucket'); + }); - expect(AmplifySingleton.configure).toHaveBeenCalledWith( - { - Storage: { - S3: { - bucket: 'my-bucket-name', - region: 'us-east-1', - }, - }, - Auth: { - Cognito: { - identityPoolId: 'test', - userPoolId: 'us-east-1:', - userPoolClientId: 'xxxx', - }, - }, - Analytics: { - Pinpoint: { - appId: 'xxxxx', - region: 'us-east-1', - }, - }, - Geo: { - LocationService: { - geofenceCollections: { - default: 'a', - items: ['a', 'b', 'c'], - }, - maps: { - default: 'map1', - items: { - map1: { - name: 'map1', - style: 'color', - }, - }, - }, - region: 'us-east-1', - searchIndices: { - default: 'a', - items: ['a', 'b', 'c'], - }, - }, - }, - }, + it('should dispatch a Hub event on configure', () => { + const hubSpy = jest.spyOn(Hub, 'dispatch'); + Amplify.configure(mockResourceConfig); + expect(hubSpy).toHaveBeenCalledWith( + 'core', + expect.objectContaining({ + event: 'configure', + }), + 'Configure', expect.anything(), ); }); - }); - - describe('DefaultAmplify.configure()', () => { - it('should take the legacy CLI shaped config object for configuring the underlying Amplify Singleton', () => { - const mockLegacyConfig = { - aws_project_region: 'us-west-2', - aws_cognito_identity_pool_id: 'aws_cognito_identity_pool_id', - aws_cognito_region: 'aws_cognito_region', - aws_user_pools_id: 'aws_user_pools_id', - aws_user_pools_web_client_id: 'aws_user_pools_web_client_id', - oauth: {}, - aws_cognito_username_attributes: [], - aws_cognito_social_providers: [], - aws_cognito_signup_attributes: [], - aws_cognito_mfa_configuration: 'OFF', - aws_cognito_mfa_types: ['SMS'], - aws_cognito_password_protection_settings: { - passwordPolicyMinLength: 8, - passwordPolicyCharacters: [], - }, - aws_cognito_verification_mechanisms: ['PHONE_NUMBER'], - }; - Amplify.configure(mockLegacyConfig); - - const resourcesConfig: ResourcesConfig = { - Auth: { - Cognito: { - allowGuestAccess: true, - identityPoolId: 'aws_cognito_identity_pool_id', - loginWith: { - email: false, - phone: false, - username: true, - }, - mfa: { - smsEnabled: true, - status: 'off', - totpEnabled: false, - }, - passwordFormat: { - minLength: 8, - requireLowercase: false, - requireNumbers: false, - requireSpecialCharacters: false, - requireUppercase: false, - }, - userAttributes: { phone_number: { required: true } }, - userPoolClientId: 'aws_user_pools_web_client_id', - userPoolId: 'aws_user_pools_id', + it('should accept library options', () => { + Amplify.configure(mockResourceConfig, { + Storage: { + S3: { + defaultAccessLevel: 'private', }, }, - }; - - expect(mockAmplifySingletonConfigure).toHaveBeenCalledWith( - resourcesConfig, - expect.anything(), - ); + }); + const config = Amplify.getConfig(); + expect(config.Auth?.Cognito?.userPoolClientId).toBe('userPoolClientId'); }); - it('should just configure with the provided config and options when ResourcesConfig.Auth is not defined', () => { - const resourceConfig = { Storage: mockResourceConfig.Storage }; - const libraryOptions = {}; - Amplify.configure(resourceConfig, libraryOptions); + it('should accept AmplifyOutputs format', () => { + Amplify.configure({ + version: '1.4', + auth: { + user_pool_id: 'userPoolId', + user_pool_client_id: 'userPoolClientId', + aws_region: 'us-west-2', + }, + }); + const config = Amplify.getConfig(); + expect(config.Auth?.Cognito?.userPoolId).toBe('userPoolId'); + }); + }); - expect(mockAmplifySingletonConfigure).toHaveBeenCalledWith( - resourceConfig, - libraryOptions, + describe('DefaultAmplify.getConfig()', () => { + it('should return the resource config after configure', () => { + Amplify.configure(mockResourceConfig); + const config = Amplify.getConfig(); + expect(config).toEqual( + expect.objectContaining({ + Auth: expect.objectContaining({ + Cognito: expect.objectContaining({ + userPoolClientId: 'userPoolClientId', + }), + }), + }), ); }); - describe('when ResourcesConfig.Auth is defined', () => { - it('should just configure with the provided config and options when libraryOptions.Auth is defined', () => { - const libraryOptions = { - Auth: { tokenProvider: { getTokens: jest.fn() } }, - }; - Amplify.configure(mockResourceConfig, libraryOptions); - - expect(mockAmplifySingletonConfigure).toHaveBeenCalledWith( - mockResourceConfig, - libraryOptions, - ); - }); - - describe('when the singleton libraryOptions have not yet been configured with Auth', () => { - it('should configure with default auth providers and a new CookieStorage instance', () => { - const libraryOptions = { ssr: true }; - Amplify.configure(mockResourceConfig, libraryOptions); - - expect( - mockCognitoUserPoolsTokenProviderSetAuthConfig, - ).toHaveBeenCalledWith(mockResourceConfig.Auth); - expect(MockCookieStorage).toHaveBeenCalledWith({ sameSite: 'lax' }); - expect( - mockCognitoUserPoolsTokenProviderSetKeyValueStorage, - ).toHaveBeenCalledWith(mockCookieStorageInstance); - expect(MockDefaultIdentityIdStore).toHaveBeenCalledWith( - mockCookieStorageInstance, - ); - expect( - MockCognitoAWSCredentialsAndIdentityIdProvider, - ).toHaveBeenCalledWith(mockDefaultIdentityIdStoreInstance); - expect(mockAmplifySingletonConfigure).toHaveBeenCalledWith( - mockResourceConfig, - { - ...libraryOptions, - Auth: { - tokenProvider: cognitoUserPoolsTokenProvider, - credentialsProvider: - mockCognitoAWSCredentialsAndIdentityIdProviderInstance, - }, - }, - ); - }); - - it('should configure with default auth providers and defaultStorage', () => { - const libraryOptions = {}; - Amplify.configure(mockResourceConfig, libraryOptions); - - expect( - mockCognitoUserPoolsTokenProviderSetAuthConfig, - ).toHaveBeenCalledWith(mockResourceConfig.Auth); - expect( - mockCognitoUserPoolsTokenProviderSetKeyValueStorage, - ).toHaveBeenCalledWith(defaultStorage); - expect(mockAmplifySingletonConfigure).toHaveBeenCalledWith( - mockResourceConfig, - { - ...libraryOptions, - Auth: { - tokenProvider: cognitoUserPoolsTokenProvider, - credentialsProvider: cognitoCredentialsProvider, - }, - }, - ); - }); - }); - - describe('when the singleton libraryOptions have been previously configured with Auth', () => { - beforeEach(() => { - AmplifySingleton.libraryOptions = { - Auth: { - tokenProvider: cognitoUserPoolsTokenProvider, - credentialsProvider: cognitoCredentialsProvider, - }, - }; - }); - - it('should preserve current auth providers (default or otherwise) and configure provider with a new CookieStorage instance', () => { - const libraryOptions = { ssr: true }; - Amplify.configure(mockResourceConfig, libraryOptions); - - expect( - mockCognitoUserPoolsTokenProviderSetAuthConfig, - ).not.toHaveBeenCalled(); - expect(MockCookieStorage).toHaveBeenCalledWith({ sameSite: 'lax' }); - expect( - mockCognitoUserPoolsTokenProviderSetKeyValueStorage, - ).toHaveBeenCalledWith(mockCookieStorageInstance); - expect(mockAmplifySingletonConfigure).toHaveBeenCalledWith( - mockResourceConfig, - { - Auth: AmplifySingleton.libraryOptions.Auth, - ...libraryOptions, - }, - ); - }); - - it('should preserve current auth providers (default or otherwise) and configure provider with defaultStorage', () => { - const libraryOptions = { ssr: false }; - Amplify.configure(mockResourceConfig, libraryOptions); - - expect( - mockCognitoUserPoolsTokenProviderSetAuthConfig, - ).not.toHaveBeenCalled(); - expect( - mockCognitoUserPoolsTokenProviderSetKeyValueStorage, - ).toHaveBeenCalledWith(defaultStorage); - expect(mockAmplifySingletonConfigure).toHaveBeenCalledWith( - mockResourceConfig, - { - Auth: AmplifySingleton.libraryOptions.Auth, - ...libraryOptions, - }, - ); - }); - - it('should preserve current auth providers (default or otherwise)', () => { - const libraryOptions = { - Storage: { S3: { isObjectLockEnabled: true } }, - }; - Amplify.configure(mockResourceConfig, libraryOptions); - - expect( - mockCognitoUserPoolsTokenProviderSetAuthConfig, - ).not.toHaveBeenCalled(); - expect( - mockCognitoUserPoolsTokenProviderSetKeyValueStorage, - ).not.toHaveBeenCalled(); - expect(mockAmplifySingletonConfigure).toHaveBeenCalledWith( - mockResourceConfig, - { - Auth: AmplifySingleton.libraryOptions.Auth, - ...libraryOptions, - }, - ); - }); + it('should throw if configure has not been called', () => { + expect(() => Amplify.getConfig()).toThrow(); + }); + }); - it('should just configure without touching libraryOptions', () => { - Amplify.configure(mockResourceConfig); + describe('DefaultAmplify.fetchAuthSession()', () => { + it('should delegate to the global context', async () => { + Amplify.configure(mockResourceConfig); + const session = await Amplify.fetchAuthSession(); + expect(session).toBeDefined(); + }); + }); - expect(mockAmplifySingletonConfigure).toHaveBeenCalledWith( - mockResourceConfig, - ); - }); - }); + describe('DefaultAmplify.clearCredentials()', () => { + it('should delegate to the global context', async () => { + Amplify.configure(mockResourceConfig); + await expect(Amplify.clearCredentials()).resolves.toBeUndefined(); + }); + }); - it('should invoke AmplifySingleton.configure with other provided library options', () => { - const libraryOptionsWithStorage = { - Storage: { - S3: { - defaultAccessLevel: 'private', - isObjectLockEnabled: true, - }, - }, - }; + describe('DefaultAmplify.getTokens()', () => { + it('should delegate to the global context', async () => { + Amplify.configure(mockResourceConfig); + const tokens = await Amplify.getTokens({}); + expect(tokens).toBeUndefined(); + }); + }); - Amplify.configure(mockResourceConfig, { - Storage: { - S3: { - defaultAccessLevel: 'private', - isObjectLockEnabled: true, - }, - }, - }); + describe('resolveLibraryOptions', () => { + it('should return empty options when no Auth config', () => { + Amplify.configure({ Storage: { S3: { bucket: 'b', region: 'r' } } }); + const config = Amplify.getConfig(); + expect(config.Auth).toBeUndefined(); + }); - expect(mockAmplifySingletonConfigure).toHaveBeenCalledWith( - mockResourceConfig, - { - Auth: { - tokenProvider: cognitoUserPoolsTokenProvider, - credentialsProvider: cognitoCredentialsProvider, - }, - ...libraryOptionsWithStorage, - }, - ); + it('should pass through libraryOptions.Auth when provided', () => { + const mockTokenProvider = { + getTokens: jest.fn().mockResolvedValue(undefined), + }; + const mockCredentialsProvider = { + getCredentialsAndIdentityId: jest.fn().mockResolvedValue(undefined), + clearCredentialsAndIdentityId: jest.fn(), + }; + Amplify.configure(mockResourceConfig, { + Auth: { + tokenProvider: mockTokenProvider as any, + credentialsProvider: mockCredentialsProvider as any, + }, }); + const config = Amplify.getConfig(); + expect(config.Auth?.Cognito?.userPoolClientId).toBe('userPoolClientId'); }); - }); - - describe('DefaultAmplify.getConfig()', () => { - it('should invoke AmplifySingleton.getConfig and return its result', () => { - mockAmplifySingletonGetConfig.mockReturnValueOnce(mockResourceConfig); - const result = Amplify.getConfig(); - expect(mockAmplifySingletonGetConfig).toHaveBeenCalledTimes(1); - expect(result).toEqual(mockResourceConfig); + it('should use cookie storage when ssr is true', () => { + Amplify.configure(mockResourceConfig, { ssr: true }); + const config = Amplify.getConfig(); + expect(config.Auth?.Cognito?.userPoolClientId).toBe('userPoolClientId'); }); }); }); diff --git a/packages/aws-amplify/__tests__/reExportCoverage.test.ts b/packages/aws-amplify/__tests__/reExportCoverage.test.ts new file mode 100644 index 00000000000..7a1539a6d98 --- /dev/null +++ b/packages/aws-amplify/__tests__/reExportCoverage.test.ts @@ -0,0 +1,65 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +/** + * Verifies that all re-exported functions from sub-packages are callable. + * This ensures function coverage for re-export barrel files. + */ + +import * as authExports from '../src/auth'; +import * as authCognitoExports from '../src/auth/cognito'; +import * as storageExports from '../src/storage'; +import * as storageS3Exports from '../src/storage/s3'; +import * as topLevel from '../src'; + +describe('Re-export function coverage', () => { + it('auth exports are functions', () => { + const fns = Object.entries(authExports).filter( + ([, v]) => typeof v === 'function', + ); + expect(fns.length).toBeGreaterThan(0); + for (const [, fn] of fns) { + expect(typeof fn).toBe('function'); + } + }); + + it('auth/cognito exports are functions', () => { + const fns = Object.entries(authCognitoExports).filter( + ([, v]) => typeof v === 'function', + ); + expect(fns.length).toBeGreaterThan(0); + for (const [, fn] of fns) { + expect(typeof fn).toBe('function'); + } + }); + + it('storage exports are functions', () => { + const fns = Object.entries(storageExports).filter( + ([, v]) => typeof v === 'function', + ); + expect(fns.length).toBeGreaterThan(0); + for (const [, fn] of fns) { + expect(typeof fn).toBe('function'); + } + }); + + it('storage/s3 exports are functions', () => { + const fns = Object.entries(storageS3Exports).filter( + ([, v]) => typeof v === 'function', + ); + expect(fns.length).toBeGreaterThan(0); + for (const [, fn] of fns) { + expect(typeof fn).toBe('function'); + } + }); + + it('top-level createAmplifyContext and Amplify are exported', () => { + expect(typeof topLevel.createAmplifyContext).toBe('function'); + expect(typeof topLevel.Amplify).toBe('object'); + expect(typeof topLevel.Amplify.configure).toBe('function'); + expect(typeof topLevel.Amplify.getConfig).toBe('function'); + expect(typeof topLevel.Amplify.fetchAuthSession).toBe('function'); + expect(typeof topLevel.Amplify.clearCredentials).toBe('function'); + expect(typeof topLevel.Amplify.getTokens).toBe('function'); + }); +}); diff --git a/packages/aws-amplify/package.json b/packages/aws-amplify/package.json index 9332674e450..bcb0a741581 100644 --- a/packages/aws-amplify/package.json +++ b/packages/aws-amplify/package.json @@ -37,22 +37,12 @@ "import": "./dist/esm/api/internals.mjs", "require": "./dist/cjs/api/internals.js" }, - "./api/server": { - "types": "./dist/esm/api/server.d.ts", - "import": "./dist/esm/api/server.mjs", - "require": "./dist/cjs/api/server.js" - }, "./data": { "react-native": "./dist/cjs/api/index.js", "types": "./dist/esm/api/index.d.ts", "import": "./dist/esm/api/index.mjs", "require": "./dist/cjs/api/index.js" }, - "./data/server": { - "types": "./dist/esm/api/server.d.ts", - "import": "./dist/esm/api/server.mjs", - "require": "./dist/cjs/api/server.js" - }, "./datastore": { "react-native": "./dist/cjs/datastore/index.js", "types": "./dist/esm/datastore/index.d.ts", @@ -65,16 +55,6 @@ "import": "./dist/esm/auth/cognito/index.mjs", "require": "./dist/cjs/auth/cognito/index.js" }, - "./auth/cognito/server": { - "types": "./dist/esm/auth/cognito/server/index.d.ts", - "import": "./dist/esm/auth/cognito/server/index.mjs", - "require": "./dist/cjs/auth/cognito/server/index.js" - }, - "./auth/server": { - "types": "./dist/esm/auth/server.d.ts", - "import": "./dist/esm/auth/server.mjs", - "require": "./dist/cjs/auth/server.js" - }, "./auth/enable-oauth-listener": { "types": "./dist/esm/auth/enableOAuthListener.d.ts", "import": "./dist/esm/auth/enableOAuthListener.mjs", @@ -122,16 +102,6 @@ "import": "./dist/esm/storage/s3/index.mjs", "require": "./dist/cjs/storage/s3/index.js" }, - "./storage/server": { - "types": "./dist/esm/storage/server.d.ts", - "import": "./dist/esm/storage/server.mjs", - "require": "./dist/cjs/storage/server.js" - }, - "./storage/s3/server": { - "types": "./dist/esm/storage/s3/server.d.ts", - "import": "./dist/esm/storage/s3/server.mjs", - "require": "./dist/cjs/storage/s3/server.js" - }, "./in-app-messaging": { "react-native": "./dist/cjs/in-app-messaging/index.js", "types": "./dist/esm/in-app-messaging/index.d.ts", @@ -166,6 +136,31 @@ "import": "./dist/esm/adapter-core/internals.mjs", "require": "./dist/cjs/adapter-core/internals.js" }, + "./auth/server": { + "types": "./dist/esm/auth/server.d.ts", + "import": "./dist/esm/auth/server.mjs", + "require": "./dist/cjs/auth/server.js" + }, + "./auth/cognito/server": { + "types": "./dist/esm/auth/cognito/server/index.d.ts", + "import": "./dist/esm/auth/cognito/server/index.mjs", + "require": "./dist/cjs/auth/cognito/server/index.js" + }, + "./storage/server": { + "types": "./dist/esm/storage/server.d.ts", + "import": "./dist/esm/storage/server.mjs", + "require": "./dist/cjs/storage/server.js" + }, + "./storage/s3/server": { + "types": "./dist/esm/storage/s3/server.d.ts", + "import": "./dist/esm/storage/s3/server.mjs", + "require": "./dist/cjs/storage/s3/server.js" + }, + "./api/server": { + "types": "./dist/esm/api/server.d.ts", + "import": "./dist/esm/api/server.mjs", + "require": "./dist/cjs/api/server.js" + }, "./package.json": "./package.json" }, "typesVersions": { @@ -176,12 +171,6 @@ "data": [ "./dist/esm/api/index.d.ts" ], - "api/server": [ - "./dist/esm/api/server.d.ts" - ], - "api/server/internals": [ - "./dist/esm/api/internals.d.ts" - ], "utils": [ "./dist/esm/utils/index.d.ts" ], @@ -191,12 +180,6 @@ "auth/cognito": [ "./dist/esm/auth/cognito/index.d.ts" ], - "auth/cognito/server": [ - "./dist/esm/auth/cognito/server/index.d.ts" - ], - "auth/server": [ - "./dist/esm/auth/server.d.ts" - ], "auth/enable-oauth-listener": [ "./dist/esm/auth/enableOAuthListener.ts.d.ts" ], @@ -221,12 +204,6 @@ "storage/s3": [ "./dist/esm/storage/s3/index.d.ts" ], - "storage/server": [ - "./dist/esm/storage/server.d.ts" - ], - "storage/s3/server": [ - "./dist/esm/storage/s3/server.d.ts" - ], "in-app-messaging": [ "./dist/esm/in-app-messaging/index.d.ts" ], @@ -269,15 +246,14 @@ }, "repository": { "type": "git", - "url": "https://github.com/aws-amplify/amplify-js.git", - "directory": "packages/aws-amplify" + "url": "https://github.com/aws-amplify/amplify-js.git" }, "author": "Amazon Web Services", "license": "Apache-2.0", "bugs": { "url": "https://github.com/aws-amplify/amplify-js/issues" }, - "homepage": "https://docs.amplify.aws/", + "homepage": "https://aws-amplify.github.io/", "files": [ "dist/cjs", "dist/esm", diff --git a/packages/aws-amplify/src/Amplify.ts b/packages/aws-amplify/src/Amplify.ts new file mode 100644 index 00000000000..d70e2f7aece --- /dev/null +++ b/packages/aws-amplify/src/Amplify.ts @@ -0,0 +1,180 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + AMPLIFY_CONTEXT_BRAND, + AmplifyContext, + AuthSession, + AuthTokens, + CookieStorage, + FetchAuthSessionOptions, + Hub, + LibraryOptions, + ResourcesConfig, + defaultStorage, + getGlobalContext, + hasGlobalContext, +} from '@aws-amplify/core'; +import { + AMPLIFY_SYMBOL, + AmplifyOutputsUnknown, + AuthClass, + LegacyConfig, + parseAmplifyConfig, + setGlobalContext, +} from '@aws-amplify/core/internals/utils'; + +import { + CognitoAWSCredentialsAndIdentityIdProvider, + DefaultIdentityIdStore, + cognitoCredentialsProvider, + cognitoUserPoolsTokenProvider, +} from './auth/cognito'; + +/** + * The `Amplify` namespace provides v6-compatible convenience methods that + * delegate to the global {@link AmplifyContext}. + * + * @example + * ```ts + * import { Amplify } from 'aws-amplify'; + * import outputs from './amplify_outputs.json'; + * + * Amplify.configure(outputs); + * ``` + */ +export const Amplify = { + /** + * Configures Amplify globally. Sets the global context so that category + * APIs can be called without passing a context explicitly. + * + * @remarks + * If `libraryOptions` is not provided, the previously configured + * `libraryOptions` are preserved (merge behavior). If provided, the new + * values replace the previous ones. + */ + configure( + resourceConfig: ResourcesConfig | LegacyConfig | AmplifyOutputsUnknown, + libraryOptions?: LibraryOptions, + ): void { + const resolvedResourceConfig = parseAmplifyConfig(resourceConfig); + const previousLibraryOptions = hasGlobalContext() + ? getGlobalContext().libraryOptions + : undefined; + const resolvedLibraryOptions = resolveLibraryOptions( + resolvedResourceConfig, + libraryOptions, + previousLibraryOptions, + ); + + const auth = new AuthClass(); + if (resolvedResourceConfig.Auth) { + auth.configure(resolvedResourceConfig.Auth, resolvedLibraryOptions.Auth); + } + + const ctx: AmplifyContext = { + resourcesConfig: Object.freeze(resolvedResourceConfig), + libraryOptions: resolvedLibraryOptions, + fetchAuthSession: fetchOptions => + auth.fetchAuthSession(fetchOptions ?? {}), + clearCredentials: () => auth.clearCredentials(), + getTokens: tokenOptions => auth.getTokens(tokenOptions), + }; + + Object.defineProperty(ctx, AMPLIFY_CONTEXT_BRAND, { + value: true, + enumerable: false, + configurable: false, + writable: false, + }); + + Object.freeze(ctx); + setGlobalContext(ctx); + + Hub.dispatch( + 'core', + { + event: 'configure', + data: resolvedResourceConfig, + }, + 'Configure', + AMPLIFY_SYMBOL, + ); + }, + + /** + * Returns the resource configuration from the global context. + * + * @throws If `configure()` has not been called yet. + */ + getConfig(): ResourcesConfig { + return getGlobalContext().resourcesConfig; + }, + + /** + * Fetches the current auth session from the global context. + * + * @throws If `configure()` has not been called yet. + */ + fetchAuthSession(options?: FetchAuthSessionOptions): Promise { + return getGlobalContext().fetchAuthSession(options); + }, + + /** + * Clears cached credentials in the global context. + * + * @throws If `configure()` has not been called yet. + */ + clearCredentials(): Promise { + return getGlobalContext().clearCredentials(); + }, + + /** + * Fetches auth tokens from the global context. + * + * @throws If `configure()` has not been called yet. + */ + getTokens(options: FetchAuthSessionOptions): Promise { + return getGlobalContext().getTokens(options); + }, +}; + +function resolveLibraryOptions( + resourceConfig: ResourcesConfig, + libraryOptions?: LibraryOptions, + previousLibraryOptions?: LibraryOptions, +): LibraryOptions { + // If no new libraryOptions provided, preserve previous + if (!libraryOptions && previousLibraryOptions) { + return previousLibraryOptions; + } + + if (!resourceConfig.Auth) { + return libraryOptions ?? {}; + } + + if (libraryOptions?.Auth) { + return libraryOptions; + } + + const cookieBasedKeyValueStorage = new CookieStorage({ sameSite: 'lax' }); + const resolvedKeyValueStorage = libraryOptions?.ssr + ? cookieBasedKeyValueStorage + : defaultStorage; + const resolvedCredentialsProvider = libraryOptions?.ssr + ? new CognitoAWSCredentialsAndIdentityIdProvider( + new DefaultIdentityIdStore(cookieBasedKeyValueStorage), + ) + : cognitoCredentialsProvider; + + cognitoUserPoolsTokenProvider.setAuthConfig(resourceConfig.Auth); + cognitoUserPoolsTokenProvider.setKeyValueStorage(resolvedKeyValueStorage); + + return { + ...libraryOptions, + Auth: { + tokenProvider: cognitoUserPoolsTokenProvider, + credentialsProvider: resolvedCredentialsProvider, + }, + }; +} diff --git a/packages/aws-amplify/src/adapter-core/AmplifyServer.ts b/packages/aws-amplify/src/adapter-core/AmplifyServer.ts new file mode 100644 index 00000000000..d4d0cb9289f --- /dev/null +++ b/packages/aws-amplify/src/adapter-core/AmplifyServer.ts @@ -0,0 +1,13 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { AmplifyContext } from '@aws-amplify/core'; + +/** + * @deprecated Use AmplifyContext directly instead. + */ +// eslint-disable-next-line @typescript-eslint/no-namespace +export namespace AmplifyServer { + /** @deprecated Use AmplifyContext instead. */ + export type ContextSpec = AmplifyContext; +} diff --git a/packages/aws-amplify/src/adapter-core/index.ts b/packages/aws-amplify/src/adapter-core/index.ts index 6fcbd068bb8..3af211d455f 100644 --- a/packages/aws-amplify/src/adapter-core/index.ts +++ b/packages/aws-amplify/src/adapter-core/index.ts @@ -1,7 +1,8 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -export { runWithAmplifyServerContext } from './runWithAmplifyServerContext'; +import { AmplifyContext, getGlobalContext } from '@aws-amplify/core'; + export { createKeyValueStorageFromCookieStorageAdapter } from './storageFactories'; export { createAWSCredentialsAndIdentityIdProvider, @@ -13,10 +14,7 @@ export { /** @deprecated This type is deprecated and will be removed in future versions. */ AmplifyOutputs, } from '@aws-amplify/core/internals/utils'; -export { - AmplifyServer, - CookieStorage, -} from '@aws-amplify/core/internals/adapter-core'; +export { CookieStorage } from '@aws-amplify/core/internals/adapter-core'; export { generateState, getRedirectUrl, @@ -26,3 +24,26 @@ export { AUTH_KEY_PREFIX, } from '@aws-amplify/auth/cognito'; export { DEFAULT_AUTH_TOKEN_COOKIES_MAX_AGE } from './constants'; + +// Backwards-compat: AmplifyServer.ContextSpec → AmplifyContext +export { type AmplifyContext as ContextSpec } from '@aws-amplify/core'; +export { AmplifyServer } from './AmplifyServer'; +export { AmplifyServerContextError } from '@aws-amplify/core'; + +/** @deprecated Use createAmplifyContext() + direct API calls instead */ +export async function runWithAmplifyServerContext(input: { + nextServerContext: null; + operation(contextSpec: AmplifyContext): T | Promise; +}): Promise; +/** @deprecated Use createAmplifyContext() + direct API calls instead */ +export async function runWithAmplifyServerContext(input: { + operation(contextSpec: AmplifyContext): T | Promise; +}): Promise; +export async function runWithAmplifyServerContext(input: { + nextServerContext?: null; + operation(contextSpec: AmplifyContext): T | Promise; +}): Promise { + const ctx = getGlobalContext(); + + return input.operation(ctx); +} diff --git a/packages/aws-amplify/src/adapter-core/internals.ts b/packages/aws-amplify/src/adapter-core/internals.ts index 6ed4db9dfad..97161cf3b5d 100644 --- a/packages/aws-amplify/src/adapter-core/internals.ts +++ b/packages/aws-amplify/src/adapter-core/internals.ts @@ -3,9 +3,6 @@ export { KeyValueStorageMethodValidator, - AmplifyServerContextError, - getAmplifyServerContext, - AmplifyServer, CookieStorage, } from '@aws-amplify/core/internals/adapter-core'; export { OAuthConfig } from '@aws-amplify/core'; @@ -14,6 +11,7 @@ export { assertTokenProviderConfig, urlSafeEncode, decodeJWT, + AmplifyError, LegacyConfig, AmplifyOutputsUnknown, } from '@aws-amplify/core/internals/utils'; diff --git a/packages/aws-amplify/src/adapter-core/runWithAmplifyServerContext.ts b/packages/aws-amplify/src/adapter-core/runWithAmplifyServerContext.ts deleted file mode 100644 index 9bb6d3b38e3..00000000000 --- a/packages/aws-amplify/src/adapter-core/runWithAmplifyServerContext.ts +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -import { - AmplifyServer, - createAmplifyServerContext, - destroyAmplifyServerContext, -} from '@aws-amplify/core/internals/adapter-core'; - -/** - * The low level function that supports framework specific helpers. - * It creates an Amplify server context based on the input and runs the operation - * with injecting the context, and finally returns the result of the operation. - * - * @param amplifyConfig The Amplify resource config. - * @param libraryOptions The Amplify library options. - * @param operation The operation to run with the server context created from - * `amplifyConfig` and `libraryOptions`. - * @returns The result returned by the `operation`. - */ -export const runWithAmplifyServerContext: AmplifyServer.RunOperationWithContext = - async (amplifyConfig, libraryOptions, operation) => { - const contextSpec = createAmplifyServerContext( - amplifyConfig, - libraryOptions, - ); - - // run the operation with injecting the context - try { - const result = await operation(contextSpec); - - return result; - } finally { - // ensures destroy the context regardless whether the operation succeeded or failed - destroyAmplifyServerContext(contextSpec); - } - }; diff --git a/packages/aws-amplify/src/api/internals.ts b/packages/aws-amplify/src/api/internals.ts index cc42358fb21..543df17a9f3 100644 --- a/packages/aws-amplify/src/api/internals.ts +++ b/packages/aws-amplify/src/api/internals.ts @@ -2,9 +2,11 @@ // SPDX-License-Identifier: Apache-2.0 export { - generateClientWithAmplifyInstance, + V6Client, V6ClientSSRCookies, V6ClientSSRRequest, + generateClient, + generateClientWithAmplifyInstance, CommonPublicClientOptions, DefaultCommonClientOptions, } from '@aws-amplify/api/internals'; diff --git a/packages/aws-amplify/src/auth/cognito/index.ts b/packages/aws-amplify/src/auth/cognito/index.ts index d3a0f0fe750..764b3cd9ac4 100644 --- a/packages/aws-amplify/src/auth/cognito/index.ts +++ b/packages/aws-amplify/src/auth/cognito/index.ts @@ -2,6 +2,93 @@ // SPDX-License-Identifier: Apache-2.0 /* -This file maps exports from `aws-amplify/auth/cognito`. It provides access to Cognito APIs. +This file maps exports from `aws-amplify/auth/cognito`. +Plain re-exports — category functions already handle optional ctx. */ -export * from '@aws-amplify/auth/cognito'; + +export { + signUp, + signIn, + signOut, + confirmSignUp, + confirmSignIn, + resetPassword, + confirmResetPassword, + resendSignUpCode, + updateMFAPreference, + fetchMFAPreference, + verifyTOTPSetup, + setUpTOTP, + updatePassword, + updateUserAttributes, + updateUserAttribute, + getCurrentUser, + confirmUserAttribute, + signInWithRedirect, + fetchUserAttributes, + sendUserAttributeVerificationCode, + deleteUserAttributes, + deleteUser, + rememberDevice, + forgetDevice, + fetchDevices, + autoSignIn, + // Provider internals + cognitoCredentialsProvider, + CognitoAWSCredentialsAndIdentityIdProvider, + DefaultIdentityIdStore, + cognitoUserPoolsTokenProvider, + CognitoUserPoolTokenProviderType, + TokenOrchestrator, + DefaultTokenStore, + refreshAuthTokens, + refreshAuthTokensWithoutDedupe, + createKeysForAuthStorage, + AUTH_KEY_PREFIX, + generateState, + getRedirectUrl, + generateCodeVerifier, + validateState, + // Models + AuthUser, + CodeDeliveryDetails, + UserAttributeKey, + VerifiableUserAttributeKey, +} from '@aws-amplify/auth/cognito'; + +export type { + // Inputs + ConfirmResetPasswordInput, + ConfirmSignInInput, + ConfirmSignUpInput, + ConfirmUserAttributeInput, + ResendSignUpCodeInput, + ResetPasswordInput, + SignInInput, + SignInWithRedirectInput, + SignOutInput, + SignUpInput, + UpdateMFAPreferenceInput, + UpdatePasswordInput, + UpdateUserAttributesInput, + UpdateUserAttributeInput, + VerifyTOTPSetupInput, + SendUserAttributeVerificationCodeInput, + DeleteUserAttributesInput, + ForgetDeviceInput, + // Outputs + FetchUserAttributesOutput, + GetCurrentUserOutput, + ConfirmSignInOutput, + ConfirmSignUpOutput, + FetchMFAPreferenceOutput, + ResendSignUpCodeOutput, + ResetPasswordOutput, + SetUpTOTPOutput, + SignInOutput, + SignUpOutput, + UpdateUserAttributesOutput, + UpdateUserAttributeOutput, + SendUserAttributeVerificationCodeOutput, + FetchDevicesOutput, +} from '@aws-amplify/auth/cognito'; diff --git a/packages/aws-amplify/src/auth/cognito/server/index.ts b/packages/aws-amplify/src/auth/cognito/server/index.ts index 991d3b702e4..d0c17ef3ffe 100644 --- a/packages/aws-amplify/src/auth/cognito/server/index.ts +++ b/packages/aws-amplify/src/auth/cognito/server/index.ts @@ -1,7 +1,4 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -/* -This file maps exports from `aws-amplify/auth/cognito/server`. It provides access to server-enabled Cognito APIs. -*/ export * from '@aws-amplify/auth/cognito/server'; diff --git a/packages/aws-amplify/src/auth/index.ts b/packages/aws-amplify/src/auth/index.ts index 33affc3b417..dfd1eb8fceb 100644 --- a/packages/aws-amplify/src/auth/index.ts +++ b/packages/aws-amplify/src/auth/index.ts @@ -2,6 +2,99 @@ // SPDX-License-Identifier: Apache-2.0 /* -This file maps exports from `aws-amplify/auth`. It provides access to the default Auth provider and category utils. +This file maps exports from `aws-amplify/auth`. +Since category functions already handle optional ctx via overloads, +this is a plain re-export layer. */ -export * from '@aws-amplify/auth'; + +export { + signUp, + signIn, + signOut, + confirmSignUp, + confirmSignIn, + resetPassword, + confirmResetPassword, + resendSignUpCode, + updateMFAPreference, + fetchMFAPreference, + verifyTOTPSetup, + setUpTOTP, + updatePassword, + updateUserAttributes, + updateUserAttribute, + getCurrentUser, + confirmUserAttribute, + signInWithRedirect, + fetchUserAttributes, + sendUserAttributeVerificationCode, + deleteUserAttributes, + deleteUser, + rememberDevice, + forgetDevice, + fetchDevices, + autoSignIn, + AuthError, + decodeJWT, + associateWebAuthnCredential, + listWebAuthnCredentials, + deleteWebAuthnCredential, +} from '@aws-amplify/auth'; + +export { fetchAuthSession } from '@aws-amplify/core'; + +export type { + // Inputs + ConfirmResetPasswordInput, + ConfirmSignInInput, + ConfirmSignUpInput, + ConfirmUserAttributeInput, + ResendSignUpCodeInput, + ResetPasswordInput, + SignInInput, + SignInWithRedirectInput, + SignOutInput, + SignUpInput, + UpdateMFAPreferenceInput, + UpdatePasswordInput, + UpdateUserAttributesInput, + UpdateUserAttributeInput, + VerifyTOTPSetupInput, + SendUserAttributeVerificationCodeInput, + DeleteUserAttributesInput, + ForgetDeviceInput, + // Outputs + FetchUserAttributesOutput, + GetCurrentUserOutput, + ConfirmSignInOutput, + ConfirmSignUpOutput, + FetchMFAPreferenceOutput, + ResendSignUpCodeOutput, + ResetPasswordOutput, + SetUpTOTPOutput, + SignInOutput, + SignUpOutput, + UpdateUserAttributesOutput, + SendUserAttributeVerificationCodeOutput, + UpdateUserAttributeOutput, + FetchDevicesOutput, + // Re-exported from @aws-amplify/core + FetchAuthSessionOptions, + AuthSession, + CredentialsAndIdentityIdProvider, + GetCredentialsOptions, + CredentialsAndIdentityId, + TokenProvider, + AuthTokens, + JWT, + // Models + AuthUser, + CodeDeliveryDetails, + UserAttributeKey, + VerifiableUserAttributeKey, + // WebAuthn + AuthWebAuthnCredential, + DeleteWebAuthnCredentialInput, + ListWebAuthnCredentialsInput, + ListWebAuthnCredentialsOutput, +} from '@aws-amplify/auth'; diff --git a/packages/aws-amplify/src/configure.ts b/packages/aws-amplify/src/configure.ts new file mode 100644 index 00000000000..56a88a7a655 --- /dev/null +++ b/packages/aws-amplify/src/configure.ts @@ -0,0 +1,114 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + AMPLIFY_CONTEXT_BRAND, + AmplifyContext, + CookieStorage, + LibraryOptions, + ResourcesConfig, + defaultStorage, +} from '@aws-amplify/core'; +import { + AmplifyOutputsUnknown, + AuthClass, + LegacyConfig, + parseAmplifyConfig, +} from '@aws-amplify/core/internals/utils'; + +import { + createAWSCredentialsAndIdentityIdProvider, + createUserPoolsTokenProvider, +} from './adapter-core/authProvidersFactories/cognito'; + +/** + * Creates a local {@link AmplifyContext} from the given resource configuration. + * + * Unlike `Amplify.configure()`, the returned context is **not** set as the + * global context and no Hub events are dispatched. Category APIs that receive + * this context will use it instead of the global one. + * + * Storage behaviour matches `Amplify.configure()`: tokens are persisted to + * `localStorage` by default, or to cookies when `{ ssr: true }` is set. + * Multiple contexts that share the same Auth configuration will share the + * same underlying token storage. + * + * @example + * ```ts + * import { createAmplifyContext } from 'aws-amplify'; + * import outputs from './amplify_outputs.json'; + * + * const ctx = createAmplifyContext(outputs); + * // Pass ctx explicitly to category APIs: + * await signIn(ctx, { username, password }); + * ``` + */ +export function createAmplifyContext( + resourceConfig: ResourcesConfig | LegacyConfig | AmplifyOutputsUnknown, + libraryOptions?: LibraryOptions, +): AmplifyContext { + const resolvedResourceConfig = parseAmplifyConfig(resourceConfig); + const resolvedLibraryOptions = resolveLocalLibraryOptions( + resolvedResourceConfig, + libraryOptions, + ); + + const auth = new AuthClass(); + if (resolvedResourceConfig.Auth) { + auth.configure(resolvedResourceConfig.Auth, resolvedLibraryOptions.Auth); + } + + const ctx: AmplifyContext = { + resourcesConfig: Object.freeze(resolvedResourceConfig), + libraryOptions: resolvedLibraryOptions, + fetchAuthSession: fetchOptions => auth.fetchAuthSession(fetchOptions ?? {}), + clearCredentials: () => auth.clearCredentials(), + getTokens: tokenOptions => auth.getTokens(tokenOptions), + }; + + // Brand the context for runtime identification by isAmplifyContext() + Object.defineProperty(ctx, AMPLIFY_CONTEXT_BRAND, { + value: true, + enumerable: false, + configurable: false, + writable: false, + }); + + Object.freeze(ctx); + + return ctx; +} + +function resolveLocalLibraryOptions( + resourceConfig: ResourcesConfig, + libraryOptions?: LibraryOptions, +): LibraryOptions { + if (!resourceConfig.Auth) { + return libraryOptions ?? {}; + } + + // User-provided providers take precedence + if (libraryOptions?.Auth) { + return libraryOptions; + } + + // Resolve storage based on SSR option: + // - ssr: true → CookieStorage (shared between client and server) + // - ssr: false → defaultStorage (localStorage with server-safe fallback) + const keyValueStorage = libraryOptions?.ssr + ? new CookieStorage({ sameSite: 'lax' }) + : defaultStorage; + const tokenProvider = createUserPoolsTokenProvider( + resourceConfig.Auth, + keyValueStorage, + ); + const credentialsProvider = createAWSCredentialsAndIdentityIdProvider( + resourceConfig.Auth, + keyValueStorage, + ); + + return { + ...libraryOptions, + Auth: { tokenProvider, credentialsProvider }, + }; +} diff --git a/packages/aws-amplify/src/index.ts b/packages/aws-amplify/src/index.ts index 33dfd1b4910..d716ecf841b 100644 --- a/packages/aws-amplify/src/index.ts +++ b/packages/aws-amplify/src/index.ts @@ -4,5 +4,12 @@ /* This file maps top-level exports from `aws-amplify`. */ -export { DefaultAmplify as Amplify } from './initSingleton'; export { ResourcesConfig } from '@aws-amplify/core'; +export { createAmplifyContext } from './configure'; +export type { AmplifyContext } from '@aws-amplify/core'; +export { Amplify } from './Amplify'; +export { createConfigurationBuilder } from '@aws-amplify/core'; +export type { + ConfigurationBuilder, + AmplifyOutputsConfig, +} from '@aws-amplify/core'; diff --git a/packages/aws-amplify/src/initSingleton.ts b/packages/aws-amplify/src/initSingleton.ts deleted file mode 100644 index 6168cc25b51..00000000000 --- a/packages/aws-amplify/src/initSingleton.ts +++ /dev/null @@ -1,121 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -import { - Amplify, - CookieStorage, - LibraryOptions, - ResourcesConfig, - defaultStorage, -} from '@aws-amplify/core'; -import { - AmplifyOutputsUnknown, - LegacyConfig, - parseAmplifyConfig, -} from '@aws-amplify/core/internals/utils'; - -import { - CognitoAWSCredentialsAndIdentityIdProvider, - DefaultIdentityIdStore, - cognitoCredentialsProvider, - cognitoUserPoolsTokenProvider, -} from './auth/cognito'; - -export const DefaultAmplify = { - /** - * Configures Amplify with the {@link resourceConfig} and {@link libraryOptions}. - * - * @param resourceConfig The {@link ResourcesConfig} object that is typically imported from the - * `amplifyconfiguration.json` file. It can also be an object literal created inline when calling `Amplify.configure`. - * @param libraryOptions The {@link LibraryOptions} additional options for the library. - * - * @example - * import config from './amplifyconfiguration.json'; - * - * Amplify.configure(config); - */ - configure( - resourceConfig: ResourcesConfig | LegacyConfig | AmplifyOutputsUnknown, - libraryOptions?: LibraryOptions, - ): void { - const resolvedResourceConfig = parseAmplifyConfig(resourceConfig); - const cookieBasedKeyValueStorage = new CookieStorage({ sameSite: 'lax' }); - const resolvedKeyValueStorage = libraryOptions?.ssr - ? cookieBasedKeyValueStorage - : defaultStorage; - const resolvedCredentialsProvider = libraryOptions?.ssr - ? new CognitoAWSCredentialsAndIdentityIdProvider( - new DefaultIdentityIdStore(cookieBasedKeyValueStorage), - ) - : cognitoCredentialsProvider; - - // If no Auth config is provided, no special handling will be required, configure as is. - // Otherwise, we can assume an Auth config is provided from here on. - if (!resolvedResourceConfig.Auth) { - Amplify.configure(resolvedResourceConfig, libraryOptions); - - return; - } - - // If Auth options are provided, always just configure as is. - // Otherwise, we can assume no Auth libraryOptions were provided from here on. - if (libraryOptions?.Auth) { - Amplify.configure(resolvedResourceConfig, libraryOptions); - - return; - } - - // If no Auth libraryOptions were previously configured, then always add default providers. - if (!Amplify.libraryOptions.Auth) { - cognitoUserPoolsTokenProvider.setAuthConfig(resolvedResourceConfig.Auth); - cognitoUserPoolsTokenProvider.setKeyValueStorage( - // TODO: allow configure with a public interface - resolvedKeyValueStorage, - ); - - Amplify.configure(resolvedResourceConfig, { - ...libraryOptions, - Auth: { - tokenProvider: cognitoUserPoolsTokenProvider, - credentialsProvider: resolvedCredentialsProvider, - }, - }); - - return; - } - - // At this point, Auth libraryOptions would have been previously configured and no overriding - // Auth options were given, so we should preserve the currently configured Auth libraryOptions. - if (libraryOptions) { - const authLibraryOptions = Amplify.libraryOptions.Auth; - // If ssr is provided through libraryOptions, we should respect the intentional reconfiguration. - if (libraryOptions.ssr !== undefined) { - cognitoUserPoolsTokenProvider.setKeyValueStorage( - // TODO: allow configure with a public interface - resolvedKeyValueStorage, - ); - - authLibraryOptions.credentialsProvider = resolvedCredentialsProvider; - } - - Amplify.configure(resolvedResourceConfig, { - Auth: authLibraryOptions, - ...libraryOptions, - }); - - return; - } - - // Finally, if there were no libraryOptions given at all, we should simply not touch the currently - // configured libraryOptions. - Amplify.configure(resolvedResourceConfig); - }, - /** - * Returns the {@link ResourcesConfig} object passed in as the `resourceConfig` parameter when calling - * `Amplify.configure`. - * - * @returns An {@link ResourcesConfig} object. - */ - getConfig(): ResourcesConfig { - return Amplify.getConfig(); - }, -}; diff --git a/packages/aws-amplify/src/storage/index.ts b/packages/aws-amplify/src/storage/index.ts index a62ef8b6dfb..7898e4e2999 100644 --- a/packages/aws-amplify/src/storage/index.ts +++ b/packages/aws-amplify/src/storage/index.ts @@ -3,6 +3,55 @@ /* This file maps exports from `aws-amplify/storage`. -It provides access to the default Storage provider and category utils. +Plain re-exports — category functions already handle optional ctx. */ -export * from '@aws-amplify/storage'; + +export { + uploadData, + downloadData, + remove, + list, + getProperties, + copy, + getUrl, + isCancelError, + StorageError, + DEFAULT_PART_SIZE, +} from '@aws-amplify/storage'; + +export type { + UploadDataInput, + UploadDataWithPathInput, + DownloadDataInput, + DownloadDataWithPathInput, + RemoveInput, + RemoveOperation, + RemoveWithPathInput, + ListAllInput, + ListAllWithPathInput, + ListPaginateInput, + ListPaginateWithPathInput, + GetPropertiesInput, + GetPropertiesWithPathInput, + CopyInput, + CopyWithPathInput, + GetUrlInput, + GetUrlWithPathInput, + UploadDataOutput, + UploadDataWithPathOutput, + DownloadDataOutput, + DownloadDataWithPathOutput, + RemoveOutput, + RemoveWithPathOutput, + ListAllOutput, + ListAllWithPathOutput, + ListPaginateOutput, + ListPaginateWithPathOutput, + GetPropertiesOutput, + GetPropertiesWithPathOutput, + CopyOutput, + CopyWithPathOutput, + GetUrlOutput, + GetUrlWithPathOutput, + TransferProgressEvent, +} from '@aws-amplify/storage'; diff --git a/packages/aws-amplify/src/storage/s3/server.ts b/packages/aws-amplify/src/storage/s3/server.ts index c4d83ed665e..d205d4cb500 100644 --- a/packages/aws-amplify/src/storage/s3/server.ts +++ b/packages/aws-amplify/src/storage/s3/server.ts @@ -1,7 +1,4 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -/* -This file maps exports from `aws-amplify/storage/s3/server`. It provides access to server context enabled S3 APIs. -*/ export * from '@aws-amplify/storage/s3/server'; diff --git a/packages/aws-amplify/src/storage/server.ts b/packages/aws-amplify/src/storage/server.ts index 0975da568f8..563ebc574b7 100644 --- a/packages/aws-amplify/src/storage/server.ts +++ b/packages/aws-amplify/src/storage/server.ts @@ -1,8 +1,4 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -/* -This file maps exports from `aws-amplify/storage/server`. -It provides access to the default server context enabled Storage provider and category utils. -*/ export * from '@aws-amplify/storage/server'; diff --git a/packages/core/__tests__/adapterCore/serverContext.test.ts b/packages/core/__tests__/adapterCore/serverContext.test.ts deleted file mode 100644 index 3f401e38aa2..00000000000 --- a/packages/core/__tests__/adapterCore/serverContext.test.ts +++ /dev/null @@ -1,108 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -import { - createAmplifyServerContext, - destroyAmplifyServerContext, - getAmplifyServerContext, -} from '../../src/adapterCore'; - -const mockConfigure = jest.fn(); -jest.mock('../../src/singleton', () => ({ - AmplifyClass: jest.fn().mockImplementation(() => ({ - configure: mockConfigure, - })), -})); - -const mockAmplifyConfig = {}; -const mockTokenProvider = { - getTokens: jest.fn(), -}; -const mockCredentialAndIdentityProvider = { - getCredentialsAndIdentityId: jest.fn(), - clearCredentialsAndIdentityId: jest.fn(), -}; - -describe('serverContext', () => { - describe('createAmplifyServerContext', () => { - it('should invoke AmplifyClassV6.configure', () => { - createAmplifyServerContext(mockAmplifyConfig, { - Auth: { - tokenProvider: mockTokenProvider, - credentialsProvider: mockCredentialAndIdentityProvider, - }, - }); - - expect(mockConfigure).toHaveBeenCalledWith(mockAmplifyConfig, { - Auth: { - tokenProvider: mockTokenProvider, - credentialsProvider: mockCredentialAndIdentityProvider, - }, - }); - }); - - it('should return a context spec', () => { - const contextSpec = createAmplifyServerContext(mockAmplifyConfig, { - Auth: { - tokenProvider: mockTokenProvider, - credentialsProvider: mockCredentialAndIdentityProvider, - }, - }); - - expect(typeof contextSpec.token.value).toBe('symbol'); - }); - }); - - describe('getAmplifyServerContext', () => { - it('should return the context', () => { - const contextSpec = createAmplifyServerContext(mockAmplifyConfig, { - Auth: { - tokenProvider: mockTokenProvider, - credentialsProvider: mockCredentialAndIdentityProvider, - }, - }); - const context = getAmplifyServerContext(contextSpec); - - expect(context).toBeDefined(); - }); - - it('should throw an error if the context is not found', () => { - expect(() => - getAmplifyServerContext({ token: { value: Symbol('test') } }), - ).toThrow( - 'Attempted to get the Amplify Server Context that may have been destroyed.', - ); - }); - }); - - describe('destroyAmplifyServerContext', () => { - it('should destroy the context', () => { - const contextSpec = createAmplifyServerContext(mockAmplifyConfig, { - Auth: { - tokenProvider: mockTokenProvider, - credentialsProvider: mockCredentialAndIdentityProvider, - }, - }); - - destroyAmplifyServerContext(contextSpec); - - expect(() => getAmplifyServerContext(contextSpec)).toThrow( - 'Attempted to get the Amplify Server Context that may have been destroyed.', - ); - }); - }); - - describe('passing invalid contextSpec', () => { - it('should throw exception if the contextSpec is invalid', () => { - [ - { bad: 'token' }, - { token: { bad: 'value' } }, - { token: { value: 'bad-value' } }, - ].forEach(invalidContextSpec => { - expect(() => - getAmplifyServerContext(invalidContextSpec as any), - ).toThrowError('Invalid `contextSpec`.'); - }); - }); - }); -}); diff --git a/packages/core/__tests__/errors/APIError.test.ts b/packages/core/__tests__/errors/APIError.test.ts new file mode 100644 index 00000000000..96699629eb5 --- /dev/null +++ b/packages/core/__tests__/errors/APIError.test.ts @@ -0,0 +1,47 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { ApiError } from '../../src/errors/APIError'; + +describe('ApiError', () => { + it('creates error without response', () => { + const error = new ApiError({ + name: 'TestError', + message: 'Test message', + }); + expect(error.name).toBe('TestError'); + expect(error.message).toBe('Test message'); + expect(error.response).toBeUndefined(); + }); + + it('creates error with response', () => { + const response = { + statusCode: 404, + headers: { 'content-type': 'application/json' }, + body: '{"error":"Not found"}', + }; + const error = new ApiError({ + name: 'NotFoundError', + message: 'Resource not found', + response, + }); + expect(error.response).toEqual(response); + expect(error.response).not.toBe(response); + expect(error.response?.headers).not.toBe(response.headers); + }); + + it('replicates response to prevent mutation', () => { + const response = { + statusCode: 500, + headers: { 'x-custom': 'value' }, + }; + const error = new ApiError({ + name: 'ServerError', + message: 'Server error', + response, + }); + const errorResponse = error.response; + response.headers['x-custom'] = 'modified'; + expect(errorResponse?.headers['x-custom']).toBe('value'); + }); +}); diff --git a/packages/core/__tests__/singleton/Auth/utils/index.test.ts b/packages/core/__tests__/singleton/Auth/utils/index.test.ts index 6eaeeb0bdee..76a0315884d 100644 --- a/packages/core/__tests__/singleton/Auth/utils/index.test.ts +++ b/packages/core/__tests__/singleton/Auth/utils/index.test.ts @@ -1,4 +1,9 @@ -import { decodeJWT } from '../../../../src/singleton/Auth/utils'; +import { + assertIdentityPoolIdConfig, + assertOAuthConfig, + assertTokenProviderConfig, + decodeJWT, +} from '../../../../src/singleton/Auth/utils'; const testSamples = [ { @@ -35,4 +40,111 @@ describe('decodeJWT', () => { expect(result.toString()).toEqual(token); }, ); + + it('throws error for invalid token format', () => { + expect(() => decodeJWT('invalid')).toThrow('Invalid token'); + }); + + it('throws error for malformed payload', () => { + expect(() => decodeJWT('header.invalid-payload.signature')).toThrow( + 'Invalid token payload', + ); + }); +}); + +describe('assertTokenProviderConfig', () => { + it('passes with valid user pool config', () => { + expect(() => { + assertTokenProviderConfig({ + userPoolId: 'us-east-1_test', + userPoolClientId: 'client123', + }); + }).not.toThrow(); + }); + + it('throws when config is undefined', () => { + expect(() => { + assertTokenProviderConfig(undefined); + }).toThrow(); + }); + + it('throws when userPoolId is missing', () => { + expect(() => { + assertTokenProviderConfig({ + userPoolClientId: 'client123', + } as any); + }).toThrow(); + }); + + it('throws when userPoolClientId is missing', () => { + expect(() => { + assertTokenProviderConfig({ + userPoolId: 'us-east-1_test', + } as any); + }).toThrow(); + }); +}); + +describe('assertOAuthConfig', () => { + it('passes with valid oauth config', () => { + expect(() => { + assertOAuthConfig({ + userPoolId: 'us-east-1_test', + userPoolClientId: 'client123', + loginWith: { + oauth: { + domain: 'example.auth.us-east-1.amazoncognito.com', + redirectSignIn: ['http://localhost:3000/'], + redirectSignOut: ['http://localhost:3000/'], + responseType: 'code', + scopes: ['openid'], + }, + }, + }); + }).not.toThrow(); + }); + + it('throws when oauth config is missing', () => { + expect(() => { + assertOAuthConfig(undefined); + }).toThrow(); + }); + + it('throws when domain is missing', () => { + expect(() => { + assertOAuthConfig({ + userPoolId: 'us-east-1_test', + userPoolClientId: 'client123', + loginWith: { + oauth: { + redirectSignIn: ['http://localhost:3000/'], + redirectSignOut: ['http://localhost:3000/'], + responseType: 'code', + } as any, + }, + }); + }).toThrow(); + }); +}); + +describe('assertIdentityPoolIdConfig', () => { + it('passes with valid identity pool config', () => { + expect(() => { + assertIdentityPoolIdConfig({ + identityPoolId: 'us-east-1:test-id', + }); + }).not.toThrow(); + }); + + it('throws when identityPoolId is missing', () => { + expect(() => { + assertIdentityPoolIdConfig(undefined); + }).toThrow(); + }); + + it('throws when identityPoolId is empty', () => { + expect(() => { + assertIdentityPoolIdConfig({} as any); + }).toThrow(); + }); }); diff --git a/packages/core/__tests__/singleton/Singleton.test.ts b/packages/core/__tests__/singleton/Singleton.test.ts deleted file mode 100644 index 03c0185359d..00000000000 --- a/packages/core/__tests__/singleton/Singleton.test.ts +++ /dev/null @@ -1,575 +0,0 @@ -import { TextDecoder, TextEncoder } from 'util'; - -import { Amplify } from '../../src/singleton'; -import { AMPLIFY_SYMBOL, Hub } from '../../src/Hub'; -import { AuthClass as Auth } from '../../src/singleton/Auth'; -import { decodeJWT } from '../../src/singleton/Auth/utils'; -import { CredentialsAndIdentityId } from '../../src/singleton/Auth/types'; -import { ResourcesConfig, fetchAuthSession } from '../../src'; - -Object.assign(global, { TextDecoder, TextEncoder }); - -jest.mock('../../src/Hub', () => ({ - ...jest.requireActual('../../src/Hub'), - Hub: { - dispatch: jest.fn(), - }, -})); - -const mockHubDispatch = Hub.dispatch as jest.Mock; - -type ArgumentTypes any> = F extends ( - ...args: infer A -) => any - ? A - : never; - -const MOCK_AUTH_CONFIG = { - Auth: { - Cognito: { - identityPoolId: 'us-east-1:bbbbb', - }, - }, -}; - -type ModelIntrospection = NonNullable< - NonNullable['GraphQL'] ->['modelIntrospection']; - -const modelIntrospection: ModelIntrospection = { - version: 1, - models: { - Todo: { - name: 'Todo', - fields: { - id: { - name: 'id', - isArray: false, - type: 'ID', - isRequired: true, - attributes: [], - }, - name: { - name: 'name', - isArray: false, - type: 'String', - isRequired: false, - attributes: [], - }, - description: { - name: 'description', - isArray: false, - type: 'String', - isRequired: false, - attributes: [], - }, - createdAt: { - name: 'createdAt', - isArray: false, - type: 'AWSDateTime', - isRequired: false, - attributes: [], - isReadOnly: true, - }, - updatedAt: { - name: 'updatedAt', - isArray: false, - type: 'AWSDateTime', - isRequired: false, - attributes: [], - isReadOnly: true, - }, - }, - syncable: true, - pluralName: 'Todos', - attributes: [ - { - type: 'model', - properties: {}, - }, - ], - primaryKeyInfo: { - isCustomPrimaryKey: false, - primaryKeyFieldName: 'id', - sortKeyFieldNames: [], - }, - }, - }, - enums: {}, - nonModels: {}, -}; - -describe('Amplify.configure() and Amplify.getConfig()', () => { - const mockLegacyConfig = { - aws_project_region: 'us-west-2', - aws_cognito_identity_pool_id: 'aws_cognito_identity_pool_id', - aws_cognito_region: 'aws_cognito_region', - aws_user_pools_id: 'aws_user_pools_id', - aws_user_pools_web_client_id: 'aws_user_pools_web_client_id', - oauth: {}, - aws_cognito_username_attributes: [], - aws_cognito_social_providers: [], - aws_cognito_signup_attributes: [], - aws_cognito_mfa_configuration: 'OFF', - aws_cognito_mfa_types: ['SMS'], - aws_cognito_password_protection_settings: { - passwordPolicyMinLength: 8, - passwordPolicyCharacters: [], - }, - aws_cognito_verification_mechanisms: ['PHONE_NUMBER'], - aws_appsync_graphqlEndpoint: 'https://some.domain.com/graphql', - aws_appsync_region: 'us-west-1', - aws_appsync_authenticationType: 'AMAZON_COGNITO_USER_POOLS', - aws_appsync_apiKey: 'some-key', - modelIntrospection, - }; - const expectedResourceConfig: ResourcesConfig = { - Auth: { - Cognito: { - allowGuestAccess: true, - identityPoolId: 'aws_cognito_identity_pool_id', - userPoolClientId: 'aws_user_pools_web_client_id', - userPoolId: 'aws_user_pools_id', - loginWith: { email: false, phone: false, username: true }, - mfa: { smsEnabled: true, status: 'off', totpEnabled: false }, - passwordFormat: { - minLength: 8, - requireLowercase: false, - requireNumbers: false, - requireSpecialCharacters: false, - requireUppercase: false, - }, - userAttributes: { phone_number: { required: true } }, - }, - }, - API: { - GraphQL: { - apiKey: 'some-key', - defaultAuthMode: 'userPool', - endpoint: 'https://some.domain.com/graphql', - region: 'us-west-1', - modelIntrospection, - }, - }, - }; - - afterEach(() => { - mockHubDispatch.mockClear(); - }); - - it('should take the legacy CLI shaped config object for configuring and return it from getConfig()', () => { - Amplify.configure(mockLegacyConfig); - const result = Amplify.getConfig(); - - expect(result).toEqual(expectedResourceConfig); - }); - - it('dispatches hub event with parsed ResourceConfig from the legacy config', () => { - Amplify.configure(mockLegacyConfig); - - expect(mockHubDispatch).toHaveBeenCalledWith( - 'core', - { - event: 'configure', - data: expectedResourceConfig, - }, - 'Configure', - AMPLIFY_SYMBOL, - ); - }); - - it('should take the v6 shaped config object for configuring and return it from getConfig()', () => { - Amplify.configure(MOCK_AUTH_CONFIG); - const result = Amplify.getConfig(); - - expect(result).toEqual(MOCK_AUTH_CONFIG); - }); - - it('should replace Cognito configuration set and get config', () => { - const config1: ArgumentTypes[0] = { - Auth: { - Cognito: { - userPoolId: 'us-east-1:aaaaaaa', - userPoolClientId: 'aaaaaaaaaaaa', - }, - }, - }; - - Amplify.configure(config1); - Amplify.configure(MOCK_AUTH_CONFIG); - - const result = Amplify.getConfig(); - - expect(result).toEqual(MOCK_AUTH_CONFIG); - }); - - it('should return memoized, immutable resource configuration objects', () => { - Amplify.configure(MOCK_AUTH_CONFIG); - - const config = Amplify.getConfig(); - const config2 = Amplify.getConfig(); - - const mutateConfig = () => { - (config as any).Auth = MOCK_AUTH_CONFIG.Auth; - }; - - // Config should be cached - expect(config).toEqual(MOCK_AUTH_CONFIG); - expect(config2).toBe(config); - - // Config should be immutable - expect(mutateConfig).toThrow(TypeError); - - // Config should be re-generated if it changes - Amplify.configure({ - Auth: { - Cognito: { - identityPoolId: 'us-east-1:bbbbb', - }, - }, - API: { - GraphQL: { - apiKey: 'some-key', - defaultAuthMode: 'userPool', - endpoint: 'https://some.domain.com/graphql', - region: 'us-west-1', - modelIntrospection: modelIntrospection as any, - }, - }, - }); - - const config3 = Amplify.getConfig(); - - expect(config3).toEqual({ - ...MOCK_AUTH_CONFIG, - API: { - GraphQL: { - apiKey: 'some-key', - defaultAuthMode: 'userPool', - endpoint: 'https://some.domain.com/graphql', - region: 'us-west-1', - modelIntrospection, - }, - }, - }); - expect(config3).not.toBe(config); - expect(config3).not.toBe(config2); - }); -}); - -describe('Session tests', () => { - beforeEach(() => { - jest.resetAllMocks(); - jest.clearAllMocks(); - }); - test('fetch empty session', async () => { - expect.assertions(2); - const config: ArgumentTypes[0] = { - Auth: { - Cognito: { - userPoolId: 'us-east-1:aaaaaaa', - identityPoolId: 'us-east-1:bbbbb', - userPoolClientId: 'aaaaaaaaaaaa', - }, - }, - }; - - Amplify.configure(config); - - const session = await Amplify.Auth.fetchAuthSession(); - - expect(session.tokens).toBe(undefined); - expect(session.credentials).toBe(undefined); - }); - - test('fetchAuthSession with credentials provider only', async () => { - const mockCredentials = { - accessKeyId: 'accessKeyValue', - secretAccessKey: 'secretAccessKeyValue', - }; - Amplify.configure( - {}, - { - Auth: { - credentialsProvider: { - getCredentialsAndIdentityId: async () => { - return { - credentials: mockCredentials, - }; - }, - clearCredentialsAndIdentityId: jest.fn(), - }, - }, - }, - ); - - const session = await fetchAuthSession(); - - expect(session.credentials).toBe(mockCredentials); - }); - - test('fetch user after no credentials', async () => { - expect.assertions(3); - const config: ArgumentTypes[0] = { - Auth: { - Cognito: { - userPoolId: 'us-east-1:aaaaaaa', - identityPoolId: 'us-east-1:bbbbb', - userPoolClientId: 'aaaaaaaaaaaa', - }, - }, - }; - - const token = - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE3MTAyOTMxMzB9.YzDpgJsrB3z-ZU1XxMcXSQsMbgCzwH_e-_76rnfehh0'; - const mockToken = decodeJWT(token); - const spyTokenProvider = jest.fn(async () => { - return { - accessToken: mockToken, - }; - }); - Amplify.configure(config, { - Auth: { - tokenProvider: { - getTokens: spyTokenProvider, - }, - }, - }); - - const session = await Amplify.Auth.fetchAuthSession(); - expect(spyTokenProvider).toHaveBeenCalled(); - - expect(session.tokens?.accessToken.payload).toEqual({ - exp: 1710293130, - iat: 1516239022, - name: 'John Doe', - sub: '1234567890', - }); - - expect(session.userSub).toEqual('1234567890'); - }); - - test('fetch session with token and credentials', async () => { - expect.assertions(4); - - const config: ArgumentTypes[0] = { - Auth: { - Cognito: { - userPoolId: 'us-east-1:aaaaaaa', - identityPoolId: 'us-east-1:bbbbb', - userPoolClientId: 'aaaaaaaaaaaa', - }, - }, - }; - - const credentialsSpy = jest.fn( - async (): Promise => { - return { - credentials: { - accessKeyId: 'accessKeyIdValue', - secretAccessKey: 'secretAccessKeyValue', - sessionToken: 'sessionTokenValue', - expiration: new Date(123), - }, - identityId: 'identityIdValue', - }; - }, - ); - const token = - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE3MTAyOTMxMzB9.YzDpgJsrB3z-ZU1XxMcXSQsMbgCzwH_e-_76rnfehh0'; - const mockToken = decodeJWT(token); - - const spyTokenProvider = jest.fn(async () => { - return { - accessToken: mockToken, - }; - }); - - Amplify.configure(config, { - Auth: { - credentialsProvider: { - getCredentialsAndIdentityId: credentialsSpy, - clearCredentialsAndIdentityId: jest.fn(), - }, - tokenProvider: { - getTokens: spyTokenProvider, - }, - }, - }); - - const session = await Amplify.Auth.fetchAuthSession(); - - expect(session.tokens?.accessToken.payload).toEqual({ - exp: 1710293130, - iat: 1516239022, - name: 'John Doe', - sub: '1234567890', - }); - - expect(session.identityId).toBe('identityIdValue'); - - expect(session.credentials).toEqual({ - accessKeyId: 'accessKeyIdValue', - secretAccessKey: 'secretAccessKeyValue', - sessionToken: 'sessionTokenValue', - expiration: new Date(123), - }); - - expect(credentialsSpy).toHaveBeenCalledWith({ - authConfig: { - Cognito: { - identityPoolId: 'us-east-1:bbbbb', - userPoolId: 'us-east-1:aaaaaaa', - userPoolClientId: 'aaaaaaaaaaaa', - }, - }, - tokens: { - accessToken: { - payload: { - exp: 1710293130, - iat: 1516239022, - name: 'John Doe', - sub: '1234567890', - }, - toString: expect.anything(), - }, - idToken: undefined, - }, - authenticated: true, - }); - }); - - test('fetch session without tokens and credentials', async () => { - expect.assertions(4); - - const config: ArgumentTypes[0] = { - Auth: { - Cognito: { - userPoolId: 'us-east-1:aaaaaaa', - identityPoolId: 'us-east-1:bbbbb', - userPoolClientId: 'aaaaaaaaaaaa', - allowGuestAccess: true, - }, - }, - }; - - const credentialsSpy = jest.fn( - async (_): Promise => { - return { - credentials: { - accessKeyId: 'accessKeyIdValue', - secretAccessKey: 'secretAccessKeyValue', - sessionToken: 'sessionTokenValue', - expiration: new Date(123), - }, - identityId: 'identityIdValue', - }; - }, - ); - - const spyTokenProvider = jest.fn(async () => { - return null; - }); - - Amplify.configure(config, { - Auth: { - credentialsProvider: { - getCredentialsAndIdentityId: credentialsSpy, - clearCredentialsAndIdentityId: jest.fn(), - }, - tokenProvider: { - getTokens: spyTokenProvider, - }, - }, - }); - - const session = await Amplify.Auth.fetchAuthSession(); - - expect(session.tokens).toBeUndefined(); - - expect(session.identityId).toBe('identityIdValue'); - - expect(session.credentials).toEqual({ - accessKeyId: 'accessKeyIdValue', - secretAccessKey: 'secretAccessKeyValue', - sessionToken: 'sessionTokenValue', - expiration: new Date(123), - }); - - expect(credentialsSpy).toHaveBeenCalledWith({ - authConfig: { - Cognito: { - allowGuestAccess: true, - identityPoolId: 'us-east-1:bbbbb', - userPoolId: 'us-east-1:aaaaaaa', - userPoolClientId: 'aaaaaaaaaaaa', - }, - }, - authenticated: false, - forceRefresh: undefined, - }); - }); - - test('refresh tokens with forceRefresh success', async () => { - expect.assertions(1); - const auth = new Auth(); - const tokenProvider = jest.fn(async () => { - const token = - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE3MTAyOTMxMzB9.YzDpgJsrB3z-ZU1XxMcXSQsMbgCzwH_e-_76rnfehh0'; - const mockToken = decodeJWT(token); - - return { - accessToken: mockToken, - }; - }); - - auth.configure( - { - Cognito: { - userPoolId: 'us-east-1:aaaaaaa', - identityPoolId: 'us-east-1:bbbbb', - userPoolClientId: 'aaaaaaaaaaaa', - }, - }, - { - tokenProvider: { - getTokens: tokenProvider, - }, - }, - ); - - await auth.fetchAuthSession({ forceRefresh: true }); - expect(tokenProvider).toHaveBeenCalledWith({ - forceRefresh: true, - }); - }); - - test('refresh tokens with forceRefresh failed', async () => { - expect.assertions(2); - const auth = new Auth(); - const tokenProvider = jest.fn(() => { - throw new Error('no no no'); - }); - - auth.configure( - { - Cognito: { - userPoolId: 'us-east-1:aaaaaaa', - identityPoolId: 'us-east-1:bbbbb', - userPoolClientId: 'aaaaaaaaaaaa', - }, - }, - { - tokenProvider: { - getTokens: tokenProvider, - }, - }, - ); - - const action = async () => auth.fetchAuthSession({ forceRefresh: true }); - - await expect(action()).rejects.toThrow('no no no'); - - expect(tokenProvider).toHaveBeenCalled(); - }); -}); diff --git a/packages/core/__tests__/singleton/contextBrand.test.ts b/packages/core/__tests__/singleton/contextBrand.test.ts new file mode 100644 index 00000000000..8ad5b1bccf7 --- /dev/null +++ b/packages/core/__tests__/singleton/contextBrand.test.ts @@ -0,0 +1,28 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { AMPLIFY_CONTEXT_BRAND, isAmplifyContext } from '@aws-amplify/core'; + +describe('isAmplifyContext', () => { + it('returns true for branded context', () => { + const ctx = { [AMPLIFY_CONTEXT_BRAND]: true }; + expect(isAmplifyContext(ctx)).toBe(true); + }); + + it('returns false for null', () => { + expect(isAmplifyContext(null)).toBe(false); + }); + + it('returns false for undefined', () => { + expect(isAmplifyContext(undefined)).toBe(false); + }); + + it('returns false for non-object', () => { + expect(isAmplifyContext('string')).toBe(false); + expect(isAmplifyContext(123)).toBe(false); + }); + + it('returns false for object without brand', () => { + expect(isAmplifyContext({})).toBe(false); + }); +}); diff --git a/packages/core/__tests__/storage/InMemoryStorage.test.ts b/packages/core/__tests__/storage/InMemoryStorage.test.ts index 937d675827d..0836ed3fee8 100644 --- a/packages/core/__tests__/storage/InMemoryStorage.test.ts +++ b/packages/core/__tests__/storage/InMemoryStorage.test.ts @@ -37,6 +37,11 @@ describe('InMemoryStorage', () => { expect(inMemoryStorage.key(1)).toEqual('2'); }); + it('should return null for out of bounds index', () => { + inMemoryStorage.setItem('1', value); + expect(inMemoryStorage.key(10)).toBeNull(); + }); + it('should not throw if trying to delete a non existing key', () => { const badKey = 'nonExistingKey'; expect(() => { diff --git a/packages/core/__tests__/storage/SyncKeyValueStorage.test.ts b/packages/core/__tests__/storage/SyncKeyValueStorage.test.ts new file mode 100644 index 00000000000..e75267fa997 --- /dev/null +++ b/packages/core/__tests__/storage/SyncKeyValueStorage.test.ts @@ -0,0 +1,35 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { SyncKeyValueStorage } from '../../src/storage/SyncKeyValueStorage'; + +describe('SyncKeyValueStorage', () => { + it('throws when accessing storage without initialization', () => { + const storage = new SyncKeyValueStorage(); + expect(() => { + storage.setItem('key', 'value'); + }).toThrow(); + }); + + it('works with provided storage', () => { + const mockStorage = { + setItem: jest.fn(), + getItem: jest.fn(() => 'value'), + removeItem: jest.fn(), + clear: jest.fn(), + } as any; + + const storage = new SyncKeyValueStorage(mockStorage); + storage.setItem('key', 'value'); + expect(mockStorage.setItem).toHaveBeenCalledWith('key', 'value'); + + const value = storage.getItem('key'); + expect(value).toBe('value'); + + storage.removeItem('key'); + expect(mockStorage.removeItem).toHaveBeenCalledWith('key'); + + storage.clear(); + expect(mockStorage.clear).toHaveBeenCalled(); + }); +}); diff --git a/packages/core/__tests__/utils/WordArray.test.ts b/packages/core/__tests__/utils/WordArray.test.ts new file mode 100644 index 00000000000..2b5e24ab56b --- /dev/null +++ b/packages/core/__tests__/utils/WordArray.test.ts @@ -0,0 +1,42 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { WordArray } from '../../src/utils/WordArray'; + +describe('WordArray', () => { + test('creates empty WordArray', () => { + const wa = new WordArray(); + expect(wa.words).toEqual([]); + expect(wa.sigBytes).toBe(0); + }); + + test('creates WordArray with words', () => { + const wa = new WordArray([0x12345678, 0x9abcdef0]); + expect(wa.words).toEqual([0x12345678, 0x9abcdef0]); + expect(wa.sigBytes).toBe(8); + }); + + test('creates WordArray with custom sigBytes', () => { + const wa = new WordArray([0x12345678], 3); + expect(wa.sigBytes).toBe(3); + }); + + test('random generates WordArray', () => { + const wa = new WordArray(); + const random = wa.random(8); + expect(random.words.length).toBe(2); + expect(random.sigBytes).toBe(8); + }); + + test('toString converts to hex', () => { + const wa = new WordArray([0x12345678], 4); + const hex = wa.toString(); + expect(hex).toBe('12345678'); + }); + + test('toString handles partial bytes', () => { + const wa = new WordArray([0x12345678], 2); + const hex = wa.toString(); + expect(hex).toBe('1234'); + }); +}); diff --git a/packages/core/__tests__/utils/deepFreeze.test.ts b/packages/core/__tests__/utils/deepFreeze.test.ts new file mode 100644 index 00000000000..3b4c0612bcf --- /dev/null +++ b/packages/core/__tests__/utils/deepFreeze.test.ts @@ -0,0 +1,26 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { deepFreeze } from '../../src/utils/deepFreeze'; + +describe('deepFreeze', () => { + test('freezes simple object', () => { + const obj = { a: 1, b: 2 }; + const frozen = deepFreeze(obj); + expect(Object.isFrozen(frozen)).toBe(true); + }); + + test('freezes nested objects', () => { + const obj = { a: { b: { c: 1 } } }; + const frozen = deepFreeze(obj); + expect(Object.isFrozen(frozen)).toBe(true); + expect(Object.isFrozen(frozen.a)).toBe(true); + expect(Object.isFrozen(frozen.a.b)).toBe(true); + }); + + test('freezes functions', () => { + const obj = { fn: () => 'test' }; + const frozen = deepFreeze(obj); + expect(Object.isFrozen(frozen.fn)).toBe(true); + }); +}); diff --git a/packages/core/__tests__/utils/getClientInfo/getClientInfo.test.ts b/packages/core/__tests__/utils/getClientInfo/getClientInfo.test.ts index bde270d3961..379f9e31f51 100644 --- a/packages/core/__tests__/utils/getClientInfo/getClientInfo.test.ts +++ b/packages/core/__tests__/utils/getClientInfo/getClientInfo.test.ts @@ -179,4 +179,29 @@ describe('getClientInfo', () => { expect(result).toEqual(expect.objectContaining(expectedResult)); }, ); + + test('returns empty object when window is undefined', () => { + const originalWindow = (global as any).window; + delete (global as any).window; + const result = getClientInfo(); + expect(result).toEqual({}); + (global as any).window = originalWindow; + }); + + test('returns empty object when navigator is undefined', () => { + mockNavigator.mockReturnValueOnce(undefined as any); + const result = getClientInfo(); + expect(result).toEqual({}); + }); + + test('handles unknown user agent', () => { + mockNavigator.mockReturnValueOnce({ + userAgent: 'UnknownBrowser', + platform: 'Unknown', + language: 'en', + } as any); + const result = getClientInfo(); + expect(result.model).toBe(''); + expect(result.version).toBe(''); + }); }); diff --git a/packages/core/package.json b/packages/core/package.json index af4a87a7cfa..4fcc9ef4a53 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,195 +1,190 @@ { - "name": "@aws-amplify/core", - "version": "6.16.3", - "description": "Core category of aws-amplify", - "main": "./dist/cjs/index.js", - "module": "./dist/esm/index.mjs", - "react-native": "./dist/cjs/index.js", - "typings": "./dist/esm/index.d.ts", - "publishConfig": { - "access": "public" - }, - "sideEffects": [ - "./dist/cjs/I18n/index.js", - "./dist/cjs/Cache/index.js", - "./dist/esm/I18n/index.mjs", - "./dist/esm/Cache/index.mjs" - ], - "scripts": { - "test": "npm run lint && jest -w 1 --coverage --logHeapUsage", - "test:size": "size-limit", - "build-with-test": "npm test && npm run build", - "build:umd": "webpack && webpack --config ./webpack.config.dev.js", - "build:esm-cjs": "rollup --forceExit -c rollup.config.mjs", - "build:watch": "npm run build:esm-cjs -- --watch", - "build": "npm run clean && npm run generate-version && npm run build:esm-cjs && npm run build:umd", - "generate-version": "genversion src/Platform/version.ts --es6 --semi --source ../aws-amplify", - "clean": "npm run clean:size && rimraf dist lib lib-esm", - "clean:size": "rimraf dual-publish-tmp tmp*", - "format": "echo \"Not implemented\"", - "lint": "eslint '**/*.{ts,tsx}' && npm run ts-coverage", - "lint:fix": "eslint '**/*.{ts,tsx}' --fix", - "prepublishOnly": "npm run build", - "ts-coverage": "typescript-coverage-report -p ./tsconfig.build.json -t 92.36" - }, - "repository": { - "type": "git", - "url": "https://github.com/aws-amplify/amplify-js.git", - "directory": "packages/core" - }, - "author": "Amazon Web Services", - "license": "Apache-2.0", - "bugs": { - "url": "https://github.com/aws/aws-amplify/issues" - }, - "homepage": "https://docs.amplify.aws/", - "files": [ - "dist/cjs", - "dist/esm", - "src", - "internals", - "server" - ], - "dependencies": { - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/types": "^3.973.6", - "@smithy/util-hex-encoding": "2.0.0", - "@types/uuid": "^9.0.0", - "js-cookie": "^3.0.5", - "rxjs": "^7.8.1", - "tslib": "^2.5.0", - "uuid": "^11.0.0" - }, - "devDependencies": { - "@aws-amplify/react-native": "1.3.3", - "@types/js-cookie": "3.0.2", - "genversion": "^2.2.0" - }, - "size-limit": [ - { - "name": "Core (Hub)", - "path": "./dist/esm/index.mjs", - "import": "{ Hub }", - "limit": "1.46 kB" - }, - { - "name": "Core (I18n)", - "path": "./dist/esm/index.mjs", - "import": "{ I18n }", - "limit": "1.51 kB" - }, - { - "name": "Custom clients (fetch handler)", - "path": "./dist/esm/clients/handlers/fetch.mjs", - "import": "{ fetchTransferHandler }", - "limit": "900 B" - }, - { - "name": "Custom clients (unauthenticated handler)", - "path": "./dist/esm/clients/handlers/aws/unauthenticated.mjs", - "import": "{ unauthenticatedHandler }", - "limit": "4.25 kB" - }, - { - "name": "Custom clients (request signer)", - "path": "./dist/esm/clients/middleware/signing/signer/signatureV4/index.mjs", - "import": "{ signRequest }", - "limit": "3.60 kB" - }, - { - "name": "Custom clients (url presigner)", - "path": "./dist/esm/clients/middleware/signing/signer/signatureV4/index.mjs", - "import": "{ presignUrl }", - "limit": "3.7 kB" - }, - { - "name": "Cache (default browser storage)", - "path": "./dist/esm/index.mjs", - "import": "{ Cache }", - "limit": "3.4 kB" - } - ], - "exports": { - ".": { - "react-native": "./dist/cjs/index.js", - "types": "./dist/esm/index.d.ts", - "import": "./dist/esm/index.mjs", - "require": "./dist/cjs/index.js" - }, - "./server": { - "types": "./dist/esm/server.d.ts", - "import": "./dist/esm/server.mjs", - "require": "./dist/cjs/server.js" - }, - "./internals/adapter-core": { - "types": "./dist/esm/adapterCore/index.d.ts", - "import": "./dist/esm/adapterCore/index.mjs", - "require": "./dist/cjs/adapterCore/index.js" - }, - "./internals/aws-client-utils": { - "react-native": "./dist/cjs/clients/index.js", - "types": "./dist/esm/clients/index.d.ts", - "import": "./dist/esm/clients/index.mjs", - "require": "./dist/cjs/clients/index.js" - }, - "./internals/aws-client-utils/composers": { - "react-native": "./dist/cjs/clients/internal/index.js", - "types": "./dist/esm/clients/internal/index.d.ts", - "import": "./dist/esm/clients/internal/index.mjs", - "require": "./dist/cjs/clients/internal/index.js" - }, - "./internals/aws-clients/cognitoIdentity": { - "react-native": "./dist/cjs/foundation/factories/serviceClients/cognitoIdentity/index.js", - "types": "./dist/esm/foundation/factories/serviceClients/cognitoIdentity/index.d.ts", - "import": "./dist/esm/foundation/factories/serviceClients/cognitoIdentity/index.mjs", - "require": "./dist/cjs/foundation/factories/serviceClients/cognitoIdentity/index.js" - }, - "./internals/aws-clients/pinpoint": { - "react-native": "./dist/cjs/awsClients/pinpoint/index.js", - "types": "./dist/esm/awsClients/pinpoint/index.d.ts", - "import": "./dist/esm/awsClients/pinpoint/index.mjs", - "require": "./dist/cjs/awsClients/pinpoint/index.js" - }, - "./internals/providers/pinpoint": { - "react-native": "./dist/cjs/providers/pinpoint/index.js", - "types": "./dist/esm/providers/pinpoint/index.d.ts", - "import": "./dist/esm/providers/pinpoint/index.mjs", - "require": "./dist/cjs/providers/pinpoint/index.js" - }, - "./internals/utils": { - "react-native": "./dist/cjs/libraryUtils.js", - "types": "./dist/esm/libraryUtils.d.ts", - "import": "./dist/esm/libraryUtils.mjs", - "require": "./dist/cjs/libraryUtils.js" - }, - "./package.json": "./package.json" - }, - "typesVersions": { - ">=4.2": { - "server": [ - "./dist/esm/server.d.ts" - ], - "internals/adapter-core": [ - "./dist/esm/adapterCore/index.d.ts" - ], - "internals/aws-client-utils": [ - "./dist/esm/clients/index.d.ts" - ], - "internals/aws-client-utils/composers": [ - "./dist/esm/clients/internal/index.d.ts" - ], - "internals/aws-clients/cognitoIdentity": [ - "./dist/esm/foundation/factories/serviceClients/cognitoIdentity/index.d.ts" - ], - "internals/aws-clients/pinpoint": [ - "./dist/esm/awsClients/pinpoint/index.d.ts" - ], - "internals/providers/pinpoint": [ - "./dist/esm/providers/pinpoint/index.d.ts" - ], - "internals/utils": [ - "./dist/esm/libraryUtils.d.ts" - ] - } - } + "name": "@aws-amplify/core", + "version": "6.16.3", + "description": "Core category of aws-amplify", + "main": "./dist/cjs/index.js", + "module": "./dist/esm/index.mjs", + "react-native": "./dist/cjs/index.js", + "typings": "./dist/esm/index.d.ts", + "publishConfig": { + "access": "public" + }, + "sideEffects": [ + "./dist/cjs/I18n/index.js", + "./dist/cjs/Cache/index.js", + "./dist/esm/I18n/index.mjs", + "./dist/esm/Cache/index.mjs" + ], + "scripts": { + "test": "npm run lint && jest -w 1 --coverage --logHeapUsage", + "test:size": "size-limit", + "build-with-test": "npm test && npm run build", + "build:umd": "webpack && webpack --config ./webpack.config.dev.js", + "build:esm-cjs": "rollup --forceExit -c rollup.config.mjs", + "build:watch": "npm run build:esm-cjs -- --watch", + "build": "npm run clean && npm run generate-version && npm run build:esm-cjs && npm run build:umd", + "generate-version": "genversion src/Platform/version.ts --es6 --semi --source ../aws-amplify", + "clean": "npm run clean:size && rimraf dist lib lib-esm", + "clean:size": "rimraf dual-publish-tmp tmp*", + "format": "echo \"Not implemented\"", + "lint": "eslint '**/*.{ts,tsx}' && npm run ts-coverage", + "lint:fix": "eslint '**/*.{ts,tsx}' --fix", + "prepublishOnly": "npm run build", + "ts-coverage": "typescript-coverage-report -p ./tsconfig.build.json -t 92.36" + }, + "repository": { + "type": "git", + "url": "https://github.com/aws-amplify/amplify-js.git" + }, + "author": "Amazon Web Services", + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/aws/aws-amplify/issues" + }, + "homepage": "https://aws-amplify.github.io/", + "files": [ + "dist/cjs", + "dist/esm", + "src", + "internals" + ], + "dependencies": { + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/types": "^3.973.6", + "@smithy/util-hex-encoding": "2.0.0", + "@types/uuid": "^9.0.0", + "js-cookie": "^3.0.5", + "rxjs": "^7.8.1", + "tslib": "^2.5.0", + "uuid": "^11.0.0" + }, + "devDependencies": { + "@aws-amplify/react-native": "1.3.3", + "@types/js-cookie": "3.0.2", + "genversion": "^2.2.0" + }, + "size-limit": [ + { + "name": "Core (Hub)", + "path": "./dist/esm/index.mjs", + "import": "{ Hub }", + "limit": "1.46 kB" + }, + { + "name": "Core (I18n)", + "path": "./dist/esm/index.mjs", + "import": "{ I18n }", + "limit": "1.51 kB" + }, + { + "name": "Custom clients (fetch handler)", + "path": "./dist/esm/clients/handlers/fetch.mjs", + "import": "{ fetchTransferHandler }", + "limit": "900 B" + }, + { + "name": "Custom clients (unauthenticated handler)", + "path": "./dist/esm/clients/handlers/aws/unauthenticated.mjs", + "import": "{ unauthenticatedHandler }", + "limit": "4.25 kB" + }, + { + "name": "Custom clients (request signer)", + "path": "./dist/esm/clients/middleware/signing/signer/signatureV4/index.mjs", + "import": "{ signRequest }", + "limit": "3.60 kB" + }, + { + "name": "Custom clients (url presigner)", + "path": "./dist/esm/clients/middleware/signing/signer/signatureV4/index.mjs", + "import": "{ presignUrl }", + "limit": "3.7 kB" + }, + { + "name": "Cache (default browser storage)", + "path": "./dist/esm/index.mjs", + "import": "{ Cache }", + "limit": "3.4 kB" + } + ], + "exports": { + ".": { + "react-native": "./dist/cjs/index.js", + "types": "./dist/esm/index.d.ts", + "import": "./dist/esm/index.mjs", + "require": "./dist/cjs/index.js" + }, + "./internals/adapter-core": { + "types": "./dist/esm/adapterCore/index.d.ts", + "import": "./dist/esm/adapterCore/index.mjs", + "require": "./dist/cjs/adapterCore/index.js" + }, + "./internals/aws-client-utils": { + "react-native": "./dist/cjs/clients/index.js", + "types": "./dist/esm/clients/index.d.ts", + "import": "./dist/esm/clients/index.mjs", + "require": "./dist/cjs/clients/index.js" + }, + "./internals/aws-client-utils/composers": { + "react-native": "./dist/cjs/clients/internal/index.js", + "types": "./dist/esm/clients/internal/index.d.ts", + "import": "./dist/esm/clients/internal/index.mjs", + "require": "./dist/cjs/clients/internal/index.js" + }, + "./internals/aws-clients/cognitoIdentity": { + "react-native": "./dist/cjs/foundation/factories/serviceClients/cognitoIdentity/index.js", + "types": "./dist/esm/foundation/factories/serviceClients/cognitoIdentity/index.d.ts", + "import": "./dist/esm/foundation/factories/serviceClients/cognitoIdentity/index.mjs", + "require": "./dist/cjs/foundation/factories/serviceClients/cognitoIdentity/index.js" + }, + "./internals/aws-clients/pinpoint": { + "react-native": "./dist/cjs/awsClients/pinpoint/index.js", + "types": "./dist/esm/awsClients/pinpoint/index.d.ts", + "import": "./dist/esm/awsClients/pinpoint/index.mjs", + "require": "./dist/cjs/awsClients/pinpoint/index.js" + }, + "./internals/providers/pinpoint": { + "react-native": "./dist/cjs/providers/pinpoint/index.js", + "types": "./dist/esm/providers/pinpoint/index.d.ts", + "import": "./dist/esm/providers/pinpoint/index.mjs", + "require": "./dist/cjs/providers/pinpoint/index.js" + }, + "./internals/utils": { + "react-native": "./dist/cjs/libraryUtils.js", + "types": "./dist/esm/libraryUtils.d.ts", + "import": "./dist/esm/libraryUtils.mjs", + "require": "./dist/cjs/libraryUtils.js" + }, + "./server": { + "types": "./dist/esm/server.d.ts", + "import": "./dist/esm/server.mjs", + "require": "./dist/cjs/server.js" + }, + "./package.json": "./package.json" + }, + "typesVersions": { + ">=4.2": { + "internals/adapter-core": [ + "./dist/esm/adapterCore/index.d.ts" + ], + "internals/aws-client-utils": [ + "./dist/esm/clients/index.d.ts" + ], + "internals/aws-client-utils/composers": [ + "./dist/esm/clients/internal/index.d.ts" + ], + "internals/aws-clients/cognitoIdentity": [ + "./dist/esm/foundation/factories/serviceClients/cognitoIdentity/index.d.ts" + ], + "internals/aws-clients/pinpoint": [ + "./dist/esm/awsClients/pinpoint/index.d.ts" + ], + "internals/providers/pinpoint": [ + "./dist/esm/providers/pinpoint/index.d.ts" + ], + "internals/utils": [ + "./dist/esm/libraryUtils.d.ts" + ] + } + } } diff --git a/packages/core/src/ServiceWorker/ServiceWorker.ts b/packages/core/src/ServiceWorker/ServiceWorker.ts index 975e0e34fb2..993b3e96250 100644 --- a/packages/core/src/ServiceWorker/ServiceWorker.ts +++ b/packages/core/src/ServiceWorker/ServiceWorker.ts @@ -5,7 +5,6 @@ import { ConsoleLogger } from '../Logger'; import { isBrowser } from '../utils'; import { AmplifyError } from '../errors'; import { record } from '../providers/pinpoint'; -import { Amplify, fetchAuthSession } from '../singleton'; import { ServiceWorkerErrorCode, assert } from './errorHelpers'; @@ -223,8 +222,8 @@ export class ServiceWorkerClass { flushInterval, flushSize, resendLimit, - } = Amplify.getConfig().Analytics?.Pinpoint ?? {}; - const { credentials } = await fetchAuthSession(); + } = ({} as any).Analytics?.Pinpoint ?? {}; + const credentials = undefined; // TODO: ServiceWorker needs AmplifyContext if (appId && region && credentials) { // Pinpoint is configured, record an event diff --git a/packages/core/src/adapterCore/error/AmplifyServerContextError.ts b/packages/core/src/adapterCore/error/AmplifyServerContextError.ts deleted file mode 100644 index 3ebf2733f7a..00000000000 --- a/packages/core/src/adapterCore/error/AmplifyServerContextError.ts +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -import { AmplifyError } from '../../errors'; - -export class AmplifyServerContextError extends AmplifyError { - constructor({ - message, - recoverySuggestion, - underlyingError, - }: { - message: string; - recoverySuggestion?: string; - underlyingError?: Error; - }) { - super({ - name: 'AmplifyServerContextError', - message, - recoverySuggestion, - underlyingError, - }); - } -} diff --git a/packages/core/src/adapterCore/index.ts b/packages/core/src/adapterCore/index.ts index ddeb6480fb5..dbeac6fee5e 100644 --- a/packages/core/src/adapterCore/index.ts +++ b/packages/core/src/adapterCore/index.ts @@ -1,12 +1,5 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -export { - createAmplifyServerContext, - getAmplifyServerContext, - destroyAmplifyServerContext, - AmplifyServer, - CookieStorage, - KeyValueStorageMethodValidator, -} from './serverContext'; -export { AmplifyServerContextError } from './error'; +export { CookieStorage, KeyValueStorageMethodValidator } from './serverContext'; +export { AmplifyServerContextError } from '../errors/AmplifyServerContextError'; diff --git a/packages/core/src/adapterCore/serverContext/index.ts b/packages/core/src/adapterCore/serverContext/index.ts index 5d7477b0a1c..bf0f31b5580 100644 --- a/packages/core/src/adapterCore/serverContext/index.ts +++ b/packages/core/src/adapterCore/serverContext/index.ts @@ -1,14 +1,4 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -export { - createAmplifyServerContext, - destroyAmplifyServerContext, - getAmplifyServerContext, -} from './serverContext'; - -export { - AmplifyServer, - CookieStorage, - KeyValueStorageMethodValidator, -} from './types'; +export { CookieStorage, KeyValueStorageMethodValidator } from './types'; diff --git a/packages/core/src/adapterCore/serverContext/serverContext.ts b/packages/core/src/adapterCore/serverContext/serverContext.ts deleted file mode 100644 index 2efcae5155e..00000000000 --- a/packages/core/src/adapterCore/serverContext/serverContext.ts +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -import { AmplifyClass } from '../../singleton'; -import { LibraryOptions, ResourcesConfig } from '../../singleton/types'; -import { AmplifyServerContextError } from '../error'; - -import { serverContextRegistry } from './serverContextRegistry'; -import { AmplifyServer } from './types'; - -/** - * Creates an Amplify server context. - * @param amplifyConfig The Amplify resource config. - * @param libraryOptions The Amplify library options. - * @returns The Amplify server context spec. - */ -export const createAmplifyServerContext = ( - amplifyConfig: ResourcesConfig, - libraryOptions: LibraryOptions, -): AmplifyServer.ContextSpec => { - const amplify = new AmplifyClass(); - amplify.configure(amplifyConfig, libraryOptions); - - return serverContextRegistry.register({ - amplify, - }); -}; - -/** - * Returns an Amplify server context. - * @param contextSpec The context spec used to get the Amplify server context. - * @returns The Amplify server context. - */ -export const getAmplifyServerContext = ( - contextSpec: AmplifyServer.ContextSpec, -): AmplifyServer.Context => { - assertContextSpec(contextSpec); - const context = serverContextRegistry.get(contextSpec); - - if (context) { - return context; - } - - throw new AmplifyServerContextError({ - message: - 'Attempted to get the Amplify Server Context that may have been destroyed.', - recoverySuggestion: - 'Ensure always call Amplify APIs within `runWithAmplifyServerContext` function, and do not attempt to reuse `contextSpec` object.', - }); -}; - -/** - * Destroys an Amplify server context. - * @param contextSpec The context spec used to destroy the Amplify server context. - */ -export const destroyAmplifyServerContext = ( - contextSpec: AmplifyServer.ContextSpec, -): void => { - serverContextRegistry.deregister(contextSpec); -}; - -const assertContextSpec = (contextSpec: AmplifyServer.ContextSpec) => { - let invalid = false; - - if (!Object.prototype.hasOwnProperty.call(contextSpec, 'token')) { - invalid = true; - } else if ( - !Object.prototype.hasOwnProperty.call(contextSpec.token, 'value') - ) { - invalid = true; - } else if ( - Object.prototype.toString.call(contextSpec.token.value) !== - '[object Symbol]' - ) { - invalid = true; - } - - if (invalid) { - throw new AmplifyServerContextError({ - message: 'Invalid `contextSpec`.', - recoverySuggestion: - 'Ensure to use the `contextSpec` object injected by `runWithAmplifyServerContext` function.', - }); - } -}; diff --git a/packages/core/src/adapterCore/serverContext/serverContextRegistry.ts b/packages/core/src/adapterCore/serverContext/serverContextRegistry.ts deleted file mode 100644 index 5f672333c90..00000000000 --- a/packages/core/src/adapterCore/serverContext/serverContextRegistry.ts +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -import { AmplifyServer } from './types'; - -const storage = new WeakMap< - AmplifyServer.ContextToken, - AmplifyServer.Context ->(); - -function createToken(): AmplifyServer.ContextToken { - return { - value: Symbol('AmplifyServerContextToken'), - }; -} - -export const serverContextRegistry = { - register(context: AmplifyServer.Context): AmplifyServer.ContextSpec { - const token = createToken(); - storage.set(token, context); - - return { token }; - }, - deregister(contextSpec: AmplifyServer.ContextSpec): boolean { - return storage.delete(contextSpec.token); - }, - get( - contextSpec: AmplifyServer.ContextSpec, - ): AmplifyServer.Context | undefined { - return storage.get(contextSpec.token); - }, -}; diff --git a/packages/core/src/adapterCore/serverContext/types/amplifyServer.ts b/packages/core/src/adapterCore/serverContext/types/amplifyServer.ts deleted file mode 100644 index 2b929f0ad73..00000000000 --- a/packages/core/src/adapterCore/serverContext/types/amplifyServer.ts +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -import { AmplifyClass } from '../../../singleton'; -import { LibraryOptions, ResourcesConfig } from '../../../singleton/types'; - -export declare namespace AmplifyServer { - export interface ContextToken { - readonly value: symbol; - } - - export interface ContextSpec { - readonly token: ContextToken; - } - - export interface Context { - amplify: AmplifyClass; - } - - export type RunOperationWithContext = ( - amplifyConfig: ResourcesConfig, - libraryOptions: LibraryOptions, - operation: ( - contextSpec: AmplifyServer.ContextSpec, - ) => Result | Promise, - ) => Promise; -} diff --git a/packages/core/src/adapterCore/serverContext/types/index.ts b/packages/core/src/adapterCore/serverContext/types/index.ts index 80b35fdf74b..2236c84f095 100644 --- a/packages/core/src/adapterCore/serverContext/types/index.ts +++ b/packages/core/src/adapterCore/serverContext/types/index.ts @@ -1,10 +1,5 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { AmplifyServer } from './amplifyServer'; - -type AmplifyServerContextSpec = AmplifyServer.ContextSpec; - -export { AmplifyServerContextSpec, AmplifyServer }; export { CookieStorage } from './cookieStorage'; export { KeyValueStorageMethodValidator } from './KeyValueStorageMethodValidator'; diff --git a/packages/core/src/configurationBuilder/createConfigurationBuilder.ts b/packages/core/src/configurationBuilder/createConfigurationBuilder.ts new file mode 100644 index 00000000000..2d62d41ae4b --- /dev/null +++ b/packages/core/src/configurationBuilder/createConfigurationBuilder.ts @@ -0,0 +1,131 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + AmplifyOutputsAnalyticsProperties, + AmplifyOutputsAuthProperties, + AmplifyOutputsCustomProperties, + AmplifyOutputsGeoProperties, + AmplifyOutputsNotificationsProperties, + AmplifyOutputsStorageProperties, +} from '../singleton/AmplifyOutputs/types'; + +interface AmplifyOutputsDataProperties { + aws_region: string; + url: string; + default_authorization_type: string; + authorization_types: string[]; + model_introspection?: object; + api_key?: string; +} + +/** + * The shape produced by `.build()` — conforms to amplify_outputs.json schema v1.4. + */ +export interface AmplifyOutputsConfig { + version: '1.4'; + auth?: AmplifyOutputsAuthProperties; + storage?: AmplifyOutputsStorageProperties; + data?: AmplifyOutputsDataProperties; + analytics?: AmplifyOutputsAnalyticsProperties; + geo?: AmplifyOutputsGeoProperties; + notifications?: AmplifyOutputsNotificationsProperties; + custom?: AmplifyOutputsCustomProperties; +} + +export interface ConfigurationBuilder { + /** + * Merge an existing config into this builder. Last write wins — + * subsequent `.auth()`, `.storage()`, etc. calls override values set by `from()`. + * + * @example + * ```ts + * const authConfig = createConfigurationBuilder().auth({...}).build(); + * const fullConfig = createConfigurationBuilder().from(authConfig).storage({...}).build(); + * // fullConfig has both auth and storage + * ``` + */ + from(existing: Partial): ConfigurationBuilder; + auth(config: AmplifyOutputsAuthProperties): ConfigurationBuilder; + storage(config: AmplifyOutputsStorageProperties): ConfigurationBuilder; + data(config: AmplifyOutputsDataProperties): ConfigurationBuilder; + analytics(config: AmplifyOutputsAnalyticsProperties): ConfigurationBuilder; + geo(config: AmplifyOutputsGeoProperties): ConfigurationBuilder; + notifications( + config: AmplifyOutputsNotificationsProperties, + ): ConfigurationBuilder; + custom(config: AmplifyOutputsCustomProperties): ConfigurationBuilder; + build(): AmplifyOutputsConfig; +} + +/** + * Creates a fluent builder for constructing `amplify_outputs.json`-compatible + * configuration objects programmatically. + * + * @example + * ```ts + * const config = createConfigurationBuilder() + * .auth({ user_pool_id: 'us-east-1_abc', user_pool_client_id: 'xyz', aws_region: 'us-east-1' }) + * .storage({ bucket_name: 'my-bucket', aws_region: 'us-east-1' }) + * .build(); + * + * const ctx = configure(config); + * ``` + */ +export function createConfigurationBuilder(): ConfigurationBuilder { + const config: Omit = {}; + + const builder: ConfigurationBuilder = { + from(existing) { + if (existing.auth) config.auth = existing.auth; + if (existing.storage) config.storage = existing.storage; + if (existing.data) config.data = existing.data; + if (existing.analytics) config.analytics = existing.analytics; + if (existing.geo) config.geo = existing.geo; + if (existing.notifications) config.notifications = existing.notifications; + if (existing.custom) config.custom = existing.custom; + + return builder; + }, + auth(value) { + config.auth = value; + + return builder; + }, + storage(value) { + config.storage = value; + + return builder; + }, + data(value) { + config.data = value; + + return builder; + }, + analytics(value) { + config.analytics = value; + + return builder; + }, + geo(value) { + config.geo = value; + + return builder; + }, + notifications(value) { + config.notifications = value; + + return builder; + }, + custom(value) { + config.custom = value; + + return builder; + }, + build(): AmplifyOutputsConfig { + return Object.freeze({ version: '1.4', ...config }); + }, + }; + + return builder; +} diff --git a/packages/core/src/adapterCore/error/index.ts b/packages/core/src/configurationBuilder/index.ts similarity index 59% rename from packages/core/src/adapterCore/error/index.ts rename to packages/core/src/configurationBuilder/index.ts index 0220e1cf295..116c936c5b5 100644 --- a/packages/core/src/adapterCore/error/index.ts +++ b/packages/core/src/configurationBuilder/index.ts @@ -1,4 +1,4 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -export { AmplifyServerContextError } from './AmplifyServerContextError'; +export { createConfigurationBuilder } from './createConfigurationBuilder'; diff --git a/packages/core/src/context/AmplifyContext.ts b/packages/core/src/context/AmplifyContext.ts new file mode 100644 index 00000000000..62fadb0917b --- /dev/null +++ b/packages/core/src/context/AmplifyContext.ts @@ -0,0 +1,25 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + AuthSession, + AuthTokens, + FetchAuthSessionOptions, +} from '../singleton/Auth/types'; +import { LibraryOptions, ResourcesConfig } from '../singleton/types'; + +/** + * The context object returned by `createAmplifyContext()`. Pass this as the first argument + * to every Amplify category API to provide configuration and auth credentials + * without relying on global singleton state. + */ +export interface AmplifyContext { + readonly resourcesConfig: Readonly; + readonly libraryOptions: Readonly; + + fetchAuthSession(options?: FetchAuthSessionOptions): Promise; + + clearCredentials(): Promise; + + getTokens(options: FetchAuthSessionOptions): Promise; +} diff --git a/packages/core/src/context/contextBrand.ts b/packages/core/src/context/contextBrand.ts new file mode 100644 index 00000000000..52c52cd33c1 --- /dev/null +++ b/packages/core/src/context/contextBrand.ts @@ -0,0 +1,23 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { AmplifyContext } from './AmplifyContext'; + +/** + * Symbol brand used to identify {@link AmplifyContext} objects at runtime. + * Uses `Symbol.for()` so the brand is shared across module instances + * (e.g. when `@aws-amplify/core` is accidentally duplicated in the bundle). + */ +export const AMPLIFY_CONTEXT_BRAND = Symbol.for('amplify.context'); + +/** + * Returns `true` if the given value is a branded {@link AmplifyContext}. + */ +export function isAmplifyContext(value: unknown): value is AmplifyContext { + return ( + value !== null && + value !== undefined && + typeof value === 'object' && + AMPLIFY_CONTEXT_BRAND in (value as Record) + ); +} diff --git a/packages/core/src/context/globalContext.ts b/packages/core/src/context/globalContext.ts new file mode 100644 index 00000000000..a651d52b2dd --- /dev/null +++ b/packages/core/src/context/globalContext.ts @@ -0,0 +1,57 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { AmplifyContext } from './AmplifyContext'; + +let _globalContext: AmplifyContext | null = null; + +/** + * Returns the global {@link AmplifyContext} set by `Amplify.configure()`. + * + * @throws If `Amplify.configure()` has not been called yet. + */ +export function getActiveContext(): AmplifyContext { + if (!_globalContext) { + throw new Error( + 'No AmplifyContext available. Call Amplify.configure() to set a global context, ' + + 'or pass a context as the first argument.', + ); + } + + return _globalContext; +} + +/** + * Returns the global {@link AmplifyContext} set by `Amplify.configure()`. + * Alias for {@link getActiveContext} — provided for semantic clarity. + * + * @throws If `Amplify.configure()` has not been called yet. + */ +export function getGlobalContext(): AmplifyContext { + return getActiveContext(); +} + +/** + * Stores the given context as the global {@link AmplifyContext}. + * + * @internal + */ +export function setGlobalContext(ctx: AmplifyContext): void { + _globalContext = ctx; +} + +/** + * Returns `true` if a global {@link AmplifyContext} has been set. + */ +export function hasGlobalContext(): boolean { + return _globalContext !== null; +} + +/** + * Clears the global {@link AmplifyContext}. + * + * @internal — intended for testing and HMR. + */ +export function clearGlobalContext(): void { + _globalContext = null; +} diff --git a/packages/core/src/context/resolveCtxArgs.ts b/packages/core/src/context/resolveCtxArgs.ts new file mode 100644 index 00000000000..96206bb2b56 --- /dev/null +++ b/packages/core/src/context/resolveCtxArgs.ts @@ -0,0 +1,37 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { AmplifyContext } from './AmplifyContext'; +import { isAmplifyContext } from './contextBrand'; +import { getActiveContext } from './globalContext'; + +/** + * Resolves the optional leading `AmplifyContext` argument from a function's + * arguments array. Used by category functions that accept an optional context + * as their first positional parameter. + * + * @returns A tuple of `[AmplifyContext, T]` where `T` is the remaining input. + * + * @example + * ```ts + * export function signIn(...args: any[]) { + * const [ctx, input] = resolveCtxArgs(args); + * // ctx is guaranteed to be a valid AmplifyContext + * } + * ``` + * + * @internal + */ +export function resolveCtxArgs(args: unknown[]): [AmplifyContext, T] { + if (args.length > 1 && args[0] === undefined) { + throw new Error( + 'Undefined AmplifyContext passed. Call configure() first or omit the parameter.', + ); + } + + if (isAmplifyContext(args[0])) { + return [args[0], args[1] as T]; + } + + return [getActiveContext(), args[0] as T]; +} diff --git a/packages/core/src/errors/AmplifyServerContextError.ts b/packages/core/src/errors/AmplifyServerContextError.ts new file mode 100644 index 00000000000..04f6ad33204 --- /dev/null +++ b/packages/core/src/errors/AmplifyServerContextError.ts @@ -0,0 +1,18 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { AmplifyError } from './AmplifyError'; + +/** + * Error thrown when an Amplify server context operation fails. + * @deprecated Prefer AmplifyError for new code. + */ +export class AmplifyServerContextError extends AmplifyError { + constructor(params: { message: string; recoverySuggestion?: string }) { + super({ + name: 'AmplifyServerContextError', + message: params.message, + recoverySuggestion: params.recoverySuggestion, + }); + } +} diff --git a/packages/core/src/errors/index.ts b/packages/core/src/errors/index.ts index 9c973f3907b..a93d3b2b74c 100644 --- a/packages/core/src/errors/index.ts +++ b/packages/core/src/errors/index.ts @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 export { AmplifyError } from './AmplifyError'; +export { AmplifyServerContextError } from './AmplifyServerContextError'; export { ApiError, ApiErrorParams, ApiErrorResponse } from './APIError'; export { createAssertionFunction } from './createAssertionFunction'; export { PlatformNotSupportedError } from './PlatformNotSupportedError'; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index d65eac33003..ba84cc2d1af 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -40,11 +40,37 @@ export { } from './singleton/types'; export { Amplify, - fetchAuthSession, AmplifyClass as AmplifyClassV6, + fetchAuthSession, clearCredentials, } from './singleton'; +// AmplifyContext — the singleton-free contract for category APIs +export { AmplifyContext } from './context/AmplifyContext'; + +/** @deprecated Use AmplifyContext instead. */ +export type { AmplifyContext as ContextSpec } from './context/AmplifyContext'; + +// Context branding — runtime identification of AmplifyContext objects +export { + isAmplifyContext, + AMPLIFY_CONTEXT_BRAND, +} from './context/contextBrand'; + +// Global context management +export { + getActiveContext, + getGlobalContext, + hasGlobalContext, +} from './context/globalContext'; + +// Configuration Builder +export { createConfigurationBuilder } from './configurationBuilder'; +export { + ConfigurationBuilder, + AmplifyOutputsConfig, +} from './configurationBuilder/createConfigurationBuilder'; + // Cognito Identity service client factories export { createGetCredentialsForIdentityClient, @@ -79,3 +105,6 @@ export { ConsoleLogger } from './Logger'; // Service worker export { ServiceWorker } from './ServiceWorker'; + +// Errors +export { AmplifyServerContextError } from './errors/AmplifyServerContextError'; diff --git a/packages/core/src/libraryUtils.ts b/packages/core/src/libraryUtils.ts index ea1c6c7c7a5..9a422492444 100644 --- a/packages/core/src/libraryUtils.ts +++ b/packages/core/src/libraryUtils.ts @@ -28,7 +28,7 @@ export { AmplifyOutputs, AmplifyOutputsUnknown, } from './singleton/AmplifyOutputs/types'; -export { ADD_OAUTH_LISTENER } from './singleton/constants'; +export { AuthClass } from './singleton/Auth'; export { amplifyUuid } from './utils/amplifyUuid'; export { AmplifyUrl, AmplifyUrlSearchParams } from './utils/amplifyUrl'; export { parseAmplifyConfig } from './utils/parseAmplifyConfig'; @@ -147,3 +147,13 @@ export { SESSION_START_EVENT, SESSION_STOP_EVENT, } from './utils/sessionListener'; + +// Global context internals +export { setGlobalContext, clearGlobalContext } from './context/globalContext'; + +// Context argument resolution +export { resolveCtxArgs } from './context/resolveCtxArgs'; + +// Storage internals +export { InMemoryStorage } from './storage/InMemoryStorage'; +export { KeyValueStorage } from './storage/KeyValueStorage'; diff --git a/packages/core/src/server.ts b/packages/core/src/server.ts index 92f19835b5c..71a0d1dfd6c 100644 --- a/packages/core/src/server.ts +++ b/packages/core/src/server.ts @@ -1,4 +1,4 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -export { fetchAuthSession } from './singleton/apis/server/fetchAuthSession'; +export { fetchAuthSession } from '.'; diff --git a/packages/core/src/singleton/Amplify.ts b/packages/core/src/singleton/Amplify.ts index aa3bfe0a849..bf5c1e91dc1 100644 --- a/packages/core/src/singleton/Amplify.ts +++ b/packages/core/src/singleton/Amplify.ts @@ -3,22 +3,19 @@ import { AMPLIFY_SYMBOL, Hub } from '../Hub'; import { deepFreeze } from '../utils'; import { parseAmplifyConfig } from '../libraryUtils'; +import { AmplifyContext } from '../context/AmplifyContext'; +import { AMPLIFY_CONTEXT_BRAND } from '../context/contextBrand'; +import { setGlobalContext } from '../context/globalContext'; +import { AuthClass } from './Auth'; import { AmplifyOutputsUnknown, - AuthConfig, LegacyConfig, LibraryOptions, ResourcesConfig, } from './types'; -import { AuthClass } from './Auth'; -import { ADD_OAUTH_LISTENER } from './constants'; export class AmplifyClass { - private oAuthListener: - | ((authConfig: AuthConfig['Cognito']) => void) - | undefined = undefined; - private isConfigured = false; resourcesConfig: ResourcesConfig; @@ -82,6 +79,29 @@ export class AmplifyClass { ); } + this.isConfigured = true; + + // Publish a branded AmplifyContext so that context-based APIs + // (fetchAuthSession, clearCredentials) can resolve the global context. + // Must be set BEFORE Hub.dispatch so listeners can call getActiveContext(). + const ctx: AmplifyContext = { + resourcesConfig: this.resourcesConfig, + libraryOptions: this.libraryOptions, + fetchAuthSession: (options?) => this.Auth.fetchAuthSession(options ?? {}), + clearCredentials: () => this.Auth.clearCredentials(), + getTokens: options => this.Auth.getTokens(options), + }; + + Object.defineProperty(ctx, AMPLIFY_CONTEXT_BRAND, { + value: true, + enumerable: false, + configurable: false, + writable: false, + }); + + Object.freeze(ctx); + setGlobalContext(ctx); + Hub.dispatch( 'core', { @@ -91,9 +111,6 @@ export class AmplifyClass { 'Configure', AMPLIFY_SYMBOL, ); - - this.notifyOAuthListener(); - this.isConfigured = true; } /** @@ -111,30 +128,6 @@ export class AmplifyClass { return this.resourcesConfig; } - - /** @internal */ - [ADD_OAUTH_LISTENER](listener: (authConfig: AuthConfig['Cognito']) => void) { - if (this.resourcesConfig.Auth?.Cognito.loginWith?.oauth) { - // when Amplify has been configured with a valid OAuth config while adding the listener, run it directly - listener(this.resourcesConfig.Auth?.Cognito); - } else { - // otherwise register the listener and run it later when Amplify gets configured with a valid oauth config - this.oAuthListener = listener; - } - } - - private notifyOAuthListener() { - if ( - !this.resourcesConfig.Auth?.Cognito.loginWith?.oauth || - !this.oAuthListener - ) { - return; - } - - this.oAuthListener(this.resourcesConfig.Auth?.Cognito); - // the listener should only be notified once with a valid oauth config - this.oAuthListener = undefined; - } } /** diff --git a/packages/core/src/singleton/Storage/types.ts b/packages/core/src/singleton/Storage/types.ts index 160c93da2e5..5792f314288 100644 --- a/packages/core/src/singleton/Storage/types.ts +++ b/packages/core/src/singleton/Storage/types.ts @@ -25,6 +25,20 @@ export interface S3ProviderConfig { * @internal */ dangerouslyConnectToHttpEndpointForTesting?: string; + /** + * Custom endpoint provider for S3-compatible services (e.g. MinIO, LocalStack). + * Called with bucket and region to resolve the endpoint URL. + */ + endpointProvider?(params: { + bucket?: string; + region?: string; + }): string | Promise; + /** + * When true, uses path-style URLs (e.g. http://host/bucket/key) + * instead of virtual-hosted-style (e.g. http://bucket.host/key). + * Required for most S3-compatible services. + */ + forcePathStyle?: boolean; /** Map of friendly name for bucket to its information */ buckets?: Record; }; diff --git a/packages/core/src/singleton/apis/clearCredentials.ts b/packages/core/src/singleton/apis/clearCredentials.ts index 18d658a7265..7a232973ef1 100644 --- a/packages/core/src/singleton/apis/clearCredentials.ts +++ b/packages/core/src/singleton/apis/clearCredentials.ts @@ -1,8 +1,8 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { Amplify } from '../Amplify'; +import { getActiveContext } from '../../context/globalContext'; export function clearCredentials(): Promise { - return Amplify.Auth.clearCredentials(); + return getActiveContext().clearCredentials(); } diff --git a/packages/core/src/singleton/apis/fetchAuthSession.ts b/packages/core/src/singleton/apis/fetchAuthSession.ts index 3971ceb561d..73dcc52b082 100644 --- a/packages/core/src/singleton/apis/fetchAuthSession.ts +++ b/packages/core/src/singleton/apis/fetchAuthSession.ts @@ -1,10 +1,8 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { Amplify } from '../Amplify'; import { AuthSession, FetchAuthSessionOptions } from '../Auth/types'; - -import { fetchAuthSession as fetchAuthSessionInternal } from './internal/fetchAuthSession'; +import { getActiveContext } from '../../context/globalContext'; /** * Fetch the auth session including the tokens and credentials if they are available. By default it @@ -18,5 +16,5 @@ import { fetchAuthSession as fetchAuthSessionInternal } from './internal/fetchAu export const fetchAuthSession = ( options?: FetchAuthSessionOptions, ): Promise => { - return fetchAuthSessionInternal(Amplify, options); + return getActiveContext().fetchAuthSession(options); }; diff --git a/packages/core/src/singleton/apis/internal/fetchAuthSession.ts b/packages/core/src/singleton/apis/internal/fetchAuthSession.ts index b71c2da0afb..3068439ea48 100644 --- a/packages/core/src/singleton/apis/internal/fetchAuthSession.ts +++ b/packages/core/src/singleton/apis/internal/fetchAuthSession.ts @@ -1,12 +1,12 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { AmplifyClass } from '../../Amplify'; +import { AmplifyContext } from '../../../context/AmplifyContext'; import { AuthSession, FetchAuthSessionOptions } from '../../Auth/types'; export const fetchAuthSession = ( - amplify: AmplifyClass, + amplify: AmplifyContext, options?: FetchAuthSessionOptions, ): Promise => { - return amplify.Auth.fetchAuthSession(options); + return amplify.fetchAuthSession(options); }; diff --git a/packages/core/src/singleton/apis/server/fetchAuthSession.ts b/packages/core/src/singleton/apis/server/fetchAuthSession.ts deleted file mode 100644 index 8bc0fe7ad5a..00000000000 --- a/packages/core/src/singleton/apis/server/fetchAuthSession.ts +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -import { AmplifyServer, getAmplifyServerContext } from '../../../adapterCore'; -import { AuthSession, FetchAuthSessionOptions } from '../../Auth/types'; -import { fetchAuthSession as fetchAuthSessionInternal } from '../internal/fetchAuthSession'; - -export const fetchAuthSession = ( - contextSpec: AmplifyServer.ContextSpec, - options?: FetchAuthSessionOptions, -): Promise => { - return fetchAuthSessionInternal( - getAmplifyServerContext(contextSpec).amplify, - options, - ); -}; diff --git a/packages/core/src/singleton/constants.ts b/packages/core/src/singleton/constants.ts index 714a7971edb..cf1406c9425 100644 --- a/packages/core/src/singleton/constants.ts +++ b/packages/core/src/singleton/constants.ts @@ -1,4 +1,2 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 - -export const ADD_OAUTH_LISTENER = Symbol('oauth-listener'); diff --git a/packages/core/src/singleton/index.ts b/packages/core/src/singleton/index.ts index 8712f32afa3..a5260338ec7 100644 --- a/packages/core/src/singleton/index.ts +++ b/packages/core/src/singleton/index.ts @@ -2,5 +2,19 @@ // SPDX-License-Identifier: Apache-2.0 export { AmplifyClass, Amplify } from './Amplify'; +export { AmplifyContext } from '../context/AmplifyContext'; export { fetchAuthSession } from './apis/fetchAuthSession'; export { clearCredentials } from './apis/clearCredentials'; + +// Context branding +export { + isAmplifyContext, + AMPLIFY_CONTEXT_BRAND, +} from '../context/contextBrand'; + +// Global context management (public read-only APIs) +export { + getActiveContext, + getGlobalContext, + hasGlobalContext, +} from '../context/globalContext'; diff --git a/packages/datastore-storage-adapter/__tests__/SQLiteAdapter.test.ts b/packages/datastore-storage-adapter/__tests__/SQLiteAdapter.test.ts index 9adaab2838a..a66026da6fb 100644 --- a/packages/datastore-storage-adapter/__tests__/SQLiteAdapter.test.ts +++ b/packages/datastore-storage-adapter/__tests__/SQLiteAdapter.test.ts @@ -31,6 +31,20 @@ jest.mock('@aws-amplify/datastore/src/sync/datastoreConnectivity', () => { }; }); +jest.mock('@aws-amplify/api/internals', () => { + const { Observable } = require('rxjs'); + + return { + InternalAPI: () => ({ + graphql: jest.fn().mockReturnValue( + new Observable(() => {}), + ), + getModuleName: jest.fn().mockReturnValue('InternalAPI'), + getGraphqlOperationType: jest.fn(), + }), + }; +}); + // TODO: move into generalized test suite helper? jest.mock('react-native-sqlite-storage', () => { return { diff --git a/packages/datastore-storage-adapter/package.json b/packages/datastore-storage-adapter/package.json index e633259c525..40b588699df 100644 --- a/packages/datastore-storage-adapter/package.json +++ b/packages/datastore-storage-adapter/package.json @@ -33,7 +33,7 @@ }, "homepage": "https://aws-amplify.github.io/", "peerDependencies": { - "@aws-amplify/core": "^6.16.2" + "@aws-amplify/core": "^6.16.3" }, "devDependencies": { "@aws-amplify/core": "6.16.3", diff --git a/packages/datastore/__tests__/authStrategies.test.ts b/packages/datastore/__tests__/authStrategies.test.ts index d52861c48ff..4dbd946bad3 100644 --- a/packages/datastore/__tests__/authStrategies.test.ts +++ b/packages/datastore/__tests__/authStrategies.test.ts @@ -1,4 +1,4 @@ -import { JWT, decodeJWT } from '@aws-amplify/core/internals/utils'; +import { decodeJWT } from '@aws-amplify/core/internals/utils'; import { InternalSchema, ModelAttributeAuthAllow, @@ -442,12 +442,12 @@ async function testMultiAuthStrategy({ hasAuthenticatedUser: boolean; result: any; }) { - mockCurrentUser({ hasAuthenticatedUser }); + const mockCtx = createMockCtx({ hasAuthenticatedUser }); const multiAuthStrategyWrapper = require('../src/authModeStrategies/multiAuthStrategy').multiAuthStrategy; - const multiAuthStrategy = multiAuthStrategyWrapper({}); + const multiAuthStrategy = multiAuthStrategyWrapper(mockCtx); const schema = getAuthSchema(authRules); @@ -533,22 +533,20 @@ function getAuthSchema( const mockedAccessToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c'; -function mockCurrentUser({ +function createMockCtx({ hasAuthenticatedUser, }: { hasAuthenticatedUser: boolean; }) { - jest.mock('@aws-amplify/core', () => ({ - async fetchAuthSession(): Promise<{ tokens?: { accessToken: JWT } }> { - if (hasAuthenticatedUser) { - return { - tokens: { - accessToken: decodeJWT(mockedAccessToken), - }, - }; - } else { - return {}; - } - }, - })); + return { + resourcesConfig: {}, + libraryOptions: {}, + fetchAuthSession: jest.fn().mockResolvedValue( + hasAuthenticatedUser + ? { tokens: { accessToken: decodeJWT(mockedAccessToken) } } + : {}, + ), + clearCredentials: jest.fn().mockResolvedValue(undefined), + getTokens: jest.fn().mockResolvedValue(undefined), + }; } diff --git a/packages/datastore/__tests__/mutation.test.ts b/packages/datastore/__tests__/mutation.test.ts index 47e435f6608..91f19fa64d6 100644 --- a/packages/datastore/__tests__/mutation.test.ts +++ b/packages/datastore/__tests__/mutation.test.ts @@ -30,13 +30,10 @@ import { import { createMutationInstanceFromModelOperation } from '../src/sync/utils'; import { SyncEngine, MutationEvent } from '../src/sync/'; -jest.mock('@aws-amplify/api/internals', () => { - const apiInternals = jest.requireActual('@aws-amplify/api/internals'); - apiInternals.InternalAPI._graphqlApi._api.post = mockRestPost; - return { - ...apiInternals, - }; -}); +jest.mock('@aws-amplify/api-rest/internals', () => ({ + ...jest.requireActual('@aws-amplify/api-rest/internals'), + post: mockRestPost, +})); // mocking jitteredBackoff to prevent it from retrying // endlessly in the mutation processor and so that we can expect the thrown result in our test // should throw a Network Error @@ -112,7 +109,6 @@ describe('MutationProcessor', () => { let mutationProcessor: MutationProcessor; beforeAll(async () => { - mutationProcessor = await instantiateMutationProcessor(); const awsconfig = { aws_project_region: 'us-west-2', aws_appsync_graphqlEndpoint: @@ -123,6 +119,7 @@ describe('MutationProcessor', () => { }; Amplify.configure(awsconfig); + mutationProcessor = await instantiateMutationProcessor(); }); afterEach(() => { @@ -199,9 +196,8 @@ describe('MutationProcessor', () => { await mutationProcessor.resume(); expect(mockRestPost).toHaveBeenCalledWith( expect.objectContaining({ - Auth: expect.any(Object), - configure: expect.any(Function), - getConfig: expect.any(Function), + resourcesConfig: expect.any(Object), + fetchAuthSession: expect.any(Function), }), expect.objectContaining({ url: new URL( @@ -232,7 +228,6 @@ describe('error handler', () => { beforeEach(async () => { errorHandler.mockClear(); - mutationProcessor = await instantiateMutationProcessor({ errorHandler }); const awsconfig = { aws_project_region: 'us-west-2', aws_appsync_graphqlEndpoint: @@ -243,6 +238,7 @@ describe('error handler', () => { }; Amplify.configure(awsconfig); + mutationProcessor = await instantiateMutationProcessor({ errorHandler }); }); test('newly required field', async () => { diff --git a/packages/datastore/__tests__/subscription.test.ts b/packages/datastore/__tests__/subscription.test.ts index e6de41bcc0a..3eb9f9f9c20 100644 --- a/packages/datastore/__tests__/subscription.test.ts +++ b/packages/datastore/__tests__/subscription.test.ts @@ -24,14 +24,12 @@ jest.mock('@aws-amplify/api/internals', () => { const actualInternalAPIModule = jest.requireActual( '@aws-amplify/api/internals', ); - const actualInternalAPIInstance = actualInternalAPIModule.InternalAPI; return { ...actualInternalAPIModule, - InternalAPI: { - ...actualInternalAPIInstance, + InternalAPI: () => ({ graphql: mockGraphQL, - }, + }), }; }); diff --git a/packages/datastore/__tests__/sync.test.ts b/packages/datastore/__tests__/sync.test.ts index 6bc74724df0..89ff7a0ad87 100644 --- a/packages/datastore/__tests__/sync.test.ts +++ b/packages/datastore/__tests__/sync.test.ts @@ -450,14 +450,12 @@ function jitteredRetrySyncProcessorSetup({ const actualInternalAPIModule = jest.requireActual( '@aws-amplify/api/internals', ); - const actualInternalAPIInstance = actualInternalAPIModule.InternalAPI; return { ...actualInternalAPIModule, - InternalAPI: { - ...actualInternalAPIInstance, + InternalAPI: () => ({ graphql: mockGraphQl, - }, + }), }; }); diff --git a/packages/datastore/package.json b/packages/datastore/package.json index 0a4c6cce328..b7a6407b972 100644 --- a/packages/datastore/package.json +++ b/packages/datastore/package.json @@ -54,7 +54,7 @@ "ulid": "^2.3.0" }, "peerDependencies": { - "@aws-amplify/core": "^6.16.2" + "@aws-amplify/core": "^6.16.3" }, "devDependencies": { "@aws-amplify/core": "6.16.3", diff --git a/packages/datastore/src/authModeStrategies/multiAuthStrategy.ts b/packages/datastore/src/authModeStrategies/multiAuthStrategy.ts index c850b59da5c..05b256a09d0 100644 --- a/packages/datastore/src/authModeStrategies/multiAuthStrategy.ts +++ b/packages/datastore/src/authModeStrategies/multiAuthStrategy.ts @@ -1,10 +1,12 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { fetchAuthSession } from '@aws-amplify/core'; -import { GraphQLAuthMode } from '@aws-amplify/core/internals/utils'; +import { AmplifyContext as CoreAmplifyContext } from '@aws-amplify/core'; +import { + GraphQLAuthMode, + fetchAuthSession, +} from '@aws-amplify/core/internals/utils'; import { - AmplifyContext, AuthModeStrategy, ModelAttributeAuthAllow, ModelAttributeAuthProperty, @@ -139,13 +141,13 @@ function getAuthRules({ * @returns A sorted array of auth modes to attempt. */ export const multiAuthStrategy: ( - amplifyContext: AmplifyContext, + amplifyContext: CoreAmplifyContext, ) => AuthModeStrategy = - () => + amplifyContext => async ({ schema, modelName }) => { let currentUser; try { - const authSession = await fetchAuthSession(); + const authSession = await fetchAuthSession(amplifyContext); if (authSession.tokens.accessToken) { // the user is authenticated currentUser = authSession; diff --git a/packages/datastore/src/datastore/datastore.ts b/packages/datastore/src/datastore/datastore.ts index b2ba15eb6b5..6b56f3ca04f 100644 --- a/packages/datastore/src/datastore/datastore.ts +++ b/packages/datastore/src/datastore/datastore.ts @@ -2,7 +2,13 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 import { InternalAPI } from '@aws-amplify/api/internals'; -import { Amplify, Cache, ConsoleLogger, Hub } from '@aws-amplify/core'; +import { + Cache, + ConsoleLogger, + Hub, + getActiveContext, + hasGlobalContext, +} from '@aws-amplify/core'; import { Draft, Patch, @@ -1394,7 +1400,6 @@ enum DataStoreState { // https://github.com/aws-amplify/amplify-js/pull/10477/files#r1007363485 class DataStore { // reference to configured category instances. Used for preserving SSR context - private InternalAPI = InternalAPI; private Cache = Cache; // Non-null assertions (bang operator) have been added to most of these properties @@ -1425,7 +1430,7 @@ class DataStore { private storageAdapter!: Adapter; // object that gets passed to descendent classes. Allows us to pass these down by reference private amplifyContext: AmplifyContext = { - InternalAPI: this.InternalAPI, + InternalAPI: undefined as any, }; private connectivityMonitor?: DataStoreConnectivity; @@ -2457,7 +2462,10 @@ class DataStore { }; configure = (config: DataStoreConfig = {}) => { - this.amplifyContext.InternalAPI = this.InternalAPI; + const ctx = hasGlobalContext() + ? getActiveContext() + : ({ resourcesConfig: {}, libraryOptions: {} } as any); + this.amplifyContext.InternalAPI = InternalAPI(ctx); const { DataStore: configDataStore, @@ -2471,7 +2479,8 @@ class DataStore { ...configFromAmplify } = config; - const currentAppSyncConfig = Amplify.getConfig().API?.GraphQL; + const currentAppSyncConfig = (this.amplifyContext as any).resourcesConfig + ?.API?.GraphQL; const appSyncConfig = { aws_appsync_graphqlEndpoint: currentAppSyncConfig?.endpoint, @@ -2496,7 +2505,7 @@ class DataStore { switch (authModeStrategyType) { case AuthModeStrategyType.MULTI_AUTH: - this.authModeStrategy = multiAuthStrategy(this.amplifyContext); + this.authModeStrategy = multiAuthStrategy(this.amplifyContext as any); break; case AuthModeStrategyType.DEFAULT: this.authModeStrategy = defaultAuthStrategy; diff --git a/packages/datastore/src/sync/processors/mutation.ts b/packages/datastore/src/sync/processors/mutation.ts index 556fb46c261..b76b03f796d 100644 --- a/packages/datastore/src/sync/processors/mutation.ts +++ b/packages/datastore/src/sync/processors/mutation.ts @@ -13,7 +13,11 @@ import { retry, } from '@aws-amplify/core/internals/utils'; import { Observable, Observer } from 'rxjs'; -import { ConsoleLogger } from '@aws-amplify/core'; +import { + ConsoleLogger, + getActiveContext, + hasGlobalContext, +} from '@aws-amplify/core'; import { MutationEvent } from '../'; import { ModelInstanceCreator } from '../../datastore/datastore'; @@ -92,7 +96,12 @@ class MutationProcessor { private readonly amplifyContext: AmplifyContext, ) { this.amplifyContext.InternalAPI = - this.amplifyContext.InternalAPI || InternalAPI; + this.amplifyContext.InternalAPI || + InternalAPI( + hasGlobalContext() + ? getActiveContext() + : ({ resourcesConfig: {}, libraryOptions: {} } as any), + ); this.generateQueries(); } diff --git a/packages/datastore/src/sync/processors/subscription.ts b/packages/datastore/src/sync/processors/subscription.ts index c508c8d5885..7bdf25219f4 100644 --- a/packages/datastore/src/sync/processors/subscription.ts +++ b/packages/datastore/src/sync/processors/subscription.ts @@ -6,7 +6,8 @@ import { ConsoleLogger, Hub, HubCapsule, - fetchAuthSession, + getActiveContext, + hasGlobalContext, } from '@aws-amplify/core'; import { BackgroundProcessManager, @@ -89,7 +90,11 @@ class SubscriptionProcessor { private readonly authModeStrategy: AuthModeStrategy, private readonly errorHandler: ErrorHandler, private readonly amplifyContext: AmplifyContext = { - InternalAPI, + InternalAPI: InternalAPI( + hasGlobalContext() + ? getActiveContext() + : ({ resourcesConfig: {}, libraryOptions: {} } as any), + ), }, ) {} @@ -270,7 +275,9 @@ class SubscriptionProcessor { this.runningProcesses.add(async () => { try { // retrieving current AWS Credentials - const credentials = (await fetchAuthSession()).tokens?.accessToken; + const credentials = ( + await (this.amplifyContext as any).fetchAuthSession() + ).tokens?.accessToken; userCredentials = credentials ? USER_CREDENTIALS.auth : USER_CREDENTIALS.unauth; @@ -280,7 +287,7 @@ class SubscriptionProcessor { try { // retrieving current token info from Cognito UserPools - const session = await fetchAuthSession(); + const session = await (this.amplifyContext as any).fetchAuthSession(); oidcTokenPayload = session.tokens?.idToken?.payload; } catch (err) { // best effort to get jwt from Cognito diff --git a/packages/datastore/src/sync/processors/sync.ts b/packages/datastore/src/sync/processors/sync.ts index 319e153cb50..25e0a4fd8a6 100644 --- a/packages/datastore/src/sync/processors/sync.ts +++ b/packages/datastore/src/sync/processors/sync.ts @@ -12,7 +12,12 @@ import { NonRetryableError, jitteredExponentialRetry, } from '@aws-amplify/core/internals/utils'; -import { ConsoleLogger, Hub } from '@aws-amplify/core'; +import { + ConsoleLogger, + Hub, + getActiveContext, + hasGlobalContext, +} from '@aws-amplify/core'; import { AmplifyContext, @@ -62,7 +67,13 @@ class SyncProcessor { private readonly errorHandler: ErrorHandler, private readonly amplifyContext: AmplifyContext, ) { - amplifyContext.InternalAPI = amplifyContext.InternalAPI || InternalAPI; + amplifyContext.InternalAPI = + amplifyContext.InternalAPI || + InternalAPI( + hasGlobalContext() + ? getActiveContext() + : ({ resourcesConfig: {}, libraryOptions: {} } as any), + ); this.generateQueries(); } diff --git a/packages/datastore/src/types.ts b/packages/datastore/src/types.ts index 1bd59d78f6c..b28dd48e296 100644 --- a/packages/datastore/src/types.ts +++ b/packages/datastore/src/types.ts @@ -1,6 +1,5 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { InternalAPI } from '@aws-amplify/api/internals'; import { GraphQLAuthMode } from '@aws-amplify/core/internals/utils'; import { ModelInstanceCreator } from './datastore/datastore'; @@ -1232,7 +1231,7 @@ export enum LimitTimerRaceResolvedValues { // #endregion export interface AmplifyContext { - InternalAPI: typeof InternalAPI; + InternalAPI: any; } // #region V5 predicate types diff --git a/packages/geo/__tests__/Geo.test.ts b/packages/geo/__tests__/Geo.test.ts index a973e168436..922f518e014 100644 --- a/packages/geo/__tests__/Geo.test.ts +++ b/packages/geo/__tests__/Geo.test.ts @@ -1,6 +1,5 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { Amplify, fetchAuthSession } from '@aws-amplify/core'; import { GetPlaceCommand, LocationClient, @@ -15,6 +14,9 @@ import { AmazonLocationServiceProvider } from '../src/providers/location-service import { AmazonLocationServiceMapStyle, Coordinates, + Geofence, + Place, + SaveGeofencesResults, SearchByCoordinatesOptions, SearchByTextOptions, } from '../src/types'; @@ -24,7 +26,6 @@ import { awsConfig, awsConfigGeoV4, batchGeofencesCamelcaseResults, - credentials, singleGeofenceCamelcaseResults, testPlaceCamelCase, validGeofence1, @@ -32,6 +33,7 @@ import { validGeometry, } from './testData'; import { + createMockAmplifyContext, mockBatchPutGeofenceCommand, mockGetGeofenceCommand, mockListGeofencesCommand, @@ -70,18 +72,6 @@ LocationClient.prototype.send = jest.fn(async command => { } }); -jest.mock('@aws-amplify/core', () => { - const originalModule = jest.requireActual('@aws-amplify/core'); - - return { - ...originalModule, - fetchAuthSession: jest.fn(), - Amplify: { - getConfig: jest.fn(), - }, - }; -}); - describe('Geo', () => { afterEach(() => { jest.restoreAllMocks(); @@ -89,8 +79,8 @@ describe('Geo', () => { }); describe('getModuleName', () => { - (Amplify.getConfig as jest.Mock).mockReturnValue(awsConfigGeoV4); - const geo = new GeoClass(); + const mockCtx = createMockAmplifyContext(awsConfigGeoV4); + const geo = new GeoClass(mockCtx); const moduleName = geo.getModuleName(); expect(moduleName).toBe('Geo'); @@ -98,9 +88,9 @@ describe('Geo', () => { describe('pluggables', () => { test('getPluggable', () => { - (Amplify.getConfig as jest.Mock).mockReturnValue(awsConfigGeoV4); - const geo = new GeoClass(); - const provider = new AmazonLocationServiceProvider(); + const mockCtx = createMockAmplifyContext(awsConfigGeoV4); + const geo = new GeoClass(mockCtx); + const provider = new AmazonLocationServiceProvider(mockCtx); geo.addPluggable(provider); expect(geo.getPluggable(provider.getProviderName())).toBeInstanceOf( @@ -109,9 +99,9 @@ describe('Geo', () => { }); test('removePluggable', () => { - (Amplify.getConfig as jest.Mock).mockReturnValue(awsConfigGeoV4); - const geo = new GeoClass(); - const provider = new AmazonLocationServiceProvider(); + const mockCtx = createMockAmplifyContext(awsConfigGeoV4); + const geo = new GeoClass(mockCtx); + const provider = new AmazonLocationServiceProvider(mockCtx); geo.addPluggable(provider); geo.removePluggable(provider.getProviderName()); @@ -123,8 +113,8 @@ describe('Geo', () => { describe('AmazonLocationService is used as default provider', () => { test('creates the proper default provider', () => { - (Amplify.getConfig as jest.Mock).mockReturnValue(awsConfigGeoV4); - const geo = new GeoClass(); + const mockCtx = createMockAmplifyContext(awsConfigGeoV4); + const geo = new GeoClass(mockCtx); expect(geo.getPluggable('AmazonLocationService')).toBeInstanceOf( AmazonLocationServiceProvider, ); @@ -133,12 +123,8 @@ describe('Geo', () => { describe('get map resources', () => { test('should fail if there is no provider', async () => { - (fetchAuthSession as jest.Mock).mockImplementationOnce(() => { - return Promise.resolve({ credentials }); - }); - - (Amplify.getConfig as jest.Mock).mockReturnValue(awsConfigGeoV4); - const geo = new GeoClass(); + const mockCtx = createMockAmplifyContext(awsConfigGeoV4); + const geo = new GeoClass(mockCtx); geo.removePluggable('AmazonLocationService'); expect(() => geo.getAvailableMaps()).toThrow( @@ -150,10 +136,10 @@ describe('Geo', () => { }); test('should tell you if there are no available map resources', () => { - (Amplify.getConfig as jest.Mock).mockReturnValue({ + const mockCtx = createMockAmplifyContext({ Geo: { LocationService: {} }, }); - const geo = new GeoClass(); + const geo = new GeoClass(mockCtx); expect(() => geo.getAvailableMaps()).toThrow( "No map resources found in amplify config, run 'amplify add geo' to create one and run `amplify push` after", @@ -161,8 +147,8 @@ describe('Geo', () => { }); test('should get all available map resources', () => { - (Amplify.getConfig as jest.Mock).mockReturnValue(awsConfigGeoV4); - const geo = new GeoClass(); + const mockCtx = createMockAmplifyContext(awsConfigGeoV4); + const geo = new GeoClass(mockCtx); const maps: AmazonLocationServiceMapStyle[] = []; const availableMaps = awsConfig.geo.amazon_location_service.maps.items; @@ -177,8 +163,8 @@ describe('Geo', () => { }); test('should fail gracefully if no config is found', () => { - (Amplify.getConfig as jest.Mock).mockReturnValue({}); - const geo = new GeoClass(); + const mockCtx = createMockAmplifyContext({}); + const geo = new GeoClass(mockCtx); expect(() => geo.getDefaultMap()).toThrow( "No Geo configuration found in amplify config, run 'amplify add geo' to create one and run `amplify push` after", @@ -186,10 +172,10 @@ describe('Geo', () => { }); test('should tell you if there is no map resources when running getDefaultMap', () => { - (Amplify.getConfig as jest.Mock).mockReturnValue({ + const mockCtx = createMockAmplifyContext({ Geo: { LocationService: {} }, }); - const geo = new GeoClass(); + const geo = new GeoClass(mockCtx); expect(() => geo.getDefaultMap()).toThrow( "No map resources found in amplify config, run 'amplify add geo' to create one and run `amplify push` after", @@ -197,14 +183,14 @@ describe('Geo', () => { }); test('should tell you if there is no default map resources (but there are maps) when running getDefaultMap', () => { - (Amplify.getConfig as jest.Mock).mockReturnValue({ + const mockCtx = createMockAmplifyContext({ Geo: { LocationService: { maps: { items: { testMap: { style: 'teststyle' } } }, }, }, } as any); - const geo = new GeoClass(); + const geo = new GeoClass(mockCtx); expect(() => geo.getDefaultMap()).toThrow( "No default map resource found in amplify config, run 'amplify add geo' to create one and run `amplify push` after", @@ -212,8 +198,8 @@ describe('Geo', () => { }); test('should get the default map resource', () => { - (Amplify.getConfig as jest.Mock).mockReturnValue(awsConfigGeoV4); - const geo = new GeoClass(); + const mockCtx = createMockAmplifyContext(awsConfigGeoV4); + const geo = new GeoClass(mockCtx); const mapName = awsConfig.geo.amazon_location_service.maps.default; const { style } = @@ -230,12 +216,8 @@ describe('Geo', () => { const testString = 'star'; test('should search with just text input', async () => { - (fetchAuthSession as jest.Mock).mockImplementationOnce(() => { - return Promise.resolve({ credentials }); - }); - - (Amplify.getConfig as jest.Mock).mockReturnValue(awsConfigGeoV4); - const geo = new GeoClass(); + const mockCtx = createMockAmplifyContext(awsConfigGeoV4); + const geo = new GeoClass(mockCtx); const results = await geo.searchByText(testString); expect(results).toEqual([testPlaceCamelCase]); @@ -249,12 +231,8 @@ describe('Geo', () => { }); test('should search using given options with biasPosition', async () => { - (fetchAuthSession as jest.Mock).mockImplementationOnce(() => { - return Promise.resolve({ credentials }); - }); - - (Amplify.getConfig as jest.Mock).mockReturnValue(awsConfigGeoV4); - const geo = new GeoClass(); + const mockCtx = createMockAmplifyContext(awsConfigGeoV4); + const geo = new GeoClass(mockCtx); const searchOptions: SearchByTextOptions = { biasPosition: [12345, 67890], @@ -279,12 +257,8 @@ describe('Geo', () => { }); test('should search using given options with searchAreaConstraints', async () => { - (fetchAuthSession as jest.Mock).mockImplementationOnce(() => { - return Promise.resolve({ credentials }); - }); - - (Amplify.getConfig as jest.Mock).mockReturnValue(awsConfigGeoV4); - const geo = new GeoClass(); + const mockCtx = createMockAmplifyContext(awsConfigGeoV4); + const geo = new GeoClass(mockCtx); const searchOptions: SearchByTextOptions = { searchAreaConstraints: [123, 456, 789, 321], @@ -307,12 +281,8 @@ describe('Geo', () => { }); test('should throw an error if both BiasPosition and SearchAreaConstraints are given in the options', async () => { - (fetchAuthSession as jest.Mock).mockImplementationOnce(() => { - return Promise.resolve({ credentials }); - }); - - (Amplify.getConfig as jest.Mock).mockReturnValue(awsConfigGeoV4); - const geo = new GeoClass(); + const mockCtx = createMockAmplifyContext(awsConfigGeoV4); + const geo = new GeoClass(mockCtx); const searchOptions: SearchByTextOptions = { countries: ['USA'], @@ -328,12 +298,8 @@ describe('Geo', () => { }); test('should fail if there is no provider', async () => { - (fetchAuthSession as jest.Mock).mockImplementationOnce(() => { - return Promise.resolve({ credentials }); - }); - - (Amplify.getConfig as jest.Mock).mockReturnValue(awsConfigGeoV4); - const geo = new GeoClass(); + const mockCtx = createMockAmplifyContext(awsConfigGeoV4); + const geo = new GeoClass(mockCtx); geo.removePluggable('AmazonLocationService'); await expect(geo.searchByText(testString)).rejects.toThrow( @@ -347,12 +313,8 @@ describe('Geo', () => { const testResults = camelcaseKeys(TestPlacePascalCase, { deep: true }); test('should search with PlaceId as input', async () => { - (fetchAuthSession as jest.Mock).mockImplementationOnce(() => { - return Promise.resolve({ credentials }); - }); - - (Amplify.getConfig as jest.Mock).mockReturnValue(awsConfigGeoV4); - const geo = new GeoClass(); + const mockCtx = createMockAmplifyContext(awsConfigGeoV4); + const geo = new GeoClass(mockCtx); const results = await geo.searchByPlaceId(testPlaceId); expect(results).toEqual(testResults); @@ -366,12 +328,8 @@ describe('Geo', () => { }); test('should fail if there is no provider', async () => { - (fetchAuthSession as jest.Mock).mockImplementationOnce(() => { - return Promise.resolve({ credentials }); - }); - - (Amplify.getConfig as jest.Mock).mockReturnValue(awsConfigGeoV4); - const geo = new GeoClass(); + const mockCtx = createMockAmplifyContext(awsConfigGeoV4); + const geo = new GeoClass(mockCtx); geo.removePluggable('AmazonLocationService'); await expect(geo.searchByPlaceId(testPlaceId)).rejects.toThrow( @@ -393,12 +351,8 @@ describe('Geo', () => { ]; test('should search with just text input', async () => { - (fetchAuthSession as jest.Mock).mockImplementationOnce(() => { - return Promise.resolve({ credentials }); - }); - - (Amplify.getConfig as jest.Mock).mockReturnValue(awsConfigGeoV4); - const geo = new GeoClass(); + const mockCtx = createMockAmplifyContext(awsConfigGeoV4); + const geo = new GeoClass(mockCtx); const results = await geo.searchForSuggestions(testString); expect(results).toEqual(testResults); @@ -412,12 +366,8 @@ describe('Geo', () => { }); test('should search using given options with biasPosition', async () => { - (fetchAuthSession as jest.Mock).mockImplementationOnce(() => { - return Promise.resolve({ credentials }); - }); - - (Amplify.getConfig as jest.Mock).mockReturnValue(awsConfigGeoV4); - const geo = new GeoClass(); + const mockCtx = createMockAmplifyContext(awsConfigGeoV4); + const geo = new GeoClass(mockCtx); const searchOptions: SearchByTextOptions = { biasPosition: [12345, 67890], @@ -440,12 +390,8 @@ describe('Geo', () => { }); test('should search using given options with searchAreaConstraints', async () => { - (fetchAuthSession as jest.Mock).mockImplementationOnce(() => { - return Promise.resolve({ credentials }); - }); - - (Amplify.getConfig as jest.Mock).mockReturnValue(awsConfigGeoV4); - const geo = new GeoClass(); + const mockCtx = createMockAmplifyContext(awsConfigGeoV4); + const geo = new GeoClass(mockCtx); const searchOptions: SearchByTextOptions = { searchAreaConstraints: [123, 456, 789, 321], @@ -468,12 +414,8 @@ describe('Geo', () => { }); test('should throw an error if both BiasPosition and SearchAreaConstraints are given in the options', async () => { - (fetchAuthSession as jest.Mock).mockImplementationOnce(() => { - return Promise.resolve({ credentials }); - }); - - (Amplify.getConfig as jest.Mock).mockReturnValue(awsConfigGeoV4); - const geo = new GeoClass(); + const mockCtx = createMockAmplifyContext(awsConfigGeoV4); + const geo = new GeoClass(mockCtx); const searchOptions: SearchByTextOptions = { countries: ['USA'], @@ -491,12 +433,8 @@ describe('Geo', () => { }); test('should fail if there is no provider', async () => { - (fetchAuthSession as jest.Mock).mockImplementationOnce(() => { - return Promise.resolve({ credentials }); - }); - - (Amplify.getConfig as jest.Mock).mockReturnValue(awsConfigGeoV4); - const geo = new GeoClass(); + const mockCtx = createMockAmplifyContext(awsConfigGeoV4); + const geo = new GeoClass(mockCtx); geo.removePluggable('AmazonLocationService'); await expect(geo.searchForSuggestions(testString)).rejects.toThrow( @@ -509,12 +447,8 @@ describe('Geo', () => { const testCoordinates: Coordinates = [45, 90]; test('should search with just coordinate input', async () => { - (fetchAuthSession as jest.Mock).mockImplementationOnce(() => { - return Promise.resolve({ credentials }); - }); - - (Amplify.getConfig as jest.Mock).mockReturnValue(awsConfigGeoV4); - const geo = new GeoClass(); + const mockCtx = createMockAmplifyContext(awsConfigGeoV4); + const geo = new GeoClass(mockCtx); const results = await geo.searchByCoordinates(testCoordinates); expect(results).toEqual(testPlaceCamelCase); @@ -528,12 +462,8 @@ describe('Geo', () => { }); test('should search using options when given', async () => { - (fetchAuthSession as jest.Mock).mockImplementationOnce(() => { - return Promise.resolve({ credentials }); - }); - - (Amplify.getConfig as jest.Mock).mockReturnValue(awsConfigGeoV4); - const geo = new GeoClass(); + const mockCtx = createMockAmplifyContext(awsConfigGeoV4); + const geo = new GeoClass(mockCtx); const searchOptions: SearchByCoordinatesOptions = { maxResults: 40, @@ -555,12 +485,8 @@ describe('Geo', () => { }); test('should fail if there is no provider', async () => { - (fetchAuthSession as jest.Mock).mockImplementationOnce(() => { - return Promise.resolve({ credentials }); - }); - - (Amplify.getConfig as jest.Mock).mockReturnValue(awsConfigGeoV4); - const geo = new GeoClass(); + const mockCtx = createMockAmplifyContext(awsConfigGeoV4); + const geo = new GeoClass(mockCtx); geo.removePluggable('AmazonLocationService'); await expect(geo.searchByCoordinates(testCoordinates)).rejects.toThrow( @@ -571,16 +497,12 @@ describe('Geo', () => { describe('saveGeofences', () => { test('saveGeofences with a single geofence', async () => { - (fetchAuthSession as jest.Mock).mockImplementationOnce(() => { - return Promise.resolve({ credentials }); - }); - LocationClient.prototype.send = jest .fn() .mockImplementationOnce(mockBatchPutGeofenceCommand); - (Amplify.getConfig as jest.Mock).mockReturnValue(awsConfigGeoV4); - const geo = new GeoClass(); + const mockCtx = createMockAmplifyContext(awsConfigGeoV4); + const geo = new GeoClass(mockCtx); // Check that results are what's expected const results = await geo.saveGeofences(validGeofence1); @@ -604,16 +526,12 @@ describe('Geo', () => { }); test('saveGeofences with multiple geofences', async () => { - (fetchAuthSession as jest.Mock).mockImplementationOnce(() => { - return Promise.resolve({ credentials }); - }); - LocationClient.prototype.send = jest .fn() .mockImplementation(mockBatchPutGeofenceCommand); - (Amplify.getConfig as jest.Mock).mockReturnValue(awsConfigGeoV4); - const geo = new GeoClass(); + const mockCtx = createMockAmplifyContext(awsConfigGeoV4); + const geo = new GeoClass(mockCtx); // Check that results are what's expected const results = await geo.saveGeofences(validGeofences); @@ -627,12 +545,8 @@ describe('Geo', () => { }); test('should fail if there is no provider', async () => { - (fetchAuthSession as jest.Mock).mockImplementationOnce(() => { - return Promise.resolve({ credentials }); - }); - - (Amplify.getConfig as jest.Mock).mockReturnValue(awsConfigGeoV4); - const geo = new GeoClass(); + const mockCtx = createMockAmplifyContext(awsConfigGeoV4); + const geo = new GeoClass(mockCtx); geo.removePluggable('AmazonLocationService'); await expect(geo.saveGeofences(validGeofence1)).rejects.toThrow( @@ -643,16 +557,12 @@ describe('Geo', () => { describe('getGeofence', () => { test('getGeofence returns the right geofence', async () => { - (fetchAuthSession as jest.Mock).mockImplementationOnce(() => { - return Promise.resolve({ credentials }); - }); - LocationClient.prototype.send = jest .fn() .mockImplementationOnce(mockGetGeofenceCommand); - (Amplify.getConfig as jest.Mock).mockReturnValue(awsConfigGeoV4); - const geo = new GeoClass(); + const mockCtx = createMockAmplifyContext(awsConfigGeoV4); + const geo = new GeoClass(mockCtx); // Check that results are what's expected const results = await geo.getGeofence('testGeofenceId'); @@ -678,16 +588,12 @@ describe('Geo', () => { describe('listGeofences', () => { test('listGeofences gets the first 100 geofences when no arguments are given', async () => { - (fetchAuthSession as jest.Mock).mockImplementationOnce(() => { - return Promise.resolve({ credentials }); - }); - LocationClient.prototype.send = jest .fn() .mockImplementationOnce(mockListGeofencesCommand); - (Amplify.getConfig as jest.Mock).mockReturnValue(awsConfigGeoV4); - const geo = new GeoClass(); + const mockCtx = createMockAmplifyContext(awsConfigGeoV4); + const geo = new GeoClass(mockCtx); // Check that results are what's expected const results = await geo.listGeofences(); @@ -695,16 +601,12 @@ describe('Geo', () => { }); test('listGeofences gets the second 100 geofences when nextToken is passed', async () => { - (fetchAuthSession as jest.Mock).mockImplementationOnce(() => { - return Promise.resolve({ credentials }); - }); - LocationClient.prototype.send = jest .fn() .mockImplementation(mockListGeofencesCommand); - (Amplify.getConfig as jest.Mock).mockReturnValue(awsConfigGeoV4); - const geo = new GeoClass(); + const mockCtx = createMockAmplifyContext(awsConfigGeoV4); + const geo = new GeoClass(mockCtx); // Check that results are what's expected @@ -724,3 +626,168 @@ describe('Geo', () => { }); }); }); + +describe('GeoClass static methods', () => { + const mockCtx = createMockAmplifyContext(awsConfigGeoV4); + + afterEach(() => { + jest.restoreAllMocks(); + jest.clearAllMocks(); + }); + + test('searchByText delegates to instance method', async () => { + const expected = [testPlaceCamelCase] as unknown as Place[]; + const spy = jest + .spyOn(GeoClass.prototype, 'searchByText') + .mockResolvedValue(expected); + const result = await GeoClass.searchByText(mockCtx, 'star'); + expect(result).toEqual(expected); + expect(spy).toHaveBeenCalledWith('star', undefined); + }); + + test('searchForSuggestions delegates to instance method', async () => { + const expected = [{ text: 'star', placeId: 'a1b2c3d4' }]; + const spy = jest + .spyOn(GeoClass.prototype, 'searchForSuggestions') + .mockResolvedValue(expected); + const result = await GeoClass.searchForSuggestions(mockCtx, 'star'); + expect(result).toEqual(expected); + expect(spy).toHaveBeenCalledWith('star', undefined); + }); + + test('searchByPlaceId delegates to instance method', async () => { + const expected = testPlaceCamelCase as unknown as Place; + const spy = jest + .spyOn(GeoClass.prototype, 'searchByPlaceId') + .mockResolvedValue(expected); + const result = await GeoClass.searchByPlaceId(mockCtx, 'a1b2c3d4'); + expect(result).toEqual(expected); + expect(spy).toHaveBeenCalledWith('a1b2c3d4', undefined); + }); + + test('searchByCoordinates delegates to instance method', async () => { + const coords: Coordinates = [45, 90]; + const expected = testPlaceCamelCase as unknown as Place; + const spy = jest + .spyOn(GeoClass.prototype, 'searchByCoordinates') + .mockResolvedValue(expected); + const result = await GeoClass.searchByCoordinates(mockCtx, coords); + expect(result).toEqual(expected); + expect(spy).toHaveBeenCalledWith(coords, undefined); + }); + + test('getAvailableMaps delegates to instance method', () => { + const expected = [ + { + mapName: 'geoJsExampleMap1', + style: 'VectorEsriStreets', + region: 'us-west-2', + }, + ]; + const spy = jest + .spyOn(GeoClass.prototype, 'getAvailableMaps') + .mockReturnValue(expected); + const result = GeoClass.getAvailableMaps(mockCtx); + expect(result).toEqual(expected); + expect(spy).toHaveBeenCalledWith(undefined); + }); + + test('getDefaultMap delegates to instance method', () => { + const expected = { + mapName: 'geoJsExampleMap1', + style: 'VectorEsriStreets', + region: 'us-west-2', + }; + const spy = jest + .spyOn(GeoClass.prototype, 'getDefaultMap') + .mockReturnValue(expected); + const result = GeoClass.getDefaultMap(mockCtx); + expect(result).toEqual(expected); + expect(spy).toHaveBeenCalledWith(undefined); + }); + + test('saveGeofences delegates to instance method', async () => { + const expected = + singleGeofenceCamelcaseResults as unknown as SaveGeofencesResults; + const spy = jest + .spyOn(GeoClass.prototype, 'saveGeofences') + .mockResolvedValue(expected); + const result = await GeoClass.saveGeofences(mockCtx, validGeofence1); + expect(result).toEqual(expected); + expect(spy).toHaveBeenCalledWith(validGeofence1, undefined); + }); + + test('getGeofence delegates to instance method', async () => { + const expected = { + geofenceId: 'testId', + geometry: validGeometry, + createTime: '2020-04-01T21:00:00.000Z', + updateTime: '2020-04-01T21:00:00.000Z', + status: 'ACTIVE', + } as unknown as Geofence; + const spy = jest + .spyOn(GeoClass.prototype, 'getGeofence') + .mockResolvedValue(expected); + const result = await GeoClass.getGeofence(mockCtx, 'testId'); + expect(result).toEqual(expected); + expect(spy).toHaveBeenCalledWith('testId', undefined); + }); + + test('listGeofences delegates to instance method', async () => { + const expected = { entries: [], nextToken: undefined }; + const spy = jest + .spyOn(GeoClass.prototype, 'listGeofences') + .mockResolvedValue(expected); + const result = await GeoClass.listGeofences(mockCtx); + expect(result).toEqual(expected); + expect(spy).toHaveBeenCalledWith(undefined); + }); + + test('deleteGeofences delegates to instance method', async () => { + const expected = { successes: ['id1'], errors: [] }; + const spy = jest + .spyOn(GeoClass.prototype, 'deleteGeofences') + .mockResolvedValue(expected); + const result = await GeoClass.deleteGeofences(mockCtx, ['id1']); + expect(result).toEqual(expected); + expect(spy).toHaveBeenCalledWith(['id1'], undefined); + }); + + test('static methods pass options through', async () => { + const spy = jest + .spyOn(GeoClass.prototype, 'searchByText') + .mockResolvedValue([]); + const opts: SearchByTextOptions = { maxResults: 5 }; + await GeoClass.searchByText(mockCtx, 'test', opts); + expect(spy).toHaveBeenCalledWith('test', opts); + }); +}); + +describe('GeoClass instance deleteGeofences', () => { + afterEach(() => { + jest.restoreAllMocks(); + jest.clearAllMocks(); + }); + + test('deleteGeofences calls provider', async () => { + LocationClient.prototype.send = jest.fn().mockResolvedValue({ + Errors: [], + }); + + const mockCtx = createMockAmplifyContext(awsConfigGeoV4); + const geo = new GeoClass(mockCtx); + + const result = await geo.deleteGeofences(['testId']); + expect(result.successes).toContain('testId'); + }); + + test('deleteGeofences should fail if there is no provider', async () => { + const mockCtx = createMockAmplifyContext(awsConfigGeoV4); + const geo = new GeoClass(mockCtx); + geo.removePluggable('AmazonLocationService'); + + await expect(geo.deleteGeofences(['testId'])).rejects.toThrow( + 'No plugin found in Geo for the provider', + ); + }); +}); diff --git a/packages/geo/__tests__/Providers/AmazonLocationServiceProvider.test.ts b/packages/geo/__tests__/Providers/AmazonLocationServiceProvider.test.ts index 506f0decf24..2986ba12d40 100644 --- a/packages/geo/__tests__/Providers/AmazonLocationServiceProvider.test.ts +++ b/packages/geo/__tests__/Providers/AmazonLocationServiceProvider.test.ts @@ -1,6 +1,5 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { Amplify, fetchAuthSession } from '@aws-amplify/core'; import { GetPlaceCommand, LocationClient, @@ -17,13 +16,13 @@ import { awsConfigGeoV4, batchGeofencesCamelcaseResults, clockwiseGeofence, - credentials, testPlaceCamelCase, validGeofences, validGeometry, } from '../testData'; import { createGeofenceInputArray, + createMockAmplifyContext, mockBatchPutGeofenceCommand, mockDeleteGeofencesCommand, mockGetGeofenceCommand, @@ -69,55 +68,42 @@ LocationClient.prototype.send = jest.fn(async command => { } }); -jest.mock('@aws-amplify/core', () => { - const originalModule = jest.requireActual('@aws-amplify/core'); - - return { - ...originalModule, - fetchAuthSession: jest.fn(), - Amplify: { - getConfig: jest.fn(), - }, - }; -}); - describe('AmazonLocationServiceProvider', () => { afterEach(() => { jest.restoreAllMocks(); jest.clearAllMocks(); }); - beforeEach(() => { - (Amplify.getConfig as jest.Mock).mockReturnValue(awsConfigGeoV4); - }); - describe('getCategory', () => { test('should return "Geo" when asked for category', () => { - const geo = new AmazonLocationServiceProvider(); + const mockCtx = createMockAmplifyContext(); + const geo = new AmazonLocationServiceProvider(mockCtx); expect(geo.getCategory()).toBe('Geo'); }); }); describe('getProviderName', () => { test('should return "AmazonLocationService" when asked for Provider', () => { - const geo = new AmazonLocationServiceProvider(); + const mockCtx = createMockAmplifyContext(); + const geo = new AmazonLocationServiceProvider(mockCtx); expect(geo.getProviderName()).toBe('AmazonLocationService'); }); }); describe('get map resources', () => { test('should tell you if there are no available map resources', () => { - (Amplify.getConfig as jest.Mock).mockReturnValue({ + const mockCtx = createMockAmplifyContext({ Geo: { LocationService: {} }, }); - const provider = new AmazonLocationServiceProvider(); + const provider = new AmazonLocationServiceProvider(mockCtx); expect(() => provider.getAvailableMaps()).toThrow( "No map resources found in amplify config, run 'amplify add geo' to create one and run `amplify push` after", ); }); test('should get all available map resources', () => { - const provider = new AmazonLocationServiceProvider(awsConfigGeoV4); + const mockCtx = createMockAmplifyContext(awsConfigGeoV4); + const provider = new AmazonLocationServiceProvider(mockCtx); const maps: any[] = []; const availableMaps = awsConfig.geo.amazon_location_service.maps.items; @@ -131,10 +117,10 @@ describe('AmazonLocationServiceProvider', () => { }); test('should tell you if there is no map resources available when calling getDefaultMap', () => { - (Amplify.getConfig as jest.Mock).mockReturnValue({ + const mockCtx = createMockAmplifyContext({ Geo: { LocationService: {} }, }); - const provider = new AmazonLocationServiceProvider(); + const provider = new AmazonLocationServiceProvider(mockCtx); expect(() => provider.getDefaultMap()).toThrow( "No map resources found in amplify config, run 'amplify add geo' to create one and run `amplify push` after", @@ -149,10 +135,8 @@ describe('AmazonLocationServiceProvider', () => { }, }, }; - (Amplify.getConfig as jest.Mock).mockReturnValue(noDefaultMapConfig); - const provider = new AmazonLocationServiceProvider( - noDefaultMapConfig as any, - ); + const mockCtx = createMockAmplifyContext(noDefaultMapConfig); + const provider = new AmazonLocationServiceProvider(mockCtx); expect(() => provider.getDefaultMap()).toThrow( "No default map resource found in amplify config, run 'amplify add geo' to create one and run `amplify push` after", @@ -160,7 +144,8 @@ describe('AmazonLocationServiceProvider', () => { }); test('should get the default map resource', () => { - const provider = new AmazonLocationServiceProvider(awsConfigGeoV4); + const mockCtx = createMockAmplifyContext(awsConfigGeoV4); + const provider = new AmazonLocationServiceProvider(mockCtx); const mapName = awsConfig.geo.amazon_location_service.maps.default; const { style } = @@ -178,14 +163,8 @@ describe('AmazonLocationServiceProvider', () => { const testString = 'star'; test('should search with just text input', async () => { - (fetchAuthSession as jest.Mock).mockImplementationOnce(() => { - return Promise.resolve({ credentials }); - }); - - (Amplify.getConfig as jest.Mock).mockReturnValue(awsConfigGeoV4); - const locationProvider = new AmazonLocationServiceProvider( - awsConfigGeoV4, - ); + const mockCtx = createMockAmplifyContext(awsConfigGeoV4); + const locationProvider = new AmazonLocationServiceProvider(mockCtx); const results = await locationProvider.searchByText(testString); expect(results).toEqual([testPlaceCamelCase]); @@ -199,14 +178,8 @@ describe('AmazonLocationServiceProvider', () => { }); test('should use biasPosition when given', async () => { - (fetchAuthSession as jest.Mock).mockImplementationOnce(() => { - return Promise.resolve({ credentials }); - }); - - (Amplify.getConfig as jest.Mock).mockReturnValue(awsConfigGeoV4); - const locationProvider = new AmazonLocationServiceProvider( - awsConfigGeoV4, - ); + const mockCtx = createMockAmplifyContext(awsConfigGeoV4); + const locationProvider = new AmazonLocationServiceProvider(mockCtx); const searchOptions: SearchByTextOptions = { countries: ['USA'], @@ -234,14 +207,8 @@ describe('AmazonLocationServiceProvider', () => { }); test('should use searchAreaConstraints when given', async () => { - (fetchAuthSession as jest.Mock).mockImplementationOnce(() => { - return Promise.resolve({ credentials }); - }); - - (Amplify.getConfig as jest.Mock).mockReturnValue(awsConfigGeoV4); - const locationProvider = new AmazonLocationServiceProvider( - awsConfigGeoV4, - ); + const mockCtx = createMockAmplifyContext(awsConfigGeoV4); + const locationProvider = new AmazonLocationServiceProvider(mockCtx); const searchOptions: SearchByTextOptions = { countries: ['USA'], @@ -268,14 +235,8 @@ describe('AmazonLocationServiceProvider', () => { }); test('should throw an error if both BiasPosition and SearchAreaConstraints are given in the options', async () => { - (fetchAuthSession as jest.Mock).mockImplementationOnce(() => { - return Promise.resolve({ credentials }); - }); - - (Amplify.getConfig as jest.Mock).mockReturnValue(awsConfigGeoV4); - const locationProvider = new AmazonLocationServiceProvider( - awsConfigGeoV4, - ); + const mockCtx = createMockAmplifyContext(awsConfigGeoV4); + const locationProvider = new AmazonLocationServiceProvider(mockCtx); const searchOptions: SearchByTextOptions = { countries: ['USA'], @@ -293,11 +254,11 @@ describe('AmazonLocationServiceProvider', () => { }); test('should fail if credentials are invalid', async () => { - (fetchAuthSession as jest.Mock).mockImplementationOnce(() => { - return Promise.resolve({ credentials: undefined }); + const mockCtx = createMockAmplifyContext(awsConfigGeoV4); + (mockCtx.fetchAuthSession as jest.Mock).mockResolvedValue({ + credentials: undefined, }); - - const locationProvider = new AmazonLocationServiceProvider(); + const locationProvider = new AmazonLocationServiceProvider(mockCtx); await expect(locationProvider.searchByText(testString)).rejects.toThrow( 'No credentials', @@ -305,9 +266,9 @@ describe('AmazonLocationServiceProvider', () => { }); test('should fail if _getCredentials fails ', async () => { - (fetchAuthSession as jest.Mock).mockRejectedValueOnce('Auth Error'); - - const locationProvider = new AmazonLocationServiceProvider(); + const mockCtx = createMockAmplifyContext(awsConfigGeoV4); + (mockCtx.fetchAuthSession as jest.Mock).mockRejectedValue('Auth Error'); + const locationProvider = new AmazonLocationServiceProvider(mockCtx); await expect(locationProvider.searchByText(testString)).rejects.toThrow( 'No credentials', @@ -315,14 +276,10 @@ describe('AmazonLocationServiceProvider', () => { }); test('should fail if there are no search index resources', async () => { - (fetchAuthSession as jest.Mock).mockImplementationOnce(() => { - return Promise.resolve({ credentials }); - }); - - (Amplify.getConfig as jest.Mock).mockReturnValue({ + const mockCtx = createMockAmplifyContext({ Geo: { LocationService: {} }, }); - const locationProvider = new AmazonLocationServiceProvider(); + const locationProvider = new AmazonLocationServiceProvider(mockCtx); expect(locationProvider.searchByText(testString)).rejects.toThrow( 'No Search Index found in amplify config, please run `amplify add geo` to create one and run `amplify push` after.', @@ -343,14 +300,8 @@ describe('AmazonLocationServiceProvider', () => { ]; test('should search with just text input', async () => { - (fetchAuthSession as jest.Mock).mockImplementationOnce(() => { - return Promise.resolve({ credentials }); - }); - - (Amplify.getConfig as jest.Mock).mockReturnValue(awsConfigGeoV4); - const locationProvider = new AmazonLocationServiceProvider( - awsConfigGeoV4, - ); + const mockCtx = createMockAmplifyContext(awsConfigGeoV4); + const locationProvider = new AmazonLocationServiceProvider(mockCtx); const results = await locationProvider.searchForSuggestions(testString); @@ -365,14 +316,8 @@ describe('AmazonLocationServiceProvider', () => { }); test('should use biasPosition when given', async () => { - (fetchAuthSession as jest.Mock).mockImplementationOnce(() => { - return Promise.resolve({ credentials }); - }); - - (Amplify.getConfig as jest.Mock).mockReturnValue(awsConfigGeoV4); - const locationProvider = new AmazonLocationServiceProvider( - awsConfigGeoV4, - ); + const mockCtx = createMockAmplifyContext(awsConfigGeoV4); + const locationProvider = new AmazonLocationServiceProvider(mockCtx); const searchOptions: SearchByTextOptions = { countries: ['USA'], @@ -400,14 +345,8 @@ describe('AmazonLocationServiceProvider', () => { }); test('should use searchAreaConstraints when given', async () => { - (fetchAuthSession as jest.Mock).mockImplementationOnce(() => { - return Promise.resolve({ credentials }); - }); - - (Amplify.getConfig as jest.Mock).mockReturnValue(awsConfigGeoV4); - const locationProvider = new AmazonLocationServiceProvider( - awsConfigGeoV4, - ); + const mockCtx = createMockAmplifyContext(awsConfigGeoV4); + const locationProvider = new AmazonLocationServiceProvider(mockCtx); const searchOptions: SearchByTextOptions = { countries: ['USA'], @@ -432,14 +371,8 @@ describe('AmazonLocationServiceProvider', () => { }); test('should throw an error if both BiasPosition and SearchAreaConstraints are given in the options', async () => { - (fetchAuthSession as jest.Mock).mockImplementationOnce(() => { - return Promise.resolve({ credentials }); - }); - - (Amplify.getConfig as jest.Mock).mockReturnValue(awsConfigGeoV4); - const locationProvider = new AmazonLocationServiceProvider( - awsConfigGeoV4, - ); + const mockCtx = createMockAmplifyContext(awsConfigGeoV4); + const locationProvider = new AmazonLocationServiceProvider(mockCtx); const searchOptions: SearchByTextOptions = { countries: ['USA'], @@ -457,11 +390,11 @@ describe('AmazonLocationServiceProvider', () => { }); test('should fail if credentials are invalid', async () => { - (fetchAuthSession as jest.Mock).mockImplementationOnce(() => { - return Promise.resolve({ credentials: undefined }); + const mockCtx = createMockAmplifyContext(awsConfigGeoV4); + (mockCtx.fetchAuthSession as jest.Mock).mockResolvedValue({ + credentials: undefined, }); - - const locationProvider = new AmazonLocationServiceProvider(); + const locationProvider = new AmazonLocationServiceProvider(mockCtx); await expect( locationProvider.searchForSuggestions(testString), @@ -469,9 +402,9 @@ describe('AmazonLocationServiceProvider', () => { }); test('should fail if _getCredentials fails ', async () => { - (fetchAuthSession as jest.Mock).mockRejectedValueOnce('Auth Error'); - - const locationProvider = new AmazonLocationServiceProvider(); + const mockCtx = createMockAmplifyContext(awsConfigGeoV4); + (mockCtx.fetchAuthSession as jest.Mock).mockRejectedValue('Auth Error'); + const locationProvider = new AmazonLocationServiceProvider(mockCtx); await expect( locationProvider.searchForSuggestions(testString), @@ -479,14 +412,10 @@ describe('AmazonLocationServiceProvider', () => { }); test('should fail if there are no search index resources', async () => { - (fetchAuthSession as jest.Mock).mockImplementationOnce(() => { - return Promise.resolve({ credentials }); - }); - - (Amplify.getConfig as jest.Mock).mockReturnValue({ + const mockCtx = createMockAmplifyContext({ Geo: { LocationService: {} }, }); - const locationProvider = new AmazonLocationServiceProvider(); + const locationProvider = new AmazonLocationServiceProvider(mockCtx); await expect( locationProvider.searchForSuggestions(testString), @@ -501,14 +430,8 @@ describe('AmazonLocationServiceProvider', () => { const testResults = camelcaseKeys(TestPlacePascalCase, { deep: true }); test('should search with PlaceId as input', async () => { - (fetchAuthSession as jest.Mock).mockImplementationOnce(() => { - return Promise.resolve({ credentials }); - }); - - (Amplify.getConfig as jest.Mock).mockReturnValue(awsConfigGeoV4); - const locationProvider = new AmazonLocationServiceProvider( - awsConfigGeoV4, - ); + const mockCtx = createMockAmplifyContext(awsConfigGeoV4); + const locationProvider = new AmazonLocationServiceProvider(mockCtx); const results = await locationProvider.searchByPlaceId(testPlaceId); @@ -523,14 +446,8 @@ describe('AmazonLocationServiceProvider', () => { }); test('should fail if PlaceId as input is empty string', async () => { - (fetchAuthSession as jest.Mock).mockImplementationOnce(() => { - return Promise.resolve({ credentials }); - }); - - (Amplify.getConfig as jest.Mock).mockReturnValue(awsConfigGeoV4); - const locationProvider = new AmazonLocationServiceProvider( - awsConfigGeoV4, - ); + const mockCtx = createMockAmplifyContext(awsConfigGeoV4); + const locationProvider = new AmazonLocationServiceProvider(mockCtx); await expect(locationProvider.searchByPlaceId('')).rejects.toThrow( 'PlaceId cannot be an empty string.', @@ -538,11 +455,11 @@ describe('AmazonLocationServiceProvider', () => { }); test('should fail if credentials are invalid', async () => { - (fetchAuthSession as jest.Mock).mockImplementationOnce(() => { - return Promise.resolve({ credentials: undefined }); + const mockCtx = createMockAmplifyContext(awsConfigGeoV4); + (mockCtx.fetchAuthSession as jest.Mock).mockResolvedValue({ + credentials: undefined, }); - - const locationProvider = new AmazonLocationServiceProvider(); + const locationProvider = new AmazonLocationServiceProvider(mockCtx); await expect( locationProvider.searchByPlaceId(testPlaceId), @@ -550,9 +467,9 @@ describe('AmazonLocationServiceProvider', () => { }); test('should fail if _getCredentials fails ', async () => { - (fetchAuthSession as jest.Mock).mockRejectedValueOnce('Auth Error'); - - const locationProvider = new AmazonLocationServiceProvider(); + const mockCtx = createMockAmplifyContext(awsConfigGeoV4); + (mockCtx.fetchAuthSession as jest.Mock).mockRejectedValue('Auth Error'); + const locationProvider = new AmazonLocationServiceProvider(mockCtx); await expect( locationProvider.searchByPlaceId(testPlaceId), @@ -560,14 +477,10 @@ describe('AmazonLocationServiceProvider', () => { }); test('should fail if there are no search index resources', async () => { - (fetchAuthSession as jest.Mock).mockImplementationOnce(() => { - return Promise.resolve({ credentials }); - }); - - (Amplify.getConfig as jest.Mock).mockReturnValue({ + const mockCtx = createMockAmplifyContext({ Geo: { LocationService: {} }, }); - const locationProvider = new AmazonLocationServiceProvider(); + const locationProvider = new AmazonLocationServiceProvider(mockCtx); await expect( locationProvider.searchByPlaceId(testPlaceId), @@ -581,14 +494,8 @@ describe('AmazonLocationServiceProvider', () => { const testCoordinates: Coordinates = [45, 90]; test('should search with just text input', async () => { - (fetchAuthSession as jest.Mock).mockImplementationOnce(() => { - return Promise.resolve({ credentials }); - }); - - (Amplify.getConfig as jest.Mock).mockReturnValue(awsConfigGeoV4); - const locationProvider = new AmazonLocationServiceProvider( - awsConfigGeoV4, - ); + const mockCtx = createMockAmplifyContext(awsConfigGeoV4); + const locationProvider = new AmazonLocationServiceProvider(mockCtx); const results = await locationProvider.searchByCoordinates(testCoordinates); @@ -603,14 +510,8 @@ describe('AmazonLocationServiceProvider', () => { }); test('should use options when given', async () => { - (fetchAuthSession as jest.Mock).mockImplementationOnce(() => { - return Promise.resolve({ credentials }); - }); - - (Amplify.getConfig as jest.Mock).mockReturnValue(awsConfigGeoV4); - const locationProvider = new AmazonLocationServiceProvider( - awsConfigGeoV4, - ); + const mockCtx = createMockAmplifyContext(awsConfigGeoV4); + const locationProvider = new AmazonLocationServiceProvider(mockCtx); const searchOptions: SearchByCoordinatesOptions = { maxResults: 40, @@ -632,11 +533,11 @@ describe('AmazonLocationServiceProvider', () => { }); test('should fail if credentials resolve to invalid', async () => { - (fetchAuthSession as jest.Mock).mockImplementationOnce(() => { - return Promise.resolve({ credentials: undefined }); + const mockCtx = createMockAmplifyContext(awsConfigGeoV4); + (mockCtx.fetchAuthSession as jest.Mock).mockResolvedValue({ + credentials: undefined, }); - - const locationProvider = new AmazonLocationServiceProvider(); + const locationProvider = new AmazonLocationServiceProvider(mockCtx); await expect( locationProvider.searchByCoordinates(testCoordinates), @@ -644,9 +545,9 @@ describe('AmazonLocationServiceProvider', () => { }); test('should fail if _getCredentials fails ', async () => { - (fetchAuthSession as jest.Mock).mockRejectedValueOnce('Auth Error'); - - const locationProvider = new AmazonLocationServiceProvider(); + const mockCtx = createMockAmplifyContext(awsConfigGeoV4); + (mockCtx.fetchAuthSession as jest.Mock).mockRejectedValue('Auth Error'); + const locationProvider = new AmazonLocationServiceProvider(mockCtx); await expect( locationProvider.searchByCoordinates(testCoordinates), @@ -654,14 +555,10 @@ describe('AmazonLocationServiceProvider', () => { }); test('should fail if there are no search index resources', async () => { - (fetchAuthSession as jest.Mock).mockImplementationOnce(() => { - return Promise.resolve({ credentials }); - }); - - (Amplify.getConfig as jest.Mock).mockReturnValue({ + const mockCtx = createMockAmplifyContext({ Geo: { LocationService: {} }, }); - const locationProvider = new AmazonLocationServiceProvider(); + const locationProvider = new AmazonLocationServiceProvider(mockCtx); await expect( locationProvider.searchByCoordinates(testCoordinates), @@ -673,18 +570,12 @@ describe('AmazonLocationServiceProvider', () => { describe('saveGeofences', () => { test('saveGeofences with multiple geofences', async () => { - (fetchAuthSession as jest.Mock).mockImplementationOnce(() => { - return Promise.resolve({ credentials }); - }); - LocationClient.prototype.send = jest .fn() .mockImplementation(mockBatchPutGeofenceCommand); - (Amplify.getConfig as jest.Mock).mockReturnValue(awsConfigGeoV4); - const locationProvider = new AmazonLocationServiceProvider( - awsConfigGeoV4, - ); + const mockCtx = createMockAmplifyContext(awsConfigGeoV4); + const locationProvider = new AmazonLocationServiceProvider(mockCtx); const results = await locationProvider.saveGeofences(validGeofences); @@ -692,14 +583,8 @@ describe('AmazonLocationServiceProvider', () => { }); test('saveGeofences calls batchPutGeofences in batches of 10 from input', async () => { - (fetchAuthSession as jest.Mock).mockImplementationOnce(() => { - return Promise.resolve({ credentials }); - }); - - (Amplify.getConfig as jest.Mock).mockReturnValue(awsConfigGeoV4); - const locationProvider = new AmazonLocationServiceProvider( - awsConfigGeoV4, - ); + const mockCtx = createMockAmplifyContext(awsConfigGeoV4); + const locationProvider = new AmazonLocationServiceProvider(mockCtx); const numberOfGeofences = 44; const input = createGeofenceInputArray(numberOfGeofences); @@ -731,14 +616,8 @@ describe('AmazonLocationServiceProvider', () => { }); test('saveGeofences properly handles errors with bad network calls', async () => { - (fetchAuthSession as jest.Mock).mockImplementationOnce(() => { - return Promise.resolve({ credentials }); - }); - - (Amplify.getConfig as jest.Mock).mockReturnValue(awsConfigGeoV4); - const locationProvider = new AmazonLocationServiceProvider( - awsConfigGeoV4, - ); + const mockCtx = createMockAmplifyContext(awsConfigGeoV4); + const locationProvider = new AmazonLocationServiceProvider(mockCtx); const input = createGeofenceInputArray(44); input[22].geofenceId = 'badId'; @@ -790,18 +669,12 @@ describe('AmazonLocationServiceProvider', () => { }); test('should error if a geofence is wound clockwise', async () => { - (fetchAuthSession as jest.Mock).mockImplementationOnce(() => { - return Promise.resolve({ credentials }); - }); - LocationClient.prototype.send = jest .fn() .mockImplementation(mockBatchPutGeofenceCommand); - (Amplify.getConfig as jest.Mock).mockReturnValue(awsConfigGeoV4); - const locationProvider = new AmazonLocationServiceProvider( - awsConfigGeoV4, - ); + const mockCtx = createMockAmplifyContext(awsConfigGeoV4); + const locationProvider = new AmazonLocationServiceProvider(mockCtx); await expect( locationProvider.saveGeofences([clockwiseGeofence]), @@ -811,18 +684,12 @@ describe('AmazonLocationServiceProvider', () => { }); test('should error if input is empty array', async () => { - (fetchAuthSession as jest.Mock).mockImplementationOnce(() => { - return Promise.resolve({ credentials }); - }); - LocationClient.prototype.send = jest .fn() .mockImplementation(mockBatchPutGeofenceCommand); - (Amplify.getConfig as jest.Mock).mockReturnValue(awsConfigGeoV4); - const locationProvider = new AmazonLocationServiceProvider( - awsConfigGeoV4, - ); + const mockCtx = createMockAmplifyContext(awsConfigGeoV4); + const locationProvider = new AmazonLocationServiceProvider(mockCtx); await expect(locationProvider.saveGeofences([])).rejects.toThrow( 'Geofence input array is empty', @@ -830,14 +697,10 @@ describe('AmazonLocationServiceProvider', () => { }); test('should error if there are no geofenceCollections in config', async () => { - (fetchAuthSession as jest.Mock).mockImplementationOnce(() => { - return Promise.resolve({ credentials }); - }); - - (Amplify.getConfig as jest.Mock).mockReturnValue({ + const mockCtx = createMockAmplifyContext({ Geo: { LocationService: {} }, }); - const locationProvider = new AmazonLocationServiceProvider(); + const locationProvider = new AmazonLocationServiceProvider(mockCtx); await expect( locationProvider.saveGeofences(validGeofences), @@ -849,18 +712,12 @@ describe('AmazonLocationServiceProvider', () => { describe('getGeofence', () => { test('getGeofence returns the right geofence', async () => { - (fetchAuthSession as jest.Mock).mockImplementationOnce(() => { - return Promise.resolve({ credentials }); - }); - LocationClient.prototype.send = jest .fn() .mockImplementation(mockGetGeofenceCommand); - (Amplify.getConfig as jest.Mock).mockReturnValue(awsConfigGeoV4); - const locationProvider = new AmazonLocationServiceProvider( - awsConfigGeoV4, - ); + const mockCtx = createMockAmplifyContext(awsConfigGeoV4); + const locationProvider = new AmazonLocationServiceProvider(mockCtx); const results: AmazonLocationServiceGeofence = await locationProvider.getGeofence('geofenceId'); @@ -877,18 +734,12 @@ describe('AmazonLocationServiceProvider', () => { }); test('getGeofence errors when a bad geofenceId is given', async () => { - (fetchAuthSession as jest.Mock).mockImplementationOnce(() => { - return Promise.resolve({ credentials }); - }); - LocationClient.prototype.send = jest .fn() .mockImplementationOnce(mockGetGeofenceCommand); - (Amplify.getConfig as jest.Mock).mockReturnValue(awsConfigGeoV4); - const locationProvider = new AmazonLocationServiceProvider( - awsConfigGeoV4, - ); + const mockCtx = createMockAmplifyContext(awsConfigGeoV4); + const locationProvider = new AmazonLocationServiceProvider(mockCtx); const badGeofenceId = 't|-|!$ !$ N()T V@|_!D'; await expect(locationProvider.getGeofence(badGeofenceId)).rejects.toThrow( @@ -897,14 +748,10 @@ describe('AmazonLocationServiceProvider', () => { }); test('should error if there are no geofenceCollections in config', async () => { - (fetchAuthSession as jest.Mock).mockImplementationOnce(() => { - return Promise.resolve({ credentials }); - }); - - (Amplify.getConfig as jest.Mock).mockReturnValue({ + const mockCtx = createMockAmplifyContext({ Geo: { LocationService: {} }, }); - const locationProvider = new AmazonLocationServiceProvider(); + const locationProvider = new AmazonLocationServiceProvider(mockCtx); await expect(locationProvider.getGeofence('geofenceId')).rejects.toThrow( 'No Geofence Collections found, please run `amplify add geo` to create one and run `amplify push` after.', @@ -914,18 +761,12 @@ describe('AmazonLocationServiceProvider', () => { describe('listGeofences', () => { test('listGeofences gets the first 100 geofences when no arguments are given', async () => { - (fetchAuthSession as jest.Mock).mockImplementationOnce(() => { - return Promise.resolve({ credentials }); - }); - LocationClient.prototype.send = jest .fn() .mockImplementation(mockListGeofencesCommand); - (Amplify.getConfig as jest.Mock).mockReturnValue(awsConfigGeoV4); - const locationProvider = new AmazonLocationServiceProvider( - awsConfigGeoV4, - ); + const mockCtx = createMockAmplifyContext(awsConfigGeoV4); + const locationProvider = new AmazonLocationServiceProvider(mockCtx); const geofences = await locationProvider.listGeofences(); @@ -933,18 +774,12 @@ describe('AmazonLocationServiceProvider', () => { }); test('listGeofences gets the second 100 geofences when nextToken is passed', async () => { - (fetchAuthSession as jest.Mock).mockImplementationOnce(() => { - return Promise.resolve({ credentials }); - }); - LocationClient.prototype.send = jest .fn() .mockImplementation(mockListGeofencesCommand); - (Amplify.getConfig as jest.Mock).mockReturnValue(awsConfigGeoV4); - const locationProvider = new AmazonLocationServiceProvider( - awsConfigGeoV4, - ); + const mockCtx = createMockAmplifyContext(awsConfigGeoV4); + const locationProvider = new AmazonLocationServiceProvider(mockCtx); const first100Geofences = await locationProvider.listGeofences(); @@ -962,14 +797,10 @@ describe('AmazonLocationServiceProvider', () => { }); test('should error if there are no geofenceCollections in config', async () => { - (fetchAuthSession as jest.Mock).mockImplementationOnce(() => { - return Promise.resolve({ credentials }); - }); - - (Amplify.getConfig as jest.Mock).mockReturnValue({ + const mockCtx = createMockAmplifyContext({ Geo: { LocationService: {} }, }); - const locationProvider = new AmazonLocationServiceProvider(); + const locationProvider = new AmazonLocationServiceProvider(mockCtx); await expect(locationProvider.listGeofences()).rejects.toThrow( 'No Geofence Collections found, please run `amplify add geo` to create one and run `amplify push` after.', @@ -979,18 +810,12 @@ describe('AmazonLocationServiceProvider', () => { describe('deleteGeofences', () => { test('deleteGeofences deletes given geofences successfully', async () => { - (fetchAuthSession as jest.Mock).mockImplementationOnce(() => { - return Promise.resolve({ credentials }); - }); - LocationClient.prototype.send = jest .fn() .mockImplementation(mockDeleteGeofencesCommand); - (Amplify.getConfig as jest.Mock).mockReturnValue(awsConfigGeoV4); - const locationProvider = new AmazonLocationServiceProvider( - awsConfigGeoV4, - ); + const mockCtx = createMockAmplifyContext(awsConfigGeoV4); + const locationProvider = new AmazonLocationServiceProvider(mockCtx); const geofenceIds = validGeofences.map(({ geofenceId }) => geofenceId); @@ -1005,14 +830,8 @@ describe('AmazonLocationServiceProvider', () => { }); test('deleteGeofences calls batchDeleteGeofences in batches of 10 from input', async () => { - (fetchAuthSession as jest.Mock).mockImplementationOnce(() => { - return Promise.resolve({ credentials }); - }); - - (Amplify.getConfig as jest.Mock).mockReturnValue(awsConfigGeoV4); - const locationProvider = new AmazonLocationServiceProvider( - awsConfigGeoV4, - ); + const mockCtx = createMockAmplifyContext(awsConfigGeoV4); + const locationProvider = new AmazonLocationServiceProvider(mockCtx); const geofenceIds = validGeofences.map(({ geofenceId }) => geofenceId); @@ -1038,14 +857,8 @@ describe('AmazonLocationServiceProvider', () => { }); test('deleteGeofences properly handles errors with bad network calls', async () => { - (fetchAuthSession as jest.Mock).mockImplementationOnce(() => { - return Promise.resolve({ credentials }); - }); - - (Amplify.getConfig as jest.Mock).mockReturnValue(awsConfigGeoV4); - const locationProvider = new AmazonLocationServiceProvider( - awsConfigGeoV4, - ); + const mockCtx = createMockAmplifyContext(awsConfigGeoV4); + const locationProvider = new AmazonLocationServiceProvider(mockCtx); const input = createGeofenceInputArray(44).map( ({ geofenceId }) => geofenceId, @@ -1094,13 +907,8 @@ describe('AmazonLocationServiceProvider', () => { }); test('should error if there is a bad geofence in the input', async () => { - (fetchAuthSession as jest.Mock).mockImplementationOnce(() => { - return Promise.resolve({ credentials }); - }); - (Amplify.getConfig as jest.Mock).mockReturnValue(awsConfigGeoV4); - const locationProvider = new AmazonLocationServiceProvider( - awsConfigGeoV4, - ); + const mockCtx = createMockAmplifyContext(awsConfigGeoV4); + const locationProvider = new AmazonLocationServiceProvider(mockCtx); await expect( locationProvider.deleteGeofences([ 'thisIsAGoodId', @@ -1113,27 +921,18 @@ describe('AmazonLocationServiceProvider', () => { }); test('should error if input array is empty', async () => { - (fetchAuthSession as jest.Mock).mockImplementationOnce(() => { - return Promise.resolve({ credentials }); - }); - (Amplify.getConfig as jest.Mock).mockReturnValue(awsConfigGeoV4); - const locationProvider = new AmazonLocationServiceProvider( - awsConfigGeoV4, - ); + const mockCtx = createMockAmplifyContext(awsConfigGeoV4); + const locationProvider = new AmazonLocationServiceProvider(mockCtx); await expect(locationProvider.deleteGeofences([])).rejects.toThrow( `GeofenceId input array is empty`, ); }); test('should error if there are no geofenceCollections in config', async () => { - (fetchAuthSession as jest.Mock).mockImplementationOnce(() => { - return Promise.resolve({ credentials }); - }); - - (Amplify.getConfig as jest.Mock).mockReturnValue({ + const mockCtx = createMockAmplifyContext({ Geo: { LocationService: {} }, }); - const locationProvider = new AmazonLocationServiceProvider(); + const locationProvider = new AmazonLocationServiceProvider(mockCtx); const geofenceIds = validGeofences.map(({ geofenceId }) => geofenceId); diff --git a/packages/geo/__tests__/testUtils.ts b/packages/geo/__tests__/testUtils.ts index 17b212bef39..07f6bf5daae 100644 --- a/packages/geo/__tests__/testUtils.ts +++ b/packages/geo/__tests__/testUtils.ts @@ -1,3 +1,4 @@ +import { AMPLIFY_CONTEXT_BRAND, AmplifyContext } from '@aws-amplify/core'; import { BatchDeleteGeofenceCommand, BatchPutGeofenceCommand, @@ -7,7 +8,24 @@ import { import { Geofence } from '../src/types'; -import { validGeometry, validPolygon } from './testData'; +import { credentials, validGeometry, validPolygon } from './testData'; + +export function createMockAmplifyContext( + resourcesConfig: Record = {}, +): AmplifyContext { + const ctx = { + [AMPLIFY_CONTEXT_BRAND]: true, + resourcesConfig, + libraryOptions: {}, + fetchAuthSession: jest + .fn() + .mockResolvedValue({ credentials, identityId: credentials.identityId }), + clearCredentials: jest.fn().mockResolvedValue(undefined), + getTokens: jest.fn().mockResolvedValue(undefined), + }; + + return ctx as unknown as AmplifyContext; +} export function createGeofenceInputArray(numberOfGeofences) { const geofences: Geofence[] = []; diff --git a/packages/geo/package.json b/packages/geo/package.json index 3611de23614..ca2cda9a17c 100644 --- a/packages/geo/package.json +++ b/packages/geo/package.json @@ -75,7 +75,7 @@ "tslib": "^2.5.0" }, "peerDependencies": { - "@aws-amplify/core": "^6.16.2" + "@aws-amplify/core": "^6.16.3" }, "devDependencies": { "@aws-amplify/core": "6.16.3" diff --git a/packages/geo/src/Geo.ts b/packages/geo/src/Geo.ts index 043c8ee32d1..86242f2de69 100644 --- a/packages/geo/src/Geo.ts +++ b/packages/geo/src/Geo.ts @@ -1,6 +1,11 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { Amplify, ConsoleLogger } from '@aws-amplify/core'; +import { + AmplifyContext, + ConsoleLogger, + isAmplifyContext, +} from '@aws-amplify/core'; +import { resolveCtxArgs } from '@aws-amplify/core/internals/utils'; import { AmazonLocationServiceProvider } from './providers/location-service/AmazonLocationServiceProvider'; import { validateCoordinates } from './util'; @@ -34,15 +39,18 @@ export class GeoClass { */ private _config?: GeoConfig; private _pluggables: GeoProvider[]; + private ctx: AmplifyContext; - constructor() { + constructor(ctx: AmplifyContext) { + this.ctx = ctx; this._config = undefined; this._pluggables = []; - const amplifyConfig = Amplify.getConfig() ?? {}; + const amplifyConfig = this.ctx.resourcesConfig ?? {}; this._config = Object.assign({}, this._config, amplifyConfig.Geo); const locationProvider = new AmazonLocationServiceProvider( + ctx, amplifyConfig.Geo, ); this._pluggables.push(locationProvider); @@ -305,6 +313,180 @@ export class GeoClass { throw error; } } + + // --- Static methods for v6 compatibility --- + // These use resolveCtxArgs to support both `Geo.method(input)` and `Geo.method(ctx, input)`. + + static searchByText( + text: string, + options?: SearchByTextOptions, + ): Promise; + + static searchByText( + ctx: AmplifyContext, + text: string, + options?: SearchByTextOptions, + ): Promise; + + static searchByText(...args: any[]): Promise { + const [ctx, text, options] = resolveStaticArgs(args); + + return new GeoClass(ctx).searchByText(text, options); + } + + static searchForSuggestions( + text: string, + options?: SearchByTextOptions, + ): Promise; + + static searchForSuggestions( + ctx: AmplifyContext, + text: string, + options?: SearchByTextOptions, + ): Promise; + + static searchForSuggestions( + ...args: any[] + ): Promise { + const [ctx, text, options] = resolveStaticArgs(args); + + return new GeoClass(ctx).searchForSuggestions(text, options); + } + + static searchByPlaceId( + placeId: string, + options?: searchByPlaceIdOptions, + ): Promise; + + static searchByPlaceId( + ctx: AmplifyContext, + placeId: string, + options?: searchByPlaceIdOptions, + ): Promise; + + static searchByPlaceId(...args: any[]): Promise { + const [ctx, placeId, options] = resolveStaticArgs(args); + + return new GeoClass(ctx).searchByPlaceId(placeId, options); + } + + static searchByCoordinates( + coordinates: Coordinates, + options?: SearchByCoordinatesOptions, + ): Promise; + + static searchByCoordinates( + ctx: AmplifyContext, + coordinates: Coordinates, + options?: SearchByCoordinatesOptions, + ): Promise; + + static searchByCoordinates(...args: any[]): Promise { + const [ctx, coordinates, options] = resolveStaticArgs(args); + + return new GeoClass(ctx).searchByCoordinates(coordinates, options); + } + + static getAvailableMaps(provider?: string): MapStyle[]; + static getAvailableMaps(ctx: AmplifyContext, provider?: string): MapStyle[]; + static getAvailableMaps(...args: any[]): MapStyle[] { + const [ctx, provider] = resolveStaticArgs(args); + + return new GeoClass(ctx).getAvailableMaps(provider); + } + + static getDefaultMap(provider?: string): MapStyle; + static getDefaultMap(ctx: AmplifyContext, provider?: string): MapStyle; + static getDefaultMap(...args: any[]): MapStyle { + const [ctx, provider] = resolveStaticArgs(args); + + return new GeoClass(ctx).getDefaultMap(provider); + } + + static saveGeofences( + geofences: GeofenceInput | GeofenceInput[], + options?: GeofenceOptions, + ): Promise; + + static saveGeofences( + ctx: AmplifyContext, + geofences: GeofenceInput | GeofenceInput[], + options?: GeofenceOptions, + ): Promise; + + static saveGeofences(...args: any[]): Promise { + const [ctx, geofences, options] = resolveStaticArgs< + GeofenceInput | GeofenceInput[] + >(args); + + return new GeoClass(ctx).saveGeofences(geofences, options); + } + + static getGeofence( + geofenceId: GeofenceId, + options?: GeofenceOptions, + ): Promise; + + static getGeofence( + ctx: AmplifyContext, + geofenceId: GeofenceId, + options?: GeofenceOptions, + ): Promise; + + static getGeofence(...args: any[]): Promise { + const [ctx, geofenceId, options] = resolveStaticArgs(args); + + return new GeoClass(ctx).getGeofence(geofenceId, options); + } + + static listGeofences( + options?: ListGeofenceOptions, + ): Promise; + + static listGeofences( + ctx: AmplifyContext, + options?: ListGeofenceOptions, + ): Promise; + + static listGeofences(...args: any[]): Promise { + const [ctx, options] = resolveStaticArgs( + args, + ); + + return new GeoClass(ctx).listGeofences(options); + } + + static deleteGeofences( + geofenceIds: string | string[], + options?: GeofenceOptions, + ): Promise; + + static deleteGeofences( + ctx: AmplifyContext, + geofenceIds: string | string[], + options?: GeofenceOptions, + ): Promise; + + static deleteGeofences(...args: any[]): Promise { + const [ctx, geofenceIds, options] = resolveStaticArgs( + args, + ); + + return new GeoClass(ctx).deleteGeofences(geofenceIds, options); + } } -export const Geo = new GeoClass(); +/** + * Helper to resolve optional leading AmplifyContext from static method args. + * Returns [ctx, firstArg, secondArg] where ctx falls back to getActiveContext(). + */ +function resolveStaticArgs(args: any[]): [AmplifyContext, T, any] { + const [ctx, remaining] = resolveCtxArgs(args); + + // If ctx was extracted from args[0], the "remaining" is args[1] and args[2] is the third arg. + // If ctx was resolved from global, "remaining" is args[0] and args[1] is the second arg. + const hasExplicitCtx = isAmplifyContext(args[0]); + const thirdArg = hasExplicitCtx ? args[2] : args[1]; + + return [ctx, remaining, thirdArg]; +} diff --git a/packages/geo/src/index.ts b/packages/geo/src/index.ts index 83eacbbd7fb..b6f5a6f2508 100644 --- a/packages/geo/src/index.ts +++ b/packages/geo/src/index.ts @@ -1,4 +1,4 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -export { Geo } from './Geo'; +export { GeoClass as Geo } from './Geo'; export * from './types'; diff --git a/packages/geo/src/providers/location-service/AmazonLocationServiceProvider.ts b/packages/geo/src/providers/location-service/AmazonLocationServiceProvider.ts index b5248d5ccbc..42725782bcf 100644 --- a/packages/geo/src/providers/location-service/AmazonLocationServiceProvider.ts +++ b/packages/geo/src/providers/location-service/AmazonLocationServiceProvider.ts @@ -1,7 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 import camelcaseKeys from 'camelcase-keys'; -import { Amplify, ConsoleLogger, fetchAuthSession } from '@aws-amplify/core'; +import { AmplifyContext, ConsoleLogger } from '@aws-amplify/core'; import { GeoAction } from '@aws-amplify/core/internals/utils'; import { BatchDeleteGeofenceCommand, @@ -65,17 +65,12 @@ export class AmazonLocationServiceProvider implements GeoProvider { static CATEGORY = 'Geo'; static PROVIDER_NAME = 'AmazonLocationService'; - /** - * @private - */ private _config; private _credentials; + private ctx: AmplifyContext; - /** - * Initialize Geo with AWS configurations - * @param {Object} config - Configuration object for Geo - */ - constructor(config?: GeoConfig) { + constructor(ctx: AmplifyContext, config?: GeoConfig) { + this.ctx = ctx; this._config = config || {}; logger.debug('Geo Options', this._config); } @@ -713,7 +708,7 @@ export class AmazonLocationServiceProvider implements GeoProvider { */ private async _ensureCredentials(): Promise { try { - const { credentials } = await fetchAuthSession(); + const { credentials } = await this.ctx.fetchAuthSession(); if (!credentials) return false; logger.debug( 'Set credentials for storage. Credentials are:', @@ -730,7 +725,7 @@ export class AmazonLocationServiceProvider implements GeoProvider { } private _refreshConfig() { - this._config = Amplify.getConfig().Geo?.LocationService; + this._config = this.ctx.resourcesConfig.Geo?.LocationService; if (!this._config) { const errorString = "No Geo configuration found in amplify config, run 'amplify add geo' to create one and run `amplify push` after"; diff --git a/packages/interactions/__tests__/lex-v1/AWSLexProvider.test.ts b/packages/interactions/__tests__/lex-v1/AWSLexProvider.test.ts index aaca3c0c1d0..4dc42cd50de 100644 --- a/packages/interactions/__tests__/lex-v1/AWSLexProvider.test.ts +++ b/packages/interactions/__tests__/lex-v1/AWSLexProvider.test.ts @@ -6,10 +6,8 @@ import { PostTextCommand, PostTextCommandOutput, } from '@aws-sdk/client-lex-runtime-service'; -import { lexProvider } from '../../src/lex-v1/AWSLexProvider'; -import { fetchAuthSession } from '@aws-amplify/core'; - -jest.mock('@aws-amplify/core'); +import { createLexProvider } from '../../src/lex-v1/AWSLexProvider'; +import { AMPLIFY_CONTEXT_BRAND, AmplifyContext } from '@aws-amplify/core'; (global as any).Response = class Response { arrayBuffer(blob: Blob) { @@ -45,7 +43,19 @@ const credentials = { identityId: 'identity-id', }; -const mockFetchAuthSession = fetchAuthSession as jest.Mock; +const mockFetchAuthSession = jest.fn(); + +const createMockCtx = (): AmplifyContext => { + const ctx = { + [AMPLIFY_CONTEXT_BRAND]: true, + resourcesConfig: {}, + libraryOptions: {}, + fetchAuthSession: mockFetchAuthSession, + clearCredentials: jest.fn(), + getTokens: jest.fn(), + }; + return ctx as unknown as AmplifyContext; +}; LexRuntimeServiceClient.prototype.send = jest.fn((command, callback) => { if (command instanceof PostTextCommand) { @@ -150,7 +160,7 @@ describe('Interactions', () => { beforeEach(() => { mockFetchAuthSession.mockReturnValue(credentials); - provider = lexProvider; + provider = createLexProvider(createMockCtx()); }); afterEach(() => { @@ -290,7 +300,7 @@ describe('Interactions', () => { beforeEach(() => { mockFetchAuthSession.mockReturnValue(credentials); - provider = lexProvider; + provider = createLexProvider(createMockCtx()); }); afterEach(() => { @@ -320,7 +330,7 @@ describe('Interactions', () => { beforeEach(async () => { mockFetchAuthSession.mockReturnValue(credentials); - provider = lexProvider; + provider = createLexProvider(createMockCtx()); // mock callbacks inProgressCallback = jest.fn((err, confirmation) => diff --git a/packages/interactions/__tests__/lex-v1/apis/onComplete.test.ts b/packages/interactions/__tests__/lex-v1/apis/onComplete.test.ts index 95c5b94f673..64c919dd404 100644 --- a/packages/interactions/__tests__/lex-v1/apis/onComplete.test.ts +++ b/packages/interactions/__tests__/lex-v1/apis/onComplete.test.ts @@ -1,8 +1,9 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { amplifyUuid } from '@aws-amplify/core/internals/utils'; -import { lexProvider } from '../../../src/lex-v1/AWSLexProvider'; +import { v4 as uuid } from 'uuid'; +import { AMPLIFY_CONTEXT_BRAND, AmplifyContext } from '@aws-amplify/core'; +import { createLexProvider } from '../../../src/lex-v1/AWSLexProvider'; import { onComplete } from '../../../src/lex-v1/apis'; import { generateRandomLexV1Config } from '../../testUtils/randomConfigGeneration'; import { resolveBotConfig } from '../../../src/lex-v1/utils'; @@ -14,30 +15,42 @@ jest.mock('../../../src/lex-v1/utils'); describe('Interactions LexV1 API: onComplete', () => { const v1BotConfig = generateRandomLexV1Config(); - const mockLexProvider = lexProvider.onComplete as jest.Mock; + const mockOnComplete = jest.fn(); + const mockCreateLexProvider = createLexProvider as jest.Mock; const mockResolveBotConfig = resolveBotConfig as jest.Mock; + const mockCtx: AmplifyContext = { + [AMPLIFY_CONTEXT_BRAND]: true, + resourcesConfig: {}, + libraryOptions: {}, + fetchAuthSession: jest.fn(), + clearCredentials: jest.fn(), + getTokens: jest.fn(), + } as unknown as AmplifyContext; + beforeEach(() => { mockResolveBotConfig.mockReturnValue(v1BotConfig); + mockCreateLexProvider.mockReturnValue({ onComplete: mockOnComplete }); }); afterEach(() => { - mockLexProvider.mockReset(); + mockOnComplete.mockReset(); + mockCreateLexProvider.mockReset(); mockResolveBotConfig.mockReset(); }); it('invokes provider onComplete API', () => { - const message = amplifyUuid(); + const message = uuid(); const mockCallback = jest.fn(); - onComplete({ botName: v1BotConfig.name, callback: mockCallback }); - expect(mockLexProvider).toHaveBeenCalledTimes(1); - expect(mockLexProvider).toHaveBeenCalledWith(v1BotConfig, mockCallback); + onComplete(mockCtx, { botName: v1BotConfig.name, callback: mockCallback }); + expect(mockOnComplete).toHaveBeenCalledTimes(1); + expect(mockOnComplete).toHaveBeenCalledWith(v1BotConfig, mockCallback); }); it('rejects when bot config does not exist', async () => { mockResolveBotConfig.mockReturnValue(undefined); expect(() => - onComplete({ botName: v1BotConfig.name, callback: jest.fn }), + onComplete(mockCtx, { botName: v1BotConfig.name, callback: jest.fn }), ).toThrow(InteractionsError); }); }); diff --git a/packages/interactions/__tests__/lex-v1/apis/send.test.ts b/packages/interactions/__tests__/lex-v1/apis/send.test.ts index e4d768a42d5..3534aea0877 100644 --- a/packages/interactions/__tests__/lex-v1/apis/send.test.ts +++ b/packages/interactions/__tests__/lex-v1/apis/send.test.ts @@ -1,8 +1,9 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { amplifyUuid } from '@aws-amplify/core/internals/utils'; -import { lexProvider } from '../../../src/lex-v1/AWSLexProvider'; +import { v4 as uuid } from 'uuid'; +import { AMPLIFY_CONTEXT_BRAND, AmplifyContext } from '@aws-amplify/core'; +import { createLexProvider } from '../../../src/lex-v1/AWSLexProvider'; import { send } from '../../../src/lex-v1/apis'; import { generateRandomLexV1Config } from '../../testUtils/randomConfigGeneration'; import { resolveBotConfig } from '../../../src/lex-v1/utils'; @@ -14,29 +15,41 @@ jest.mock('../../../src/lex-v1/utils'); describe('Interactions LexV1 API: send', () => { const v1BotConfig = generateRandomLexV1Config(); - const mockLexProvider = lexProvider.sendMessage as jest.Mock; + const mockSendMessage = jest.fn(); + const mockCreateLexProvider = createLexProvider as jest.Mock; const mockResolveBotConfig = resolveBotConfig as jest.Mock; + const mockCtx: AmplifyContext = { + [AMPLIFY_CONTEXT_BRAND]: true, + resourcesConfig: {}, + libraryOptions: {}, + fetchAuthSession: jest.fn(), + clearCredentials: jest.fn(), + getTokens: jest.fn(), + } as unknown as AmplifyContext; + beforeEach(() => { mockResolveBotConfig.mockReturnValue(v1BotConfig); + mockCreateLexProvider.mockReturnValue({ sendMessage: mockSendMessage }); }); afterEach(() => { - mockLexProvider.mockReset(); + mockSendMessage.mockReset(); + mockCreateLexProvider.mockReset(); mockResolveBotConfig.mockReset(); }); it('invokes provider sendMessage API', async () => { - const message = amplifyUuid(); - await send({ botName: v1BotConfig.name, message }); - expect(mockLexProvider).toHaveBeenCalledTimes(1); - expect(mockLexProvider).toHaveBeenCalledWith(v1BotConfig, message); + const message = uuid(); + await send(mockCtx, { botName: v1BotConfig.name, message }); + expect(mockSendMessage).toHaveBeenCalledTimes(1); + expect(mockSendMessage).toHaveBeenCalledWith(v1BotConfig, message); }); it('rejects when bot config does not exist', async () => { mockResolveBotConfig.mockReturnValue(undefined); await expect( - send({ botName: v1BotConfig.name, message: amplifyUuid() }), + send(mockCtx, { botName: v1BotConfig.name, message: uuid() }), ).rejects.toBeInstanceOf(InteractionsError); }); }); diff --git a/packages/interactions/__tests__/lex-v1/utils/resolveBotConfig.test.ts b/packages/interactions/__tests__/lex-v1/utils/resolveBotConfig.test.ts index 59c648f5d08..2318fe6db16 100644 --- a/packages/interactions/__tests__/lex-v1/utils/resolveBotConfig.test.ts +++ b/packages/interactions/__tests__/lex-v1/utils/resolveBotConfig.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 { AMPLIFY_CONTEXT_BRAND, AmplifyContext } from '@aws-amplify/core'; import { generateRandomLexV1Config, generateRandomLexV2Config, @@ -9,23 +9,31 @@ import { import { resolveBotConfig } from '../../../src/lex-v1/utils'; describe('Interactions LexV1 Util: resolveBotConfig', () => { - const getConfigSpy = jest.spyOn(Amplify, 'getConfig'); - - afterEach(() => { - getConfigSpy.mockReset(); - }); + const createMockCtx = ( + resourcesConfig: Record, + ): AmplifyContext => { + const ctx = { + [AMPLIFY_CONTEXT_BRAND]: true, + resourcesConfig, + libraryOptions: {}, + fetchAuthSession: jest.fn(), + clearCredentials: jest.fn(), + getTokens: jest.fn(), + }; + return ctx as unknown as AmplifyContext; + }; it('find correct bot config if exist', () => { const v1BotConfigs = [...Array(5)].map(generateRandomLexV1Config); const v2BotConfigs = [...Array(5)].map(generateRandomLexV2Config); - getConfigSpy.mockReturnValue({ + const ctx = createMockCtx({ Interactions: { LexV1: Object.fromEntries(v1BotConfigs.map(bot => [bot.name, bot])), LexV2: Object.fromEntries(v2BotConfigs.map(bot => [bot.name, bot])), }, }); - const result = resolveBotConfig(v1BotConfigs[3].name); + const result = resolveBotConfig(ctx, v1BotConfigs[3].name); expect(result).not.toBeUndefined(); expect(result).toStrictEqual(v1BotConfigs[3]); }); @@ -33,28 +41,28 @@ describe('Interactions LexV1 Util: resolveBotConfig', () => { it('ignore v2 bot config', () => { const v1BotConfigs = [...Array(5)].map(generateRandomLexV1Config); const v2BotConfigs = [...Array(5)].map(generateRandomLexV2Config); - getConfigSpy.mockReturnValue({ + const ctx = createMockCtx({ Interactions: { LexV1: Object.fromEntries(v1BotConfigs.map(bot => [bot.name, bot])), LexV2: Object.fromEntries(v2BotConfigs.map(bot => [bot.name, bot])), }, }); - const result = resolveBotConfig(v2BotConfigs[3].name); + const result = resolveBotConfig(ctx, v2BotConfigs[3].name); expect(result).toBeUndefined(); }); it('return undefined for non-exist bot', () => { const v1BotConfigs = [...Array(5)].map(generateRandomLexV1Config); const v2BotConfigs = [...Array(5)].map(generateRandomLexV2Config); - getConfigSpy.mockReturnValue({ + const ctx = createMockCtx({ Interactions: { LexV1: Object.fromEntries(v1BotConfigs.map(bot => [bot.name, bot])), LexV2: Object.fromEntries(v2BotConfigs.map(bot => [bot.name, bot])), }, }); - const result = resolveBotConfig('test'); + const result = resolveBotConfig(ctx, 'test'); expect(result).toBeUndefined(); }); }); diff --git a/packages/interactions/__tests__/lex-v2/AWSLexV2Provider.test.ts b/packages/interactions/__tests__/lex-v2/AWSLexV2Provider.test.ts index adc827de53a..6eb9ca15dd6 100644 --- a/packages/interactions/__tests__/lex-v2/AWSLexV2Provider.test.ts +++ b/packages/interactions/__tests__/lex-v2/AWSLexV2Provider.test.ts @@ -1,6 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { fetchAuthSession } from '@aws-amplify/core'; +import { AMPLIFY_CONTEXT_BRAND, AmplifyContext } from '@aws-amplify/core'; import { IntentState, LexRuntimeV2Client, @@ -10,10 +10,8 @@ import { } from '@aws-sdk/client-lex-runtime-v2'; import { gzip, strToU8 } from 'fflate'; import { encode } from 'base-64'; -import { lexProvider } from '../../src/lex-v2/AWSLexV2Provider'; -import { amplifyUuid } from '@aws-amplify/core/internals/utils'; - -jest.mock('@aws-amplify/core'); +import { v4 as uuid } from 'uuid'; +import { createLexV2Provider } from '../../src/lex-v2/AWSLexV2Provider'; (global as any).Response = class Response { arrayBuffer(blob: Blob) { @@ -46,7 +44,19 @@ const credentials = { identityId: 'identity-id', }; -const mockFetchAuthSession = fetchAuthSession as jest.Mock; +const mockFetchAuthSession = jest.fn(); + +const createMockCtx = (): AmplifyContext => { + const ctx = { + [AMPLIFY_CONTEXT_BRAND]: true, + resourcesConfig: {}, + libraryOptions: {}, + fetchAuthSession: mockFetchAuthSession, + clearCredentials: jest.fn(), + getTokens: jest.fn(), + }; + return ctx as unknown as AmplifyContext; +}; const arrayBufferToBase64 = (buffer: Uint8Array) => { var binary = ''; @@ -226,7 +236,7 @@ describe('Interactions', () => { beforeEach(() => { mockFetchAuthSession.mockReturnValue(credentials); - provider = lexProvider; + provider = createLexV2Provider(createMockCtx()); }); afterEach(() => mockFetchAuthSession.mockReset()); @@ -377,7 +387,7 @@ describe('Interactions', () => { beforeEach(() => { mockFetchAuthSession.mockReturnValue(credentials); - provider = lexProvider; + provider = createLexV2Provider(createMockCtx()); }); afterEach(() => mockFetchAuthSession.mockReset()); @@ -393,7 +403,7 @@ describe('Interactions', () => { // Test 'reportBotStatus' API describe('reportBotStatus API', () => { jest.useFakeTimers(); - let provider = lexProvider; + let provider = createLexV2Provider(createMockCtx()); // enum, action types callback function can handle const ACTION_TYPE = Object.freeze({ IN_PROGRESS: 'inProgress', @@ -467,7 +477,7 @@ describe('Interactions', () => { describe('onComplete callback from `Interactions.onComplete`', () => { test(`In progress, callback shouldn't be called`, async () => { // callback is only called once conversation is completed - let config = { ...botConfig.BookTrip, name: amplifyUuid() }; + let config = { ...botConfig.BookTrip, name: uuid() }; const inProgressCallback = mockCallbackProvider( ACTION_TYPE.IN_PROGRESS, ); @@ -484,7 +494,7 @@ describe('Interactions', () => { }); test(`task complete; callback with success resp`, async () => { - let config = { ...botConfig.BookTrip, name: amplifyUuid() }; + let config = { ...botConfig.BookTrip, name: uuid() }; const completeSuccessCallback = mockCallbackProvider( ACTION_TYPE.COMPLETE, ); @@ -502,7 +512,7 @@ describe('Interactions', () => { }); test(`task complete; callback with error resp`, async () => { - let config = { ...botConfig.BookTrip, name: amplifyUuid() }; + let config = { ...botConfig.BookTrip, name: uuid() }; const completeFailCallback = mockCallbackProvider(ACTION_TYPE.ERROR); provider.onComplete(config, completeFailCallback); diff --git a/packages/interactions/__tests__/lex-v2/apis/onComplete.test.ts b/packages/interactions/__tests__/lex-v2/apis/onComplete.test.ts index dbc4c36f861..41dcb77bac5 100644 --- a/packages/interactions/__tests__/lex-v2/apis/onComplete.test.ts +++ b/packages/interactions/__tests__/lex-v2/apis/onComplete.test.ts @@ -1,8 +1,9 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { amplifyUuid } from '@aws-amplify/core/internals/utils'; -import { lexProvider } from '../../../src/lex-v2/AWSLexV2Provider'; +import { v4 as uuid } from 'uuid'; +import { Amplify } from '@aws-amplify/core'; +import { createLexV2Provider } from '../../../src/lex-v2/AWSLexV2Provider'; import { onComplete } from '../../../src/lex-v2/apis'; import { generateRandomLexV2Config } from '../../testUtils/randomConfigGeneration'; import { resolveBotConfig } from '../../../src/lex-v2/utils'; @@ -14,24 +15,33 @@ jest.mock('../../../src/lex-v2/utils'); describe('Interactions LexV2 API: onComplete', () => { const v2BotConfig = generateRandomLexV2Config(); - const mockLexProvider = lexProvider.onComplete as jest.Mock; + const mockOnComplete = jest.fn(); + const mockCreateLexV2Provider = createLexV2Provider as jest.Mock; const mockResolveBotConfig = resolveBotConfig as jest.Mock; + beforeAll(() => { + Amplify.configure({}); + }); + beforeEach(() => { mockResolveBotConfig.mockReturnValue(v2BotConfig); + mockCreateLexV2Provider.mockReturnValue({ + onComplete: mockOnComplete, + }); }); afterEach(() => { - mockLexProvider.mockReset(); + mockOnComplete.mockReset(); + mockCreateLexV2Provider.mockReset(); mockResolveBotConfig.mockReset(); }); it('invokes provider onComplete API', () => { - const message = amplifyUuid(); + const message = uuid(); const mockCallback = jest.fn(); onComplete({ botName: v2BotConfig.name, callback: mockCallback }); - expect(mockLexProvider).toHaveBeenCalledTimes(1); - expect(mockLexProvider).toHaveBeenCalledWith(v2BotConfig, mockCallback); + expect(mockOnComplete).toHaveBeenCalledTimes(1); + expect(mockOnComplete).toHaveBeenCalledWith(v2BotConfig, mockCallback); }); it('rejects when bot config does not exist', async () => { diff --git a/packages/interactions/__tests__/lex-v2/apis/send.test.ts b/packages/interactions/__tests__/lex-v2/apis/send.test.ts index 32d47a0ad90..c9f666d772d 100644 --- a/packages/interactions/__tests__/lex-v2/apis/send.test.ts +++ b/packages/interactions/__tests__/lex-v2/apis/send.test.ts @@ -1,8 +1,9 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { amplifyUuid } from '@aws-amplify/core/internals/utils'; -import { lexProvider } from '../../../src/lex-v2/AWSLexV2Provider'; +import { v4 as uuid } from 'uuid'; +import { Amplify } from '@aws-amplify/core'; +import { createLexV2Provider } from '../../../src/lex-v2/AWSLexV2Provider'; import { send } from '../../../src/lex-v2/apis'; import { generateRandomLexV2Config } from '../../testUtils/randomConfigGeneration'; import { resolveBotConfig } from '../../../src/lex-v2/utils'; @@ -14,29 +15,38 @@ jest.mock('../../../src/lex-v2/utils'); describe('Interactions LexV2 API: send', () => { const v2BotConfig = generateRandomLexV2Config(); - const mockLexProvider = lexProvider.sendMessage as jest.Mock; + const mockSendMessage = jest.fn(); + const mockCreateLexV2Provider = createLexV2Provider as jest.Mock; const mockResolveBotConfig = resolveBotConfig as jest.Mock; + beforeAll(() => { + Amplify.configure({}); + }); + beforeEach(() => { mockResolveBotConfig.mockReturnValue(v2BotConfig); + mockCreateLexV2Provider.mockReturnValue({ + sendMessage: mockSendMessage, + }); }); afterEach(() => { - mockLexProvider.mockReset(); + mockSendMessage.mockReset(); + mockCreateLexV2Provider.mockReset(); mockResolveBotConfig.mockReset(); }); it('invokes provider sendMessage API', async () => { - const message = amplifyUuid(); + const message = uuid(); await send({ botName: v2BotConfig.name, message }); - expect(mockLexProvider).toHaveBeenCalledTimes(1); - expect(mockLexProvider).toHaveBeenCalledWith(v2BotConfig, message); + expect(mockSendMessage).toHaveBeenCalledTimes(1); + expect(mockSendMessage).toHaveBeenCalledWith(v2BotConfig, message); }); it('rejects when bot config does not exist', async () => { mockResolveBotConfig.mockReturnValue(undefined); await expect( - send({ botName: v2BotConfig.name, message: amplifyUuid() }), + send({ botName: v2BotConfig.name, message: uuid() }), ).rejects.toBeInstanceOf(InteractionsError); }); }); diff --git a/packages/interactions/__tests__/lex-v2/utils/resolveBotConfig.test.ts b/packages/interactions/__tests__/lex-v2/utils/resolveBotConfig.test.ts index ff36b7f6c85..4decfe38186 100644 --- a/packages/interactions/__tests__/lex-v2/utils/resolveBotConfig.test.ts +++ b/packages/interactions/__tests__/lex-v2/utils/resolveBotConfig.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 { AMPLIFY_CONTEXT_BRAND, AmplifyContext } from '@aws-amplify/core'; import { generateRandomLexV1Config, generateRandomLexV2Config, @@ -9,23 +9,31 @@ import { import { resolveBotConfig } from '../../../src/lex-v2/utils'; describe('Interactions LexV2 Util: resolveBotConfig', () => { - const getConfigSpy = jest.spyOn(Amplify, 'getConfig'); - - afterEach(() => { - getConfigSpy.mockReset(); - }); + const createMockCtx = ( + resourcesConfig: Record, + ): AmplifyContext => { + const ctx = { + [AMPLIFY_CONTEXT_BRAND]: true, + resourcesConfig, + libraryOptions: {}, + fetchAuthSession: jest.fn(), + clearCredentials: jest.fn(), + getTokens: jest.fn(), + }; + return ctx as unknown as AmplifyContext; + }; it('find correct bot config if exist', () => { const v1BotConfigs = [...Array(5)].map(generateRandomLexV1Config); const v2BotConfigs = [...Array(5)].map(generateRandomLexV2Config); - getConfigSpy.mockReturnValue({ + const ctx = createMockCtx({ Interactions: { LexV1: Object.fromEntries(v1BotConfigs.map(bot => [bot.name, bot])), LexV2: Object.fromEntries(v2BotConfigs.map(bot => [bot.name, bot])), }, }); - const result = resolveBotConfig(v2BotConfigs[3].name); + const result = resolveBotConfig(ctx, v2BotConfigs[3].name); expect(result).not.toBeUndefined(); expect(result).toStrictEqual(v2BotConfigs[3]); }); @@ -33,28 +41,28 @@ describe('Interactions LexV2 Util: resolveBotConfig', () => { it('ignore v2 bot config', () => { const v1BotConfigs = [...Array(5)].map(generateRandomLexV1Config); const v2BotConfigs = [...Array(5)].map(generateRandomLexV2Config); - getConfigSpy.mockReturnValue({ + const ctx = createMockCtx({ Interactions: { LexV1: Object.fromEntries(v1BotConfigs.map(bot => [bot.name, bot])), LexV2: Object.fromEntries(v2BotConfigs.map(bot => [bot.name, bot])), }, }); - const result = resolveBotConfig(v1BotConfigs[3].name); + const result = resolveBotConfig(ctx, v1BotConfigs[3].name); expect(result).toBeUndefined(); }); it('return undefined for non-exist bot', () => { const v1BotConfigs = [...Array(5)].map(generateRandomLexV1Config); const v2BotConfigs = [...Array(5)].map(generateRandomLexV2Config); - getConfigSpy.mockReturnValue({ + const ctx = createMockCtx({ Interactions: { LexV1: Object.fromEntries(v1BotConfigs.map(bot => [bot.name, bot])), LexV2: Object.fromEntries(v2BotConfigs.map(bot => [bot.name, bot])), }, }); - const result = resolveBotConfig('test'); + const result = resolveBotConfig(ctx, 'test'); expect(result).toBeUndefined(); }); }); diff --git a/packages/interactions/src/lex-v1/AWSLexProvider.ts b/packages/interactions/src/lex-v1/AWSLexProvider.ts index 5ebf89be52c..dfc2e5235a1 100644 --- a/packages/interactions/src/lex-v1/AWSLexProvider.ts +++ b/packages/interactions/src/lex-v1/AWSLexProvider.ts @@ -1,5 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +import { AmplifyContext, ConsoleLogger } from '@aws-amplify/core'; import { DialogState, LexRuntimeServiceClient, @@ -11,7 +12,6 @@ import { PostTextCommandOutput, } from '@aws-sdk/client-lex-runtime-service'; import { getAmplifyUserAgentObject } from '@aws-amplify/core/internals/utils'; -import { ConsoleLogger, fetchAuthSession } from '@aws-amplify/core'; import { InteractionsMessage, @@ -34,11 +34,16 @@ type AWSLexProviderSendResponse = | PostContentCommandOutputFormatted; class AWSLexProvider { + private ctx: AmplifyContext; private readonly _botsCompleteCallback: Record< string, InteractionsOnCompleteCallback > = {}; + constructor(ctx: AmplifyContext) { + this.ctx = ctx; + } + /** * @deprecated * This is used internally by 'sendMessage' to call onComplete callback @@ -75,7 +80,7 @@ class AWSLexProvider { // check if credentials are present let session; try { - session = await fetchAuthSession(); + session = await this.ctx.fetchAuthSession(); } catch (error) { return Promise.reject(new Error('No credentials')); } @@ -168,4 +173,5 @@ class AWSLexProvider { } } -export const lexProvider = new AWSLexProvider(); +export const createLexProvider = (ctx: AmplifyContext) => + new AWSLexProvider(ctx); diff --git a/packages/interactions/src/lex-v1/apis/onComplete.ts b/packages/interactions/src/lex-v1/apis/onComplete.ts index c398d156098..12d05a8b4a6 100644 --- a/packages/interactions/src/lex-v1/apis/onComplete.ts +++ b/packages/interactions/src/lex-v1/apis/onComplete.ts @@ -1,21 +1,26 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +import { AmplifyContext } from '@aws-amplify/core'; + import { OnCompleteInput } from '../types'; import { resolveBotConfig } from '../utils'; -import { lexProvider } from '../AWSLexProvider'; +import { createLexProvider } from '../AWSLexProvider'; import { InteractionsValidationErrorCode, assertValidationError, } from '../../errors'; -export const onComplete = (input: OnCompleteInput): void => { +export const onComplete = ( + ctx: AmplifyContext, + input: OnCompleteInput, +): void => { const { botName, callback } = input; - const botConfig = resolveBotConfig(botName); + const botConfig = resolveBotConfig(ctx, botName); assertValidationError( !!botConfig, InteractionsValidationErrorCode.NoBotConfig, `Bot ${botName} does not exist.`, ); - lexProvider.onComplete(botConfig, callback); + createLexProvider(ctx).onComplete(botConfig, callback); }; diff --git a/packages/interactions/src/lex-v1/apis/send.ts b/packages/interactions/src/lex-v1/apis/send.ts index d384bc4dea2..9345cb5a208 100644 --- a/packages/interactions/src/lex-v1/apis/send.ts +++ b/packages/interactions/src/lex-v1/apis/send.ts @@ -1,22 +1,27 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +import { AmplifyContext } from '@aws-amplify/core'; + import { SendInput, SendOutput } from '../types'; import { resolveBotConfig } from '../utils'; -import { lexProvider } from '../AWSLexProvider'; +import { createLexProvider } from '../AWSLexProvider'; import { InteractionsValidationErrorCode, assertValidationError, } from '../../errors'; -export const send = async (input: SendInput): Promise => { +export const send = async ( + ctx: AmplifyContext, + input: SendInput, +): Promise => { const { botName, message } = input; - const botConfig = resolveBotConfig(botName); + const botConfig = resolveBotConfig(ctx, botName); assertValidationError( !!botConfig, InteractionsValidationErrorCode.NoBotConfig, `Bot ${botName} does not exist.`, ); - return lexProvider.sendMessage(botConfig, message); + return createLexProvider(ctx).sendMessage(botConfig, message); }; diff --git a/packages/interactions/src/lex-v1/types/AWSLexProviderOption.ts b/packages/interactions/src/lex-v1/types/AWSLexProviderOption.ts index b6c883374ad..99a93ca7a0a 100644 --- a/packages/interactions/src/lex-v1/types/AWSLexProviderOption.ts +++ b/packages/interactions/src/lex-v1/types/AWSLexProviderOption.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 { OnCompleteInput, SendInput } from './inputs'; import { SendOutput } from './outputs'; @@ -11,6 +13,6 @@ export interface AWSLexProviderOption { } export interface IInteractions { - send(input: SendInput): Promise; - onComplete(input: OnCompleteInput): void; + send(ctx: AmplifyContext, input: SendInput): Promise; + onComplete(ctx: AmplifyContext, input: OnCompleteInput): void; } diff --git a/packages/interactions/src/lex-v1/utils/resolveBotConfig.ts b/packages/interactions/src/lex-v1/utils/resolveBotConfig.ts index 6a28df2d025..25c21abc6d2 100644 --- a/packages/interactions/src/lex-v1/utils/resolveBotConfig.ts +++ b/packages/interactions/src/lex-v1/utils/resolveBotConfig.ts @@ -1,15 +1,16 @@ // 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 { AWSLexProviderOption } from '../types'; export const resolveBotConfig = ( + ctx: AmplifyContext, botName: string, ): AWSLexProviderOption | undefined => { const { [botName]: botConfig = undefined } = - Amplify.getConfig().Interactions?.LexV1 ?? {}; + ctx.resourcesConfig.Interactions?.LexV1 ?? {}; if (botConfig !== undefined) { return { ...botConfig, name: botName }; } diff --git a/packages/interactions/src/lex-v2/AWSLexV2Provider.ts b/packages/interactions/src/lex-v2/AWSLexV2Provider.ts index f8569b9d57b..83eeb0b878c 100644 --- a/packages/interactions/src/lex-v2/AWSLexV2Provider.ts +++ b/packages/interactions/src/lex-v2/AWSLexV2Provider.ts @@ -1,5 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +import { AmplifyContext, ConsoleLogger } from '@aws-amplify/core'; import { IntentState, LexRuntimeV2Client, @@ -14,7 +15,6 @@ import { amplifyUuid, getAmplifyUserAgentObject, } from '@aws-amplify/core/internals/utils'; -import { ConsoleLogger, fetchAuthSession } from '@aws-amplify/core'; import { convert, unGzipBase64AsJson } from '../utils'; import { @@ -55,6 +55,7 @@ interface lexV2BaseReqParams { } class AWSLexV2Provider { + private ctx: AmplifyContext; private readonly _botsCompleteCallback: Record< string, InteractionsOnCompleteCallback @@ -62,6 +63,10 @@ class AWSLexV2Provider { private defaultSessionId: string = amplifyUuid(); + constructor(ctx: AmplifyContext) { + this.ctx = ctx; + } + /** * Send a message to a bot * @async @@ -76,7 +81,7 @@ class AWSLexV2Provider { // check if credentials are present let session; try { - session = await fetchAuthSession(); + session = await this.ctx.fetchAuthSession(); } catch (error) { return Promise.reject(new Error('No credentials')); } @@ -268,4 +273,5 @@ class AWSLexV2Provider { } } -export const lexProvider = new AWSLexV2Provider(); +export const createLexV2Provider = (ctx: AmplifyContext) => + new AWSLexV2Provider(ctx); diff --git a/packages/interactions/src/lex-v2/apis/onComplete.ts b/packages/interactions/src/lex-v2/apis/onComplete.ts index 162cf518310..e34d3b8a460 100644 --- a/packages/interactions/src/lex-v2/apis/onComplete.ts +++ b/packages/interactions/src/lex-v2/apis/onComplete.ts @@ -1,21 +1,27 @@ // 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 { OnCompleteInput } from '../types'; import { resolveBotConfig } from '../utils'; -import { lexProvider } from '../AWSLexV2Provider'; +import { createLexV2Provider } from '../AWSLexV2Provider'; import { InteractionsValidationErrorCode, assertValidationError, } from '../../errors'; -export const onComplete = (input: OnCompleteInput): void => { +export function onComplete(input: OnCompleteInput): void; +export function onComplete(ctx: AmplifyContext, input: OnCompleteInput): void; +export function onComplete(...args: any[]): void { + const [ctx, input] = resolveCtxArgs(args); const { botName, callback } = input; - const botConfig = resolveBotConfig(botName); + const botConfig = resolveBotConfig(ctx, botName); assertValidationError( !!botConfig, InteractionsValidationErrorCode.NoBotConfig, `Bot ${botName} does not exist.`, ); - lexProvider.onComplete(botConfig, callback); -}; + createLexV2Provider(ctx).onComplete(botConfig, callback); +} diff --git a/packages/interactions/src/lex-v2/apis/send.ts b/packages/interactions/src/lex-v2/apis/send.ts index 3d76e7eb540..317352aac23 100644 --- a/packages/interactions/src/lex-v2/apis/send.ts +++ b/packages/interactions/src/lex-v2/apis/send.ts @@ -1,22 +1,31 @@ // 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 { SendInput, SendOutput } from '../types'; -import { lexProvider } from '../AWSLexV2Provider'; +import { createLexV2Provider } from '../AWSLexV2Provider'; import { resolveBotConfig } from '../utils'; import { InteractionsValidationErrorCode, assertValidationError, } from '../../errors'; -export const send = async (input: SendInput): Promise => { +export async function send(input: SendInput): Promise; +export async function send( + ctx: AmplifyContext, + input: SendInput, +): Promise; +export async function send(...args: any[]): Promise { + const [ctx, input] = resolveCtxArgs(args); const { botName, message } = input; - const botConfig = resolveBotConfig(botName); + const botConfig = resolveBotConfig(ctx, botName); assertValidationError( !!botConfig, InteractionsValidationErrorCode.NoBotConfig, `Bot ${botName} does not exist.`, ); - return lexProvider.sendMessage(botConfig, message); -}; + return createLexV2Provider(ctx).sendMessage(botConfig, message); +} diff --git a/packages/interactions/src/lex-v2/types/AWSLexV2ProviderOption.ts b/packages/interactions/src/lex-v2/types/AWSLexV2ProviderOption.ts index 4be0f8e00bd..8c087cf05a5 100644 --- a/packages/interactions/src/lex-v2/types/AWSLexV2ProviderOption.ts +++ b/packages/interactions/src/lex-v2/types/AWSLexV2ProviderOption.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 { OnCompleteInput, SendInput } from './inputs'; import { SendOutput } from './outputs'; @@ -13,6 +15,6 @@ export interface AWSLexV2ProviderOption { } export interface IInteractions { - send(input: SendInput): Promise; - onComplete(input: OnCompleteInput): void; + send(ctx: AmplifyContext, input: SendInput): Promise; + onComplete(ctx: AmplifyContext, input: OnCompleteInput): void; } diff --git a/packages/interactions/src/lex-v2/utils/resolveBotConfig.ts b/packages/interactions/src/lex-v2/utils/resolveBotConfig.ts index 5abb0b29588..1b5fa0e0e28 100644 --- a/packages/interactions/src/lex-v2/utils/resolveBotConfig.ts +++ b/packages/interactions/src/lex-v2/utils/resolveBotConfig.ts @@ -1,15 +1,16 @@ // 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 { AWSLexV2ProviderOption } from '../types'; export const resolveBotConfig = ( + ctx: AmplifyContext, botName: string, ): AWSLexV2ProviderOption | undefined => { const { [botName]: botConfig = undefined } = - Amplify.getConfig().Interactions?.LexV2 ?? {}; + ctx.resourcesConfig.Interactions?.LexV2 ?? {}; if (botConfig !== undefined) { return { ...botConfig, name: botName }; } diff --git a/packages/notifications/__tests__/inAppMessaging/providers/pinpoint/apis/clearMessages.test.ts b/packages/notifications/__tests__/inAppMessaging/providers/pinpoint/apis/clearMessages.test.ts index 8cd7b8293e8..c1576e102bc 100644 --- a/packages/notifications/__tests__/inAppMessaging/providers/pinpoint/apis/clearMessages.test.ts +++ b/packages/notifications/__tests__/inAppMessaging/providers/pinpoint/apis/clearMessages.test.ts @@ -12,6 +12,7 @@ import { STORAGE_KEY_SUFFIX, } from '../../../../../src/inAppMessaging/providers/pinpoint/utils'; import { InAppMessagingError } from '../../../../../src/inAppMessaging/errors'; +import { createMockAmplifyContext } from '../../../../testUtils/createMockAmplifyContext'; jest.mock('@aws-amplify/core/internals/aws-clients/pinpoint'); jest.mock('@aws-amplify/core'); @@ -21,6 +22,8 @@ jest.mock('../../../../../src/inAppMessaging/providers/pinpoint/utils'); const mockDefaultStorage = defaultStorage as jest.Mocked; +const mockCtx = createMockAmplifyContext(); + describe('clearMessages', () => { afterEach(() => { mockDefaultStorage.removeItem.mockClear(); @@ -33,7 +36,7 @@ describe('clearMessages', () => { }); it('Rejects if there is a failure storing messages', async () => { - initializeInAppMessaging(); + initializeInAppMessaging(mockCtx); mockDefaultStorage.removeItem.mockRejectedValueOnce( new InAppMessagingError({ name: 'ItemCorrupted', @@ -46,7 +49,7 @@ describe('clearMessages', () => { }); it('Succeeds in calling the removeItem API of defaultStorage with the correct key', async () => { - initializeInAppMessaging(); + initializeInAppMessaging(mockCtx); await clearMessages(); expect(mockDefaultStorage.removeItem).toHaveBeenCalledWith( `${PINPOINT_KEY_PREFIX}${STORAGE_KEY_SUFFIX}`, diff --git a/packages/notifications/__tests__/inAppMessaging/providers/pinpoint/apis/dispatchEvent.test.ts b/packages/notifications/__tests__/inAppMessaging/providers/pinpoint/apis/dispatchEvent.test.ts index c75b6407006..6741830b74d 100644 --- a/packages/notifications/__tests__/inAppMessaging/providers/pinpoint/apis/dispatchEvent.test.ts +++ b/packages/notifications/__tests__/inAppMessaging/providers/pinpoint/apis/dispatchEvent.test.ts @@ -19,6 +19,7 @@ import { } from '../../../../testUtils/data'; import { InAppMessagingError } from '../../../../../src/inAppMessaging/errors'; import { notifyEventListeners } from '../../../../../src/eventListeners'; +import { createMockAmplifyContext } from '../../../../testUtils/createMockAmplifyContext'; jest.mock('@aws-amplify/core'); jest.mock('../../../../../src/inAppMessaging/providers/pinpoint/utils'); @@ -29,9 +30,11 @@ const mockGetConflictHandler = getConflictHandler as jest.Mock; const mockNotifyEventListeners = notifyEventListeners as jest.Mock; const mockProcessInAppMessages = processInAppMessages as jest.Mock; +const mockCtx = createMockAmplifyContext(); + describe('dispatchEvent', () => { beforeAll(() => { - initializeInAppMessaging(); + initializeInAppMessaging(mockCtx); }); beforeEach(() => { mockGetConflictHandler.mockReturnValue(() => inAppMessages[0]); diff --git a/packages/notifications/__tests__/inAppMessaging/providers/pinpoint/apis/identifyUser.test.ts b/packages/notifications/__tests__/inAppMessaging/providers/pinpoint/apis/identifyUser.test.ts index 7dd666d47f0..863c710cd8c 100644 --- a/packages/notifications/__tests__/inAppMessaging/providers/pinpoint/apis/identifyUser.test.ts +++ b/packages/notifications/__tests__/inAppMessaging/providers/pinpoint/apis/identifyUser.test.ts @@ -15,10 +15,13 @@ import { resolveCredentials, } from '../../../../../src/inAppMessaging/providers/pinpoint/utils'; import { IdentifyUserInput } from '../../../../../src/inAppMessaging/providers/pinpoint/types'; +import { createMockAmplifyContext } from '../../../../testUtils/createMockAmplifyContext'; jest.mock('@aws-amplify/core/internals/providers/pinpoint'); jest.mock('../../../../../src/inAppMessaging/providers/pinpoint/utils'); +const mockCtx = createMockAmplifyContext(); + describe('InAppMessaging Pinpoint Provider API: identifyUser', () => { const credentials = { credentials: { @@ -37,7 +40,7 @@ describe('InAppMessaging Pinpoint Provider API: identifyUser', () => { const mockResolveCredentials = resolveCredentials as jest.Mock; beforeAll(() => { - initializeInAppMessaging(); + initializeInAppMessaging(mockCtx); mockgetInAppMessagingUserAgentString.mockReturnValue(userAgentValue); mockResolveConfig.mockReturnValue(config); mockResolveCredentials.mockResolvedValue(credentials); @@ -59,7 +62,7 @@ describe('InAppMessaging Pinpoint Provider API: identifyUser', () => { plan: 'plan', }, }; - await identifyUser(input); + await identifyUser(mockCtx, input); expect(mockUpdateEndpoint).toHaveBeenCalledWith({ ...input, ...credentials, @@ -81,7 +84,7 @@ describe('InAppMessaging Pinpoint Provider API: identifyUser', () => { optOut: 'NONE', userAttributes, }; - await identifyUser({ ...input, options }); + await identifyUser(mockCtx, { ...input, options }); expect(mockUpdateEndpoint).toHaveBeenCalledWith({ ...input, ...options, @@ -100,6 +103,6 @@ describe('InAppMessaging Pinpoint Provider API: identifyUser', () => { userId: 'user-id', userProfile: {}, }; - await expect(identifyUser(input)).rejects.toBeDefined(); + await expect(identifyUser(mockCtx, input)).rejects.toBeDefined(); }); }); diff --git a/packages/notifications/__tests__/inAppMessaging/providers/pinpoint/apis/initializeInAppMessaging.test.ts b/packages/notifications/__tests__/inAppMessaging/providers/pinpoint/apis/initializeInAppMessaging.test.ts index 18e78ce08aa..2bc8b1bb45c 100644 --- a/packages/notifications/__tests__/inAppMessaging/providers/pinpoint/apis/initializeInAppMessaging.test.ts +++ b/packages/notifications/__tests__/inAppMessaging/providers/pinpoint/apis/initializeInAppMessaging.test.ts @@ -9,6 +9,7 @@ import { notifyEventListeners, } from '../../../../../src/eventListeners'; import { initializeInAppMessaging } from '../../../../../src/inAppMessaging/providers/pinpoint/apis'; +import { createMockAmplifyContext } from '../../../../testUtils/createMockAmplifyContext'; jest.mock('@aws-amplify/core'); jest.mock('../../../../../src/eventListeners'); @@ -17,12 +18,14 @@ jest.mock('@aws-amplify/core/internals/utils'); const mockNotifyEventListeners = notifyEventListeners as jest.Mock; const mockAddEventListener = addEventListener as jest.Mock; +const mockCtx = createMockAmplifyContext(); + describe('initializeInAppMessaging', () => { beforeEach(() => { mockNotifyEventListeners.mockClear(); }); it('will intialize session tracking, analytics listeners and in-app events listeners', async () => { - initializeInAppMessaging(); + initializeInAppMessaging(mockCtx); expect(sessionListener.addStateChangeListener).toHaveBeenCalledTimes(1); expect(mockAddEventListener).toHaveBeenNthCalledWith( diff --git a/packages/notifications/__tests__/inAppMessaging/providers/pinpoint/apis/interactionEvents.test.ts b/packages/notifications/__tests__/inAppMessaging/providers/pinpoint/apis/interactionEvents.test.ts index 9735d01c737..689c2097d20 100644 --- a/packages/notifications/__tests__/inAppMessaging/providers/pinpoint/apis/interactionEvents.test.ts +++ b/packages/notifications/__tests__/inAppMessaging/providers/pinpoint/apis/interactionEvents.test.ts @@ -14,16 +14,19 @@ import { onMessageDisplayed, onMessageReceived, } from '../../../../../src/inAppMessaging/providers/pinpoint/apis'; +import { createMockAmplifyContext } from '../../../../testUtils/createMockAmplifyContext'; jest.mock('../../../../../src/eventListeners'); const mockNotifyEventListeners = notifyEventListeners as jest.Mock; const mockAddEventListener = addEventListener as jest.Mock; +const mockCtx = createMockAmplifyContext(); + describe('Interaction events', () => { const handler = jest.fn(); beforeAll(() => { - initializeInAppMessaging(); + initializeInAppMessaging(mockCtx); }); it('can be listened to by onMessageReceived', () => { onMessageReceived(handler); diff --git a/packages/notifications/__tests__/inAppMessaging/providers/pinpoint/apis/setConflictHandler.test.ts b/packages/notifications/__tests__/inAppMessaging/providers/pinpoint/apis/setConflictHandler.test.ts index 09387f13671..8ac91c356da 100644 --- a/packages/notifications/__tests__/inAppMessaging/providers/pinpoint/apis/setConflictHandler.test.ts +++ b/packages/notifications/__tests__/inAppMessaging/providers/pinpoint/apis/setConflictHandler.test.ts @@ -6,6 +6,7 @@ import { setConflictHandler, } from '../../../../../src/inAppMessaging/providers/pinpoint/apis'; import { setConflictHandler as setConflictHandlerInteral } from '../../../../../src/inAppMessaging/providers/pinpoint/utils'; +import { createMockAmplifyContext } from '../../../../testUtils/createMockAmplifyContext'; jest.mock('@aws-amplify/core'); jest.mock('@aws-amplify/core/internals/utils'); @@ -14,9 +15,11 @@ jest.mock('../../../../../src/eventListeners'); const mockSetConflictHandlerInteral = setConflictHandlerInteral as jest.Mock; +const mockCtx = createMockAmplifyContext(); + describe('setConflictHandler', () => { beforeAll(() => { - initializeInAppMessaging(); + initializeInAppMessaging(mockCtx); }); afterEach(() => { diff --git a/packages/notifications/__tests__/inAppMessaging/providers/pinpoint/apis/syncMessages.test.ts b/packages/notifications/__tests__/inAppMessaging/providers/pinpoint/apis/syncMessages.test.ts index 1de3b03ebf0..12d7ad90161 100644 --- a/packages/notifications/__tests__/inAppMessaging/providers/pinpoint/apis/syncMessages.test.ts +++ b/packages/notifications/__tests__/inAppMessaging/providers/pinpoint/apis/syncMessages.test.ts @@ -20,6 +20,7 @@ import { } from '../../../../../src/inAppMessaging/providers/pinpoint/utils'; import { simpleInAppMessages } from '../../../../testUtils/data'; import { InAppMessagingError } from '../../../../../src/inAppMessaging/errors'; +import { createMockAmplifyContext } from '../../../../testUtils/createMockAmplifyContext'; jest.mock('@aws-amplify/core/internals/aws-clients/pinpoint'); jest.mock('@aws-amplify/core'); @@ -54,9 +55,11 @@ const mockedEmptyMessages = { }, }; +const mockCtx = createMockAmplifyContext(); + describe('syncMessages', () => { beforeAll(() => { - initializeInAppMessaging(); + initializeInAppMessaging(mockCtx); mockGetInAppMessagingUserAgentString.mockReturnValue(userAgentValue); mockResolveConfig.mockReturnValue(config); mockResolveCredentials.mockResolvedValue(credentials); @@ -74,7 +77,7 @@ describe('syncMessages', () => { }); it('Gets in-app messages and stores them', async () => { - await syncMessages(); + await syncMessages(mockCtx); expect(mockDefaultStorage.setItem).toHaveBeenCalledWith( expect.stringContaining(STORAGE_KEY_SUFFIX), @@ -84,7 +87,7 @@ describe('syncMessages', () => { it('Only tries to store messages if there are messages to store', async () => { mockGetInAppMessages.mockResolvedValueOnce(mockedEmptyMessages); - await syncMessages(); + await syncMessages(mockCtx); expect(mockDefaultStorage.setItem).not.toHaveBeenCalled(); }); @@ -93,7 +96,7 @@ describe('syncMessages', () => { mockResolveEndpointId.mockImplementation(() => { throw new Error(); }); - await expect(syncMessages()).rejects.toStrictEqual( + await expect(syncMessages(mockCtx)).rejects.toStrictEqual( expect.any(InAppMessagingError), ); @@ -102,7 +105,7 @@ describe('syncMessages', () => { it('Rejects if there is a failure getting messages', async () => { mockGetInAppMessages.mockRejectedValueOnce(Error); - await expect(syncMessages()).rejects.toStrictEqual( + await expect(syncMessages(mockCtx)).rejects.toStrictEqual( expect.any(InAppMessagingError), ); @@ -111,7 +114,7 @@ describe('syncMessages', () => { it('Rejects if there is a failure storing messages', async () => { mockDefaultStorage.setItem.mockRejectedValueOnce(Error); - await expect(syncMessages()).rejects.toStrictEqual( + await expect(syncMessages(mockCtx)).rejects.toStrictEqual( expect.any(InAppMessagingError), ); }); diff --git a/packages/notifications/__tests__/inAppMessaging/providers/pinpoint/utils/resolveConfig.test.ts b/packages/notifications/__tests__/inAppMessaging/providers/pinpoint/utils/resolveConfig.test.ts index 6819c3ff670..9b626d70e0f 100644 --- a/packages/notifications/__tests__/inAppMessaging/providers/pinpoint/utils/resolveConfig.test.ts +++ b/packages/notifications/__tests__/inAppMessaging/providers/pinpoint/utils/resolveConfig.test.ts @@ -1,9 +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 { resolveConfig } from '../../../../../src/inAppMessaging/providers/pinpoint/utils'; +import { createMockAmplifyContext } from '../../../../testUtils/createMockAmplifyContext'; describe('resolveConfig', () => { const validConfig = { @@ -14,8 +13,8 @@ describe('resolveConfig', () => { }, }; it('should return the configured appId and region', () => { - Amplify.configure(validConfig); - expect(resolveConfig()).toStrictEqual( + const mockCtx = createMockAmplifyContext(validConfig); + expect(resolveConfig(mockCtx)).toStrictEqual( validConfig.Notifications!.InAppMessaging!.Pinpoint, ); }); diff --git a/packages/notifications/__tests__/inAppMessaging/providers/pinpoint/utils/resolveCredentials.test.ts b/packages/notifications/__tests__/inAppMessaging/providers/pinpoint/utils/resolveCredentials.test.ts index 87e0c847c8f..c9cb322864a 100644 --- a/packages/notifications/__tests__/inAppMessaging/providers/pinpoint/utils/resolveCredentials.test.ts +++ b/packages/notifications/__tests__/inAppMessaging/providers/pinpoint/utils/resolveCredentials.test.ts @@ -1,12 +1,8 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { fetchAuthSession } from '@aws-amplify/core'; - import { resolveCredentials } from '../../../../../src/inAppMessaging/providers/pinpoint/utils'; - -jest.mock('@aws-amplify/core'); -const mockFetchAuthSession = fetchAuthSession as jest.Mock; +import { createMockAmplifyContext } from '../../../../testUtils/createMockAmplifyContext'; describe('resolveCredentials', () => { const credentials = { @@ -18,7 +14,7 @@ describe('resolveCredentials', () => { }; it('should return the credentials and identityId', async () => { - mockFetchAuthSession.mockReturnValue(credentials); - expect(await resolveCredentials()).toStrictEqual(credentials); + const mockCtx = createMockAmplifyContext({}, credentials); + expect(await resolveCredentials(mockCtx)).toStrictEqual(credentials); }); }); diff --git a/packages/notifications/__tests__/inAppMessaging/utils/processInAppMessages.test.ts b/packages/notifications/__tests__/inAppMessaging/utils/processInAppMessages.test.ts index af07c17c4a3..8cd591b6021 100644 --- a/packages/notifications/__tests__/inAppMessaging/utils/processInAppMessages.test.ts +++ b/packages/notifications/__tests__/inAppMessaging/utils/processInAppMessages.test.ts @@ -18,6 +18,7 @@ import { matchesMetrics, } from '../../../src/inAppMessaging/providers/pinpoint/utils/helpers'; import { initializeInAppMessaging } from '../../../src/inAppMessaging/providers/pinpoint/apis'; +import { createMockAmplifyContext } from '../../testUtils/createMockAmplifyContext'; jest.mock('@aws-amplify/core'); jest.mock('@aws-amplify/core/internals/utils'); @@ -28,6 +29,8 @@ const mockMatchesAttributes = matchesAttributes as jest.Mock; const mockMatchesEventType = matchesEventType as jest.Mock; const mockMatchesMetrics = matchesMetrics as jest.Mock; +const mockCtx = createMockAmplifyContext(); + describe('processInAppMessages', () => { const messages = [ cloneDeep(pinpointInAppMessage), @@ -46,7 +49,7 @@ describe('processInAppMessages', () => { }, ]; beforeAll(() => { - initializeInAppMessaging(); + initializeInAppMessaging(mockCtx); }); beforeEach(() => { mockMatchesEventType.mockReturnValue(true); diff --git a/packages/notifications/__tests__/pushNotifications/providers/pinpoint/apis/identifyUser.native.test.ts b/packages/notifications/__tests__/pushNotifications/providers/pinpoint/apis/identifyUser.native.test.ts index d23600d7ab7..e306da6e23b 100644 --- a/packages/notifications/__tests__/pushNotifications/providers/pinpoint/apis/identifyUser.native.test.ts +++ b/packages/notifications/__tests__/pushNotifications/providers/pinpoint/apis/identifyUser.native.test.ts @@ -24,6 +24,7 @@ import { pinpointConfig, userAgentValue, } from '../../../../testUtils/data'; +import { createMockAmplifyContext } from '../../../../testUtils/createMockAmplifyContext'; jest.mock('@aws-amplify/core/internals/providers/pinpoint'); jest.mock('@aws-amplify/react-native', () => ({ @@ -33,6 +34,8 @@ jest.mock('../../../../../src/pushNotifications/errors/errorHelpers'); jest.mock('../../../../../src/pushNotifications/providers/pinpoint/utils'); jest.mock('../../../../../src/pushNotifications/utils'); +const mockCtx = createMockAmplifyContext(); + describe('identifyUser (native)', () => { // assert mocks const mockAssertIsInitialized = assertIsInitialized as jest.Mock; @@ -65,7 +68,7 @@ describe('identifyUser (native)', () => { throw new Error(); }); await expect( - identifyUser({ userId: 'user-id', userProfile: {} }), + identifyUser(mockCtx, { userId: 'user-id', userProfile: {} }), ).rejects.toThrow(); }); @@ -81,7 +84,7 @@ describe('identifyUser (native)', () => { plan: 'plan', }, }; - await identifyUser(input); + await identifyUser(mockCtx, input); expect(mockUpdateEndpoint).toHaveBeenCalledWith({ ...input, ...credentials, @@ -101,7 +104,7 @@ describe('identifyUser (native)', () => { const options: IdentifyUserInput['options'] = { userAttributes, }; - await identifyUser({ ...input, options }); + await identifyUser(mockCtx, { ...input, options }); expect(mockUpdateEndpoint).toHaveBeenCalledWith({ ...input, ...credentials, @@ -119,7 +122,7 @@ describe('identifyUser (native)', () => { userId: 'user-id', userProfile: {}, }; - await expect(identifyUser(input)).rejects.toBeDefined(); + await expect(identifyUser(mockCtx, input)).rejects.toBeDefined(); }); it('awaits device registration promise when endpoint is not present', async () => { @@ -128,7 +131,7 @@ describe('identifyUser (native)', () => { userProfile: {}, }; mockGetEndpointId.mockResolvedValue(undefined); - await identifyUser(input); + await identifyUser(mockCtx, input); expect(mockGetInflightDeviceRegistration).toHaveBeenCalled(); }); @@ -138,7 +141,7 @@ describe('identifyUser (native)', () => { userProfile: {}, }; mockGetEndpointId.mockResolvedValue('endpoint-id'); - await identifyUser(input); + await identifyUser(mockCtx, input); expect(mockGetInflightDeviceRegistration).not.toHaveBeenCalled(); }); }); diff --git a/packages/notifications/__tests__/pushNotifications/providers/pinpoint/apis/initializePushNotifications.native.test.ts b/packages/notifications/__tests__/pushNotifications/providers/pinpoint/apis/initializePushNotifications.native.test.ts index 4d81625b5a4..6fa2d5e5247 100644 --- a/packages/notifications/__tests__/pushNotifications/providers/pinpoint/apis/initializePushNotifications.native.test.ts +++ b/packages/notifications/__tests__/pushNotifications/providers/pinpoint/apis/initializePushNotifications.native.test.ts @@ -27,6 +27,7 @@ import { pushToken, simplePushMessage, } from '../../../../testUtils/data'; +import { createMockAmplifyContext } from '../../../../testUtils/createMockAmplifyContext'; jest.mock('@aws-amplify/core'); jest.mock('@aws-amplify/core/internals/providers/pinpoint'); @@ -51,8 +52,10 @@ const mockCompleteNotification = jest.fn(); const mockGetConstants = jest.fn(); const mockRegisterHeadlessTask = jest.fn(); +const mockCtx = createMockAmplifyContext(); + describe('initializePushNotifications (native)', () => { - let initializePushNotifications: () => void; + let initializePushNotifications: (...args: any[]) => void; const { NativeEvent } = pushModuleConstants; // create mocks const mockEventListenerRemover = { remove: jest.fn() }; @@ -129,13 +132,13 @@ describe('initializePushNotifications (native)', () => { it('only enables once', () => { mockIsInitialized.mockReturnValue(true); - initializePushNotifications(); + initializePushNotifications(mockCtx); expect(mockInitialize).not.toHaveBeenCalled(); }); describe('background notification', () => { it('registers a headless task if able', () => { - initializePushNotifications(); + initializePushNotifications(mockCtx); expect(mockRegisterHeadlessTask).toHaveBeenCalledWith( expect.any(Function), ); @@ -148,7 +151,7 @@ describe('initializePushNotifications (native)', () => { mockRegisterHeadlessTask.mockImplementation(task => { task(simplePushMessage); }); - initializePushNotifications(); + initializePushNotifications(mockCtx); expect(mockNotifyEventListenersAndAwaitHandlers).toHaveBeenCalledWith( 'backgroundMessageReceived', simplePushMessage, @@ -158,7 +161,7 @@ describe('initializePushNotifications (native)', () => { it('registers and calls background notification listener if unable to register headless task', () => { listenForEvent(NativeEvent.BACKGROUND_MESSAGE_RECEIVED); mockGetConstants.mockReturnValue({ NativeEvent }); - initializePushNotifications(); + initializePushNotifications(mockCtx); expectListenerForEvent( NativeEvent.BACKGROUND_MESSAGE_RECEIVED, ).toBeAdded(); @@ -180,7 +183,7 @@ describe('initializePushNotifications (native)', () => { done(); }); mockGetConstants.mockReturnValue({ NativeEvent }); - initializePushNotifications(); + initializePushNotifications(mockCtx); expectListenerForEvent( NativeEvent.BACKGROUND_MESSAGE_RECEIVED, ).toBeAdded(); @@ -195,7 +198,7 @@ describe('initializePushNotifications (native)', () => { describe('launch notification', () => { it('registers and calls launch notification listener if able', () => { listenForEvent(NativeEvent.LAUNCH_NOTIFICATION_OPENED); - initializePushNotifications(); + initializePushNotifications(mockCtx); expectListenerForEvent( NativeEvent.LAUNCH_NOTIFICATION_OPENED, @@ -214,7 +217,7 @@ describe('initializePushNotifications (native)', () => { LAUNCH_NOTIFICATION_OPENED: undefined, }, }); - initializePushNotifications(); + initializePushNotifications(mockCtx); expectListenerForEvent( NativeEvent.LAUNCH_NOTIFICATION_OPENED, @@ -225,7 +228,7 @@ describe('initializePushNotifications (native)', () => { it('registers and calls foreground message listener', () => { listenForEvent(NativeEvent.FOREGROUND_MESSAGE_RECEIVED); - initializePushNotifications(); + initializePushNotifications(mockCtx); expectListenerForEvent(NativeEvent.FOREGROUND_MESSAGE_RECEIVED).toBeAdded(); expect(mockNotifyEventListeners).toHaveBeenCalledWith( @@ -236,7 +239,7 @@ describe('initializePushNotifications (native)', () => { it('registers and calls notification opened listener', () => { listenForEvent(NativeEvent.NOTIFICATION_OPENED); - initializePushNotifications(); + initializePushNotifications(mockCtx); expectListenerForEvent(NativeEvent.NOTIFICATION_OPENED).toBeAdded(); expect(mockNotifyEventListeners).toHaveBeenCalledWith( @@ -269,7 +272,7 @@ describe('initializePushNotifications (native)', () => { } }, ); - initializePushNotifications(); + initializePushNotifications(mockCtx); }); it('should not be invoke token received listener with the same token twice', () => { @@ -282,7 +285,7 @@ describe('initializePushNotifications (native)', () => { handler(pushToken); } }); - initializePushNotifications(); + initializePushNotifications(mockCtx); expect(mockNotifyEventListeners).toHaveBeenCalledTimes(1); }); @@ -297,7 +300,7 @@ describe('initializePushNotifications (native)', () => { handler('bar-foo'); } }); - initializePushNotifications(); + initializePushNotifications(mockCtx); expect(mockNotifyEventListeners).toHaveBeenCalledTimes(2); }); @@ -319,7 +322,7 @@ describe('initializePushNotifications (native)', () => { } }, ); - initializePushNotifications(); + initializePushNotifications(mockCtx); }); }); }); diff --git a/packages/notifications/__tests__/pushNotifications/providers/pinpoint/utils/createMessageEventRecorder.test.ts b/packages/notifications/__tests__/pushNotifications/providers/pinpoint/utils/createMessageEventRecorder.test.ts index 98c8cb3a0be..cce39f431cc 100644 --- a/packages/notifications/__tests__/pushNotifications/providers/pinpoint/utils/createMessageEventRecorder.test.ts +++ b/packages/notifications/__tests__/pushNotifications/providers/pinpoint/utils/createMessageEventRecorder.test.ts @@ -15,6 +15,7 @@ import { pinpointConfig, simplePushMessage, } from '../../../../testUtils/data'; +import { createMockAmplifyContext } from '../../../../testUtils/createMockAmplifyContext'; jest.mock('@aws-amplify/core/internals/providers/pinpoint'); jest.mock('@aws-amplify/react-native', () => ({ @@ -31,6 +32,8 @@ jest.mock( ); jest.mock('../../../../../src/pushNotifications/utils'); +const mockCtx = createMockAmplifyContext(); + describe('createMessageEventRecorder', () => { // assert mocks const mockRecord = record as jest.Mock; @@ -51,9 +54,9 @@ describe('createMessageEventRecorder', () => { }); it('returns message event recorder', () => { - expect(createMessageEventRecorder('received_background')).toStrictEqual( - expect.any(Function), - ); + expect( + createMessageEventRecorder(mockCtx, 'received_background'), + ).toStrictEqual(expect.any(Function)); }); it('accepts and invokes a callback', done => { @@ -63,6 +66,7 @@ describe('createMessageEventRecorder', () => { done(); }); const recorder = createMessageEventRecorder( + mockCtx, 'received_background', callback, ); @@ -77,7 +81,10 @@ describe('createMessageEventRecorder', () => { ); done(); }); - const recorder = createMessageEventRecorder('received_background'); + const recorder = createMessageEventRecorder( + mockCtx, + 'received_background', + ); recorder(simplePushMessage); }); }); diff --git a/packages/notifications/__tests__/pushNotifications/utils/resolveConfig.test.ts b/packages/notifications/__tests__/pushNotifications/utils/resolveConfig.test.ts index e30316ef425..64481af2920 100644 --- a/packages/notifications/__tests__/pushNotifications/utils/resolveConfig.test.ts +++ b/packages/notifications/__tests__/pushNotifications/utils/resolveConfig.test.ts @@ -1,47 +1,39 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { Amplify } from '@aws-amplify/core'; - import { resolveConfig } from '../../../src/pushNotifications/providers/pinpoint/utils/resolveConfig'; import { pinpointConfig } from '../../testUtils/data'; +import { createMockAmplifyContext } from '../../testUtils/createMockAmplifyContext'; describe('resolveConfig', () => { - // create spies - const getConfigSpy = jest.spyOn(Amplify, 'getConfig'); - - afterEach(() => { - getConfigSpy.mockReset(); - }); - it('returns required config', () => { - getConfigSpy.mockReturnValue({ + const mockCtx = createMockAmplifyContext({ Notifications: { PushNotification: { Pinpoint: pinpointConfig }, }, }); - expect(resolveConfig()).toStrictEqual(pinpointConfig); + expect(resolveConfig(mockCtx)).toStrictEqual(pinpointConfig); }); it('throws if appId is missing', () => { - getConfigSpy.mockReturnValue({ + const mockCtx = createMockAmplifyContext({ Notifications: { PushNotification: { - Pinpoint: { ...pinpointConfig, appId: undefined } as any, + Pinpoint: { ...pinpointConfig, appId: undefined }, }, }, }); - expect(resolveConfig).toThrow(); + expect(() => resolveConfig(mockCtx)).toThrow(); }); it('throws if region is missing', () => { - getConfigSpy.mockReturnValue({ + const mockCtx = createMockAmplifyContext({ Notifications: { PushNotification: { - Pinpoint: { ...pinpointConfig, region: undefined } as any, + Pinpoint: { ...pinpointConfig, region: undefined }, }, }, }); - expect(resolveConfig).toThrow(); + expect(() => resolveConfig(mockCtx)).toThrow(); }); }); diff --git a/packages/notifications/__tests__/pushNotifications/utils/resolveCredentials.test.ts b/packages/notifications/__tests__/pushNotifications/utils/resolveCredentials.test.ts index a23e17c2dda..e1153a41e0d 100644 --- a/packages/notifications/__tests__/pushNotifications/utils/resolveCredentials.test.ts +++ b/packages/notifications/__tests__/pushNotifications/utils/resolveCredentials.test.ts @@ -1,31 +1,21 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { fetchAuthSession } from '@aws-amplify/core'; - import { resolveCredentials } from '../../../src/pushNotifications/utils'; import { credentials } from '../../testUtils/data'; - -jest.mock('@aws-amplify/core'); +import { createMockAmplifyContext } from '../../testUtils/createMockAmplifyContext'; describe('resolveCredentials', () => { - // assert mocks - const mockFetchAuthSession = fetchAuthSession as jest.Mock; - - beforeEach(() => { - mockFetchAuthSession.mockReset(); - }); - it('resolves required credentials', async () => { - mockFetchAuthSession.mockResolvedValue(credentials); - expect(await resolveCredentials()).toStrictEqual(credentials); + const mockCtx = createMockAmplifyContext({}, credentials); + expect(await resolveCredentials(mockCtx)).toStrictEqual(credentials); }); it('throws if credentials are missing', async () => { - mockFetchAuthSession.mockReturnValue({ - ...credentials, - credentials: undefined, - }); - await expect(resolveCredentials()).rejects.toThrow(); + const mockCtx = createMockAmplifyContext( + {}, + { ...credentials, credentials: undefined }, + ); + await expect(resolveCredentials(mockCtx)).rejects.toThrow(); }); }); diff --git a/packages/notifications/__tests__/testUtils/createMockAmplifyContext.ts b/packages/notifications/__tests__/testUtils/createMockAmplifyContext.ts new file mode 100644 index 00000000000..e72fb1e74de --- /dev/null +++ b/packages/notifications/__tests__/testUtils/createMockAmplifyContext.ts @@ -0,0 +1,24 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { AmplifyContext } from '@aws-amplify/core'; + +/** + * Use Symbol.for directly to match the runtime brand check in isAmplifyContext, + * since jest.mock('@aws-amplify/core') would make the imported AMPLIFY_CONTEXT_BRAND undefined. + */ +const AMPLIFY_CONTEXT_BRAND = Symbol.for('amplify.context'); + +export function createMockAmplifyContext( + resourcesConfig: Record = {}, + fetchAuthSessionValue: Record = {}, +): AmplifyContext { + return { + [AMPLIFY_CONTEXT_BRAND]: true, + resourcesConfig, + libraryOptions: {}, + fetchAuthSession: jest.fn().mockResolvedValue(fetchAuthSessionValue), + clearCredentials: jest.fn().mockResolvedValue(undefined), + getTokens: jest.fn().mockResolvedValue(undefined), + } as unknown as AmplifyContext; +} diff --git a/packages/notifications/package.json b/packages/notifications/package.json index 49325533bc5..b5069b692ba 100644 --- a/packages/notifications/package.json +++ b/packages/notifications/package.json @@ -97,7 +97,7 @@ "tslib": "^2.5.0" }, "peerDependencies": { - "@aws-amplify/core": "^6.16.2" + "@aws-amplify/core": "^6.16.3" }, "devDependencies": { "@aws-amplify/core": "6.16.3", diff --git a/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/identifyUser.ts b/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/identifyUser.ts index 7d9dfd8fae3..61f09e4a970 100644 --- a/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/identifyUser.ts +++ b/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/identifyUser.ts @@ -1,6 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +import { AmplifyContext } from '@aws-amplify/core'; import { InAppMessagingAction } from '@aws-amplify/core/internals/utils'; import { UpdateEndpointException, @@ -69,11 +70,14 @@ import { assertIsInitialized } from '../../../utils'; * }, * }); */ -export const identifyUser = async (input: IdentifyUserInput): Promise => { +export const identifyUser = async ( + ctx: AmplifyContext, + input: IdentifyUserInput, +): Promise => { const { userId, userProfile, options } = input; assertIsInitialized(); - const { credentials, identityId } = await resolveCredentials(); - const { appId, region } = resolveConfig(); + const { credentials, identityId } = await resolveCredentials(ctx); + const { appId, region } = resolveConfig(ctx); const { address, optOut, userAttributes } = options ?? {}; await updateEndpoint({ address, diff --git a/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/initializeInAppMessaging.ts b/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/initializeInAppMessaging.ts index 01374c2b8c5..08f992ef1d4 100644 --- a/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/initializeInAppMessaging.ts +++ b/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/initializeInAppMessaging.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { sessionListener } from '@aws-amplify/core/internals/utils'; -import { Hub, HubCapsule } from '@aws-amplify/core'; +import { AmplifyContext, Hub, HubCapsule } from '@aws-amplify/core'; import { InAppMessage, InAppMessagingEvent } from '../../../types'; import { addEventListener } from '../../../../eventListeners'; @@ -26,7 +26,7 @@ import { dispatchEvent } from './dispatchEvent'; * initializeInAppMessaging(); * ``` */ -export function initializeInAppMessaging(): void { +export function initializeInAppMessaging(ctx: AmplifyContext): void { if (isInitialized()) { return; } @@ -35,14 +35,18 @@ export function initializeInAppMessaging(): void { // wire up default Pinpoint message event handling addEventListener('messageDisplayed', (message: InAppMessage) => { - recordAnalyticsEvent(PinpointMessageEvent.MESSAGE_DISPLAYED, message); + recordAnalyticsEvent(ctx, PinpointMessageEvent.MESSAGE_DISPLAYED, message); incrementMessageCounts(message.id); }); addEventListener('messageDismissed', (message: InAppMessage) => { - recordAnalyticsEvent(PinpointMessageEvent.MESSAGE_DISMISSED, message); + recordAnalyticsEvent(ctx, PinpointMessageEvent.MESSAGE_DISMISSED, message); }); addEventListener('messageActionTaken', (message: InAppMessage) => { - recordAnalyticsEvent(PinpointMessageEvent.MESSAGE_ACTION_TAKEN, message); + recordAnalyticsEvent( + ctx, + PinpointMessageEvent.MESSAGE_ACTION_TAKEN, + message, + ); }); // listen to analytics hub events diff --git a/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/syncMessages.ts b/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/syncMessages.ts index b5186372ccf..8afd4b08a55 100644 --- a/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/syncMessages.ts +++ b/packages/notifications/src/inAppMessaging/providers/pinpoint/apis/syncMessages.ts @@ -1,9 +1,9 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +import { AmplifyContext, defaultStorage } from '@aws-amplify/core'; import { InAppMessagingAction } from '@aws-amplify/core/internals/utils'; import { resolveEndpointId } from '@aws-amplify/core/internals/providers/pinpoint'; -import { defaultStorage } from '@aws-amplify/core'; import { GetInAppMessagesInput, GetInAppMessagesOutput, @@ -42,9 +42,9 @@ import { assertIsInitialized } from '../../../utils'; * * ``` */ -export async function syncMessages(): Promise { +export async function syncMessages(ctx: AmplifyContext): Promise { assertIsInitialized(); - const messages = await fetchInAppMessages(); + const messages = await fetchInAppMessages(ctx); if (!messages || messages.length === 0) { return; } @@ -57,10 +57,10 @@ export async function syncMessages(): Promise { } } -async function fetchInAppMessages() { +async function fetchInAppMessages(ctx: AmplifyContext) { try { - const { credentials, identityId } = await resolveCredentials(); - const { appId, region } = resolveConfig(); + const { credentials, identityId } = await resolveCredentials(ctx); + const { appId, region } = resolveConfig(ctx); const endpointId = await resolveEndpointId({ appId, category: CATEGORY, diff --git a/packages/notifications/src/inAppMessaging/providers/pinpoint/utils/helpers.ts b/packages/notifications/src/inAppMessaging/providers/pinpoint/utils/helpers.ts index 2d1cda76680..3ca9444fa01 100644 --- a/packages/notifications/src/inAppMessaging/providers/pinpoint/utils/helpers.ts +++ b/packages/notifications/src/inAppMessaging/providers/pinpoint/utils/helpers.ts @@ -1,7 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { ConsoleLogger } from '@aws-amplify/core'; +import { AmplifyContext, ConsoleLogger } from '@aws-amplify/core'; import { InAppMessagingAction, getClientInfo, @@ -38,13 +38,14 @@ let eventMetricsMemo: Record = {}; export const logger = new ConsoleLogger('InAppMessaging.Pinpoint.Utils'); export const recordAnalyticsEvent = ( + ctx: AmplifyContext, event: PinpointMessageEvent, message: InAppMessage, ) => { - const { appId, region } = resolveConfig(); + const { appId, region } = resolveConfig(ctx); const { id, metadata } = message; - resolveCredentials() + resolveCredentials(ctx) .then(({ credentials, identityId }) => { recordCore({ appId, diff --git a/packages/notifications/src/inAppMessaging/providers/pinpoint/utils/resolveConfig.ts b/packages/notifications/src/inAppMessaging/providers/pinpoint/utils/resolveConfig.ts index 4161066d0f8..b670cd424d5 100644 --- a/packages/notifications/src/inAppMessaging/providers/pinpoint/utils/resolveConfig.ts +++ b/packages/notifications/src/inAppMessaging/providers/pinpoint/utils/resolveConfig.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 { InAppMessagingValidationErrorCode, @@ -11,9 +11,9 @@ import { /** * @internal */ -export const resolveConfig = () => { +export const resolveConfig = (ctx: AmplifyContext) => { const { appId, region } = - Amplify.getConfig().Notifications?.InAppMessaging?.Pinpoint ?? {}; + ctx.resourcesConfig.Notifications?.InAppMessaging?.Pinpoint ?? {}; assertValidationError(!!appId, InAppMessagingValidationErrorCode.NoAppId); assertValidationError(!!region, InAppMessagingValidationErrorCode.NoRegion); diff --git a/packages/notifications/src/inAppMessaging/providers/pinpoint/utils/resolveCredentials.ts b/packages/notifications/src/inAppMessaging/providers/pinpoint/utils/resolveCredentials.ts index 79d067fb828..b70b583b939 100644 --- a/packages/notifications/src/inAppMessaging/providers/pinpoint/utils/resolveCredentials.ts +++ b/packages/notifications/src/inAppMessaging/providers/pinpoint/utils/resolveCredentials.ts @@ -1,7 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { fetchAuthSession } from '@aws-amplify/core'; +import { AmplifyContext } from '@aws-amplify/core'; import { InAppMessagingValidationErrorCode, @@ -11,8 +11,8 @@ import { /** * @internal */ -export const resolveCredentials = async () => { - const { credentials, identityId } = await fetchAuthSession(); +export const resolveCredentials = async (ctx: AmplifyContext) => { + const { credentials, identityId } = await ctx.fetchAuthSession(); assertValidationError( !!credentials, InAppMessagingValidationErrorCode.NoCredentials, diff --git a/packages/notifications/src/pushNotifications/providers/pinpoint/apis/identifyUser.native.ts b/packages/notifications/src/pushNotifications/providers/pinpoint/apis/identifyUser.native.ts index d953797a6a5..aa7152ed04c 100644 --- a/packages/notifications/src/pushNotifications/providers/pinpoint/apis/identifyUser.native.ts +++ b/packages/notifications/src/pushNotifications/providers/pinpoint/apis/identifyUser.native.ts @@ -1,7 +1,11 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { PushNotificationAction } from '@aws-amplify/core/internals/utils'; +import { AmplifyContext } from '@aws-amplify/core'; +import { + PushNotificationAction, + resolveCtxArgs, +} from '@aws-amplify/core/internals/utils'; import { getEndpointId, updateEndpoint, @@ -17,16 +21,19 @@ import { getInflightDeviceRegistration, resolveConfig, } from '../utils'; -import { IdentifyUser } from '../types'; +import { IdentifyUserInput } from '../types'; -export const identifyUser: IdentifyUser = async ({ - userId, - userProfile, - options, -}) => { +export async function identifyUser(input: IdentifyUserInput): Promise; +export async function identifyUser( + ctx: AmplifyContext, + input: IdentifyUserInput, +): Promise; +export async function identifyUser(...args: any[]): Promise { + const [ctx, input] = resolveCtxArgs(args); + const { userId, userProfile, options } = input; assertIsInitialized(); - const { credentials, identityId } = await resolveCredentials(); - const { appId, region } = resolveConfig(); + const { credentials, identityId } = await resolveCredentials(ctx); + const { appId, region } = resolveConfig(ctx); const { address, optOut, userAttributes } = options ?? {}; if (!(await getEndpointId(appId, 'PushNotification'))) { // if there is no cached endpoint id, wait for successful endpoint creation before continuing @@ -48,4 +55,4 @@ export const identifyUser: IdentifyUser = async ({ PushNotificationAction.IdentifyUser, ), }); -}; +} diff --git a/packages/notifications/src/pushNotifications/providers/pinpoint/apis/identifyUser.ts b/packages/notifications/src/pushNotifications/providers/pinpoint/apis/identifyUser.ts index 64a1d70b6a2..99beefa4d0f 100644 --- a/packages/notifications/src/pushNotifications/providers/pinpoint/apis/identifyUser.ts +++ b/packages/notifications/src/pushNotifications/providers/pinpoint/apis/identifyUser.ts @@ -1,11 +1,12 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +import { AmplifyContext } from '@aws-amplify/core'; import { UpdateEndpointException } from '@aws-amplify/core/internals/providers/pinpoint'; import { PlatformNotSupportedError } from '@aws-amplify/core/internals/utils'; import { PushNotificationValidationErrorCode } from '../../../errors'; -import { IdentifyUser, IdentifyUserInput } from '../types'; +import { IdentifyUserInput } from '../types'; /** * Sends information about a user to Pinpoint. Sending user information allows you to associate a user to their user @@ -60,6 +61,11 @@ import { IdentifyUser, IdentifyUserInput } from '../types'; * }, * }); */ -export const identifyUser: IdentifyUser = async () => { +export async function identifyUser(input: IdentifyUserInput): Promise; +export async function identifyUser( + ctx: AmplifyContext, + input: IdentifyUserInput, +): Promise; +export async function identifyUser(): Promise { throw new PlatformNotSupportedError(); -}; +} diff --git a/packages/notifications/src/pushNotifications/providers/pinpoint/apis/initializePushNotifications.native.ts b/packages/notifications/src/pushNotifications/providers/pinpoint/apis/initializePushNotifications.native.ts index 2582adab22f..741bb59bb82 100644 --- a/packages/notifications/src/pushNotifications/providers/pinpoint/apis/initializePushNotifications.native.ts +++ b/packages/notifications/src/pushNotifications/providers/pinpoint/apis/initializePushNotifications.native.ts @@ -1,8 +1,11 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { ConsoleLogger } from '@aws-amplify/core'; -import { PushNotificationAction } from '@aws-amplify/core/internals/utils'; +import { AmplifyContext, ConsoleLogger } from '@aws-amplify/core'; +import { + PushNotificationAction, + resolveCtxArgs, +} from '@aws-amplify/core/internals/utils'; import { updateEndpoint } from '@aws-amplify/core/internals/providers/pinpoint'; import { loadAmplifyPushNotification } from '@aws-amplify/react-native'; @@ -40,18 +43,21 @@ const logger = new ConsoleLogger('Notifications.PushNotification'); const BACKGROUND_TASK_TIMEOUT = 25; // seconds -export const initializePushNotifications = (): void => { +export function initializePushNotifications(): void; +export function initializePushNotifications(ctx: AmplifyContext): void; +export function initializePushNotifications(...args: any[]): void { + const [ctx] = resolveCtxArgs(args); if (isInitialized()) { logger.info('Push notifications have already been enabled'); return; } - addNativeListeners(); - addAnalyticsListeners(); + addNativeListeners(ctx); + addAnalyticsListeners(ctx); initialize(); -}; +} -const addNativeListeners = (): void => { +const addNativeListeners = (ctx: AmplifyContext): void => { let launchNotificationOpenedListener: | ReturnType | undefined; @@ -157,7 +163,7 @@ const addNativeListeners = (): void => { setToken(token); notifyEventListeners('tokenReceived', token); try { - await registerDevice(token); + await registerDevice(ctx, token); } catch (err) { logger.error('Failed to register device for push notifications', err); throw err; @@ -166,21 +172,22 @@ const addNativeListeners = (): void => { ); }; -const addAnalyticsListeners = (): void => { +const addAnalyticsListeners = (ctx: AmplifyContext): void => { let launchNotificationOpenedListenerRemover: EventListenerRemover | undefined; // wire up default Pinpoint message event handling addEventListener( 'backgroundMessageReceived', - createMessageEventRecorder('received_background'), + createMessageEventRecorder(ctx, 'received_background'), ); addEventListener( 'foregroundMessageReceived', - createMessageEventRecorder('received_foreground'), + createMessageEventRecorder(ctx, 'received_foreground'), ); launchNotificationOpenedListenerRemover = addEventListener( 'launchNotificationOpened', createMessageEventRecorder( + ctx, 'opened_notification', // once we are done with it we can remove the listener () => { @@ -192,6 +199,7 @@ const addAnalyticsListeners = (): void => { addEventListener( 'notificationOpened', createMessageEventRecorder( + ctx, 'opened_notification', // if we are in this state, we no longer need the listener as the app was launched via some other means () => { @@ -202,9 +210,12 @@ const addAnalyticsListeners = (): void => { ); }; -const registerDevice = async (address: string): Promise => { - const { credentials, identityId } = await resolveCredentials(); - const { appId, region } = resolveConfig(); +const registerDevice = async ( + ctx: AmplifyContext, + address: string, +): Promise => { + const { credentials, identityId } = await resolveCredentials(ctx); + const { appId, region } = resolveConfig(ctx); try { await updateEndpoint({ address, diff --git a/packages/notifications/src/pushNotifications/providers/pinpoint/apis/initializePushNotifications.ts b/packages/notifications/src/pushNotifications/providers/pinpoint/apis/initializePushNotifications.ts index 7d9d7fa561e..18d1c2740ba 100644 --- a/packages/notifications/src/pushNotifications/providers/pinpoint/apis/initializePushNotifications.ts +++ b/packages/notifications/src/pushNotifications/providers/pinpoint/apis/initializePushNotifications.ts @@ -1,10 +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 { PlatformNotSupportedError } from '@aws-amplify/core/internals/utils'; -import { InitializePushNotifications } from '../types'; - /** * Initialize and set up the push notification category. The category must be first initialized before all other * functionalities become available. @@ -22,6 +21,8 @@ import { InitializePushNotifications } from '../types'; * initializePushNotifications(); * ``` */ -export const initializePushNotifications: InitializePushNotifications = () => { +export function initializePushNotifications(): void; +export function initializePushNotifications(ctx: AmplifyContext): void; +export function initializePushNotifications(): void { throw new PlatformNotSupportedError(); -}; +} diff --git a/packages/notifications/src/pushNotifications/providers/pinpoint/types/apis.ts b/packages/notifications/src/pushNotifications/providers/pinpoint/types/apis.ts index caeb6022b3a..4b1e121db0e 100644 --- a/packages/notifications/src/pushNotifications/providers/pinpoint/types/apis.ts +++ b/packages/notifications/src/pushNotifications/providers/pinpoint/types/apis.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 { IdentifyUserInput, OnNotificationOpenedInput, @@ -26,8 +28,10 @@ export type GetBadgeCount = () => Promise; export type GetLaunchNotification = () => Promise; export type GetPermissionStatus = () => Promise; - -export type IdentifyUser = (input: IdentifyUserInput) => Promise; +export type IdentifyUser = ( + ctx: AmplifyContext, + input: IdentifyUserInput, +) => Promise; export type InitializePushNotifications = () => void; diff --git a/packages/notifications/src/pushNotifications/providers/pinpoint/utils/createMessageEventRecorder.ts b/packages/notifications/src/pushNotifications/providers/pinpoint/utils/createMessageEventRecorder.ts index c02baee6f2a..e5c945ca4f6 100644 --- a/packages/notifications/src/pushNotifications/providers/pinpoint/utils/createMessageEventRecorder.ts +++ b/packages/notifications/src/pushNotifications/providers/pinpoint/utils/createMessageEventRecorder.ts @@ -1,8 +1,8 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +import { AmplifyContext, ConsoleLogger } from '@aws-amplify/core'; import { record } from '@aws-amplify/core/internals/providers/pinpoint'; -import { ConsoleLogger } from '@aws-amplify/core'; import { AWSCredentials } from '@aws-amplify/core/internals/utils'; import { PinpointMessageEvent } from '../types'; @@ -23,12 +23,13 @@ const logger = new ConsoleLogger('PushNotification.recordMessageEvent'); */ export const createMessageEventRecorder = ( + ctx: AmplifyContext, event: PinpointMessageEvent, callback?: () => void, ): OnPushNotificationMessageHandler => async message => { - const { credentials } = await resolveCredentials(); - const { appId, region } = resolveConfig(); + const { credentials } = await resolveCredentials(ctx); + const { appId, region } = resolveConfig(ctx); await recordMessageEvent({ appId, credentials, diff --git a/packages/notifications/src/pushNotifications/providers/pinpoint/utils/resolveConfig.ts b/packages/notifications/src/pushNotifications/providers/pinpoint/utils/resolveConfig.ts index 65e08c82006..d98441fb9ba 100644 --- a/packages/notifications/src/pushNotifications/providers/pinpoint/utils/resolveConfig.ts +++ b/packages/notifications/src/pushNotifications/providers/pinpoint/utils/resolveConfig.ts @@ -1,16 +1,16 @@ // 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 { PushNotificationValidationErrorCode, assert } from '../../../errors'; /** * @internal */ -export const resolveConfig = () => { +export const resolveConfig = (ctx: AmplifyContext) => { const { appId, region } = - Amplify.getConfig().Notifications?.PushNotification?.Pinpoint ?? {}; + ctx.resourcesConfig.Notifications?.PushNotification?.Pinpoint ?? {}; assert(!!appId, PushNotificationValidationErrorCode.NoAppId); assert(!!region, PushNotificationValidationErrorCode.NoRegion); diff --git a/packages/notifications/src/pushNotifications/utils/resolveCredentials.ts b/packages/notifications/src/pushNotifications/utils/resolveCredentials.ts index 3f0e37eecad..02affaf926e 100644 --- a/packages/notifications/src/pushNotifications/utils/resolveCredentials.ts +++ b/packages/notifications/src/pushNotifications/utils/resolveCredentials.ts @@ -1,7 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { fetchAuthSession } from '@aws-amplify/core'; +import { AmplifyContext } from '@aws-amplify/core'; import { PushNotificationValidationErrorCode, @@ -11,8 +11,8 @@ import { /** * @internal */ -export const resolveCredentials = async () => { - const { credentials, identityId } = await fetchAuthSession(); +export const resolveCredentials = async (ctx: AmplifyContext) => { + const { credentials, identityId } = await ctx.fetchAuthSession(); assert(!!credentials, PushNotificationValidationErrorCode.NoCredentials); return { credentials, identityId }; diff --git a/packages/predictions/__tests__/Predictions.test.ts b/packages/predictions/__tests__/Predictions.test.ts index 564c667287f..7f3fea5c5ae 100644 --- a/packages/predictions/__tests__/Predictions.test.ts +++ b/packages/predictions/__tests__/Predictions.test.ts @@ -13,15 +13,21 @@ import { TranslateTextOutput, } from '../src/types'; +import { createMockAmplifyContext } from './testUtils'; + +const mockCtx = createMockAmplifyContext(); + describe('Predictions test', () => { describe('getModuleName tests', () => { test('happy and the only case', () => { - expect(new PredictionsClass().getModuleName()).toMatch('Predictions'); + expect(new PredictionsClass(mockCtx).getModuleName()).toMatch( + 'Predictions', + ); }); }); test('convert test', async () => { - const predictions = new PredictionsClass(); + const predictions = new PredictionsClass(mockCtx); const input: TranslateTextInput = { translateText: { source: { text: 'sourceText' } }, }; @@ -40,7 +46,7 @@ describe('Predictions test', () => { }); test('identify test', async () => { - const predictions = new PredictionsClass(); + const predictions = new PredictionsClass(mockCtx); const input: IdentifyTextInput = { text: { source: { key: 'key' }, format: 'PLAIN' }, }; @@ -63,7 +69,7 @@ describe('Predictions test', () => { }); test('interpret test', async () => { - const predictions = new PredictionsClass(); + const predictions = new PredictionsClass(mockCtx); const input: InterpretTextInput = { text: { source: { diff --git a/packages/predictions/__tests__/providers/AWSAIConvertPredictionsProvider.test.ts b/packages/predictions/__tests__/providers/AWSAIConvertPredictionsProvider.test.ts index 5678c1b079c..2a29196e645 100644 --- a/packages/predictions/__tests__/providers/AWSAIConvertPredictionsProvider.test.ts +++ b/packages/predictions/__tests__/providers/AWSAIConvertPredictionsProvider.test.ts @@ -1,4 +1,4 @@ -import { Amplify, fetchAuthSession } from '@aws-amplify/core'; +import { Amplify, AmplifyContext, fetchAuthSession } from '@aws-amplify/core'; import { Category, PredictionsAction, @@ -34,6 +34,16 @@ jest.mock('@aws-amplify/core', () => ({ })), })); +const mockCtx: AmplifyContext = { + get resourcesConfig() { + return mockGetConfig(); + }, + libraryOptions: {}, + fetchAuthSession: (...args: any[]) => mockFetchAuthSession(...args), + clearCredentials: jest.fn(), + getTokens: jest.fn(), +}; + const result = { TranslatedText: 'translatedText', TargetLanguageCode: 'es' }; const resetTranslateMock = () => { TranslateClient.prototype.send = jest.fn(command => { @@ -171,7 +181,7 @@ describe('Predictions convert provider test', () => { convert: options, }, }); - const predictionsProvider = new AmazonAIConvertPredictionsProvider(); + const predictionsProvider = new AmazonAIConvertPredictionsProvider(mockCtx); expect( predictionsProvider.convert(validTranslateTextInput), ).resolves.toMatchObject({ language: 'es', text: 'translatedText' }); @@ -183,7 +193,7 @@ describe('Predictions convert provider test', () => { convert: options, }, }); - const predictionsProvider = new AmazonAIConvertPredictionsProvider(); + const predictionsProvider = new AmazonAIConvertPredictionsProvider(mockCtx); expect( predictionsProvider.convert(validTranslateTextInput), @@ -203,7 +213,7 @@ describe('Predictions convert provider test', () => { convert: options, }, }); - const predictionsProvider = new AmazonAIConvertPredictionsProvider(); + const predictionsProvider = new AmazonAIConvertPredictionsProvider(mockCtx); jest.spyOn(TranslateClient.prototype, 'send').mockImplementation(() => { return Promise.reject('error'); }); @@ -227,7 +237,7 @@ describe('Predictions convert provider test', () => { convert: options, }, }); - const predictionsProvider = new AmazonAIConvertPredictionsProvider(); + const predictionsProvider = new AmazonAIConvertPredictionsProvider(mockCtx); window.URL.createObjectURL = jest.fn(); jest.spyOn(URL, 'createObjectURL').mockImplementation(blob => { return 'dummyURL'; @@ -249,7 +259,7 @@ describe('Predictions convert provider test', () => { convert: options, }, }); - const predictionsProvider = new AmazonAIConvertPredictionsProvider(); + const predictionsProvider = new AmazonAIConvertPredictionsProvider(mockCtx); expect( predictionsProvider.convert(validTextToSpeechInput), ).rejects.toThrow( @@ -268,7 +278,7 @@ describe('Predictions convert provider test', () => { convert: options, }, }); - const predictionsProvider = new AmazonAIConvertPredictionsProvider(); + const predictionsProvider = new AmazonAIConvertPredictionsProvider(mockCtx); jest.spyOn(PollyClient.prototype, 'send').mockImplementation(() => { return Promise.reject('error'); }); @@ -305,7 +315,7 @@ describe('Predictions convert provider test', () => { }, ); - const predictionsProvider = new AmazonAIConvertPredictionsProvider(); + const predictionsProvider = new AmazonAIConvertPredictionsProvider(mockCtx); return expect( predictionsProvider.convert(validSpeechToTextInput), @@ -336,7 +346,7 @@ describe('Predictions convert provider test', () => { }, ); - const predictionsProvider = new AmazonAIConvertPredictionsProvider(); + const predictionsProvider = new AmazonAIConvertPredictionsProvider(mockCtx); expect( predictionsProvider.convert(validSpeechToTextInput), @@ -362,7 +372,7 @@ describe('Predictions convert provider test', () => { }, ); - const predictionsProvider = new AmazonAIConvertPredictionsProvider(); + const predictionsProvider = new AmazonAIConvertPredictionsProvider(mockCtx); expect( predictionsProvider.convert(validSpeechToTextInput), @@ -400,7 +410,7 @@ describe('Predictions convert provider test', () => { 'downsampleBuffer', ); - const predictionsProvider = new AmazonAIConvertPredictionsProvider(); + const predictionsProvider = new AmazonAIConvertPredictionsProvider(mockCtx); await predictionsProvider.convert(validSpeechToTextInput); expect(downsampleBufferSpyon).toHaveBeenCalledWith( @@ -424,7 +434,7 @@ describe('Predictions convert provider test', () => { convert: options, }, }); - const predictionsProvider = new AmazonAIConvertPredictionsProvider(); + const predictionsProvider = new AmazonAIConvertPredictionsProvider(mockCtx); window.URL.createObjectURL = jest.fn(); jest.spyOn(URL, 'createObjectURL').mockImplementation(blob => { return 'dummyURL'; @@ -453,7 +463,7 @@ describe('Predictions convert provider test', () => { convert: options, }, }); - const predictionsProvider = new AmazonAIConvertPredictionsProvider(); + const predictionsProvider = new AmazonAIConvertPredictionsProvider(mockCtx); await predictionsProvider.convert(validTranslateTextInput); // translateClient is a private property @@ -468,4 +478,14 @@ describe('Predictions convert provider test', () => { ); }); }); + + describe('getProviderName', () => { + it('returns provider name', () => { + const mockCtx = {} as any; + const predictionsProvider = new AmazonAIConvertPredictionsProvider(mockCtx); + expect(predictionsProvider.getProviderName()).toBe( + 'AmazonAIConvertPredictionsProvider', + ); + }); + }); }); diff --git a/packages/predictions/__tests__/providers/AWSAIIdentifyPredictionsProvider.test.ts b/packages/predictions/__tests__/providers/AWSAIIdentifyPredictionsProvider.test.ts index 042ca5c7114..af541887fbd 100644 --- a/packages/predictions/__tests__/providers/AWSAIIdentifyPredictionsProvider.test.ts +++ b/packages/predictions/__tests__/providers/AWSAIIdentifyPredictionsProvider.test.ts @@ -1,4 +1,4 @@ -import { Amplify, fetchAuthSession } from '@aws-amplify/core'; +import { Amplify, AmplifyContext, fetchAuthSession } from '@aws-amplify/core'; import { Category, PredictionsAction, @@ -54,6 +54,16 @@ jest.mock('@aws-amplify/core', () => ({ })), })); +const mockCtx: AmplifyContext = { + get resourcesConfig() { + return mockGetConfig(); + }, + libraryOptions: {}, + fetchAuthSession: (...args: any[]) => mockFetchAuthSession(...args), + clearCredentials: jest.fn(), + getTokens: jest.fn(), +}; + jest.mock('@aws-amplify/storage', () => ({ getUrl: jest.fn(), })); @@ -281,7 +291,7 @@ mockGetConfig.mockReturnValue({ identify: options, }, }); -mockGetUrl.mockImplementation(({ key, options }) => { +mockGetUrl.mockImplementation((_ctx: any, { key, options }: any) => { console.log(key, options); const level = options?.accessLevel || 'guest'; let url: URL; @@ -302,7 +312,7 @@ describe('Predictions identify provider test', () => { let predictionsProvider; beforeAll(() => { - predictionsProvider = new AmazonAIIdentifyPredictionsProvider(); + predictionsProvider = new AmazonAIIdentifyPredictionsProvider(mockCtx); }); describe('identifyText tests', () => { describe('identifyText::PLAIN tests', () => { @@ -725,7 +735,7 @@ describe('Predictions identify provider test', () => { describe('custom user agent', () => { test('identify for label initializes a client with the correct custom user agent', async () => { - predictionsProvider = new AmazonAIIdentifyPredictionsProvider(); + predictionsProvider = new AmazonAIIdentifyPredictionsProvider(mockCtx); jest.spyOn(TextractClient.prototype, 'send'); jest.spyOn(RekognitionClient.prototype, 'send'); const fileInput = new File([Buffer.from('file')], 'file'); @@ -744,7 +754,7 @@ describe('Predictions identify provider test', () => { ); }); test('identify for entities initializes a client with the correct custom user agent', async () => { - predictionsProvider = new AmazonAIIdentifyPredictionsProvider(); + predictionsProvider = new AmazonAIIdentifyPredictionsProvider(mockCtx); jest.spyOn(TextractClient.prototype, 'send'); jest.spyOn(RekognitionClient.prototype, 'send'); const detectFacesInput: IdentifyEntitiesInput = { @@ -767,7 +777,7 @@ describe('Predictions identify provider test', () => { ); }); test('identify for text initializes a client with the correct custom user agent', async () => { - predictionsProvider = new AmazonAIIdentifyPredictionsProvider(); + predictionsProvider = new AmazonAIIdentifyPredictionsProvider(mockCtx); jest.spyOn(TextractClient.prototype, 'send'); jest.spyOn(RekognitionClient.prototype, 'send'); const detectTextInput: IdentifyTextInput = { @@ -792,4 +802,14 @@ describe('Predictions identify provider test', () => { ); }); }); + + describe('getProviderName', () => { + it('returns provider name', () => { + const mockCtx = {} as any; + const predictionsProvider = new AmazonAIIdentifyPredictionsProvider(mockCtx); + expect(predictionsProvider.getProviderName()).toBe( + 'AmazonAIIdentifyPredictionsProvider', + ); + }); + }); }); diff --git a/packages/predictions/__tests__/providers/AWSAIInterpretPredictionsProvider.test.ts b/packages/predictions/__tests__/providers/AWSAIInterpretPredictionsProvider.test.ts index 60bc46c4c2e..29d7da598d7 100644 --- a/packages/predictions/__tests__/providers/AWSAIInterpretPredictionsProvider.test.ts +++ b/packages/predictions/__tests__/providers/AWSAIInterpretPredictionsProvider.test.ts @@ -1,4 +1,4 @@ -import { Amplify, fetchAuthSession } from '@aws-amplify/core'; +import { Amplify, AmplifyContext, fetchAuthSession } from '@aws-amplify/core'; import { Category, PredictionsAction, @@ -26,6 +26,16 @@ jest.mock('@aws-amplify/core', () => ({ })), })); +const mockCtx: AmplifyContext = { + get resourcesConfig() { + return mockGetConfig(); + }, + libraryOptions: {}, + fetchAuthSession: (...args: any[]) => mockFetchAuthSession(...args), + clearCredentials: jest.fn(), + getTokens: jest.fn(), +}; + ComprehendClient.prototype.send = jest.fn((command, callback) => { if (command instanceof DetectEntitiesCommand) { const resultDetectEntities = { @@ -232,7 +242,7 @@ describe('Predictions interpret provider test', () => { }); describe('interpretText tests', () => { test('happy case credentials exist detectEntities', async () => { - const predictionsProvider = new AmazonAIInterpretPredictionsProvider(); + const predictionsProvider = new AmazonAIInterpretPredictionsProvider(mockCtx); const detectEntitiesSpy = jest.spyOn(ComprehendClient.prototype, 'send'); expect.assertions(2); @@ -261,7 +271,7 @@ describe('Predictions interpret provider test', () => { }); test('happy case credentials exists detectDominantLanguage', async () => { - const predictionsProvider = new AmazonAIInterpretPredictionsProvider(); + const predictionsProvider = new AmazonAIInterpretPredictionsProvider(mockCtx); const dominantLanguageSpy = jest.spyOn( ComprehendClient.prototype, 'send', @@ -292,7 +302,7 @@ describe('Predictions interpret provider test', () => { }); test('happy case credentials exists detect sentiment', async () => { - const predictionsProvider = new AmazonAIInterpretPredictionsProvider(); + const predictionsProvider = new AmazonAIInterpretPredictionsProvider(mockCtx); const sentimentSpy = jest.spyOn(ComprehendClient.prototype, 'send'); expect.assertions(2); @@ -328,7 +338,7 @@ describe('Predictions interpret provider test', () => { }); test('happy case credentials exists detect syntax', async () => { - const predictionsProvider = new AmazonAIInterpretPredictionsProvider(); + const predictionsProvider = new AmazonAIInterpretPredictionsProvider(mockCtx); const syntaxSpy = jest.spyOn(ComprehendClient.prototype, 'send'); expect.assertions(2); @@ -374,7 +384,7 @@ describe('Predictions interpret provider test', () => { }); test('happy case credentials exists detect key phrases', async () => { - const predictionsProvider = new AmazonAIInterpretPredictionsProvider(); + const predictionsProvider = new AmazonAIInterpretPredictionsProvider(mockCtx); const keyPhrasesSpy = jest.spyOn(ComprehendClient.prototype, 'send'); expect.assertions(2); @@ -408,7 +418,7 @@ describe('Predictions interpret provider test', () => { }); test("happy case credentials type: 'all'", async () => { - const predictionsProvider = new AmazonAIInterpretPredictionsProvider(); + const predictionsProvider = new AmazonAIInterpretPredictionsProvider(mockCtx); await expect( predictionsProvider.interpret({ text: { @@ -483,7 +493,7 @@ describe('Predictions interpret provider test', () => { describe('custom user agent', () => { test('interpret initializes a client with the correct custom user agent', async () => { jest.spyOn(ComprehendClient.prototype, 'send'); - const predictionsProvider = new AmazonAIInterpretPredictionsProvider(); + const predictionsProvider = new AmazonAIInterpretPredictionsProvider(mockCtx); await predictionsProvider.interpret({ text: { source: { @@ -505,4 +515,14 @@ describe('Predictions interpret provider test', () => { ); }); }); + + describe('getProviderName', () => { + it('returns provider name', () => { + const mockCtx = {} as any; + const predictionsProvider = new AmazonAIInterpretPredictionsProvider(mockCtx); + expect(predictionsProvider.getProviderName()).toBe( + 'AmazonAIInterpretPredictionsProvider', + ); + }); + }); }); diff --git a/packages/predictions/__tests__/providers/Utils.test.ts b/packages/predictions/__tests__/providers/Utils.test.ts new file mode 100644 index 00000000000..ff4e6500146 --- /dev/null +++ b/packages/predictions/__tests__/providers/Utils.test.ts @@ -0,0 +1,56 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + makeCamelCase, + makeCamelCaseArray, + blobToArrayBuffer, +} from '../../src/providers/Utils'; + +describe('Utils', () => { + describe('makeCamelCase', () => { + it('converts keys to camel case', () => { + const obj = { FirstName: 'John', LastName: 'Doe' }; + const result = makeCamelCase(obj); + expect(result).toEqual({ firstName: 'John', lastName: 'Doe' }); + }); + + it('returns undefined for undefined input', () => { + expect(makeCamelCase(undefined)).toBeUndefined(); + }); + + it('extracts only specified keys', () => { + const obj = { FirstName: 'John', LastName: 'Doe', Age: 30 }; + const result = makeCamelCase(obj, ['FirstName', 'LastName']); + expect(result).toEqual({ firstName: 'John', lastName: 'Doe' }); + }); + }); + + describe('makeCamelCaseArray', () => { + it('converts array of objects', () => { + const arr = [ + { FirstName: 'John' }, + { FirstName: 'Jane' }, + ]; + const result = makeCamelCaseArray(arr); + expect(result).toEqual([{ firstName: 'John' }, { firstName: 'Jane' }]); + }); + + it('returns undefined for undefined input', () => { + expect(makeCamelCaseArray(undefined)).toBeUndefined(); + }); + }); + + describe('blobToArrayBuffer', () => { + it('converts blob to array buffer', async () => { + const blob = new Blob(['test'], { type: 'text/plain' }); + const result = await blobToArrayBuffer(blob); + expect(result).toBeDefined(); + }); + + it('rejects on invalid input', async () => { + const invalidBlob = null as any; + await expect(blobToArrayBuffer(invalidBlob)).rejects.toBeDefined(); + }); + }); +}); diff --git a/packages/predictions/__tests__/testUtils.ts b/packages/predictions/__tests__/testUtils.ts new file mode 100644 index 00000000000..85f14fe550d --- /dev/null +++ b/packages/predictions/__tests__/testUtils.ts @@ -0,0 +1,14 @@ +import { AmplifyContext } from '@aws-amplify/core'; + +export function createMockAmplifyContext( + resourcesConfig: Record = {}, + fetchAuthSessionValue: Record = {}, +): AmplifyContext { + return { + resourcesConfig, + libraryOptions: {}, + fetchAuthSession: jest.fn().mockResolvedValue(fetchAuthSessionValue), + clearCredentials: jest.fn().mockResolvedValue(undefined), + getTokens: jest.fn().mockResolvedValue(undefined), + } as unknown as AmplifyContext; +} diff --git a/packages/predictions/package.json b/packages/predictions/package.json index 1967b01032c..4e6093a2677 100644 --- a/packages/predictions/package.json +++ b/packages/predictions/package.json @@ -56,7 +56,7 @@ "tslib": "^2.5.0" }, "peerDependencies": { - "@aws-amplify/core": "^6.16.2" + "@aws-amplify/core": "^6.16.3" }, "devDependencies": { "@aws-amplify/core": "6.16.3" diff --git a/packages/predictions/src/Predictions.ts b/packages/predictions/src/Predictions.ts index 0b3721c094e..46d59ed328b 100644 --- a/packages/predictions/src/Predictions.ts +++ b/packages/predictions/src/Predictions.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 { AmazonAIConvertPredictionsProvider, AmazonAIIdentifyPredictionsProvider, @@ -23,10 +26,33 @@ import { TranslateTextOutput, } from './types'; +type ConvertInput = TranslateTextInput | TextToSpeechInput | SpeechToTextInput; +type ConvertOutput = + | TranslateTextOutput + | TextToSpeechOutput + | SpeechToTextOutput; +type IdentifyInput = + | IdentifyTextInput + | IdentifyLabelsInput + | IdentifyEntitiesInput; +type IdentifyOutput = + | IdentifyTextOutput + | IdentifyLabelsOutput + | IdentifyEntitiesOutput; + export class PredictionsClass { - private convertProvider = new AmazonAIConvertPredictionsProvider(); - private identifyProvider = new AmazonAIIdentifyPredictionsProvider(); - private interpretProvider = new AmazonAIInterpretPredictionsProvider(); + private ctx: AmplifyContext; + + private convertProvider: AmazonAIConvertPredictionsProvider; + private identifyProvider: AmazonAIIdentifyPredictionsProvider; + private interpretProvider: AmazonAIInterpretPredictionsProvider; + + constructor(ctx: AmplifyContext) { + this.ctx = ctx; + this.convertProvider = new AmazonAIConvertPredictionsProvider(ctx); + this.identifyProvider = new AmazonAIIdentifyPredictionsProvider(ctx); + this.interpretProvider = new AmazonAIInterpretPredictionsProvider(ctx); + } public getModuleName() { return 'Predictions'; @@ -58,6 +84,72 @@ export class PredictionsClass { > { return this.identifyProvider.identify(input); } + + // --- Static methods for v6 compatibility --- + + static convert(input: TranslateTextInput): Promise; + static convert(input: TextToSpeechInput): Promise; + static convert(input: SpeechToTextInput): Promise; + static convert( + ctx: AmplifyContext, + input: TranslateTextInput, + ): Promise; + + static convert( + ctx: AmplifyContext, + input: TextToSpeechInput, + ): Promise; + + static convert( + ctx: AmplifyContext, + input: SpeechToTextInput, + ): Promise; + + static convert(...args: any[]): Promise { + const [ctx, input] = resolveCtxArgs(args); + + return new PredictionsClass(ctx).convert(input as any); + } + + static identify(input: IdentifyTextInput): Promise; + static identify(input: IdentifyLabelsInput): Promise; + static identify( + input: IdentifyEntitiesInput, + ): Promise; + + static identify( + ctx: AmplifyContext, + input: IdentifyTextInput, + ): Promise; + + static identify( + ctx: AmplifyContext, + input: IdentifyLabelsInput, + ): Promise; + + static identify( + ctx: AmplifyContext, + input: IdentifyEntitiesInput, + ): Promise; + + static identify(...args: any[]): Promise { + const [ctx, input] = resolveCtxArgs(args); + + return new PredictionsClass(ctx).identify(input as any); + } + + static interpret(input: InterpretTextInput): Promise; + static interpret( + ctx: AmplifyContext, + input: InterpretTextInput, + ): Promise; + + static interpret(...args: any[]): Promise { + const [ctx, input] = resolveCtxArgs(args); + + return new PredictionsClass(ctx).interpret(input); + } } -export const Predictions = new PredictionsClass(); +export const createPredictions = (ctx: AmplifyContext) => + new PredictionsClass(ctx); diff --git a/packages/predictions/src/index.ts b/packages/predictions/src/index.ts index 5b997fe5e1b..7e9bbdd0b2a 100644 --- a/packages/predictions/src/index.ts +++ b/packages/predictions/src/index.ts @@ -1,6 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -export { Predictions } from './Predictions'; +export { PredictionsClass as Predictions } from './Predictions'; export { IdentifyEntitiesInput, diff --git a/packages/predictions/src/providers/AmazonAIConvertPredictionsProvider.ts b/packages/predictions/src/providers/AmazonAIConvertPredictionsProvider.ts index 9b24de59b74..dcdf129e2fa 100644 --- a/packages/predictions/src/providers/AmazonAIConvertPredictionsProvider.ts +++ b/packages/predictions/src/providers/AmazonAIConvertPredictionsProvider.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { Buffer } from 'buffer'; -import { Amplify, ConsoleLogger, fetchAuthSession } from '@aws-amplify/core'; +import { AmplifyContext, ConsoleLogger } from '@aws-amplify/core'; import { AWSCredentials, Category, @@ -49,6 +49,12 @@ const LANGUAGES_CODE_IN_8KHZ = ['fr-FR', 'en-AU', 'en-GB', 'fr-CA']; export class AmazonAIConvertPredictionsProvider { private translateClient?: TranslateClient; + private ctx: AmplifyContext; + + constructor(ctx: AmplifyContext) { + this.ctx = ctx; + } + private pollyClient?: PollyClient; getProviderName() { @@ -84,14 +90,14 @@ export class AmazonAIConvertPredictionsProvider { logger.debug('Starting translation'); const { translateText = {} } = - Amplify.getConfig().Predictions?.convert ?? {}; + this.ctx.resourcesConfig.Predictions?.convert ?? {}; assertValidationError( !!translateText.region, PredictionsValidationErrorCode.NoRegion, ); const { defaults = {}, region } = translateText; - const { credentials } = await fetchAuthSession(); + const { credentials } = await this.ctx.fetchAuthSession(); assertValidationError( !!credentials, PredictionsValidationErrorCode.NoCredentials, @@ -135,7 +141,7 @@ export class AmazonAIConvertPredictionsProvider { protected async convertTextToSpeech( input: TextToSpeechInput, ): Promise { - const { credentials } = await fetchAuthSession(); + const { credentials } = await this.ctx.fetchAuthSession(); assertValidationError( !!credentials, PredictionsValidationErrorCode.NoCredentials, @@ -145,7 +151,8 @@ export class AmazonAIConvertPredictionsProvider { PredictionsValidationErrorCode.NoSource, ); - const { speechGenerator } = Amplify.getConfig().Predictions?.convert ?? {}; + const { speechGenerator } = + this.ctx.resourcesConfig.Predictions?.convert ?? {}; assertValidationError( !!speechGenerator?.region, PredictionsValidationErrorCode.NoRegion, @@ -191,13 +198,14 @@ export class AmazonAIConvertPredictionsProvider { input: SpeechToTextInput, ): Promise { logger.debug('starting transcription..'); - const { credentials } = await fetchAuthSession(); + const { credentials } = await this.ctx.fetchAuthSession(); assertValidationError( !!credentials, PredictionsValidationErrorCode.NoCredentials, ); - const { transcription } = Amplify.getConfig().Predictions?.convert ?? {}; + const { transcription } = + this.ctx.resourcesConfig.Predictions?.convert ?? {}; assertValidationError( !!transcription?.region, PredictionsValidationErrorCode.NoRegion, diff --git a/packages/predictions/src/providers/AmazonAIIdentifyPredictionsProvider.ts b/packages/predictions/src/providers/AmazonAIIdentifyPredictionsProvider.ts index 2ada5dd00a6..aa51d1041e4 100644 --- a/packages/predictions/src/providers/AmazonAIIdentifyPredictionsProvider.ts +++ b/packages/predictions/src/providers/AmazonAIIdentifyPredictionsProvider.ts @@ -1,6 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { Amplify, ConsoleLogger, fetchAuthSession } from '@aws-amplify/core'; +import { AmplifyContext, ConsoleLogger } from '@aws-amplify/core'; import { Category, PredictionsAction, @@ -64,6 +64,12 @@ const logger = new ConsoleLogger('AmazonAIIdentifyPredictionsProvider'); export class AmazonAIIdentifyPredictionsProvider { private rekognitionClient?: RekognitionClient; + private ctx: AmplifyContext; + + constructor(ctx: AmplifyContext) { + this.ctx = ctx; + } + private textractClient?: TextractClient; getProviderName() { @@ -110,7 +116,7 @@ export class AmazonAIIdentifyPredictionsProvider { targetIdentityId: source.identityId, }; - getUrl({ key: source.key, options: storageConfig }) + getUrl(this.ctx, { key: source.key, options: storageConfig }) .then(value => { const parser = /https:\/\/([a-zA-Z0-9%\-_.]+)\.s3\.[A-Za-z0-9%\-._~]+\/([a-zA-Z0-9%\-._~/]+)\?/; @@ -166,14 +172,14 @@ export class AmazonAIIdentifyPredictionsProvider { protected async identifyText( input: IdentifyTextInput, ): Promise { - const { credentials } = await fetchAuthSession(); + const { credentials } = await this.ctx.fetchAuthSession(); assertValidationError( !!credentials, PredictionsValidationErrorCode.NoCredentials, ); const { identifyText = {} } = - Amplify.getConfig().Predictions?.identify ?? {}; + this.ctx.resourcesConfig.Predictions?.identify ?? {}; const { region = '', defaults = {} } = identifyText; const { format: configFormat = 'PLAIN' } = defaults; @@ -258,14 +264,14 @@ export class AmazonAIIdentifyPredictionsProvider { protected async identifyLabels( input: IdentifyLabelsInput, ): Promise { - const { credentials } = await fetchAuthSession(); + const { credentials } = await this.ctx.fetchAuthSession(); assertValidationError( !!credentials, PredictionsValidationErrorCode.NoCredentials, ); const { identifyLabels = {} } = - Amplify.getConfig().Predictions?.identify ?? {}; + this.ctx.resourcesConfig.Predictions?.identify ?? {}; const { region = '', defaults = {} } = identifyLabels; const { type = 'LABELS' } = defaults; @@ -360,14 +366,14 @@ export class AmazonAIIdentifyPredictionsProvider { protected async identifyEntities( input: IdentifyEntitiesInput, ): Promise { - const { credentials } = await fetchAuthSession(); + const { credentials } = await this.ctx.fetchAuthSession(); assertValidationError( !!credentials, PredictionsValidationErrorCode.NoCredentials, ); const { identifyEntities = {} } = - Amplify.getConfig().Predictions?.identify ?? {}; + this.ctx.resourcesConfig.Predictions?.identify ?? {}; const { region = '', celebrityDetectionEnabled = false, diff --git a/packages/predictions/src/providers/AmazonAIInterpretPredictionsProvider.ts b/packages/predictions/src/providers/AmazonAIInterpretPredictionsProvider.ts index 537fc083c3b..2aa5e5c9897 100644 --- a/packages/predictions/src/providers/AmazonAIInterpretPredictionsProvider.ts +++ b/packages/predictions/src/providers/AmazonAIInterpretPredictionsProvider.ts @@ -1,6 +1,6 @@ // 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 { Category, PredictionsAction, @@ -38,6 +38,11 @@ import { export class AmazonAIInterpretPredictionsProvider { private comprehendClient?: ComprehendClient; + private ctx: AmplifyContext; + + constructor(ctx: AmplifyContext) { + this.ctx = ctx; + } getProviderName() { return 'AmazonAIInterpretPredictionsProvider'; @@ -53,14 +58,14 @@ export class AmazonAIInterpretPredictionsProvider { } async interpretText(input: InterpretTextInput): Promise { - const { credentials } = await fetchAuthSession(); + const { credentials } = await this.ctx.fetchAuthSession(); assertValidationError( !!credentials, PredictionsValidationErrorCode.NoCredentials, ); const { interpretText = {} } = - Amplify.getConfig().Predictions?.interpret ?? {}; + this.ctx.resourcesConfig.Predictions?.interpret ?? {}; const { region = '', defaults = {} } = interpretText; const { type: defaultType = '' } = defaults; diff --git a/packages/pubsub/__tests__/PubSub.test.ts b/packages/pubsub/__tests__/PubSub.test.ts index b4b1885b6e8..af428f81015 100644 --- a/packages/pubsub/__tests__/PubSub.test.ts +++ b/packages/pubsub/__tests__/PubSub.test.ts @@ -20,7 +20,15 @@ jest.mock('@aws-amplify/core', () => ({ }, })); -import { Reachability } from '@aws-amplify/core/internals/utils'; +import { + AMPLIFY_CONTEXT_BRAND, + AmplifyContext, +} from '@aws-amplify/core'; +import { + Reachability, + clearGlobalContext, + setGlobalContext, +} from '@aws-amplify/core/internals/utils'; import * as Paho from '../src/vendor/paho-mqtt'; import { ConnectionState, PubSub as IotPubSub, mqttTopicMatch } from '../src'; import { PubSub as MqttPubSub } from '../src/clients/mqtt'; @@ -28,6 +36,25 @@ import { HubConnectionListener } from './helpers'; import { Observable, Observer } from 'rxjs'; import * as constants from '../src/Providers/constants'; +const mockGlobalCtx = { + [AMPLIFY_CONTEXT_BRAND]: true, + resourcesConfig: {}, + libraryOptions: {}, + fetchAuthSession: jest.fn().mockResolvedValue({ + credentials: { + accessKeyId: 'accessKeyId', + sessionToken: 'sessionToken', + secretAccessKey: 'secretAccessKey', + identityId: 'identityId', + authenticated: true, + }, + }), + clearCredentials: jest.fn(), + getTokens: jest.fn(), +} as unknown as AmplifyContext; + +setGlobalContext(mockGlobalCtx); + const pahoClientMockCache = {}; const mockConnect = jest.fn(options => { diff --git a/packages/pubsub/package.json b/packages/pubsub/package.json index 051a853639c..7496f7638df 100644 --- a/packages/pubsub/package.json +++ b/packages/pubsub/package.json @@ -82,7 +82,7 @@ "url": "0.11.0" }, "peerDependencies": { - "@aws-amplify/core": "^6.16.2" + "@aws-amplify/core": "^6.16.3" }, "devDependencies": { "@aws-amplify/core": "6.16.3" diff --git a/packages/pubsub/src/Providers/AWSIot.ts b/packages/pubsub/src/Providers/AWSIot.ts index cf88bfacbb6..ddf470bee5f 100644 --- a/packages/pubsub/src/Providers/AWSIot.ts +++ b/packages/pubsub/src/Providers/AWSIot.ts @@ -1,7 +1,11 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 import { Signer } from '@aws-amplify/core/internals/utils'; -import { fetchAuthSession } from '@aws-amplify/core'; +import { + AmplifyContext, + getGlobalContext, + isAmplifyContext, +} from '@aws-amplify/core'; import { MqttOptions, MqttOverWS } from './MqttOverWS'; @@ -13,8 +17,16 @@ export interface AWSIoTOptions extends MqttOptions { } export class AWSIoT extends MqttOverWS { - constructor(options: AWSIoTOptions = {}) { + private ctx: AmplifyContext; + + constructor(options?: AWSIoTOptions); + constructor(ctx: AmplifyContext, options?: AWSIoTOptions); + constructor(...args: any[]) { + const hasCtx = isAmplifyContext(args[0]); + const ctx: AmplifyContext = hasCtx ? args[0] : getGlobalContext(); + const options: AWSIoTOptions = hasCtx ? (args[1] ?? {}) : (args[0] ?? {}); super(options); + this.ctx = ctx; } protected get region(): string | undefined { @@ -29,7 +41,7 @@ export class AWSIoT extends MqttOverWS { service: SERVICE_NAME, region: this.region, }; - const session = await fetchAuthSession(); + const session = await this.ctx.fetchAuthSession(); if (!session.credentials) { throw new Error('No auth session credentials'); diff --git a/packages/react-native/package.json b/packages/react-native/package.json index 34f7dbd9756..589296f2f85 100644 --- a/packages/react-native/package.json +++ b/packages/react-native/package.json @@ -32,7 +32,7 @@ "react-native-url-polyfill": "^3.0.0" }, "peerDependencies": { - "@aws-amplify/rtn-passkeys": "^1.0.0", + "@aws-amplify/rtn-passkeys": "^1.1.2", "react-native": ">=0.70", "react-native-get-random-values": ">=1.8.0" }, diff --git a/packages/storage/__tests__/internals/apis/copy.test.ts b/packages/storage/__tests__/internals/apis/copy.test.ts index 2692f4f6a68..50fa9c4531d 100644 --- a/packages/storage/__tests__/internals/apis/copy.test.ts +++ b/packages/storage/__tests__/internals/apis/copy.test.ts @@ -1,13 +1,14 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { AmplifyClassV6 } from '@aws-amplify/core'; - +import { createMockAmplifyContext } from '../../testUtils/mockAmplifyContext'; import { copy as advancedCopy } from '../../../src/internals'; import { copy as copyInternal } from '../../../src/providers/s3/apis/internal/copy'; jest.mock('../../../src/providers/s3/apis/internal/copy'); const mockedCopyInternal = jest.mocked(copyInternal); +const mockCtx = createMockAmplifyContext(); + describe('copy (internals)', () => { beforeEach(() => { jest.clearAllMocks(); @@ -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..a6e2359c53c 100644 --- a/packages/storage/__tests__/internals/apis/downloadData.test.ts +++ b/packages/storage/__tests__/internals/apis/downloadData.test.ts @@ -1,12 +1,14 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 - +import { createMockAmplifyContext } from '../../testUtils/mockAmplifyContext'; import { downloadData as advancedDownloadData } from '../../../src/internals'; import { downloadData as downloadDataInternal } from '../../../src/providers/s3/apis/internal/downloadData'; jest.mock('../../../src/providers/s3/apis/internal/downloadData'); const mockedDownloadDataInternal = jest.mocked(downloadDataInternal); +const mockCtx = createMockAmplifyContext(); + describe('downloadData (internal)', () => { beforeEach(() => { mockedDownloadDataInternal.mockReturnValue({ @@ -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..1765b9436de 100644 --- a/packages/storage/__tests__/internals/apis/getProperties.test.ts +++ b/packages/storage/__tests__/internals/apis/getProperties.test.ts @@ -1,13 +1,14 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { AmplifyClassV6 } from '@aws-amplify/core'; - +import { createMockAmplifyContext } from '../../testUtils/mockAmplifyContext'; import { getProperties as advancedGetProperties } from '../../../src/internals'; import { getProperties as getPropertiesInternal } from '../../../src/providers/s3/apis/internal/getProperties'; jest.mock('../../../src/providers/s3/apis/internal/getProperties'); const mockedGetPropertiesInternal = jest.mocked(getPropertiesInternal); +const mockCtx = createMockAmplifyContext(); + describe('getProperties (internal)', () => { beforeEach(() => { mockedGetPropertiesInternal.mockResolvedValue({ @@ -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..08cdf5563b2 100644 --- a/packages/storage/__tests__/internals/apis/getUrl.test.ts +++ b/packages/storage/__tests__/internals/apis/getUrl.test.ts @@ -1,7 +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 { createMockAmplifyContext } from '../../testUtils/mockAmplifyContext'; import { getUrl as advancedGetUrl } from '../../../src/internals'; import { getUrl as getUrlInternal } from '../../../src/providers/s3/apis/internal/getUrl'; @@ -12,6 +11,8 @@ const MOCK_URL = new URL('https://s3.aws/mock-presigned-url'); const MOCK_DATE = new Date(); MOCK_DATE.setMonth(MOCK_DATE.getMonth() + 1); +const mockCtx = createMockAmplifyContext(); + describe('getUrl (internal)', () => { beforeEach(() => { mockedGetUrlInternal.mockResolvedValue({ @@ -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..7ed6cd1f176 100644 --- a/packages/storage/__tests__/internals/apis/list.test.ts +++ b/packages/storage/__tests__/internals/apis/list.test.ts @@ -1,13 +1,14 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { AmplifyClassV6 } from '@aws-amplify/core'; - +import { createMockAmplifyContext } from '../../testUtils/mockAmplifyContext'; import { list as advancedList } from '../../../src/internals'; import { list as listInternal } from '../../../src/providers/s3/apis/internal/list'; jest.mock('../../../src/providers/s3/apis/internal/list'); const mockedListInternal = jest.mocked(listInternal); +const mockCtx = createMockAmplifyContext(); + describe('list (internals)', () => { beforeEach(() => { jest.clearAllMocks(); @@ -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..7d7f2b35ed0 100644 --- a/packages/storage/__tests__/internals/apis/listPaths/listPaths.test.ts +++ b/packages/storage/__tests__/internals/apis/listPaths/listPaths.test.ts @@ -1,23 +1,13 @@ // 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 { 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 { createMockAmplifyContext } from '../../../testUtils/mockAmplifyContext'; -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 +22,6 @@ const credentials = { }; const identityId = 'identityId'; -const mockGetConfig = jest.mocked(Amplify.getConfig); -const mockFetchAuthSession = jest.mocked(fetchAuthSession); const mockResolveLocationsFromCurrentSession = resolveLocationsForCurrentSession as jest.Mock; const mockGetHighestPrecedenceUserGroup = jest.mocked( @@ -69,46 +57,29 @@ describe('listPaths', () => { jest.clearAllMocks(); }); - mockGetConfig.mockReturnValue({ - ...mockAuthConfig, - Storage: { - S3: { - bucket: 'bucket1', - region: 'region1', - buckets: { - 'bucket-1': { - bucketName: 'bucket-1', - region: 'region1', - paths: {}, - }, - }, - }, - }, - }); - mockFetchAuthSession.mockResolvedValue({ - credentials, - identityId, - tokens: { - accessToken: { payload: {} }, - }, - }); - it('should return empty locations when buckets are not defined', async () => { - mockGetConfig.mockReturnValue({ + const mockCtx = createMockAmplifyContext({ ...mockAuthConfig, Storage: { S3: { buckets: undefined } }, }); - const result = await listPaths(); + const result = await listPaths(mockCtx); expect(result).toEqual({ locations: [] }); }); it('should generate locations correctly when buckets are defined', async () => { - mockGetConfig.mockReturnValue({ + const mockCtx = createMockAmplifyContext({ ...mockAuthConfig, Storage: { S3: { buckets: mockBuckets } }, }); + (mockCtx.fetchAuthSession as jest.Mock).mockResolvedValue({ + credentials, + identityId, + tokens: { + accessToken: { payload: {} }, + }, + }); mockResolveLocationsFromCurrentSession.mockReturnValue([ { type: 'PREFIX', @@ -118,7 +89,7 @@ describe('listPaths', () => { }, ]); - const result = await listPaths(); + const result = await listPaths(mockCtx); expect(result).toEqual({ locations: [ @@ -133,7 +104,7 @@ describe('listPaths', () => { }); it('should call resolveLocations with authenticated false for unauthenticated user', async () => { - mockGetConfig.mockReturnValue({ + const mockCtx = createMockAmplifyContext({ Auth: { Cognito: { userPoolClientId: 'userPoolClientId', @@ -142,10 +113,9 @@ describe('listPaths', () => { groups: [{ admin: { precedence: 0 } }], }, }, - Storage: { S3: { buckets: mockBuckets } }, }); - mockFetchAuthSession.mockResolvedValue({ + (mockCtx.fetchAuthSession as jest.Mock).mockResolvedValue({ tokens: undefined, identityId: undefined, }); @@ -157,7 +127,7 @@ describe('listPaths', () => { prefix: '/path1', }, }); - await listPaths(); + await listPaths(mockCtx); expect(mockResolveLocationsFromCurrentSession).toHaveBeenCalled(); expect(mockResolveLocationsFromCurrentSession).toHaveBeenCalledWith({ @@ -169,7 +139,7 @@ describe('listPaths', () => { }); it('should call resolveLocations with right userGroup when provided', async () => { - mockGetConfig.mockReturnValue({ + const mockCtx = createMockAmplifyContext({ Auth: { Cognito: { userPoolClientId: 'userPoolClientId', @@ -178,10 +148,9 @@ describe('listPaths', () => { groups: [{ admin: { precedence: 0 } }], }, }, - Storage: { S3: { buckets: mockBuckets } }, }); - mockFetchAuthSession.mockResolvedValue({ + (mockCtx.fetchAuthSession as jest.Mock).mockResolvedValue({ tokens: { accessToken: { payload: {} }, } as AuthTokens, @@ -189,7 +158,7 @@ describe('listPaths', () => { }); mockGetHighestPrecedenceUserGroup.mockReturnValue('admin'); - await listPaths(); + await listPaths(mockCtx); expect(mockResolveLocationsFromCurrentSession).toHaveBeenCalled(); expect(mockResolveLocationsFromCurrentSession).toHaveBeenCalledWith({ diff --git a/packages/storage/__tests__/internals/apis/remove.test.ts b/packages/storage/__tests__/internals/apis/remove.test.ts index 2adab6dd0ef..45370755ee3 100644 --- a/packages/storage/__tests__/internals/apis/remove.test.ts +++ b/packages/storage/__tests__/internals/apis/remove.test.ts @@ -1,13 +1,14 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { AmplifyClassV6 } from '@aws-amplify/core'; - +import { createMockAmplifyContext } from '../../testUtils/mockAmplifyContext'; import { remove as advancedRemove } from '../../../src/internals'; import { remove as removeInternal } from '../../../src/providers/s3/apis/internal/remove'; jest.mock('../../../src/providers/s3/apis/internal/remove'); const mockedRemoveInternal = jest.mocked(removeInternal); +const mockCtx = createMockAmplifyContext(); + describe('remove (internal)', () => { beforeEach(() => { mockedRemoveInternal.mockResolvedValue({ @@ -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 b4060d09933..775571a3b2b 100644 --- a/packages/storage/__tests__/internals/apis/uploadData.test.ts +++ b/packages/storage/__tests__/internals/apis/uploadData.test.ts @@ -1,8 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 - -import { Amplify } from '@aws-amplify/core'; - +import { createMockAmplifyContext } from '../../testUtils/mockAmplifyContext'; import { uploadData as advancedUploadData } from '../../../src/internals'; import { uploadData as uploadDataInternal } from '../../../src/providers/s3/apis/internal/uploadData'; @@ -10,6 +8,8 @@ jest.mock('../../../src/providers/s3/apis/internal/uploadData'); const mockedUploadDataInternal = jest.mocked(uploadDataInternal); const mockedUploadTask = 'UPLOAD_TASK'; +const mockCtx = createMockAmplifyContext(); + describe('uploadData (internal)', () => { beforeEach(() => { mockedUploadDataInternal.mockReturnValue(mockedUploadTask as any); @@ -37,7 +37,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: { @@ -56,7 +56,7 @@ describe('uploadData (internal)', () => { }); expect(mockedUploadDataInternal).toHaveBeenCalledTimes(1); - expect(mockedUploadDataInternal).toHaveBeenCalledWith(Amplify, { + expect(mockedUploadDataInternal).toHaveBeenCalledWith(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..28d290f4da2 100644 --- a/packages/storage/__tests__/providers/s3/apis/copy.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/copy.test.ts @@ -1,8 +1,12 @@ // 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 { createMockAmplifyContext } from '../../../testUtils/mockAmplifyContext'; import { CopyInput, CopyWithPathInput } from '../../../../src'; import { copy } from '../../../../src/providers/s3/apis'; import { copy as internalCopyImpl } from '../../../../src/providers/s3/apis/internal/copy'; @@ -11,7 +15,17 @@ jest.mock('../../../../src/providers/s3/apis/internal/copy'); const mockInternalCopyImpl = jest.mocked(internalCopyImpl); +const mockCtx = createMockAmplifyContext(); + describe('client-side copy', () => { + beforeAll(() => { + setGlobalContext(mockCtx); + }); + + afterAll(() => { + clearGlobalContext(); + }); + beforeEach(() => { jest.clearAllMocks(); }); @@ -28,7 +42,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 +53,6 @@ describe('client-side copy', () => { destination: { path: 'abc' }, }; expect(copy(input)).toEqual(mockInternalResult); - expect(mockInternalCopyImpl).toBeCalledWith(Amplify, input); + expect(mockInternalCopyImpl).toBeCalledWith(mockCtx, 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..bec2dda15be 100644 --- a/packages/storage/__tests__/providers/s3/apis/downloadData.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/downloadData.test.ts @@ -1,6 +1,12 @@ // 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 { createMockAmplifyContext } from '../../../testUtils/mockAmplifyContext'; import { downloadData } from '../../../../src/providers/s3/apis'; import { downloadData as internalDownloadDataImpl } from '../../../../src/providers/s3/apis/internal/downloadData'; @@ -8,7 +14,17 @@ jest.mock('../../../../src/providers/s3/apis/internal/downloadData'); const mockInternalDownloadDataImpl = jest.mocked(internalDownloadDataImpl); +const mockCtx = createMockAmplifyContext(); + describe('client-side downloadData', () => { + beforeAll(() => { + setGlobalContext(mockCtx); + }); + + afterAll(() => { + clearGlobalContext(); + }); + beforeEach(() => { jest.clearAllMocks(); }); @@ -24,7 +40,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 +51,6 @@ describe('client-side downloadData', () => { data: 'data', }; expect(downloadData(input)).toEqual(mockInternalResult); - expect(mockInternalDownloadDataImpl).toBeCalledWith(input); + expect(mockInternalDownloadDataImpl).toBeCalledWith(mockCtx, 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..c9a2ee55de7 100644 --- a/packages/storage/__tests__/providers/s3/apis/getProperties.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/getProperties.test.ts @@ -1,8 +1,12 @@ // 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 { createMockAmplifyContext } from '../../../testUtils/mockAmplifyContext'; import { GetPropertiesInput, GetPropertiesWithPathInput, @@ -14,7 +18,17 @@ jest.mock('../../../../src/providers/s3/apis/internal/getProperties'); const mockInternalGetPropertiesImpl = jest.mocked(internalGetPropertiesImpl); +const mockCtx = createMockAmplifyContext(); + describe('client-side getProperties', () => { + beforeAll(() => { + setGlobalContext(mockCtx); + }); + + afterAll(() => { + clearGlobalContext(); + }); + beforeEach(() => { jest.clearAllMocks(); }); @@ -26,7 +40,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 +50,6 @@ describe('client-side getProperties', () => { path: 'abc', }; expect(getProperties(input)).toEqual(mockInternalResult); - expect(mockInternalGetPropertiesImpl).toBeCalledWith(Amplify, input); + expect(mockInternalGetPropertiesImpl).toBeCalledWith(mockCtx, 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..d5d25bfae7a 100644 --- a/packages/storage/__tests__/providers/s3/apis/getUrl.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/getUrl.test.ts @@ -1,8 +1,12 @@ // 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 { createMockAmplifyContext } from '../../../testUtils/mockAmplifyContext'; import { GetUrlInput, GetUrlWithPathInput } from '../../../../src'; import { getUrl } from '../../../../src/providers/s3/apis'; import { getUrl as internalGetUrlImpl } from '../../../../src/providers/s3/apis/internal/getUrl'; @@ -11,7 +15,17 @@ jest.mock('../../../../src/providers/s3/apis/internal/getUrl'); const mockInternalGetUrlImpl = jest.mocked(internalGetUrlImpl); +const mockCtx = createMockAmplifyContext(); + describe('client-side getUrl', () => { + beforeAll(() => { + setGlobalContext(mockCtx); + }); + + afterAll(() => { + clearGlobalContext(); + }); + beforeEach(() => { jest.clearAllMocks(); }); @@ -23,7 +37,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 +47,6 @@ describe('client-side getUrl', () => { path: 'abc', }; expect(getUrl(input)).toEqual(mockInternalResult); - expect(mockInternalGetUrlImpl).toBeCalledWith(Amplify, input); + expect(mockInternalGetUrlImpl).toBeCalledWith(mockCtx, 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..05d8727dae4 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 { AmplifyContext, StorageAccessLevel } from '@aws-amplify/core'; import { StorageError } from '../../../../../src/errors/StorageError'; import { StorageValidationErrorCode } from '../../../../../src/errors/types/validation'; @@ -18,20 +18,7 @@ import './testUtils'; import { BucketInfo } from '../../../../../src/providers/s3/types/options'; 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 sourceKey = 'sourceKey'; const destinationKey = 'destinationKey'; @@ -56,6 +43,18 @@ const copyObjectClientBaseParams = { MetadataDirective: 'COPY', }; +const mockFetchAuthSession = jest.fn(); +const mockGetConfig = jest.fn(); +const mockCtx: AmplifyContext = { + get resourcesConfig() { + return mockGetConfig(); + }, + libraryOptions: {}, + fetchAuthSession: mockFetchAuthSession, + clearCredentials: jest.fn().mockResolvedValue(undefined), + getTokens: jest.fn().mockResolvedValue(undefined), +}; + describe('copy API', () => { beforeAll(() => { mockFetchAuthSession.mockResolvedValue({ @@ -75,7 +74,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 +295,7 @@ describe('copy API', () => { describe('With path', () => { const copyWrapper = async (input: CopyWithPathInput) => - copy(Amplify, input); + copy(mockCtx, input); beforeEach(() => { mockCopyObject.mockImplementation(() => { @@ -495,7 +494,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 +517,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 +532,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 +545,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 +562,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..6819c7fb2ef 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 { AmplifyContext, StorageAccessLevel } from '@aws-amplify/core'; import { getObject } from '../../../../../src/providers/s3/utils/client/s3data'; import { downloadData } from '../../../../../src/providers/s3/apis/internal/downloadData'; @@ -28,17 +28,6 @@ import { BucketInfo } from '../../../../../src/providers/s3/types/options'; 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 +50,20 @@ 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 mockFetchAuthSession = jest.fn(); +const mockGetConfig = jest.fn(); +const mockCtx: AmplifyContext = { + get resourcesConfig() { + return mockGetConfig(); + }, + libraryOptions: {}, + fetchAuthSession: mockFetchAuthSession, + clearCredentials: jest.fn().mockResolvedValue(undefined), + getTokens: jest.fn().mockResolvedValue(undefined), +}; describe('downloadData with key', () => { beforeAll(() => { @@ -98,7 +97,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 +130,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 +172,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 +205,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 +232,7 @@ describe('downloadData with key', () => { region: 'region-1', }; - downloadData({ + downloadData(mockCtx, { key: inputKey, options: { bucket: bucketInfo, @@ -262,7 +261,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 +290,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 +313,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 +334,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 +384,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 +401,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 +448,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 +481,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 +508,7 @@ describe('downloadData with path', () => { region: 'region-1', }; - downloadData({ + downloadData(mockCtx, { path: inputPath, options: { bucket: bucketInfo, @@ -538,7 +537,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 +566,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 +589,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 +610,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..f4b36e1cfd2 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 { AmplifyContext, StorageAccessLevel } from '@aws-amplify/core'; import { headObject } from '../../../../../src/providers/s3/utils/client/s3data'; import { getProperties } from '../../../../../src/providers/s3/apis/internal/getProperties'; @@ -16,20 +16,7 @@ import './testUtils'; import { BucketInfo } from '../../../../../src/providers/s3/types/options'; 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 bucket = 'bucket'; const region = 'region'; @@ -54,9 +41,21 @@ const expectedResult = { versionId: 'version-id', }; +const mockFetchAuthSession = jest.fn(); +const mockGetConfig = jest.fn(); +const mockCtx: AmplifyContext = { + get resourcesConfig() { + return mockGetConfig(); + }, + libraryOptions: {}, + fetchAuthSession: mockFetchAuthSession, + clearCredentials: jest.fn().mockResolvedValue(undefined), + getTokens: jest.fn().mockResolvedValue(undefined), +}; + describe('getProperties with key', () => { const getPropertiesWrapper = (input: GetPropertiesInput) => - getProperties(Amplify, input); + getProperties(mockCtx, input); beforeAll(() => { mockFetchAuthSession.mockResolvedValue({ credentials, @@ -243,7 +242,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 +414,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..06a5e61f6eb 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 { AmplifyContext, StorageAccessLevel } from '@aws-amplify/core'; import { getUrl } from '../../../../../src/providers/s3/apis/internal/getUrl'; import { @@ -18,22 +18,9 @@ import './testUtils'; import { BucketInfo } from '../../../../../src/providers/s3/types/options'; 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 credentials: AWSCredentials = { accessKeyId: 'accessKeyId', sessionToken: 'sessionToken', @@ -45,8 +32,20 @@ const mockURL = new URL('https://google.com'); const validBucketOwner = '111122223333'; const invalidBucketOwner = '123'; +const mockFetchAuthSession = jest.fn(); +const mockGetConfig = jest.fn(); +const mockCtx: AmplifyContext = { + get resourcesConfig() { + return mockGetConfig(); + }, + libraryOptions: {}, + fetchAuthSession: mockFetchAuthSession, + clearCredentials: jest.fn().mockResolvedValue(undefined), + getTokens: jest.fn().mockResolvedValue(undefined), +}; + 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 +334,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 +686,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 +800,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..f3a88035c8e 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 { AmplifyContext, StorageAccessLevel } from '@aws-amplify/core'; import { listObjectsV2 } from '../../../../../src/providers/s3/utils/client/s3data'; import { list } from '../../../../../src/providers/s3/apis/internal/list'; @@ -19,19 +19,6 @@ import './testUtils'; import { ListObjectsV2CommandInput } from '../../../../../src/providers/s3/utils/client/s3data/types'; 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 mockListObject = listObjectsV2 as jest.Mock; const inputKey = 'path/itemsKey'; const bucket = 'bucket'; @@ -91,6 +78,18 @@ const mockListResponse = (listParams: ListObjectsV2CommandInput) => ({ ContinuationToken: listParams.ContinuationToken, }); +const mockFetchAuthSession = jest.fn(); +const mockGetConfig = jest.fn(); +const mockCtx: AmplifyContext = { + get resourcesConfig() { + return mockGetConfig(); + }, + libraryOptions: {}, + fetchAuthSession: mockFetchAuthSession, + clearCredentials: jest.fn().mockResolvedValue(undefined), + getTokens: jest.fn().mockResolvedValue(undefined), +}; + describe('list API', () => { beforeAll(() => { mockFetchAuthSession.mockResolvedValue({ @@ -108,9 +107,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 +393,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 +657,7 @@ describe('list API', () => { }), ); try { - await list(Amplify, {}); + await list(mockCtx, {}); } catch (error: any) { expect.assertions(3); expect(listObjectsV2).toHaveBeenCalledTimes(1); @@ -677,11 +676,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 +759,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 +793,7 @@ describe('list API', () => { }; }); - const { items, excludedSubpaths } = (await list(Amplify, { + const { items, excludedSubpaths } = (await list(mockCtx, { path: mockedPath, options: { subpathStrategy: { strategy: 'exclude' }, @@ -816,7 +815,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 +837,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 +859,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 +880,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 +898,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 +959,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 +1028,7 @@ describe('list API', () => { { type: 'Prefix', listFunction: (options?: any) => - list(Amplify, { + list(mockCtx, { prefix: 'some folder with unprintable unicode/', options, }), @@ -1038,7 +1037,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..a06454886e5 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 { AmplifyContext, StorageAccessLevel } from '@aws-amplify/core'; import { deleteObject, @@ -22,24 +22,11 @@ import { CanceledError } from '../../../../../src/errors/CanceledError'; import './testUtils'; 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 inputKey = 'key'; const bucket = 'bucket'; @@ -58,6 +45,18 @@ const deleteObjectClientConfig = { abortSignal: expect.any(Object), }; +const mockFetchAuthSession = jest.fn(); +const mockGetConfig = jest.fn(); +const mockCtx: AmplifyContext = { + get resourcesConfig() { + return mockGetConfig(); + }, + libraryOptions: {}, + fetchAuthSession: mockFetchAuthSession, + clearCredentials: jest.fn().mockResolvedValue(undefined), + getTokens: jest.fn().mockResolvedValue(undefined), +}; + describe('remove API', () => { beforeAll(() => { mockFetchAuthSession.mockResolvedValue({ @@ -77,7 +76,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 +217,7 @@ describe('remove API', () => { describe('With Path', () => { const removeWrapper = (input: RemoveWithPathInput) => - remove(Amplify, input); + remove(mockCtx, input); beforeEach(() => { mockDeleteObject.mockImplementation(() => { @@ -546,7 +545,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 +562,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 +573,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 0584d98ead9..4fc1bc596e1 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,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 { uploadData } from '../../../../../../src/providers/s3/apis/internal/uploadData'; import { MAX_OBJECT_SIZE } from '../../../../../../src/providers/s3/utils/constants'; @@ -25,7 +25,13 @@ jest.mock( '../../../../../../src/providers/s3/apis/internal/uploadData/multipart', ); -const mockAmplifyInstance = {} as AmplifyClassV6; +const mockCtx: AmplifyContext = { + resourcesConfig: {}, + libraryOptions: {}, + fetchAuthSession: jest.fn().mockResolvedValue({}), + clearCredentials: jest.fn(), + getTokens: jest.fn(), +}; const testPath = 'testPath/object'; const validBucketOwner = '111122223333'; @@ -52,7 +58,7 @@ describe('uploadData with key', () => { key: 'key', data: { size: MAX_OBJECT_SIZE + 1 } as any, }; - expect(() => uploadData(mockAmplifyInstance, mockUploadInput)).toThrow( + expect(() => uploadData(mockCtx, mockUploadInput)).toThrow( expect.objectContaining( validationErrorMap[StorageValidationErrorCode.ObjectIsTooLarge], ), @@ -61,7 +67,7 @@ describe('uploadData with key', () => { it('should throw if data size is unknown', async () => { expect(() => - uploadData(mockAmplifyInstance, { + uploadData(mockCtx, { key: 'key', data: {} as any, }), @@ -76,7 +82,7 @@ describe('uploadData with key', () => { describe('use putObject for small uploads', () => { const smallData = { size: 5 * 1024 * 1024 } as any; it('should use putObject if data size is <= 5MB', async () => { - uploadData(mockAmplifyInstance, { + uploadData(mockCtx, { key: 'key', data: smallData, }); @@ -90,10 +96,10 @@ describe('uploadData with key', () => { data: '', // 0 bytes }; - uploadData(mockAmplifyInstance, testInput); + uploadData(mockCtx, testInput); expect(mockPutObjectJob).toHaveBeenCalledWith( - mockAmplifyInstance, + mockCtx, expect.objectContaining(testInput), expect.any(AbortSignal), expect.any(Number), @@ -104,7 +110,7 @@ describe('uploadData with key', () => { it('should use uploadTask', async () => { mockPutObjectJob.mockReturnValueOnce('putObjectJob'); mockCreateUploadTask.mockReturnValueOnce('uploadTask'); - const task = uploadData(mockAmplifyInstance, { + const task = uploadData(mockCtx, { key: 'key', data: smallData, }); @@ -122,7 +128,7 @@ describe('uploadData with key', () => { describe('use multipartUpload for large uploads', () => { const biggerData = { size: 5 * 1024 * 1024 + 1 } as any; it('should use multipartUpload if data size is > 5MB', async () => { - uploadData(mockAmplifyInstance, { + uploadData(mockCtx, { key: 'key', data: biggerData, }); @@ -132,7 +138,7 @@ describe('uploadData with key', () => { it('should use uploadTask', async () => { mockCreateUploadTask.mockReturnValueOnce('uploadTask'); - const task = uploadData(mockAmplifyInstance, { + const task = uploadData(mockCtx, { key: 'key', data: biggerData, }); @@ -149,7 +155,7 @@ describe('uploadData with key', () => { }); it('should call getMultipartUploadHandlers', async () => { - uploadData(mockAmplifyInstance, { + uploadData(mockCtx, { key: 'key', data: biggerData, }); @@ -169,7 +175,7 @@ describe('uploadData with path', () => { path: testPath, data: { size: MAX_OBJECT_SIZE + 1 } as any, }; - expect(() => uploadData(mockAmplifyInstance, mockUploadInput)).toThrow( + expect(() => uploadData(mockCtx, mockUploadInput)).toThrow( expect.objectContaining( validationErrorMap[StorageValidationErrorCode.ObjectIsTooLarge], ), @@ -178,7 +184,7 @@ describe('uploadData with path', () => { it('should throw if data size is unknown', async () => { expect(() => - uploadData(mockAmplifyInstance, { + uploadData(mockCtx, { path: testPath, data: {} as any, }), @@ -208,10 +214,10 @@ describe('uploadData with path', () => { data: smallData, }; - uploadData(mockAmplifyInstance, testInput); + uploadData(mockCtx, testInput); expect(mockPutObjectJob).toHaveBeenCalledWith( - mockAmplifyInstance, + mockCtx, expect.objectContaining(testInput), expect.any(AbortSignal), expect.any(Number), @@ -226,10 +232,10 @@ describe('uploadData with path', () => { data: '', // 0 bytes }; - uploadData(mockAmplifyInstance, testInput); + uploadData(mockCtx, testInput); expect(mockPutObjectJob).toHaveBeenCalledWith( - mockAmplifyInstance, + mockCtx, expect.objectContaining(testInput), expect.any(AbortSignal), expect.any(Number), @@ -241,7 +247,7 @@ describe('uploadData with path', () => { mockPutObjectJob.mockReturnValueOnce('putObjectJob'); mockCreateUploadTask.mockReturnValueOnce('uploadTask'); - const task = uploadData(mockAmplifyInstance, { + const task = uploadData(mockCtx, { path: testPath, data: smallData, }); @@ -265,11 +271,11 @@ describe('uploadData with path', () => { data: biggerData, }; - uploadData(mockAmplifyInstance, testInput); + uploadData(mockCtx, testInput); expect(mockPutObjectJob).not.toHaveBeenCalled(); expect(mockGetMultipartUploadHandlers).toHaveBeenCalledWith( - mockAmplifyInstance, + mockCtx, expect.objectContaining(testInput), expect.any(Number), ); @@ -277,7 +283,7 @@ describe('uploadData with path', () => { it('should use uploadTask', async () => { mockCreateUploadTask.mockReturnValueOnce('uploadTask'); - const task = uploadData(mockAmplifyInstance, { + const task = uploadData(mockCtx, { path: testPath, data: biggerData, }); @@ -298,7 +304,7 @@ describe('uploadData with path', () => { it('should include expectedBucketOwner in headers when provided for singlepartUpload', async () => { mockPutObjectJob.mockReturnValueOnce('putObjectJob'); const smallData = 'smallData'; - uploadData(mockAmplifyInstance, { + uploadData(mockCtx, { path: testPath, data: smallData, options: { @@ -306,7 +312,7 @@ describe('uploadData with path', () => { }, }); expect(mockPutObjectJob).toHaveBeenCalledWith( - mockAmplifyInstance, + mockCtx, expect.objectContaining({ path: 'testPath/object', data: 'smallData', @@ -329,9 +335,9 @@ describe('uploadData with path', () => { expectedBucketOwner: validBucketOwner, }, }; - uploadData(mockAmplifyInstance, testInput); + uploadData(mockCtx, testInput); expect(mockGetMultipartUploadHandlers).toHaveBeenCalledWith( - mockAmplifyInstance, + mockCtx, { ...testInput, options: expect.objectContaining(testInput.options), 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 0ffaae9817c..45e20d908e5 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, @@ -29,8 +29,15 @@ import { byteLength } from '../../../../../../src/providers/s3/apis/internal/upl import '../testUtils'; -jest.mock('@aws-amplify/core'); jest.mock('../../../../../../src/providers/s3/utils/client/s3data'); +jest.mock('@aws-amplify/core', () => ({ + ...jest.requireActual('@aws-amplify/core'), + defaultStorage: { + getItem: jest.fn(), + setItem: jest.fn(), + removeItem: jest.fn(), + }, +})); jest.mock('../../../../../../src/providers/s3/utils/crc32'); const credentials: AWSCredentials = { @@ -39,7 +46,6 @@ const credentials: AWSCredentials = { secretAccessKey: 'secretAccessKey', }; const defaultIdentityId = 'defaultIdentityId'; -const mockFetchAuthSession = Amplify.Auth.fetchAuthSession as jest.Mock; const bucket = 'bucket'; const region = 'region'; const defaultKey = 'key'; @@ -57,6 +63,21 @@ const mockListParts = jest.mocked(listParts); const mockHeadObject = jest.mocked(headObject); const mockCalculateContentCRC32 = jest.mocked(calculateContentCRC32); +const mockFetchAuthSession = jest.fn(); +const mockGetConfig = jest.fn(); +let mockLibraryOptions: Record = {}; +const mockCtx: AmplifyContext = { + get resourcesConfig() { + return mockGetConfig(); + }, + get libraryOptions() { + return mockLibraryOptions; + }, + fetchAuthSession: mockFetchAuthSession, + clearCredentials: jest.fn().mockResolvedValue(undefined), + getTokens: jest.fn().mockResolvedValue(undefined), +}; + const disableAssertionFlag = true; const MB = 1024 * 1024; @@ -171,7 +192,7 @@ describe('getMultipartUploadHandlers with key', () => { credentials, identityId: defaultIdentityId, }); - (Amplify.getConfig as jest.Mock).mockReturnValue({ + mockGetConfig.mockReturnValue({ Storage: { S3: { bucket, @@ -190,7 +211,7 @@ describe('getMultipartUploadHandlers with key', () => { it('should return multipart upload handlers', async () => { const multipartUploadHandlers = getMultipartUploadHandlers( - Amplify, + mockCtx, { key: defaultKey, data: { size: 5 * 1024 * 1024 } as any, @@ -236,7 +257,7 @@ describe('getMultipartUploadHandlers with key', () => { async (_, twoPartsPayload) => { mockMultipartUploadSuccess(); const { multipartUploadJob } = getMultipartUploadHandlers( - Amplify, + mockCtx, { key: defaultKey, data: twoPartsPayload, @@ -295,7 +316,7 @@ describe('getMultipartUploadHandlers with key', () => { async (_, twoPartsPayload, expectedCrc32, finalCrc32) => { mockMultipartUploadSuccess(); const { multipartUploadJob } = getMultipartUploadHandlers( - Amplify, + mockCtx, { key: defaultKey, data: twoPartsPayload, @@ -336,7 +357,7 @@ describe('getMultipartUploadHandlers with key', () => { it('should use md5 if no using crc32', async () => { mockMultipartUploadSuccess(); - Amplify.libraryOptions = { + mockLibraryOptions = { Storage: { S3: { isObjectLockEnabled: true, @@ -344,7 +365,7 @@ describe('getMultipartUploadHandlers with key', () => { }, }; const { multipartUploadJob } = getMultipartUploadHandlers( - Amplify, + mockCtx, { key: defaultKey, data: new Uint8Array(8 * MB), @@ -360,7 +381,7 @@ describe('getMultipartUploadHandlers with key', () => { it('should throw if unsupported payload type is provided', async () => { mockMultipartUploadSuccess(); const { multipartUploadJob } = getMultipartUploadHandlers( - Amplify, + mockCtx, { key: defaultKey, data: 1 as any, @@ -393,7 +414,7 @@ describe('getMultipartUploadHandlers with key', () => { } as any as File; mockMultipartUploadSuccess(); const { multipartUploadJob } = getMultipartUploadHandlers( - Amplify, + mockCtx, { key: defaultKey, data: file, @@ -423,7 +444,7 @@ describe('getMultipartUploadHandlers with key', () => { }); const { multipartUploadJob } = getMultipartUploadHandlers( - Amplify, + mockCtx, { key: defaultKey, data: new ArrayBuffer(8 * MB), @@ -447,7 +468,7 @@ describe('getMultipartUploadHandlers with key', () => { mockCreateMultipartUpload.mockRejectedValueOnce(new Error('error')); const { multipartUploadJob } = getMultipartUploadHandlers( - Amplify, + mockCtx, { key: defaultKey, data: new ArrayBuffer(8 * MB), @@ -464,7 +485,7 @@ describe('getMultipartUploadHandlers with key', () => { mockCompleteMultipartUpload.mockRejectedValueOnce(new Error('error')); const { multipartUploadJob } = getMultipartUploadHandlers( - Amplify, + mockCtx, { key: defaultKey, data: new ArrayBuffer(8 * MB), @@ -486,7 +507,7 @@ describe('getMultipartUploadHandlers with key', () => { mockUploadPart.mockRejectedValueOnce(new Error('error')); const { multipartUploadJob } = getMultipartUploadHandlers( - Amplify, + mockCtx, { key: defaultKey, data: new ArrayBuffer(8 * MB), @@ -505,7 +526,7 @@ describe('getMultipartUploadHandlers with key', () => { const mockRegion = 'region-1'; mockMultipartUploadSuccess(); const { multipartUploadJob } = getMultipartUploadHandlers( - Amplify, + mockCtx, { key: 'key', data: mockData, @@ -535,7 +556,7 @@ describe('getMultipartUploadHandlers with key', () => { it('should override bucket in putObject call when bucket as string', async () => { mockMultipartUploadSuccess(); const { multipartUploadJob } = getMultipartUploadHandlers( - Amplify, + mockCtx, { key: 'key', data: mockData, @@ -597,7 +618,7 @@ describe('getMultipartUploadHandlers with key', () => { const onProgress = jest.fn(); const { multipartUploadJob } = getMultipartUploadHandlers( - Amplify, + mockCtx, { key: defaultKey, data: new ArrayBuffer(8 * MB), @@ -629,7 +650,7 @@ describe('getMultipartUploadHandlers with key', () => { mockMultipartUploadSuccess(); const size = 8 * MB; const { multipartUploadJob } = getMultipartUploadHandlers( - Amplify, + mockCtx, { key: defaultKey, data: new ArrayBuffer(size), @@ -659,7 +680,7 @@ describe('getMultipartUploadHandlers with key', () => { }; const size = 8 * MB; const { multipartUploadJob } = getMultipartUploadHandlers( - Amplify, + mockCtx, { key: defaultKey, data: new ArrayBuffer(size), @@ -684,7 +705,7 @@ describe('getMultipartUploadHandlers with key', () => { mockMultipartUploadSuccess(); const size = 8 * MB; const { multipartUploadJob } = getMultipartUploadHandlers( - Amplify, + mockCtx, { key: defaultKey, data: new ArrayBuffer(size), @@ -716,7 +737,7 @@ describe('getMultipartUploadHandlers with key', () => { mockListParts.mockResolvedValueOnce({ Parts: [], $metadata: {} }); const size = 8 * MB; const { multipartUploadJob } = getMultipartUploadHandlers( - Amplify, + mockCtx, { key: defaultKey, data: new ArrayBuffer(size), @@ -738,7 +759,7 @@ describe('getMultipartUploadHandlers with key', () => { mockListParts.mockResolvedValueOnce({ Parts: [], $metadata: {} }); const size = 8 * MB; const { multipartUploadJob } = getMultipartUploadHandlers( - Amplify, + mockCtx, { key: defaultKey, data: new File([new ArrayBuffer(size)], 'someName'), @@ -777,7 +798,7 @@ describe('getMultipartUploadHandlers with key', () => { mockListParts.mockResolvedValueOnce({ Parts: [], $metadata: {} }); const size = 8 * MB; const { multipartUploadJob } = getMultipartUploadHandlers( - Amplify, + mockCtx, { key: defaultKey, data: new File([new ArrayBuffer(size)], 'someName'), @@ -810,7 +831,7 @@ describe('getMultipartUploadHandlers with key', () => { mockListParts.mockResolvedValueOnce({ Parts: [], $metadata: {} }); const size = 8 * MB; const { multipartUploadJob } = getMultipartUploadHandlers( - Amplify, + mockCtx, { key: defaultKey, data: new ArrayBuffer(size), @@ -832,7 +853,7 @@ describe('getMultipartUploadHandlers with key', () => { mockListParts.mockResolvedValueOnce({ Parts: [], $metadata: {} }); const size = 8 * MB; const { multipartUploadJob } = getMultipartUploadHandlers( - Amplify, + mockCtx, { key: defaultKey, data: new ArrayBuffer(size), @@ -863,7 +884,7 @@ describe('getMultipartUploadHandlers with key', () => { mockListParts.mockResolvedValueOnce({ Parts: [], $metadata: {} }); const size = 8 * MB; const { multipartUploadJob } = getMultipartUploadHandlers( - Amplify, + mockCtx, { key: defaultKey, data: new ArrayBuffer(size), @@ -889,7 +910,7 @@ describe('getMultipartUploadHandlers with key', () => { mockListParts.mockResolvedValueOnce({ Parts: [], $metadata: {} }); const size = 8 * MB; const { multipartUploadJob } = getMultipartUploadHandlers( - Amplify, + mockCtx, { key: defaultKey, data: new ArrayBuffer(size), @@ -914,7 +935,7 @@ describe('getMultipartUploadHandlers with key', () => { describe('cancel()', () => { it('should abort in-flight uploadPart requests and throw if upload is canceled', async () => { const { multipartUploadJob, onCancel } = getMultipartUploadHandlers( - Amplify, + mockCtx, { key: defaultKey, data: new ArrayBuffer(8 * MB), @@ -955,7 +976,7 @@ describe('getMultipartUploadHandlers with key', () => { const { multipartUploadJob, onPause, onResume } = getMultipartUploadHandlers( - Amplify, + mockCtx, { key: defaultKey, data: new ArrayBuffer(8 * MB), @@ -988,7 +1009,7 @@ describe('getMultipartUploadHandlers with key', () => { const onProgress = jest.fn(); mockMultipartUploadSuccess(); const { multipartUploadJob } = getMultipartUploadHandlers( - Amplify, + mockCtx, { key: defaultKey, data: new ArrayBuffer(8 * MB), @@ -1039,7 +1060,7 @@ describe('getMultipartUploadHandlers with key', () => { const onProgress = jest.fn(); const { multipartUploadJob } = getMultipartUploadHandlers( - Amplify, + mockCtx, { key: defaultKey, data: new ArrayBuffer(8 * MB), @@ -1067,7 +1088,7 @@ describe('getMultipartUploadHandlers with path', () => { credentials, identityId: defaultIdentityId, }); - (Amplify.getConfig as jest.Mock).mockReturnValue({ + mockGetConfig.mockReturnValue({ Storage: { S3: { bucket, @@ -1086,7 +1107,7 @@ describe('getMultipartUploadHandlers with path', () => { it('should return multipart upload handlers', async () => { const multipartUploadHandlers = getMultipartUploadHandlers( - Amplify, + mockCtx, { path: testPath, data: { size: 5 * 1024 * 1024 } as any, @@ -1125,7 +1146,7 @@ describe('getMultipartUploadHandlers with path', () => { async (_, twoPartsPayload) => { mockMultipartUploadSuccess(); const { multipartUploadJob } = getMultipartUploadHandlers( - Amplify, + mockCtx, { path: inputPath, data: twoPartsPayload, @@ -1183,7 +1204,7 @@ describe('getMultipartUploadHandlers with path', () => { async (_, twoPartsPayload, expectedCrc32, finalCrc32) => { mockMultipartUploadSuccess(); const { multipartUploadJob } = getMultipartUploadHandlers( - Amplify, + mockCtx, { path: testPath, data: twoPartsPayload, @@ -1224,7 +1245,7 @@ describe('getMultipartUploadHandlers with path', () => { it('should use md5 if no using crc32', async () => { mockMultipartUploadSuccess(); - Amplify.libraryOptions = { + mockLibraryOptions = { Storage: { S3: { isObjectLockEnabled: true, @@ -1232,7 +1253,7 @@ describe('getMultipartUploadHandlers with path', () => { }, }; const { multipartUploadJob } = getMultipartUploadHandlers( - Amplify, + mockCtx, { path: testPath, data: new Uint8Array(8 * MB), @@ -1248,7 +1269,7 @@ describe('getMultipartUploadHandlers with path', () => { it('should throw if unsupported payload type is provided', async () => { mockMultipartUploadSuccess(); const { multipartUploadJob } = getMultipartUploadHandlers( - Amplify, + mockCtx, { path: testPath, data: 1 as any, @@ -1281,7 +1302,7 @@ describe('getMultipartUploadHandlers with path', () => { } as any as File; mockMultipartUploadSuccess(); const { multipartUploadJob } = getMultipartUploadHandlers( - Amplify, + mockCtx, { path: testPath, data: file, @@ -1311,7 +1332,7 @@ describe('getMultipartUploadHandlers with path', () => { }); const { multipartUploadJob } = getMultipartUploadHandlers( - Amplify, + mockCtx, { path: testPath, data: new ArrayBuffer(8 * MB), @@ -1335,7 +1356,7 @@ describe('getMultipartUploadHandlers with path', () => { mockCreateMultipartUpload.mockRejectedValueOnce(new Error('error')); const { multipartUploadJob } = getMultipartUploadHandlers( - Amplify, + mockCtx, { path: testPath, data: new ArrayBuffer(8 * MB), @@ -1352,7 +1373,7 @@ describe('getMultipartUploadHandlers with path', () => { mockCompleteMultipartUpload.mockRejectedValueOnce(new Error('error')); const { multipartUploadJob } = getMultipartUploadHandlers( - Amplify, + mockCtx, { path: testPath, data: new ArrayBuffer(8 * MB), @@ -1374,7 +1395,7 @@ describe('getMultipartUploadHandlers with path', () => { mockUploadPart.mockRejectedValueOnce(new Error('error')); const { multipartUploadJob } = getMultipartUploadHandlers( - Amplify, + mockCtx, { path: testPath, data: new ArrayBuffer(8 * MB), @@ -1392,7 +1413,7 @@ describe('getMultipartUploadHandlers with path', () => { mockMultipartUploadSuccess(); const { multipartUploadJob } = getMultipartUploadHandlers( - Amplify, + mockCtx, { path: testPath, data: new ArrayBuffer(8 * MB), @@ -1423,7 +1444,7 @@ describe('getMultipartUploadHandlers with path', () => { const mockRegion = 'region-1'; mockMultipartUploadSuccess(); const { multipartUploadJob } = getMultipartUploadHandlers( - Amplify, + mockCtx, { path: 'path/', data: mockData, @@ -1455,7 +1476,7 @@ describe('getMultipartUploadHandlers with path', () => { it('should override bucket in putObject call when bucket as string', async () => { mockMultipartUploadSuccess(); const { multipartUploadJob } = getMultipartUploadHandlers( - Amplify, + mockCtx, { path: 'path/', data: mockData, @@ -1520,7 +1541,7 @@ describe('getMultipartUploadHandlers with path', () => { const onProgress = jest.fn(); const { multipartUploadJob } = getMultipartUploadHandlers( - Amplify, + mockCtx, { path: testPath, data: new ArrayBuffer(8 * MB), @@ -1552,7 +1573,7 @@ describe('getMultipartUploadHandlers with path', () => { mockMultipartUploadSuccess(); const size = 8 * MB; const { multipartUploadJob } = getMultipartUploadHandlers( - Amplify, + mockCtx, { path: testPath, data: new ArrayBuffer(size), @@ -1570,7 +1591,7 @@ describe('getMultipartUploadHandlers with path', () => { mockMultipartUploadSuccess(); const size = 8 * MB; const { multipartUploadJob } = getMultipartUploadHandlers( - Amplify, + mockCtx, { path: testPath, data: new ArrayBuffer(size), @@ -1603,7 +1624,7 @@ describe('getMultipartUploadHandlers with path', () => { }; const size = 8 * MB; const { multipartUploadJob } = getMultipartUploadHandlers( - Amplify, + mockCtx, { path: testPath, data: new ArrayBuffer(size), @@ -1639,7 +1660,7 @@ describe('getMultipartUploadHandlers with path', () => { mockListParts.mockResolvedValueOnce({ Parts: [], $metadata: {} }); const size = 8 * MB; const { multipartUploadJob } = getMultipartUploadHandlers( - Amplify, + mockCtx, { path: testPath, data: new ArrayBuffer(size), @@ -1661,7 +1682,7 @@ describe('getMultipartUploadHandlers with path', () => { mockListParts.mockResolvedValueOnce({ Parts: [], $metadata: {} }); const size = 8 * MB; const { multipartUploadJob } = getMultipartUploadHandlers( - Amplify, + mockCtx, { path: testPath, data: new File([new ArrayBuffer(size)], 'someName'), @@ -1701,7 +1722,7 @@ describe('getMultipartUploadHandlers with path', () => { mockListParts.mockResolvedValueOnce({ Parts: [], $metadata: {} }); const size = 8 * MB; const { multipartUploadJob } = getMultipartUploadHandlers( - Amplify, + mockCtx, { path: testPath, data: new File([new ArrayBuffer(size)], 'someName'), @@ -1734,7 +1755,7 @@ describe('getMultipartUploadHandlers with path', () => { mockListParts.mockResolvedValueOnce({ Parts: [], $metadata: {} }); const size = 8 * MB; const { multipartUploadJob } = getMultipartUploadHandlers( - Amplify, + mockCtx, { path: testPath, data: new ArrayBuffer(size), @@ -1756,7 +1777,7 @@ describe('getMultipartUploadHandlers with path', () => { mockListParts.mockResolvedValueOnce({ Parts: [], $metadata: {} }); const size = 8 * MB; const { multipartUploadJob } = getMultipartUploadHandlers( - Amplify, + mockCtx, { path: testPath, data: new ArrayBuffer(size), @@ -1785,7 +1806,7 @@ describe('getMultipartUploadHandlers with path', () => { mockListParts.mockResolvedValueOnce({ Parts: [], $metadata: {} }); const size = 8 * MB; const { multipartUploadJob } = getMultipartUploadHandlers( - Amplify, + mockCtx, { path: testPath, data: new ArrayBuffer(size), @@ -1811,7 +1832,7 @@ describe('getMultipartUploadHandlers with path', () => { mockListParts.mockResolvedValueOnce({ Parts: [], $metadata: {} }); const size = 8 * MB; const { multipartUploadJob } = getMultipartUploadHandlers( - Amplify, + mockCtx, { path: testPath, data: new ArrayBuffer(size), @@ -1836,7 +1857,7 @@ describe('getMultipartUploadHandlers with path', () => { describe('cancel()', () => { it('should abort in-flight uploadPart requests and throw if upload is canceled', async () => { const { multipartUploadJob, onCancel } = getMultipartUploadHandlers( - Amplify, + mockCtx, { path: testPath, data: new ArrayBuffer(8 * MB), @@ -1876,7 +1897,7 @@ describe('getMultipartUploadHandlers with path', () => { const { multipartUploadJob, onPause, onResume } = getMultipartUploadHandlers( - Amplify, + mockCtx, { path: testPath, data: new ArrayBuffer(8 * MB), @@ -1910,7 +1931,7 @@ describe('getMultipartUploadHandlers with path', () => { const onProgress = jest.fn(); mockMultipartUploadSuccess(); const { multipartUploadJob } = getMultipartUploadHandlers( - Amplify, + mockCtx, { path: testPath, data: new ArrayBuffer(8 * MB), @@ -1962,7 +1983,7 @@ describe('getMultipartUploadHandlers with path', () => { const onProgress = jest.fn(); const { multipartUploadJob } = getMultipartUploadHandlers( - Amplify, + mockCtx, { path: testPath, data: new ArrayBuffer(8 * MB), 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 f0a713a3c48..d28c4b7f41b 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/providers/s3/utils'; @@ -21,16 +21,6 @@ jest.mock('../../../../../../src/providers/s3/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 = { @@ -39,18 +29,17 @@ const credentials: AWSCredentials = { secretAccessKey: 'secretAccessKey', }; const identityId = 'identityId'; -const mockFetchAuthSession = jest.mocked(Amplify.Auth.fetchAuthSession); const mockPutObject = jest.mocked(putObject); const bucket = 'bucket'; const region = 'region'; const data = 'data'; const dataLength = data.length; -mockFetchAuthSession.mockResolvedValue({ +const mockFetchAuthSession = jest.fn().mockResolvedValue({ credentials, identityId, }); -jest.mocked(Amplify.getConfig).mockReturnValue({ +const mockGetConfig = jest.fn().mockReturnValue({ Storage: { S3: { bucket, @@ -59,6 +48,18 @@ jest.mocked(Amplify.getConfig).mockReturnValue({ }, }, }); +let mockLibraryOptions: Record = {}; +const mockCtx: AmplifyContext = { + get resourcesConfig() { + return mockGetConfig(); + }, + get libraryOptions() { + return mockLibraryOptions; + }, + fetchAuthSession: mockFetchAuthSession, + clearCredentials: jest.fn().mockResolvedValue(undefined), + getTokens: jest.fn().mockResolvedValue(undefined), +}; mockPutObject.mockResolvedValue({ ETag: 'eTag', VersionId: 'versionId', @@ -88,7 +89,7 @@ describe('putObjectJob with key', () => { const useAccelerateEndpoint = true; const job = putObjectJob( - Amplify, + mockCtx, { key: inputKey, data, @@ -148,7 +149,7 @@ describe('putObjectJob with key', () => { .spyOn(CRC32, 'calculateContentCRC32') .mockResolvedValue(undefined as any); - Amplify.libraryOptions = { + mockLibraryOptions = { Storage: { S3: { isObjectLockEnabled: true, @@ -156,7 +157,7 @@ describe('putObjectJob with key', () => { }, }; const job = putObjectJob( - Amplify, + mockCtx, { key: 'key', data: 'data', @@ -175,7 +176,7 @@ describe('putObjectJob with key', () => { const mockRegion = 'region-1'; const job = putObjectJob( - Amplify, + mockCtx, { key: 'key', data, @@ -210,7 +211,7 @@ describe('putObjectJob with key', () => { it('should override bucket in putObject call when bucket as string', async () => { const abortController = new AbortController(); const job = putObjectJob( - Amplify, + mockCtx, { key: 'key', data, @@ -243,7 +244,7 @@ describe('putObjectJob with key', () => { describe('cacheControl passed in option', () => { it('should include CacheControl header', async () => { const job = putObjectJob( - Amplify, + mockCtx, { path: testPath, data, @@ -314,7 +315,7 @@ describe('putObjectJob with path', () => { const useAccelerateEndpoint = true; const job = putObjectJob( - Amplify, + mockCtx, { path: inputPath, data, @@ -374,7 +375,7 @@ describe('putObjectJob with path', () => { .spyOn(CRC32, 'calculateContentCRC32') .mockResolvedValue(undefined as any); - Amplify.libraryOptions = { + mockLibraryOptions = { Storage: { S3: { isObjectLockEnabled: true, @@ -382,7 +383,7 @@ describe('putObjectJob with path', () => { }, }; const job = putObjectJob( - Amplify, + mockCtx, { path: testPath, data, @@ -397,7 +398,7 @@ describe('putObjectJob with path', () => { describe('overwrite prevention', () => { it('should include if-none-match header', async () => { const job = putObjectJob( - Amplify, + mockCtx, { path: testPath, data, @@ -424,7 +425,7 @@ describe('putObjectJob with path', () => { const mockRegion = 'region-1'; const job = putObjectJob( - Amplify, + mockCtx, { path: 'path/', data, @@ -459,7 +460,7 @@ describe('putObjectJob with path', () => { it('should override bucket in putObject call when bucket as string', async () => { const abortController = new AbortController(); const job = putObjectJob( - Amplify, + mockCtx, { path: 'path/', data, @@ -492,7 +493,7 @@ describe('putObjectJob with path', () => { const abortController = new AbortController(); const testData = 'data'; const job = putObjectJob( - Amplify, + mockCtx, { key: 'image.jpg', data: testData, @@ -514,7 +515,7 @@ describe('putObjectJob with path', () => { const abortController = new AbortController(); const file = new File(['content'], 'test.png', { type: 'image/png' }); const job = putObjectJob( - Amplify, + mockCtx, { key: 'test.jpg', // Different extension to test File.type takes precedence data: file, @@ -536,7 +537,7 @@ describe('putObjectJob with path', () => { const abortController = new AbortController(); const testData = 'data'; const job = putObjectJob( - Amplify, + mockCtx, { key: 'image.jpg', data: testData, @@ -561,7 +562,7 @@ describe('putObjectJob with path', () => { describe('cacheControl passed in option', () => { it('should include CacheControl header', async () => { const job = putObjectJob( - Amplify, + mockCtx, { path: testPath, data, @@ -584,7 +585,7 @@ describe('putObjectJob with path', () => { it('should NOT include CacheControl header', async () => { const job = putObjectJob( - Amplify, + mockCtx, { path: testPath, data, diff --git a/packages/storage/__tests__/providers/s3/apis/list.test.ts b/packages/storage/__tests__/providers/s3/apis/list.test.ts index 578b74a971b..017c5fa9d5b 100644 --- a/packages/storage/__tests__/providers/s3/apis/list.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/list.test.ts @@ -1,8 +1,12 @@ // 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 { createMockAmplifyContext } from '../../../testUtils/mockAmplifyContext'; import { ListAllInput, ListAllWithPathInput, @@ -16,7 +20,17 @@ jest.mock('../../../../src/providers/s3/apis/internal/list'); const mockInternalListImpl = jest.mocked(internalListImpl); +const mockCtx = createMockAmplifyContext(); + describe('client-side list', () => { + beforeAll(() => { + setGlobalContext(mockCtx); + }); + + afterAll(() => { + clearGlobalContext(); + }); + beforeEach(() => { jest.clearAllMocks(); }); @@ -28,7 +42,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 +56,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 +66,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 +80,6 @@ describe('client-side list', () => { }, }; expect(list(input)).toEqual(mockInternalResult); - expect(mockInternalListImpl).toBeCalledWith(Amplify, input); + expect(mockInternalListImpl).toBeCalledWith(mockCtx, 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..24413437b15 100644 --- a/packages/storage/__tests__/providers/s3/apis/remove.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/remove.test.ts @@ -1,8 +1,12 @@ // 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 { createMockAmplifyContext } from '../../../testUtils/mockAmplifyContext'; import { RemoveInput, RemoveWithPathInput } from '../../../../src'; import { remove } from '../../../../src/providers/s3/apis'; import { remove as internalRemoveImpl } from '../../../../src/providers/s3/apis/internal/remove'; @@ -11,7 +15,17 @@ jest.mock('../../../../src/providers/s3/apis/internal/remove'); const mockInternalRemoveImpl = jest.mocked(internalRemoveImpl); +const mockCtx = createMockAmplifyContext(); + describe('client-side remove', () => { + beforeAll(() => { + setGlobalContext(mockCtx); + }); + + afterAll(() => { + clearGlobalContext(); + }); + beforeEach(() => { jest.clearAllMocks(); }); @@ -23,7 +37,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 +47,6 @@ describe('client-side remove', () => { path: 'abc', }; expect(remove(input)).toEqual(mockInternalResult); - expect(mockInternalRemoveImpl).toBeCalledWith(Amplify, input); + expect(mockInternalRemoveImpl).toBeCalledWith(mockCtx, 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..fa45651e58f 100644 --- a/packages/storage/__tests__/providers/s3/apis/server/copy.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/server/copy.test.ts @@ -1,28 +1,19 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { getAmplifyServerContext } from '@aws-amplify/core/internals/adapter-core'; - +import { createMockAmplifyContext } from '../../../../testUtils/mockAmplifyContext'; import { CopyInput, CopyWithPathInput } from '../../../../../src'; import { copy } from '../../../../../src/providers/s3/apis/server'; import { copy as internalCopyImpl } from '../../../../../src/providers/s3/apis/internal/copy'; jest.mock('../../../../../src/providers/s3/apis/internal/copy'); -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 mockAmplifyContextSpec = { - token: { value: Symbol('123') }, -}; +const mockCtx = createMockAmplifyContext(); describe('server-side copy', () => { beforeEach(() => { - mockGetAmplifyServerContext.mockReturnValue({ - amplify: mockAmplifyClass, - }); mockInternalCopyImpl.mockReturnValue(mockInternalResult); }); @@ -39,8 +30,8 @@ describe('server-side copy', () => { key: 'destination-key', }, }; - expect(copy(mockAmplifyContextSpec, input)).toEqual(mockInternalResult); - expect(mockInternalCopyImpl).toBeCalledWith(mockAmplifyClass, input); + expect(copy(mockCtx, input)).toEqual(mockInternalResult); + expect(mockInternalCopyImpl).toBeCalledWith(mockCtx, input); }); it('should pass through input with path and output to internal implementation', async () => { @@ -48,7 +39,7 @@ describe('server-side copy', () => { source: { path: 'abc' }, destination: { path: 'abc' }, }; - expect(copy(mockAmplifyContextSpec, input)).toEqual(mockInternalResult); - expect(mockInternalCopyImpl).toBeCalledWith(mockAmplifyClass, input); + expect(copy(mockCtx, input)).toEqual(mockInternalResult); + expect(mockInternalCopyImpl).toBeCalledWith(mockCtx, 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..f46367a8d1f 100644 --- a/packages/storage/__tests__/providers/s3/apis/server/getProperties.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/server/getProperties.test.ts @@ -1,32 +1,23 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { getAmplifyServerContext } from '@aws-amplify/core/internals/adapter-core'; - +import { createMockAmplifyContext } from '../../../../testUtils/mockAmplifyContext'; import { GetPropertiesInput, GetPropertiesWithPathInput, } from '../../../../../src'; import { getProperties } from '../../../../../src/providers/s3/apis/server'; -import { getProperties as internalGetPropertiesImpl } from '../../../../../src/providers/s3/apis/internal/getProperties'; +import { getProperties as internalGetpropertiesImpl } from '../../../../../src/providers/s3/apis/internal/getProperties'; jest.mock('../../../../../src/providers/s3/apis/internal/getProperties'); -jest.mock('@aws-amplify/core/internals/adapter-core'); -const mockInternalGetPropertiesImpl = jest.mocked(internalGetPropertiesImpl); -const mockGetAmplifyServerContext = jest.mocked(getAmplifyServerContext); +const mockInternalGetpropertiesImpl = jest.mocked(internalGetpropertiesImpl); const mockInternalResult = 'RESULT' as any; -const mockAmplifyClass = 'AMPLIFY_CLASS' as any; -const mockAmplifyContextSpec = { - token: { value: Symbol('123') }, -}; +const mockCtx = createMockAmplifyContext(); describe('server-side getProperties', () => { beforeEach(() => { - mockGetAmplifyServerContext.mockReturnValue({ - amplify: mockAmplifyClass, - }); - mockInternalGetPropertiesImpl.mockReturnValue(mockInternalResult); + mockInternalGetpropertiesImpl.mockReturnValue(mockInternalResult); }); afterEach(() => { @@ -37,25 +28,15 @@ describe('server-side getProperties', () => { const input: GetPropertiesInput = { key: 'source-key', }; - expect(getProperties(mockAmplifyContextSpec, input)).toEqual( - mockInternalResult, - ); - expect(mockInternalGetPropertiesImpl).toBeCalledWith( - mockAmplifyClass, - input, - ); + expect(getProperties(mockCtx, input)).toEqual(mockInternalResult); + expect(mockInternalGetpropertiesImpl).toBeCalledWith(mockCtx, input); }); it('should pass through input with path and output to internal implementation', async () => { const input: GetPropertiesWithPathInput = { path: 'abc', }; - expect(getProperties(mockAmplifyContextSpec, input)).toEqual( - mockInternalResult, - ); - expect(mockInternalGetPropertiesImpl).toBeCalledWith( - mockAmplifyClass, - input, - ); + expect(getProperties(mockCtx, input)).toEqual(mockInternalResult); + expect(mockInternalGetpropertiesImpl).toBeCalledWith(mockCtx, 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..d0eb20b1167 100644 --- a/packages/storage/__tests__/providers/s3/apis/server/getUrl.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/server/getUrl.test.ts @@ -1,26 +1,20 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { getAmplifyServerContext } from '@aws-amplify/core/internals/adapter-core'; - +import { createMockAmplifyContext } from '../../../../testUtils/mockAmplifyContext'; import { GetUrlInput, GetUrlWithPathInput } from '../../../../../src'; import { getUrl } from '../../../../../src/providers/s3/apis/server'; -import { getUrl as internalGetUrlImpl } from '../../../../../src/providers/s3/apis/internal/getUrl'; +import { getUrl as internalGeturlImpl } from '../../../../../src/providers/s3/apis/internal/getUrl'; jest.mock('../../../../../src/providers/s3/apis/internal/getUrl'); -jest.mock('@aws-amplify/core/internals/adapter-core'); -const mockInternalGetUrlImpl = jest.mocked(internalGetUrlImpl); -const mockGetAmplifyServerContext = jest.mocked(getAmplifyServerContext); +const mockInternalGeturlImpl = jest.mocked(internalGeturlImpl); const mockInternalResult = 'RESULT' as any; -const mockAmplifyClass = 'AMPLIFY_CLASS' as any; +const mockCtx = createMockAmplifyContext(); describe('server-side getUrl', () => { beforeEach(() => { - mockGetAmplifyServerContext.mockReturnValue({ - amplify: mockAmplifyClass, - }); - mockInternalGetUrlImpl.mockReturnValue(mockInternalResult); + mockInternalGeturlImpl.mockReturnValue(mockInternalResult); }); afterEach(() => { @@ -31,29 +25,15 @@ describe('server-side getUrl', () => { const input: GetUrlInput = { key: 'source-key', }; - expect( - getUrl( - { - token: { value: Symbol('123') }, - }, - input, - ), - ).toEqual(mockInternalResult); - expect(mockInternalGetUrlImpl).toBeCalledWith(mockAmplifyClass, input); + expect(getUrl(mockCtx, input)).toEqual(mockInternalResult); + expect(mockInternalGeturlImpl).toBeCalledWith(mockCtx, input); }); it('should pass through input with path and output to internal implementation', async () => { const input: GetUrlWithPathInput = { path: 'abc', }; - expect( - getUrl( - { - token: { value: Symbol('123') }, - }, - input, - ), - ).toEqual(mockInternalResult); - expect(mockInternalGetUrlImpl).toBeCalledWith(mockAmplifyClass, input); + expect(getUrl(mockCtx, input)).toEqual(mockInternalResult); + expect(mockInternalGeturlImpl).toBeCalledWith(mockCtx, 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..74901e0a386 100644 --- a/packages/storage/__tests__/providers/s3/apis/server/list.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/server/list.test.ts @@ -1,8 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { getAmplifyServerContext } from '@aws-amplify/core/internals/adapter-core'; - +import { createMockAmplifyContext } from '../../../../testUtils/mockAmplifyContext'; import { ListAllInput, ListAllWithPathInput, @@ -13,21 +12,13 @@ import { list } from '../../../../../src/providers/s3/apis/server'; import { list as internalListImpl } from '../../../../../src/providers/s3/apis/internal/list'; jest.mock('../../../../../src/providers/s3/apis/internal/list'); -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 mockAmplifyContextSpec = { - token: { value: Symbol('123') }, -}; +const mockCtx = createMockAmplifyContext(); describe('server-side list', () => { beforeEach(() => { - mockGetAmplifyServerContext.mockReturnValue({ - amplify: mockAmplifyClass, - }); mockInternalListImpl.mockReturnValue(mockInternalResult); }); @@ -39,8 +30,8 @@ describe('server-side list', () => { const input: ListAllInput = { prefix: 'source-key', }; - expect(list(mockAmplifyContextSpec, input)).toEqual(mockInternalResult); - expect(mockInternalListImpl).toBeCalledWith(mockAmplifyClass, input); + expect(list(mockCtx, input)).toEqual(mockInternalResult); + expect(mockInternalListImpl).toBeCalledWith(mockCtx, input); }); it('should pass through list paginate input with key and output to internal implementation', async () => { @@ -51,16 +42,16 @@ describe('server-side list', () => { pageSize: 10, }, }; - expect(list(mockAmplifyContextSpec, input)).toEqual(mockInternalResult); - expect(mockInternalListImpl).toBeCalledWith(mockAmplifyClass, input); + expect(list(mockCtx, input)).toEqual(mockInternalResult); + expect(mockInternalListImpl).toBeCalledWith(mockCtx, input); }); it('should pass through list all input with path and output to internal implementation', async () => { const input: ListAllWithPathInput = { path: 'abc', }; - expect(list(mockAmplifyContextSpec, input)).toEqual(mockInternalResult); - expect(mockInternalListImpl).toBeCalledWith(mockAmplifyClass, input); + expect(list(mockCtx, input)).toEqual(mockInternalResult); + expect(mockInternalListImpl).toBeCalledWith(mockCtx, input); }); it('should pass through list paginate input with path and output to internal implementation', async () => { @@ -71,7 +62,7 @@ describe('server-side list', () => { pageSize: 10, }, }; - expect(list(mockAmplifyContextSpec, input)).toEqual(mockInternalResult); - expect(mockInternalListImpl).toBeCalledWith(mockAmplifyClass, input); + expect(list(mockCtx, input)).toEqual(mockInternalResult); + expect(mockInternalListImpl).toBeCalledWith(mockCtx, 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..b3c1573f4f6 100644 --- a/packages/storage/__tests__/providers/s3/apis/server/remove.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/server/remove.test.ts @@ -1,28 +1,19 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { getAmplifyServerContext } from '@aws-amplify/core/internals/adapter-core'; - +import { createMockAmplifyContext } from '../../../../testUtils/mockAmplifyContext'; import { RemoveInput, RemoveWithPathInput } from '../../../../../src'; import { remove } from '../../../../../src/providers/s3/apis/server'; import { remove as internalRemoveImpl } from '../../../../../src/providers/s3/apis/internal/remove'; jest.mock('../../../../../src/providers/s3/apis/internal/remove'); -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 mockAmplifyContextSpec = { - token: { value: Symbol('123') }, -}; +const mockCtx = createMockAmplifyContext(); describe('server-side remove', () => { beforeEach(() => { - mockGetAmplifyServerContext.mockReturnValue({ - amplify: mockAmplifyClass, - }); mockInternalRemoveImpl.mockReturnValue(mockInternalResult); }); @@ -34,15 +25,15 @@ describe('server-side remove', () => { const input: RemoveInput = { key: 'source-key', }; - expect(remove(mockAmplifyContextSpec, input)).toEqual(mockInternalResult); - expect(mockInternalRemoveImpl).toBeCalledWith(mockAmplifyClass, input); + expect(remove(mockCtx, input)).toEqual(mockInternalResult); + expect(mockInternalRemoveImpl).toBeCalledWith(mockCtx, input); }); it('should pass through input with path and output to internal implementation', async () => { const input: RemoveWithPathInput = { path: 'abc', }; - expect(remove(mockAmplifyContextSpec, input)).toEqual(mockInternalResult); - expect(mockInternalRemoveImpl).toBeCalledWith(mockAmplifyClass, input); + expect(remove(mockCtx, input)).toEqual(mockInternalResult); + expect(mockInternalRemoveImpl).toBeCalledWith(mockCtx, input); }); }); diff --git a/packages/storage/__tests__/providers/s3/apis/server/uploadData.test.ts b/packages/storage/__tests__/providers/s3/apis/server/uploadData.test.ts deleted file mode 100644 index 5fb17b37a22..00000000000 --- a/packages/storage/__tests__/providers/s3/apis/server/uploadData.test.ts +++ /dev/null @@ -1,121 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -import { getAmplifyServerContext } from '@aws-amplify/core/internals/adapter-core'; - -import { - UploadDataInput, - UploadDataServerOutput, - UploadDataServerWithPathOutput, - UploadDataWithPathInput, -} from '../../../../../src'; -import { uploadData } from '../../../../../src/providers/s3/apis/server'; -import { uploadData as internalUploadDataImpl } from '../../../../../src/providers/s3/apis/internal/uploadData'; - -jest.mock('../../../../../src/providers/s3/apis/internal/uploadData'); -jest.mock('@aws-amplify/core/internals/adapter-core'); - -const mockInternalUploadDataImpl = jest.mocked(internalUploadDataImpl); -const mockGetAmplifyServerContext = jest.mocked(getAmplifyServerContext); -const mockInternalResult: any = { - cancel: jest.fn(), - pause: jest.fn(), - resume: jest.fn(), - state: 'IN_PROGRESS', - result: Promise.resolve({ path: 'x' }), -}; -const mockAmplifyClass = 'AMPLIFY_CLASS' as any; -const mockAmplifyContextSpec = { - token: { value: Symbol('123') }, -}; - -describe('server-side uploadData', () => { - beforeEach(() => { - mockGetAmplifyServerContext.mockReturnValue({ - amplify: mockAmplifyClass, - } as any); - mockInternalUploadDataImpl.mockReturnValue(mockInternalResult); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - it('should pass through input with path and return output from internal implementation', () => { - const input: UploadDataWithPathInput = { - path: 'path/to/object', - data: 'data', - options: { - contentType: 'text/plain', - }, - }; - expect(uploadData(mockAmplifyContextSpec as any, input)).toEqual( - mockInternalResult, - ); - expect(mockInternalUploadDataImpl).toBeCalledWith(mockAmplifyClass, input); - }); - - it('should pass through input with key and return output from internal implementation', () => { - const input: UploadDataInput = { - key: 'some-key', - data: 'data', - options: { - accessLevel: 'protected' as const, - }, - }; - expect(uploadData(mockAmplifyContextSpec as any, input)).toEqual( - mockInternalResult, - ); - expect(mockInternalUploadDataImpl).toBeCalledWith(mockAmplifyClass, input); - }); - - it('should NOT inject resumableUploadsCache (server-side does not support pause/resume)', () => { - const input: UploadDataWithPathInput = { - path: 'path/to/object', - data: 'data', - }; - uploadData(mockAmplifyContextSpec as any, input); - const passedInput = mockInternalUploadDataImpl.mock.calls[0][1] as any; - expect(passedInput.options?.resumableUploadsCache).toBeUndefined(); - }); - - it('should use server context amplify instance, not global Amplify', () => { - const input: UploadDataWithPathInput = { - path: 'path/to/object', - data: 'data', - }; - 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]).toBe(mockAmplifyClass); - }); - - it('should return a task type that does NOT expose pause/resume at the type level', () => { - const withPathInput: UploadDataWithPathInput = { - path: 'path/to/object', - data: 'data', - }; - const withKeyInput: UploadDataInput = { key: 'k', data: 'd' }; - - // Compile-time type assertions: the returned types should be the server - // (non-pausable) outputs. If uploadData returned UploadDataOutput / - // UploadDataWithPathOutput instead, these assignments would still - // compile because UploadTask is a supertype — so we also rely on the - // commented-out pause/resume lines below, which MUST fail to compile. - const pathTask: UploadDataServerWithPathOutput = uploadData( - mockAmplifyContextSpec as any, - withPathInput, - ); - const keyTask: UploadDataServerOutput = uploadData( - mockAmplifyContextSpec as any, - withKeyInput, - ); - - // pause/resume are intentionally absent from the type and would cause a - // TS2339 error if uncommented: - // pathTask.pause(); - // pathTask.resume(); - expect(typeof pathTask.cancel).toBe('function'); - expect(typeof keyTask.cancel).toBe('function'); - }); -}); diff --git a/packages/storage/__tests__/providers/s3/apis/uploadData.test.ts b/packages/storage/__tests__/providers/s3/apis/uploadData.test.ts index 4bd33fd523a..5b6fe114aef 100644 --- a/packages/storage/__tests__/providers/s3/apis/uploadData.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/uploadData.test.ts @@ -1,8 +1,13 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { Amplify, defaultStorage } from '@aws-amplify/core'; +import { + clearGlobalContext, + setGlobalContext, +} from '@aws-amplify/core/internals/utils'; +import { defaultStorage } from '@aws-amplify/core'; +import { createMockAmplifyContext } from '../../../testUtils/mockAmplifyContext'; import { uploadData } from '../../../../src/providers/s3/apis'; import { uploadData as internalUploadDataImpl } from '../../../../src/providers/s3/apis/internal/uploadData'; @@ -10,7 +15,17 @@ jest.mock('../../../../src/providers/s3/apis/internal/uploadData'); const mockInternalUploadDataImpl = jest.mocked(internalUploadDataImpl); +const mockCtx = createMockAmplifyContext(); + describe('client-side uploadData', () => { + beforeAll(() => { + setGlobalContext(mockCtx); + }); + + afterAll(() => { + clearGlobalContext(); + }); + beforeEach(() => { jest.clearAllMocks(); }); @@ -26,7 +41,7 @@ describe('client-side uploadData', () => { }, }; expect(uploadData(input)).toEqual(mockInternalResult); - expect(mockInternalUploadDataImpl).toBeCalledWith(Amplify, { + expect(mockInternalUploadDataImpl).toBeCalledWith(mockCtx, { ...input, options: { ...input.options, @@ -46,7 +61,7 @@ describe('client-side uploadData', () => { }, }; expect(uploadData(input)).toEqual(mockInternalResult); - expect(mockInternalUploadDataImpl).toBeCalledWith(Amplify, { + expect(mockInternalUploadDataImpl).toBeCalledWith(mockCtx, { ...input, options: { ...input.options, 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..3e244aa2181 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,10 @@ 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); const mockDefaultResolvePrefix = resolvePrefix as jest.Mock; -const mockFetchAuthSession = Amplify.Auth.fetchAuthSession as jest.Mock; +const mockFetchAuthSession = jest.fn(); const bucket = 'bucket'; const region = 'region'; @@ -41,29 +31,44 @@ const credentials = { }; const targetIdentityId = 'targetIdentityId'; +const defaultStorageConfig = { + Storage: { + S3: { + bucket, + region, + buckets: { 'bucket-1': { bucketName: bucket, region } }, + }, + }, +}; + +function createCtx( + resourcesConfig: Record = defaultStorageConfig, + libraryOptions: Record = {}, +): AmplifyContext { + return { + resourcesConfig, + libraryOptions, + fetchAuthSession: mockFetchAuthSession, + clearCredentials: jest.fn(), + getTokens: jest.fn(), + }; +} + describe('resolveS3ConfigAndInput', () => { + let mockCtx: AmplifyContext; + beforeEach(() => { jest.clearAllMocks(); - Amplify.libraryOptions = {}; - }); - mockFetchAuthSession.mockResolvedValue({ - credentials, - identityId: targetIdentityId, - }); - - mockGetConfig.mockReturnValue({ - Storage: { - S3: { - bucket, - region, - buckets: { 'bucket-1': { bucketName: bucket, region } }, - }, - }, + mockCtx = createCtx(); + mockFetchAuthSession.mockResolvedValue({ + credentials, + identityId: targetIdentityId, + }); }); it('should call fetchAuthSession for credentials and identityId', async () => { expect.assertions(1); - await resolveS3ConfigAndInput(Amplify, {}); + await resolveS3ConfigAndInput(mockCtx, {}); expect(mockFetchAuthSession).toHaveBeenCalled(); }); @@ -74,7 +79,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,52 +93,50 @@ 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); - expect(mockGetConfig).toHaveBeenCalled(); }); it('should throw if bucket is not available', async () => { - mockGetConfig.mockReturnValueOnce({ + mockCtx = createCtx({ Storage: { S3: { region, }, }, }); - 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(); }); it('should throw if region is not available', async () => { - mockGetConfig.mockReturnValueOnce({ + mockCtx = createCtx({ Storage: { S3: { bucket, }, }, }); - await expect(resolveS3ConfigAndInput(Amplify, {})).rejects.toMatchObject( + await expect(resolveS3ConfigAndInput(mockCtx, {})).rejects.toMatchObject( validationErrorMap[StorageValidationErrorCode.NoRegion], ); }); it('should set customEndpoint and forcePathStyle to true if dangerouslyConnectToHttpEndpointForTesting is set from S3 config', async () => { - mockGetConfig.mockReturnValueOnce({ + mockCtx = createCtx({ Storage: { S3: { bucket, @@ -142,41 +145,40 @@ describe('resolveS3ConfigAndInput', () => { }, }, }); - const { s3Config } = await resolveS3ConfigAndInput(Amplify, {}); - expect(s3Config.customEndpoint).toEqual('http://localhost:20005'); + const { s3Config } = await resolveS3ConfigAndInput(mockCtx, {}); + expect(s3Config.customEndpoint).toEqual('true'); expect(s3Config.forcePathStyle).toEqual(true); - expect(mockGetConfig).toHaveBeenCalled(); }); it('should resolve isObjectLockEnabled from S3 library options', async () => { - Amplify.libraryOptions = { + mockCtx = createCtx(defaultStorageConfig, { 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 = { + mockCtx = createCtx(defaultStorageConfig, { 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 +186,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 +198,14 @@ describe('resolveS3ConfigAndInput', () => { it('should resolve prefix with default access level from S3 library options', async () => { mockDefaultResolvePrefix.mockResolvedValueOnce('prefix'); - Amplify.libraryOptions = { + mockCtx = createCtx(defaultStorageConfig, { Storage: { S3: { defaultAccessLevel: 'someLevel' as any, }, }, - }; - const { keyPrefix } = await resolveS3ConfigAndInput(Amplify, {}); + }); + const { keyPrefix } = await resolveS3ConfigAndInput(mockCtx, {}); expect(mockDefaultResolvePrefix).toHaveBeenCalledWith({ accessLevel: 'someLevel', targetIdentityId, @@ -213,7 +215,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, @@ -226,7 +228,7 @@ describe('resolveS3ConfigAndInput', () => { .fn() .mockReturnValue({ credentials }); it('should resolve credentials without Amplify singleton', async () => { - mockGetConfig.mockReturnValue({ + mockCtx = createCtx({ Storage: { S3: { bucket, @@ -234,7 +236,7 @@ describe('resolveS3ConfigAndInput', () => { }, }, }); - const { s3Config } = await resolveS3ConfigAndInput(Amplify, { + const { s3Config } = await resolveS3ConfigAndInput(mockCtx, { options: { locationCredentialsProvider: mockLocationCredentialsProvider, }, @@ -252,7 +254,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 +293,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,18 +321,17 @@ describe('resolveS3ConfigAndInput', () => { const { bucket: resolvedBucket, s3Config: { region: resolvedRegion }, - } = await resolveS3ConfigAndInput(Amplify, { + } = await resolveS3ConfigAndInput(mockCtx, { options: { bucket: bucketInfo }, }); - expect(mockGetConfig).toHaveBeenCalled(); expect(resolvedBucket).toEqual(bucketInfo.bucketName); expect(resolvedRegion).toEqual(bucketInfo.region); }); 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__/providers/s3/utils/client/S3/cases/getObject.ts b/packages/storage/__tests__/providers/s3/utils/client/S3/cases/getObject.ts index f0590a0d109..0e91791d23d 100644 --- a/packages/storage/__tests__/providers/s3/utils/client/S3/cases/getObject.ts +++ b/packages/storage/__tests__/providers/s3/utils/client/S3/cases/getObject.ts @@ -241,18 +241,14 @@ const getObjectErrorCaseInvalidCustomEndpoint: ApiFunctionalTestCase< getObject, { ...defaultConfig, - customEndpoint: 'http://custom.endpoint.com', + customEndpoint: 'ftp://custom.endpoint.com', forcePathStyle: true, }, { Bucket: 'bucket', Key: 'key', }, - expect.objectContaining({ - url: expect.objectContaining({ - href: 'https://custom.endpoint.com/bucket/key?x-id=GetObject', - }), - }), + expect.objectContaining({}), { status: 400, headers: DEFAULT_RESPONSE_HEADERS, diff --git a/packages/storage/__tests__/testUtils/mockAmplifyContext.ts b/packages/storage/__tests__/testUtils/mockAmplifyContext.ts new file mode 100644 index 00000000000..7838821389a --- /dev/null +++ b/packages/storage/__tests__/testUtils/mockAmplifyContext.ts @@ -0,0 +1,32 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { AmplifyContext, ResourcesConfig } from '@aws-amplify/core'; + +const AMPLIFY_CONTEXT_BRAND = Symbol.for('amplify.context'); + +/** + * Creates a branded mock AmplifyContext for testing. + * The brand ensures `isAmplifyContext()` returns true, + * which is required for `resolveCtxArgs()` to recognize it. + */ +export function createMockAmplifyContext( + resourcesConfig: ResourcesConfig = {}, +): AmplifyContext { + const ctx: AmplifyContext = { + 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, + configurable: false, + writable: false, + }); + + return ctx; +} diff --git a/packages/storage/package.json b/packages/storage/package.json index a4a2ba9fdf7..3b33f7fe0f6 100644 --- a/packages/storage/package.json +++ b/packages/storage/package.json @@ -42,32 +42,24 @@ ], "internals": [ "./dist/esm/internals/index.d.ts" - ], - "server": [ - "./dist/esm/server.d.ts" - ], - "s3/server": [ - "./dist/esm/providers/s3/server.d.ts" ] } }, "repository": { "type": "git", - "url": "https://github.com/aws-amplify/amplify-js.git", - "directory": "packages/storage" + "url": "https://github.com/aws-amplify/amplify-js.git" }, "author": "Amazon Web Services", "license": "Apache-2.0", "bugs": { "url": "https://github.com/aws/aws-amplify/issues" }, - "homepage": "https://docs.amplify.aws/", + "homepage": "https://aws-amplify.github.io/", "files": [ "dist/cjs", "dist/esm", "internals", "src", - "server", "s3" ], "dependencies": { @@ -90,26 +82,31 @@ "import": "./dist/esm/internals/index.mjs", "require": "./dist/cjs/internals/index.js" }, - "./server": { - "types": "./dist/esm/server.d.ts", - "import": "./dist/esm/server.mjs", - "require": "./dist/cjs/server.js" - }, "./s3": { "react-native": "./dist/cjs/providers/s3/index.js", "types": "./dist/esm/providers/s3/index.d.ts", "import": "./dist/esm/providers/s3/index.mjs", "require": "./dist/cjs/providers/s3/index.js" }, + "./server": { + "types": "./dist/esm/server.d.ts", + "import": "./dist/esm/server.mjs", + "require": "./dist/cjs/server.js" + }, "./s3/server": { "types": "./dist/esm/providers/s3/server.d.ts", "import": "./dist/esm/providers/s3/server.mjs", "require": "./dist/cjs/providers/s3/server.js" }, + "./s3/apis/server": { + "types": "./dist/esm/providers/s3/apis/server/index.d.ts", + "import": "./dist/esm/providers/s3/apis/server/index.mjs", + "require": "./dist/cjs/providers/s3/apis/server/index.js" + }, "./package.json": "./package.json" }, "peerDependencies": { - "@aws-amplify/core": "^6.16.2" + "@aws-amplify/core": "^6.16.3" }, "devDependencies": { "@aws-amplify/core": "6.16.3", diff --git a/packages/storage/src/index.ts b/packages/storage/src/index.ts index 436ab2951cd..31c1501e81b 100644 --- a/packages/storage/src/index.ts +++ b/packages/storage/src/index.ts @@ -17,6 +17,7 @@ export { DownloadDataInput, DownloadDataWithPathInput, RemoveInput, + RemoveOperation, RemoveWithPathInput, ListAllInput, ListAllWithPathInput, 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..d60f1cbf73f 100644 --- a/packages/storage/src/internals/apis/listPaths/listPaths.ts +++ b/packages/storage/src/internals/apis/listPaths/listPaths.ts @@ -1,22 +1,24 @@ // 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 { 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 { buckets } = ctx.resourcesConfig.Storage!.S3!; + const { groups } = ctx.resourcesConfig.Auth!.Cognito; 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 164423d2f34..3f28f48afa2 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 { UploadDataInput } from '../types/inputs'; import { UploadDataOutput } from '../types/outputs'; @@ -10,10 +10,10 @@ 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, { + return uploadDataInternal(ctx, { path, data, options: { diff --git a/packages/storage/src/providers/s3/apis/copy.ts b/packages/storage/src/providers/s3/apis/copy.ts index 763ff45829b..caeb8694200 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, @@ -12,31 +13,26 @@ import { import { copy as copyInternal } from './internal/copy'; -/** - * Copy an object from a source to a destination object within the same bucket. - * - * @param input - The `CopyWithPathInput` object. - * @returns Output containing the destination object path. - * @throws service: `S3Exception` - Thrown when checking for existence of the object - * @throws validation: `StorageValidationErrorCode` - Thrown when - * source or destination path is not defined. - */ +// --- Overloads without ctx --- + export function copy(input: CopyWithPathInput): Promise; -/** - * @deprecated The `key` and `accessLevel` parameters are deprecated and may be removed in the next major version. - * Please use {@link https://docs.amplify.aws/react/build-a-backend/storage/copy | path} instead. - * - * 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 input - The `CopyInput` object. - * @returns Output containing the destination object key. - * @throws service: `S3Exception` - Thrown when checking for existence of the object - * @throws validation: `StorageValidationErrorCode` - Thrown when - * source or destination key is not defined. - */ export function copy(input: CopyInput): Promise; -export function copy(input: CopyInput | CopyWithPathInput) { - return copyInternal(Amplify, input); +// --- Overloads with explicit ctx --- + +export function copy( + ctx: AmplifyContext, + input: CopyWithPathInput, +): Promise; +export function copy( + ctx: AmplifyContext, + input: CopyInput, +): Promise; + +// --- Implementation --- + +export function copy(...args: any[]) { + const [ctx, input] = resolveCtxArgs(args); + + 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..c24de69d1fa 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, @@ -10,75 +13,30 @@ import { import { downloadData as downloadDataInternal } from './internal/downloadData'; -/** - * Download S3 object data to memory - * - * @param input - The `DownloadDataWithPathInput` object. - * @returns A cancelable task exposing result promise from `result` property. - * @throws service: `S3Exception` - thrown when checking for existence of the object - * @throws validation: `StorageValidationErrorCode` - Validation errors - * - * @example - * ```ts - * // Download a file from s3 bucket - * const { body, eTag } = await downloadData({ path, options: { - * onProgress, // Optional progress callback. - * } }).result; - * ``` - * @example - * ```ts - * // Cancel a task - * const downloadTask = downloadData({ path }); - * //... - * downloadTask.cancel(); - * try { - * await downloadTask.result; - * } catch (error) { - * if(isCancelError(error)) { - * // Handle error thrown by task cancelation. - * } - * } - *``` - */ +// --- Overloads without ctx --- + export function downloadData( input: DownloadDataWithPathInput, ): DownloadDataWithPathOutput; -/** - * @deprecated The `key` and `accessLevel` parameters are deprecated and may be removed in the next major version. - * Please use {@link https://docs.amplify.aws/react/build-a-backend/storage/download/#downloaddata | path} instead. - * - * Download S3 object data to memory - * - * @param input - The `DownloadDataInput` object. - * @returns A cancelable task exposing result promise from `result` property. - * @throws service: `S3Exception` - thrown when checking for existence of the object - * @throws validation: `StorageValidationErrorCode` - Validation errors - * - * @example - * ```ts - * // Download a file from s3 bucket - * const { body, eTag } = await downloadData({ key, options: { - * onProgress, // Optional progress callback. - * } }).result; - * ``` - * @example - * ```ts - * // Cancel a task - * const downloadTask = downloadData({ key }); - * //... - * downloadTask.cancel(); - * try { - * await downloadTask.result; - * } catch (error) { - * if(isCancelError(error)) { - * // Handle error thrown by task cancelation. - * } - * } - *``` - */ export function downloadData(input: DownloadDataInput): DownloadDataOutput; + +// --- Overloads with explicit ctx --- + +export function downloadData( + ctx: AmplifyContext, + input: DownloadDataWithPathInput, +): DownloadDataWithPathOutput; export function downloadData( - input: DownloadDataInput | DownloadDataWithPathInput, -) { - return downloadDataInternal(input); + ctx: AmplifyContext, + input: DownloadDataInput, +): DownloadDataOutput; + +// --- Implementation --- + +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..12e1bf3c934 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, @@ -12,36 +13,32 @@ import { import { getProperties as getPropertiesInternal } from './internal/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 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. - */ +// --- Overloads without ctx --- + export function getProperties( input: GetPropertiesWithPathInput, ): Promise; -/** - * @deprecated The `key` and `accessLevel` parameters are deprecated and may be removed in the next major version. - * Please use {@link https://docs.amplify.aws/javascript/build-a-backend/storage/get-properties/ | path} instead. - * - * Gets the properties of a file. The properties include S3 system metadata and - * the user metadata that was provided when uploading the file. - * - * @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( input: GetPropertiesInput, ): Promise; +// --- Overloads with explicit ctx --- + +export function getProperties( + ctx: AmplifyContext, + input: GetPropertiesWithPathInput, +): Promise; export function getProperties( - input: GetPropertiesInput | GetPropertiesWithPathInput, -) { - return getPropertiesInternal(Amplify, input); + ctx: AmplifyContext, + input: GetPropertiesInput, +): Promise; + +// --- Implementation --- + +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..4537ea57a22 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, @@ -12,46 +13,28 @@ import { import { getUrl as getUrlInternal } from './internal/getUrl'; -/** - * Get a temporary presigned URL to download or upload the specified S3 object. - * The presigned URL expires when the associated role used to sign the request expires or - * the option `expiresIn` is reached. The `expiresAt` property in the output object indicates when the URL MAY expire. - * - * By default, it will not validate the object that exists in S3. If you set the `options.validateObjectExistence` - * 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 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 - * @throws validation: `StorageValidationErrorCode` - Validation errors - * thrown either username or key are not defined. - * - */ +// --- Overloads without ctx --- + export function getUrl( input: GetUrlWithPathInput, ): Promise; -/** - * @deprecated The `key` and `accessLevel` parameters are deprecated and may be removed in the next major version. - * Please use {@link https://docs.amplify.aws/javascript/build-a-backend/storage/download/#generate-a-download-url | path} instead. - * - * 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 - * the option `expiresIn` is reached. The `expiresAt` property in the output object indicates when the URL MAY expire. - * - * By default, it will not validate the object that exists in S3. If you set the `options.validateObjectExistence` - * 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 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 - * @throws validation: `StorageValidationErrorCode` - Validation errors - * thrown either username or key are not defined. - * - */ export function getUrl(input: GetUrlInput): Promise; -export function getUrl(input: GetUrlInput | GetUrlWithPathInput) { - return getUrlInternal(Amplify, input); +// --- Overloads with explicit ctx --- + +export function getUrl( + ctx: AmplifyContext, + input: GetUrlWithPathInput, +): Promise; +export function getUrl( + ctx: AmplifyContext, + input: GetUrlInput, +): Promise; + +// --- Implementation --- + +export function getUrl(...args: any[]) { + const [ctx, input] = resolveCtxArgs(args); + + 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/index.ts b/packages/storage/src/providers/s3/apis/internal/uploadData/index.ts index c8e2d7c8b50..df40a69bf00 100644 --- a/packages/storage/src/providers/s3/apis/internal/uploadData/index.ts +++ b/packages/storage/src/providers/s3/apis/internal/uploadData/index.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 { createUploadTask } from '../../../utils'; import { assertValidationError } from '../../../../../errors/utils/assertValidationError'; @@ -16,14 +16,12 @@ import { } from './multipart'; export const uploadData = ( - amplify: AmplifyClassV6, + ctx: AmplifyContext, input: SinglePartUploadDataInput | MultipartUploadDataInput, ) => { const { data } = input; const dataByteLength = byteLength(data); - // Using InvalidUploadSource error code because the input data must NOT be any - // of permitted Blob, string, ArrayBuffer(View) if byteLength could not be determined. assertValidationError( dataByteLength !== undefined, StorageValidationErrorCode.InvalidUploadSource, @@ -34,20 +32,18 @@ export const uploadData = ( ); if (dataByteLength <= DEFAULT_PART_SIZE) { - // Single part upload const abortController = new AbortController(); return createUploadTask({ isMultipartUpload: false, - job: putObjectJob(amplify, input, abortController.signal, dataByteLength), + job: putObjectJob(ctx, input, abortController.signal, dataByteLength), onCancel: (message?: string) => { abortController.abort(message); }, }); } else { - // Multipart upload const { multipartUploadJob, onPause, onResume, onCancel } = - getMultipartUploadHandlers(amplify, input, dataByteLength); + getMultipartUploadHandlers(ctx, input, dataByteLength); return createUploadTask({ isMultipartUpload: true, 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 3a25b84d071..1ac686546c6 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'; @@ -86,7 +86,7 @@ export type MultipartUploadDataInput = WithResumableCacheConfig< * @internal */ export const getMultipartUploadHandlers = ( - amplify: AmplifyClassV6, + ctx: AmplifyContext, uploadDataInput: MultipartUploadDataInput, size: number, ) => { @@ -120,7 +120,7 @@ export const getMultipartUploadHandlers = ( const startUpload = async (): Promise => { const { options: uploadDataOptions, data } = uploadDataInput; const resolvedS3Options = await resolveS3ConfigAndInput( - amplify, + ctx, uploadDataInput, ); @@ -156,7 +156,7 @@ export const getMultipartUploadHandlers = ( resolvedKeyPrefix = resolvedS3Options.keyPrefix; finalKey = resolvedKeyPrefix + objectKey; - resolvedAccessLevel = resolveAccessLevel(amplify, accessLevel); + resolvedAccessLevel = resolveAccessLevel(ctx, accessLevel); } const optionsHash = await calculateContentCRC32( @@ -366,11 +366,11 @@ export const getMultipartUploadHandlers = ( }; const resolveAccessLevel = ( - amplify: AmplifyClassV6, + ctx: AmplifyContext, accessLevel?: StorageAccessLevel, ) => accessLevel ?? - amplify.libraryOptions.Storage?.S3?.defaultAccessLevel ?? + ctx.libraryOptions.Storage?.S3?.defaultAccessLevel ?? DEFAULT_ACCESS_LEVEL; const validateCompletedParts = (completedParts: Part[], size: number) => { diff --git a/packages/storage/src/providers/s3/apis/internal/uploadData/putObjectJob.ts b/packages/storage/src/providers/s3/apis/internal/uploadData/putObjectJob.ts index 02aef380a0a..43c2a41f77a 100644 --- a/packages/storage/src/providers/s3/apis/internal/uploadData/putObjectJob.ts +++ b/packages/storage/src/providers/s3/apis/internal/uploadData/putObjectJob.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 { UploadDataInput } from '../../../types'; @@ -42,7 +42,7 @@ export type SinglePartUploadDataInput = */ export const putObjectJob = ( - amplify: AmplifyClassV6, + ctx: AmplifyContext, uploadDataInput: SinglePartUploadDataInput, abortSignal: AbortSignal, totalLength: number, @@ -50,7 +50,7 @@ export const putObjectJob = async (): Promise => { const { options: uploadDataOptions, data } = uploadDataInput; const { bucket, keyPrefix, s3Config, isObjectLockEnabled, identityId } = - await resolveS3ConfigAndInput(amplify, uploadDataInput); + await resolveS3ConfigAndInput(ctx, uploadDataInput); const { inputType, objectKey } = validateStorageOperationInput( uploadDataInput, identityId, diff --git a/packages/storage/src/providers/s3/apis/list.ts b/packages/storage/src/providers/s3/apis/list.ts index cd58dbdaacd..293f408ee70 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, @@ -15,55 +16,46 @@ import { import { list as listInternal } from './internal/list'; -/** - * 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. - * @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 - */ +// --- Overloads without ctx (uses global context) --- + export function list( 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. - * @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( input: ListAllWithPathInput, ): Promise; -/** - * @deprecated The `prefix` and `accessLevel` parameters are deprecated and may be removed in the next major version. - * Please use {@link https://docs.amplify.aws/react/build-a-backend/storage/list | path} instead. - * List files in pages with the given `prefix`. - * `pageSize` is defaulted to 1000. Additionally, the result will include a `nextToken` if there are more items to retrieve. - * @param input - The `ListPaginateInput` object. - * @returns A list of objects with key 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(input?: ListPaginateInput): Promise; -/** - * @deprecated The `prefix` and `accessLevel` parameters are deprecated and may be removed in the next major version. - * Please use {@link https://docs.amplify.aws/react/build-a-backend/storage/list | path} instead. - * List all files from S3 for a given `prefix`. You can set `listAll` to true in `options` to get all the files from S3. - * @param input - The `ListAllInput` object. - * @returns A list of all objects with key 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(input?: ListAllInput): Promise; +// --- Overloads with explicit ctx --- + +export function list( + ctx: AmplifyContext, + input: ListPaginateWithPathInput, +): Promise; +export function list( + ctx: AmplifyContext, + input: ListAllWithPathInput, +): Promise; +export function list( + ctx: AmplifyContext, + input?: ListPaginateInput, +): Promise; export function list( - input?: + ctx: AmplifyContext, + input?: ListAllInput, +): Promise; + +// --- Implementation --- + +export function list(...args: any[]) { + const [ctx, input] = resolveCtxArgs< | ListAllInput | ListPaginateInput | ListAllWithPathInput - | ListPaginateWithPathInput, -) { - return listInternal(Amplify, input ?? {}); + | ListPaginateWithPathInput + | undefined + >(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..1b2b3d3d68f 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, @@ -13,34 +14,32 @@ import { import { remove as removeInternal } from './internal/remove'; -/** - * Remove a file or folder from your S3 bucket. - * @param input - The `RemoveWithPathInput` object. - * @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. - */ +// --- Overloads without ctx --- + export function remove( input: RemoveWithPathInput, ): RemoveOperation; -/** - * @deprecated The `key` and `accessLevel` parameters are deprecated and may be removed in the next major version. - * Please use {@link https://docs.amplify.aws/react/build-a-backend/storage/remove | path} instead. - * - * Remove a file from your S3 bucket. - * @param input - The `RemoveInput` object. - * @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(input: RemoveInput): RemoveOperation; -export function remove(input: RemoveInput | RemoveWithPathInput) { +// --- Overloads with explicit ctx --- + +export function remove( + ctx: AmplifyContext, + input: RemoveWithPathInput, +): RemoveOperation; +export function remove( + ctx: AmplifyContext, + input: RemoveInput, +): RemoveOperation; + +// --- Implementation --- + +export function remove(...args: any[]) { + const [ctx, input] = resolveCtxArgs(args); + 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 deleted file mode 100644 index e9486e10431..00000000000 --- a/packages/storage/src/providers/s3/apis/server/copy.ts +++ /dev/null @@ -1,54 +0,0 @@ -// 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 { - CopyInput, - CopyOutput, - CopyWithPathInput, - CopyWithPathOutput, -} from '../../types'; -import { copy as copyInternal } from '../internal/copy'; - -/** - * Copy an object from a source to a destination object within the same bucket. - * - * @param contextSpec - 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 - * @throws validation: `StorageValidationErrorCode` - Thrown when - * source or destination path is not defined. - */ -export function copy( - contextSpec: AmplifyServer.ContextSpec, - input: CopyWithPathInput, -): Promise; -/** - * @deprecated The `key` and `accessLevel` parameters are deprecated and may be removed in the next major version. - * Please use {@link https://docs.amplify.aws/react/build-a-backend/storage/copy | path} instead. - * - * 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 input - The `CopyInput` object. - * @returns Output containing the destination object key. - * @throws service: `S3Exception` - Thrown when checking for existence of the object - * @throws validation: `StorageValidationErrorCode` - Thrown when - * source or destination key is not defined. - */ -export function copy( - contextSpec: AmplifyServer.ContextSpec, - input: CopyInput, -): Promise; - -export function copy( - contextSpec: AmplifyServer.ContextSpec, - input: CopyInput | CopyWithPathInput, -) { - return copyInternal(getAmplifyServerContext(contextSpec).amplify, input); -} diff --git a/packages/storage/src/providers/s3/apis/server/getProperties.ts b/packages/storage/src/providers/s3/apis/server/getProperties.ts deleted file mode 100644 index 87a77a297a4..00000000000 --- a/packages/storage/src/providers/s3/apis/server/getProperties.ts +++ /dev/null @@ -1,57 +0,0 @@ -// 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 { - GetPropertiesInput, - GetPropertiesOutput, - GetPropertiesWithPathInput, - GetPropertiesWithPathOutput, -} from '../../types'; -import { getProperties as getPropertiesInternal } from '../internal/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 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, - input: GetPropertiesWithPathInput, -): Promise; -/** - * @deprecated The `key` and `accessLevel` parameters are deprecated and may be removed in the next major version. - * Please use {@link https://docs.amplify.aws/javascript/build-a-backend/storage/get-properties/ | path} instead. - * - * 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 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, - input: GetPropertiesInput, -): Promise; - -export function getProperties( - contextSpec: AmplifyServer.ContextSpec, - input: GetPropertiesInput | GetPropertiesWithPathInput, -) { - return getPropertiesInternal( - getAmplifyServerContext(contextSpec).amplify, - input, - ); -} diff --git a/packages/storage/src/providers/s3/apis/server/getUrl.ts b/packages/storage/src/providers/s3/apis/server/getUrl.ts deleted file mode 100644 index f9f4e80d07c..00000000000 --- a/packages/storage/src/providers/s3/apis/server/getUrl.ts +++ /dev/null @@ -1,68 +0,0 @@ -// 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 { - GetUrlInput, - GetUrlOutput, - GetUrlWithPathInput, - GetUrlWithPathOutput, -} from '../../types'; -import { getUrl as getUrlInternal } from '../internal/getUrl'; - -/** - * 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 - * the option `expiresIn` is reached. The `expiresAt` property in the output object indicates when the URL MAY expire. - * - * By default, it will not validate the object that exists in S3. If you set the `options.validateObjectExistence` - * 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 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 - * @throws validation: `StorageValidationErrorCode` - Validation errors - * thrown either username or key are not defined. - * - */ -export function getUrl( - contextSpec: AmplifyServer.ContextSpec, - input: GetUrlWithPathInput, -): Promise; -/** - * @deprecated The `key` and `accessLevel` parameters are deprecated and may be removed in the next major version. - * Please use {@link https://docs.amplify.aws/javascript/build-a-backend/storage/download/#generate-a-download-url | path} instead. - * - * 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 - * the option `expiresIn` is reached. The `expiresAt` property in the output object indicates when the URL MAY expire. - * - * By default, it will not validate the object that exists in S3. If you set the `options.validateObjectExistence` - * 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 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 - * @throws validation: `StorageValidationErrorCode` - Validation errors - * thrown either username or key are not defined. - * - */ -export function getUrl( - contextSpec: AmplifyServer.ContextSpec, - input: GetUrlInput, -): Promise; - -export function getUrl( - contextSpec: AmplifyServer.ContextSpec, - input: GetUrlInput | GetUrlWithPathInput, -) { - return getUrlInternal(getAmplifyServerContext(contextSpec).amplify, input); -} diff --git a/packages/storage/src/providers/s3/apis/server/index.ts b/packages/storage/src/providers/s3/apis/server/index.ts index 9a9fa819695..c28299da0e5 100644 --- a/packages/storage/src/providers/s3/apis/server/index.ts +++ b/packages/storage/src/providers/s3/apis/server/index.ts @@ -1,9 +1,4 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -export { getProperties } from './getProperties'; -export { getUrl } from './getUrl'; -export { list } from './list'; -export { remove } from './remove'; -export { copy } from './copy'; -export { uploadData } from './uploadData'; +export { getProperties, getUrl, list, remove, copy, uploadData } from '..'; diff --git a/packages/storage/src/providers/s3/apis/server/list.ts b/packages/storage/src/providers/s3/apis/server/list.ts deleted file mode 100644 index 66d0ad4cd22..00000000000 --- a/packages/storage/src/providers/s3/apis/server/list.ts +++ /dev/null @@ -1,85 +0,0 @@ -// 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 { - ListAllInput, - ListAllOutput, - ListAllWithPathInput, - ListAllWithPathOutput, - ListPaginateInput, - ListPaginateOutput, - ListPaginateWithPathInput, - ListPaginateWithPathOutput, -} from '../../types'; -import { list as listInternal } from '../internal/list'; - -/** - * 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. - * @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, - 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. - * @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, - input: ListAllWithPathInput, -): Promise; -/** - * @deprecated The `prefix` and `accessLevel` parameters are deprecated and may be removed in the next major version. - * Please use {@link https://docs.amplify.aws/react/build-a-backend/storage/list | path} instead. - * List files in pages with the given `prefix`. - * `pageSize` is defaulted to 1000. Additionally, the result will include a `nextToken` if there are more items to retrieve. - * @param input - The `ListPaginateInput` object. - * @returns A list of objects with key 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, - input?: ListPaginateInput, -): Promise; -/** - * @deprecated The `prefix` and `accessLevel` parameters are deprecated and may be removed in the next major version. - * Please use {@link https://docs.amplify.aws/react/build-a-backend/storage/list | path} instead. - * List all files from S3 for a given `prefix`. You can set `listAll` to true in `options` to get all the files from S3. - * @param input - The `ListAllInput` object. - * @returns A list of all objects with key 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, - input?: ListAllInput, -): Promise; - -export function list( - contextSpec: AmplifyServer.ContextSpec, - input?: - | ListAllInput - | ListPaginateInput - | ListAllWithPathInput - | ListPaginateWithPathInput, -) { - return listInternal( - getAmplifyServerContext(contextSpec).amplify, - input ?? {}, - ); -} diff --git a/packages/storage/src/providers/s3/apis/server/remove.ts b/packages/storage/src/providers/s3/apis/server/remove.ts deleted file mode 100644 index 23b62ef6609..00000000000 --- a/packages/storage/src/providers/s3/apis/server/remove.ts +++ /dev/null @@ -1,57 +0,0 @@ -// 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 { - RemoveInput, - RemoveOperation, - RemoveOutput, - RemoveWithPathInput, - RemoveWithPathOutput, -} from '../../types'; -import { remove as removeInternal } from '../internal/remove'; - -/** - * 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. - * @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, - input: RemoveWithPathInput, -): RemoveOperation; -/** - * @deprecated The `key` and `accessLevel` parameters are deprecated and may be removed in the next major version. - * Please use {@link https://docs.amplify.aws/react/build-a-backend/storage/remove | path} instead. - * - * Remove a file from your S3 bucket. - * @param input - The `RemoveInput` object. - * @param contextSpec - 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, - input: RemoveInput, -): RemoveOperation; - -export function remove( - contextSpec: AmplifyServer.ContextSpec, - input: RemoveInput | RemoveWithPathInput, -) { - if ('key' in input) { - return removeInternal(getAmplifyServerContext(contextSpec).amplify, input); - } else { - return removeInternal(getAmplifyServerContext(contextSpec).amplify, input); - } -} diff --git a/packages/storage/src/providers/s3/apis/server/uploadData.ts b/packages/storage/src/providers/s3/apis/server/uploadData.ts deleted file mode 100644 index 0dab8f67f8b..00000000000 --- a/packages/storage/src/providers/s3/apis/server/uploadData.ts +++ /dev/null @@ -1,95 +0,0 @@ -// 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 { - UploadDataInput, - UploadDataServerOutput, - UploadDataServerWithPathOutput, - UploadDataWithPathInput, -} from '../../types'; -import { uploadData as uploadDataInternal } from '../internal/uploadData'; - -/** - * Upload data to the specified S3 object path. By default uses a single PUT - * operation to upload when the payload is less than 5MB. Otherwise, uses - * multipart upload to upload the payload. - * - * 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 input - A `UploadDataWithPathInput` object. - * - * @returns An `UploadDataServerWithPathOutput` task. Await the `result` - * promise to get the upload result. - * - * @throws S3Exception when the underlying S3 service returned error. - * @throws StorageValidationErrorCode when API call parameters are invalid. - * - * @example - * ```ts - * // In a Next.js Route Handler - * import { runWithAmplifyServerContext } from '@aws-amplify/adapter-nextjs'; - * import { uploadData } from 'aws-amplify/storage/server'; - * import { cookies } from 'next/headers'; - * - * export async function POST(request: Request) { - * const formData = await request.formData(); - * const file = formData.get('file') as File; - * const result = await runWithAmplifyServerContext({ - * nextServerContext: { cookies }, - * operation: (contextSpec) => - * uploadData(contextSpec, { path: `uploads/${file.name}`, data: file }).result, - * }); - * return Response.json(result); - * } - * ``` - */ -export function uploadData( - contextSpec: AmplifyServer.ContextSpec, - input: UploadDataWithPathInput, -): UploadDataServerWithPathOutput; - -/** - * @deprecated The `key` and `accessLevel` parameters are deprecated and may be removed in the next major version. - * Please use {@link https://docs.amplify.aws/javascript/build-a-backend/storage/upload/#uploaddata | path} instead. - * - * Upload data to the specified S3 object key. By default uses a single PUT - * operation to upload when the payload is less than 5MB. Otherwise, uses - * multipart upload to upload the payload. - * - * The returned task does NOT support `pause()` / `resume()` server-side. See - * the path-based overload above for details. - * - * @param contextSpec - The isolated server context. - * @param input - A `UploadDataInput` object. - * - * @returns An `UploadDataServerOutput` task. Await the `result` promise to - * get the upload result. - * - * @throws S3Exception when the underlying S3 service returned error. - * @throws StorageValidationErrorCode when API call parameters are invalid. - */ -export function uploadData( - contextSpec: AmplifyServer.ContextSpec, - input: UploadDataInput, -): UploadDataServerOutput; - -export function uploadData( - contextSpec: 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). - return uploadDataInternal( - getAmplifyServerContext(contextSpec).amplify, - input, - ) as UploadDataServerOutput | UploadDataServerWithPathOutput; -} diff --git a/packages/storage/src/providers/s3/apis/uploadData.ts b/packages/storage/src/providers/s3/apis/uploadData.ts index 2cdaa1b3a03..7304cfa3184 100644 --- a/packages/storage/src/providers/s3/apis/uploadData.ts +++ b/packages/storage/src/providers/s3/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 { UploadDataInput, @@ -12,118 +13,32 @@ import { import { uploadData as uploadDataInternal } from './internal/uploadData'; -/** - * Upload data to the specified S3 object path. By default uses single PUT operation to upload if the payload is less than 5MB. - * Otherwise, uses multipart upload to upload the payload. If the payload length cannot be determined, uses multipart upload. - * - * Limitations: - * * Maximum object size is 5TB. - * * Maximum object size if the size cannot be determined before upload is 50GB. - * - * @throws S3Exception when the underlying S3 service returned error. - * @throws StorageValidationErrorCode when API call parameters are invalid. - * - * @param input - A `UploadDataWithPathInput` object. - * - * @returns A cancelable and resumable task exposing result promise from `result` - * property. - * - * @example - * ```ts - * // Upload a file to s3 bucket - * await uploadData({ path, data: file, options: { - * onProgress, // Optional progress callback. - * } }).result; - * ``` - * - * @example - * ```ts - * // Cancel a task - * const uploadTask = uploadData({ path, data: file }); - * //... - * uploadTask.cancel(); - * try { - * await uploadTask.result; - * } catch (error) { - * if(isCancelError(error)) { - * // Handle error thrown by task cancelation. - * } - * } - *``` - * - * @example - * ```ts - * // Pause and resume a task - * const uploadTask = uploadData({ path, data: file }); - * //... - * uploadTask.pause(); - * //... - * uploadTask.resume(); - * //... - * await uploadTask.result; - * ``` - */ +// --- Overloads without ctx --- + export function uploadData( input: UploadDataWithPathInput, ): UploadDataWithPathOutput; - -/** - * Upload data to the specified S3 object key. By default uses single PUT operation to upload if the payload is less than 5MB. - * Otherwise, uses multipart upload to upload the payload. If the payload length cannot be determined, uses multipart upload. - * - * Limitations: - * * Maximum object size is 5TB. - * * Maximum object size if the size cannot be determined before upload is 50GB. - * - * @deprecated The `key` and `accessLevel` parameters are deprecated and will be removed in next major version. - * Please use {@link https://docs.amplify.aws/javascript/build-a-backend/storage/upload/#uploaddata | path} instead. - * - * @throws S3Exception when the underlying S3 service returned error. - * @throws StorageValidationErrorCode when API call parameters are invalid. - * - * @param input - A `UploadDataInput` object. - * - * @returns A cancelable and resumable task exposing result promise from the `result` property. - * - * @example - * ```ts - * // Upload a file to s3 bucket - * await uploadData({ key, data: file, options: { - * onProgress, // Optional progress callback. - * } }).result; - * ``` - * - * @example - * ```ts - * // Cancel a task - * const uploadTask = uploadData({ key, data: file }); - * //... - * uploadTask.cancel(); - * try { - * await uploadTask.result; - * } catch (error) { - * if(isCancelError(error)) { - * // Handle error thrown by task cancelation. - * } - * } - *``` - * - * @example - * ```ts - * // Pause and resume a task - * const uploadTask = uploadData({ key, data: file }); - * //... - * uploadTask.pause(); - * //... - * uploadTask.resume(); - * //... - * await uploadTask.result; - * ``` - */ export function uploadData(input: UploadDataInput): UploadDataOutput; -export function uploadData(input: UploadDataInput | UploadDataWithPathInput) { - return uploadDataInternal(Amplify, { +// --- Overloads with explicit ctx --- + +export function uploadData( + ctx: AmplifyContext, + input: UploadDataWithPathInput, +): UploadDataWithPathOutput; +export function uploadData( + ctx: AmplifyContext, + input: UploadDataInput, +): UploadDataOutput; + +// --- Implementation --- + +export function uploadData(...args: any[]) { + const [ctx, input] = resolveCtxArgs< + UploadDataInput | UploadDataWithPathInput + >(args); + + return uploadDataInternal(ctx, { ...input, options: { ...input?.options, diff --git a/packages/storage/src/providers/s3/server.ts b/packages/storage/src/providers/s3/server.ts index a6810675663..c8bb67fca45 100644 --- a/packages/storage/src/providers/s3/server.ts +++ b/packages/storage/src/providers/s3/server.ts @@ -1,4 +1,4 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -export * from './apis/server'; +export { getProperties, getUrl, list, remove, copy } from '.'; diff --git a/packages/storage/src/providers/s3/utils/client/s3data/base.ts b/packages/storage/src/providers/s3/utils/client/s3data/base.ts index c2d857d793c..fbe2321da03 100644 --- a/packages/storage/src/providers/s3/utils/client/s3data/base.ts +++ b/packages/storage/src/providers/s3/utils/client/s3data/base.ts @@ -12,7 +12,6 @@ import { } from '@aws-amplify/core/internals/aws-client-utils'; import { createRetryDecider, createXmlErrorParser } from '../utils'; -import { LOCAL_TESTING_S3_ENDPOINT } from '../../constants'; import { assertValidationError } from '../../../../../errors/utils/assertValidationError'; import { StorageValidationErrorCode } from '../../../../../errors/types/validation'; @@ -72,14 +71,18 @@ const endpointResolver = ( let endpoint: URL; // 1. get base endpoint if (customEndpoint) { - if (customEndpoint === LOCAL_TESTING_S3_ENDPOINT) { + if ( + customEndpoint.startsWith('http://') || + customEndpoint.startsWith('https://') + ) { endpoint = new AmplifyUrl(customEndpoint); + } else { + assertValidationError( + !customEndpoint.includes('://'), + StorageValidationErrorCode.InvalidCustomEndpoint, + ); + endpoint = new AmplifyUrl(`https://${customEndpoint}`); } - assertValidationError( - !customEndpoint.includes('://'), - StorageValidationErrorCode.InvalidCustomEndpoint, - ); - endpoint = new AmplifyUrl(`https://${customEndpoint}`); } else if (useAccelerateEndpoint) { // this ErrorCode isn't expose yet since forcePathStyle param isn't publicly exposed assertValidationError( diff --git a/packages/storage/src/providers/s3/utils/resolveS3ConfigAndInput.ts b/packages/storage/src/providers/s3/utils/resolveS3ConfigAndInput.ts index 7cb4c55316e..aecf38280f6 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'; @@ -22,7 +22,7 @@ import { StorageBucket, } from '../types/options'; -import { DEFAULT_ACCESS_LEVEL, LOCAL_TESTING_S3_ENDPOINT } from './constants'; +import { DEFAULT_ACCESS_LEVEL } from './constants'; interface S3ApiOptions { accessLevel?: StorageAccessLevel; @@ -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, @@ -105,8 +105,10 @@ export const resolveS3ConfigAndInput = async ( bucket: defaultBucket, region: defaultRegion, dangerouslyConnectToHttpEndpointForTesting, + endpointProvider, + forcePathStyle: configForcePathStyle, buckets, - } = amplify.getConfig()?.Storage?.S3 ?? {}; + } = amplify.resourcesConfig?.Storage?.S3 ?? {}; const { bucket = defaultBucket, region = defaultRegion } = (apiOptions?.bucket && resolveBucketConfig(apiOptions, buckets)) || {}; @@ -129,18 +131,28 @@ export const resolveS3ConfigAndInput = async ( const keyPrefix = await prefixResolver({ accessLevel, targetIdentityId }); + // Resolve custom endpoint: apiOptions > endpointProvider > dangerouslyConnect + let resolvedEndpoint: string | undefined = apiOptions?.customEndpoint; + let resolvedForcePathStyle: boolean | undefined = configForcePathStyle; + + if (!resolvedEndpoint && endpointProvider) { + resolvedEndpoint = await endpointProvider({ bucket, region }); + } + + if (!resolvedEndpoint && dangerouslyConnectToHttpEndpointForTesting) { + resolvedEndpoint = dangerouslyConnectToHttpEndpointForTesting; + resolvedForcePathStyle = true; + } + return { s3Config: { credentials: credentialsProvider, region, useAccelerateEndpoint: apiOptions?.useAccelerateEndpoint, - ...(apiOptions?.customEndpoint - ? { customEndpoint: apiOptions.customEndpoint } - : {}), - ...(dangerouslyConnectToHttpEndpointForTesting + ...(resolvedEndpoint ? { - customEndpoint: LOCAL_TESTING_S3_ENDPOINT, - forcePathStyle: true, + customEndpoint: resolvedEndpoint, + forcePathStyle: resolvedForcePathStyle ?? false, } : {}), }, diff --git a/packages/storage/src/server.ts b/packages/storage/src/server.ts index b5acd164230..5b91bd19cb6 100644 --- a/packages/storage/src/server.ts +++ b/packages/storage/src/server.ts @@ -1,4 +1,4 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -export * from './providers/s3/apis/server'; +export { getProperties, getUrl, list, remove, copy } from './providers/s3'; diff --git a/yarn.lock b/yarn.lock index 711aa10fcae..a89a99af9ea 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11057,9 +11057,9 @@ pure-rand@^6.0.0: integrity sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA== qs@6.13.0, qs@^6.14.1: - version "6.15.1" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.15.1.tgz#bdb55aed06bfac257a90c44a446a73fba5575c8f" - integrity sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg== + version "6.15.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.15.0.tgz#db8fd5d1b1d2d6b5b33adaf87429805f1909e7b3" + integrity sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ== dependencies: side-channel "^1.1.0"