Skip to content

Commit

Permalink
Emulator Idempotency: Auth (#8750)
Browse files Browse the repository at this point in the history
Update the `connectAuthEmulator` function to support its invocation more than once. If the Auth instance is already in use, and `connectAuthEmulator` is invoked with the same configuration, then the invocation will now succeed instead of assert.

This unlocks support for web frameworks which may render the page numerous times with the same instances of auth. Before this PR customers needed to add extra code to guard against calling `connectAuthEmulator` in their SSR logic. Now we do that guarding logic on their behalf which should simplify our customer's apps.

Fixes #6824.
  • Loading branch information
DellaBitta authored Feb 11, 2025
1 parent 70e08cf commit c791ecf
Show file tree
Hide file tree
Showing 3 changed files with 74 additions and 9 deletions.
8 changes: 8 additions & 0 deletions .changeset/lemon-candles-vanish.md
Original file line number Diff line number Diff line change
@@ -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).

35 changes: 35 additions & 0 deletions packages/auth/src/core/auth/emulator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
40 changes: 31 additions & 9 deletions packages/auth/src/core/auth/emulator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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();
}
Expand Down

0 comments on commit c791ecf

Please sign in to comment.