From f00beb7dd982ae021caa9ae20fa1b3f6bf6cb37e Mon Sep 17 00:00:00 2001 From: ShrutiPundir17 Date: Sat, 16 May 2026 16:02:55 +0530 Subject: [PATCH 1/2] fix(aws-amplify): sync Cognito auth config and merge libraryOptions on reconfigure - Merge existing Amplify.libraryOptions when libraryOptions.Auth is provided so other categories are not dropped. - Call setAuthConfig on the default Cognito token provider when resourcesConfig.Auth changes on reconfigure (partial libraryOptions or config-only). - Merge prior libraryOptions on partial reconfigure without Auth override. - Add regression tests for the above behaviors. Co-authored-by: Cursor --- .../fix-init-singleton-auth-reconfigure.md | 5 + .../__tests__/initSingleton.test.ts | 95 +++++++++++++++++-- packages/aws-amplify/src/initSingleton.ts | 42 +++++++- 3 files changed, 128 insertions(+), 14 deletions(-) create mode 100644 .changeset/fix-init-singleton-auth-reconfigure.md diff --git a/.changeset/fix-init-singleton-auth-reconfigure.md b/.changeset/fix-init-singleton-auth-reconfigure.md new file mode 100644 index 00000000000..a1d8bd01b7d --- /dev/null +++ b/.changeset/fix-init-singleton-auth-reconfigure.md @@ -0,0 +1,5 @@ +--- +'aws-amplify': patch +--- + +fix(aws-amplify): merge libraryOptions when Auth is overridden and refresh default Cognito auth config on reconfigure. diff --git a/packages/aws-amplify/__tests__/initSingleton.test.ts b/packages/aws-amplify/__tests__/initSingleton.test.ts index 5d021b36743..c31ea32ab44 100644 --- a/packages/aws-amplify/__tests__/initSingleton.test.ts +++ b/packages/aws-amplify/__tests__/initSingleton.test.ts @@ -246,16 +246,29 @@ describe('initSingleton (DefaultAmplify)', () => { }); describe('when ResourcesConfig.Auth is defined', () => { - it('should just configure with the provided config and options when libraryOptions.Auth is defined', () => { + it('should merge with existing libraryOptions when libraryOptions.Auth is defined', () => { + const customTokenProvider = { getTokens: jest.fn() }; + const storageLibraryOptions = { + S3: { defaultAccessLevel: 'private' as const }, + }; + AmplifySingleton.libraryOptions = { + Storage: storageLibraryOptions, + }; const libraryOptions = { - Auth: { tokenProvider: { getTokens: jest.fn() } }, + Auth: { tokenProvider: customTokenProvider }, }; Amplify.configure(mockResourceConfig, libraryOptions); expect(mockAmplifySingletonConfigure).toHaveBeenCalledWith( mockResourceConfig, - libraryOptions, + { + Storage: storageLibraryOptions, + Auth: libraryOptions.Auth, + }, ); + expect( + mockCognitoUserPoolsTokenProviderSetAuthConfig, + ).not.toHaveBeenCalled(); }); describe('when the singleton libraryOptions have not yet been configured with Auth', () => { @@ -324,11 +337,12 @@ describe('initSingleton (DefaultAmplify)', () => { it('should preserve current auth providers (default or otherwise) and configure provider with a new CookieStorage instance', () => { const libraryOptions = { ssr: true }; + const authLibraryOptions = AmplifySingleton.libraryOptions.Auth; Amplify.configure(mockResourceConfig, libraryOptions); expect( mockCognitoUserPoolsTokenProviderSetAuthConfig, - ).not.toHaveBeenCalled(); + ).toHaveBeenCalledWith(mockResourceConfig.Auth); expect(MockCookieStorage).toHaveBeenCalledWith({ sameSite: 'lax' }); expect( mockCognitoUserPoolsTokenProviderSetKeyValueStorage, @@ -336,7 +350,7 @@ describe('initSingleton (DefaultAmplify)', () => { expect(mockAmplifySingletonConfigure).toHaveBeenCalledWith( mockResourceConfig, { - Auth: AmplifySingleton.libraryOptions.Auth, + Auth: authLibraryOptions, ...libraryOptions, }, ); @@ -344,18 +358,19 @@ describe('initSingleton (DefaultAmplify)', () => { it('should preserve current auth providers (default or otherwise) and configure provider with defaultStorage', () => { const libraryOptions = { ssr: false }; + const authLibraryOptions = AmplifySingleton.libraryOptions.Auth; Amplify.configure(mockResourceConfig, libraryOptions); expect( mockCognitoUserPoolsTokenProviderSetAuthConfig, - ).not.toHaveBeenCalled(); + ).toHaveBeenCalledWith(mockResourceConfig.Auth); expect( mockCognitoUserPoolsTokenProviderSetKeyValueStorage, ).toHaveBeenCalledWith(defaultStorage); expect(mockAmplifySingletonConfigure).toHaveBeenCalledWith( mockResourceConfig, { - Auth: AmplifySingleton.libraryOptions.Auth, + Auth: authLibraryOptions, ...libraryOptions, }, ); @@ -365,30 +380,90 @@ describe('initSingleton (DefaultAmplify)', () => { const libraryOptions = { Storage: { S3: { isObjectLockEnabled: true } }, }; + const authLibraryOptions = AmplifySingleton.libraryOptions.Auth; Amplify.configure(mockResourceConfig, libraryOptions); expect( mockCognitoUserPoolsTokenProviderSetAuthConfig, - ).not.toHaveBeenCalled(); + ).toHaveBeenCalledWith(mockResourceConfig.Auth); expect( mockCognitoUserPoolsTokenProviderSetKeyValueStorage, ).not.toHaveBeenCalled(); expect(mockAmplifySingletonConfigure).toHaveBeenCalledWith( mockResourceConfig, { - Auth: AmplifySingleton.libraryOptions.Auth, + Auth: authLibraryOptions, ...libraryOptions, }, ); }); - it('should just configure without touching libraryOptions', () => { + it('should preserve non-Auth library options when reconfiguring with partial libraryOptions', () => { + const storageLibraryOptions = { + S3: { defaultAccessLevel: 'private' as const }, + }; + AmplifySingleton.libraryOptions = { + Auth: { + tokenProvider: cognitoUserPoolsTokenProvider, + credentialsProvider: cognitoCredentialsProvider, + }, + Storage: storageLibraryOptions, + }; + const authLibraryOptions = AmplifySingleton.libraryOptions.Auth; + + Amplify.configure(mockResourceConfig, { ssr: true }); + + expect( + mockCognitoUserPoolsTokenProviderSetAuthConfig, + ).toHaveBeenCalledWith(mockResourceConfig.Auth); + expect(mockAmplifySingletonConfigure).toHaveBeenCalledWith( + mockResourceConfig, + { + Auth: authLibraryOptions, + Storage: storageLibraryOptions, + ssr: true, + }, + ); + }); + + it('should sync default Cognito auth config when reconfiguring with resource config only', () => { Amplify.configure(mockResourceConfig); + expect( + mockCognitoUserPoolsTokenProviderSetAuthConfig, + ).toHaveBeenCalledWith(mockResourceConfig.Auth); expect(mockAmplifySingletonConfigure).toHaveBeenCalledWith( mockResourceConfig, ); }); + + it('should sync default Cognito auth config when libraryOptions.Auth overrides with default provider', () => { + const updatedResourceConfig: ResourcesConfig = { + Auth: { + Cognito: { + userPoolClientId: 'newClientId', + userPoolId: 'newPoolId', + }, + }, + }; + AmplifySingleton.libraryOptions = { + Auth: { + tokenProvider: cognitoUserPoolsTokenProvider, + credentialsProvider: cognitoCredentialsProvider, + }, + }; + + Amplify.configure(updatedResourceConfig, { + Auth: { + tokenProvider: cognitoUserPoolsTokenProvider, + credentialsProvider: cognitoCredentialsProvider, + }, + }); + + expect( + mockCognitoUserPoolsTokenProviderSetAuthConfig, + ).toHaveBeenCalledWith(updatedResourceConfig.Auth); + }); }); it('should invoke AmplifySingleton.configure with other provided library options', () => { diff --git a/packages/aws-amplify/src/initSingleton.ts b/packages/aws-amplify/src/initSingleton.ts index 6168cc25b51..fa77d668955 100644 --- a/packages/aws-amplify/src/initSingleton.ts +++ b/packages/aws-amplify/src/initSingleton.ts @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { Amplify, + AuthConfig, CookieStorage, LibraryOptions, ResourcesConfig, @@ -20,6 +21,15 @@ import { cognitoUserPoolsTokenProvider, } from './auth/cognito'; +const usesDefaultCognitoTokenProvider = ( + authLibraryOptions?: LibraryOptions['Auth'], +): boolean => + authLibraryOptions?.tokenProvider === cognitoUserPoolsTokenProvider; + +const syncDefaultCognitoAuthConfig = (authConfig: AuthConfig): void => { + cognitoUserPoolsTokenProvider.setAuthConfig(authConfig); +}; + export const DefaultAmplify = { /** * Configures Amplify with the {@link resourceConfig} and {@link libraryOptions}. @@ -56,10 +66,25 @@ export const DefaultAmplify = { return; } - // If Auth options are provided, always just configure as is. - // Otherwise, we can assume no Auth libraryOptions were provided from here on. + // If Auth options are provided, merge with existing libraryOptions so other categories are preserved. if (libraryOptions?.Auth) { - Amplify.configure(resolvedResourceConfig, libraryOptions); + const mergedLibraryOptions: LibraryOptions = { + ...Amplify.libraryOptions, + ...libraryOptions, + }; + + if (usesDefaultCognitoTokenProvider(mergedLibraryOptions.Auth)) { + syncDefaultCognitoAuthConfig(resolvedResourceConfig.Auth); + + if (libraryOptions.ssr !== undefined) { + cognitoUserPoolsTokenProvider.setKeyValueStorage( + // TODO: allow configure with a public interface + resolvedKeyValueStorage, + ); + } + } + + Amplify.configure(resolvedResourceConfig, mergedLibraryOptions); return; } @@ -97,9 +122,14 @@ export const DefaultAmplify = { authLibraryOptions.credentialsProvider = resolvedCredentialsProvider; } + if (usesDefaultCognitoTokenProvider(authLibraryOptions)) { + syncDefaultCognitoAuthConfig(resolvedResourceConfig.Auth); + } + Amplify.configure(resolvedResourceConfig, { - Auth: authLibraryOptions, + ...Amplify.libraryOptions, ...libraryOptions, + Auth: authLibraryOptions, }); return; @@ -107,6 +137,10 @@ export const DefaultAmplify = { // Finally, if there were no libraryOptions given at all, we should simply not touch the currently // configured libraryOptions. + if (usesDefaultCognitoTokenProvider(Amplify.libraryOptions.Auth)) { + syncDefaultCognitoAuthConfig(resolvedResourceConfig.Auth); + } + Amplify.configure(resolvedResourceConfig); }, /** From f67c6517280618d477aa3e99398100f3ac1782e1 Mon Sep 17 00:00:00 2001 From: ShrutiPundir17 Date: Sat, 16 May 2026 16:30:20 +0530 Subject: [PATCH 2/2] test(aws-amplify): add DefaultAmplify integration tests with real core singleton Validates Storage preservation and setAuthConfig on reconfigure without mocking @aws-amplify/core. Co-authored-by: Cursor --- .../initSingleton.integration.test.ts | 123 ++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 packages/aws-amplify/__tests__/initSingleton.integration.test.ts diff --git a/packages/aws-amplify/__tests__/initSingleton.integration.test.ts b/packages/aws-amplify/__tests__/initSingleton.integration.test.ts new file mode 100644 index 00000000000..df87b946ec7 --- /dev/null +++ b/packages/aws-amplify/__tests__/initSingleton.integration.test.ts @@ -0,0 +1,123 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +/** + * Integration-style tests using the real @aws-amplify/core Amplify singleton + * (initSingleton.test.ts mocks core). + */ +import { Amplify as CoreAmplify, ResourcesConfig } from '@aws-amplify/core'; + +import { cognitoUserPoolsTokenProvider } from '../src/auth/cognito'; +import { Amplify as DefaultAmplify } from '../src'; + +const poolAConfig: ResourcesConfig = { + Auth: { + Cognito: { + userPoolClientId: 'client-a', + userPoolId: 'pool-a', + }, + }, +}; + +const poolBConfig: ResourcesConfig = { + Auth: { + Cognito: { + userPoolClientId: 'client-b', + userPoolId: 'pool-b', + }, + }, +}; + +const storageLibraryOptions = { + Storage: { + S3: { + defaultAccessLevel: 'private' as const, + isObjectLockEnabled: true, + }, + }, +}; + +describe('DefaultAmplify.configure integration', () => { + let setAuthConfigSpy: jest.SpyInstance; + let setKeyValueStorageSpy: jest.SpyInstance; + + beforeEach(() => { + CoreAmplify.libraryOptions = {}; + CoreAmplify.resourcesConfig = {}; + setAuthConfigSpy = jest.spyOn( + cognitoUserPoolsTokenProvider, + 'setAuthConfig', + ); + setKeyValueStorageSpy = jest.spyOn( + cognitoUserPoolsTokenProvider, + 'setKeyValueStorage', + ); + }); + + afterEach(() => { + setAuthConfigSpy.mockRestore(); + setKeyValueStorageSpy.mockRestore(); + CoreAmplify.libraryOptions = {}; + CoreAmplify.resourcesConfig = {}; + }); + + it('keeps Storage and refreshes Cognito auth config on partial reconfigure', () => { + DefaultAmplify.configure(poolAConfig, storageLibraryOptions); + + expect(CoreAmplify.libraryOptions.Storage).toEqual( + storageLibraryOptions.Storage, + ); + expect(CoreAmplify.libraryOptions.Auth?.tokenProvider).toBe( + cognitoUserPoolsTokenProvider, + ); + expect(setAuthConfigSpy).toHaveBeenCalledWith(poolAConfig.Auth); + + setAuthConfigSpy.mockClear(); + + DefaultAmplify.configure(poolBConfig, { ssr: false }); + + expect(CoreAmplify.libraryOptions.Storage).toEqual( + storageLibraryOptions.Storage, + ); + expect(CoreAmplify.getConfig().Auth?.Cognito?.userPoolClientId).toBe( + 'client-b', + ); + expect(setAuthConfigSpy).toHaveBeenCalledWith(poolBConfig.Auth); + }); + + it('merges prior libraryOptions when libraryOptions.Auth overrides default provider', () => { + DefaultAmplify.configure(poolAConfig, storageLibraryOptions); + + setAuthConfigSpy.mockClear(); + + DefaultAmplify.configure(poolBConfig, { + Auth: { + tokenProvider: cognitoUserPoolsTokenProvider, + credentialsProvider: + CoreAmplify.libraryOptions.Auth!.credentialsProvider!, + }, + }); + + expect(CoreAmplify.libraryOptions.Storage).toEqual( + storageLibraryOptions.Storage, + ); + expect(setAuthConfigSpy).toHaveBeenCalledWith(poolBConfig.Auth); + expect(CoreAmplify.getConfig().Auth?.Cognito?.userPoolClientId).toBe( + 'client-b', + ); + }); + + it('syncs default Cognito auth config when only resource config is passed', () => { + DefaultAmplify.configure(poolAConfig, storageLibraryOptions); + + setAuthConfigSpy.mockClear(); + + DefaultAmplify.configure(poolBConfig); + + expect(CoreAmplify.libraryOptions.Storage).toEqual( + storageLibraryOptions.Storage, + ); + expect(setAuthConfigSpy).toHaveBeenCalledWith(poolBConfig.Auth); + expect(CoreAmplify.getConfig().Auth?.Cognito?.userPoolId).toBe('pool-b'); + }); +});