diff --git a/.changeset/lemon-candles-vanish.md b/.changeset/lemon-candles-vanish.md new file mode 100644 index 00000000000..715db44d0d7 --- /dev/null +++ b/.changeset/lemon-candles-vanish.md @@ -0,0 +1,8 @@ +--- +'@firebase/auth': patch +'firebase': patch +--- + +Fixed: invoking `connectAuthEmulator` multiple times with the same parameters will no longer cause +an error. Fixes [GitHub Issue #6824](https://github.com/firebase/firebase-js-sdk/issues/6824). + diff --git a/packages/auth/src/core/auth/emulator.test.ts b/packages/auth/src/core/auth/emulator.test.ts index 71a30883218..47c5d927c44 100644 --- a/packages/auth/src/core/auth/emulator.test.ts +++ b/packages/auth/src/core/auth/emulator.test.ts @@ -76,6 +76,41 @@ describe('core/auth/emulator', () => { ); }); + it('passes with same config if a network request has already been made', async () => { + expect(() => connectAuthEmulator(auth, 'http://127.0.0.1:2020')).to.not + .throw; + await user.delete(); + expect(() => connectAuthEmulator(auth, 'http://127.0.0.1:2020')).to.not + .throw; + }); + + it('fails with alternate config if a network request has already been made', async () => { + expect(() => connectAuthEmulator(auth, 'http://127.0.0.1:2020')).to.not + .throw; + await user.delete(); + expect(() => connectAuthEmulator(auth, 'http://127.0.0.1:2021')).to.throw( + FirebaseError, + 'auth/emulator-config-failed' + ); + }); + + it('subsequent calls update the endpoint appropriately', async () => { + connectAuthEmulator(auth, 'http://127.0.0.1:2021'); + expect(auth.emulatorConfig).to.eql({ + protocol: 'http', + host: '127.0.0.1', + port: 2021, + options: { disableWarnings: false } + }); + connectAuthEmulator(auth, 'http://127.0.0.1:2020'); + expect(auth.emulatorConfig).to.eql({ + protocol: 'http', + host: '127.0.0.1', + port: 2020, + options: { disableWarnings: false } + }); + }); + it('updates the endpoint appropriately', async () => { connectAuthEmulator(auth, 'http://127.0.0.1:2020'); await user.delete(); diff --git a/packages/auth/src/core/auth/emulator.ts b/packages/auth/src/core/auth/emulator.ts index f0ccb048f1f..60cc9403d3d 100644 --- a/packages/auth/src/core/auth/emulator.ts +++ b/packages/auth/src/core/auth/emulator.ts @@ -18,6 +18,7 @@ import { Auth } from '../../model/public_types'; import { AuthErrorCode } from '../errors'; import { _assert } from '../util/assert'; import { _castAuth } from './auth_impl'; +import { deepEqual } from '@firebase/util'; /** * Changes the {@link Auth} instance to communicate with the Firebase Auth Emulator, instead of production @@ -47,12 +48,6 @@ export function connectAuthEmulator( options?: { disableWarnings: boolean } ): void { const authInternal = _castAuth(auth); - _assert( - authInternal._canInitEmulator, - authInternal, - AuthErrorCode.EMULATOR_CONFIG_FAILED - ); - _assert( /^https?:\/\//.test(url), authInternal, @@ -66,15 +61,42 @@ export function connectAuthEmulator( const portStr = port === null ? '' : `:${port}`; // Always replace path with "/" (even if input url had no path at all, or had a different one). - authInternal.config.emulator = { url: `${protocol}//${host}${portStr}/` }; - authInternal.settings.appVerificationDisabledForTesting = true; - authInternal.emulatorConfig = Object.freeze({ + const emulator = { url: `${protocol}//${host}${portStr}/` }; + const emulatorConfig = Object.freeze({ host, port, protocol: protocol.replace(':', ''), options: Object.freeze({ disableWarnings }) }); + // There are a few scenarios to guard against if the Auth instance has already started: + if (!authInternal._canInitEmulator) { + // Applications may not initialize the emulator for the first time if Auth has already started + // to make network requests. + _assert( + authInternal.config.emulator && authInternal.emulatorConfig, + authInternal, + AuthErrorCode.EMULATOR_CONFIG_FAILED + ); + + // Applications may not alter the configuration of the emulator (aka pass a different config) + // once Auth has started to make network requests. + _assert( + deepEqual(emulator, authInternal.config.emulator) && + deepEqual(emulatorConfig, authInternal.emulatorConfig), + authInternal, + AuthErrorCode.EMULATOR_CONFIG_FAILED + ); + + // It's valid, however, to invoke connectAuthEmulator() after Auth has started making + // connections, so long as the config matches the existing config. This results in a no-op. + return; + } + + authInternal.config.emulator = emulator; + authInternal.emulatorConfig = emulatorConfig; + authInternal.settings.appVerificationDisabledForTesting = true; + if (!disableWarnings) { emitEmulatorWarning(); }