Skip to content

Commit c791ecf

Browse files
authored
Emulator Idempotency: Auth (#8750)
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.
1 parent 70e08cf commit c791ecf

File tree

3 files changed

+74
-9
lines changed

3 files changed

+74
-9
lines changed

.changeset/lemon-candles-vanish.md

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@firebase/auth': patch
3+
'firebase': patch
4+
---
5+
6+
Fixed: invoking `connectAuthEmulator` multiple times with the same parameters will no longer cause
7+
an error. Fixes [GitHub Issue #6824](https://github.com/firebase/firebase-js-sdk/issues/6824).
8+

packages/auth/src/core/auth/emulator.test.ts

+35
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,41 @@ describe('core/auth/emulator', () => {
7676
);
7777
});
7878

79+
it('passes with same config if a network request has already been made', async () => {
80+
expect(() => connectAuthEmulator(auth, 'http://127.0.0.1:2020')).to.not
81+
.throw;
82+
await user.delete();
83+
expect(() => connectAuthEmulator(auth, 'http://127.0.0.1:2020')).to.not
84+
.throw;
85+
});
86+
87+
it('fails with alternate config if a network request has already been made', async () => {
88+
expect(() => connectAuthEmulator(auth, 'http://127.0.0.1:2020')).to.not
89+
.throw;
90+
await user.delete();
91+
expect(() => connectAuthEmulator(auth, 'http://127.0.0.1:2021')).to.throw(
92+
FirebaseError,
93+
'auth/emulator-config-failed'
94+
);
95+
});
96+
97+
it('subsequent calls update the endpoint appropriately', async () => {
98+
connectAuthEmulator(auth, 'http://127.0.0.1:2021');
99+
expect(auth.emulatorConfig).to.eql({
100+
protocol: 'http',
101+
host: '127.0.0.1',
102+
port: 2021,
103+
options: { disableWarnings: false }
104+
});
105+
connectAuthEmulator(auth, 'http://127.0.0.1:2020');
106+
expect(auth.emulatorConfig).to.eql({
107+
protocol: 'http',
108+
host: '127.0.0.1',
109+
port: 2020,
110+
options: { disableWarnings: false }
111+
});
112+
});
113+
79114
it('updates the endpoint appropriately', async () => {
80115
connectAuthEmulator(auth, 'http://127.0.0.1:2020');
81116
await user.delete();

packages/auth/src/core/auth/emulator.ts

+31-9
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { Auth } from '../../model/public_types';
1818
import { AuthErrorCode } from '../errors';
1919
import { _assert } from '../util/assert';
2020
import { _castAuth } from './auth_impl';
21+
import { deepEqual } from '@firebase/util';
2122

2223
/**
2324
* Changes the {@link Auth} instance to communicate with the Firebase Auth Emulator, instead of production
@@ -47,12 +48,6 @@ export function connectAuthEmulator(
4748
options?: { disableWarnings: boolean }
4849
): void {
4950
const authInternal = _castAuth(auth);
50-
_assert(
51-
authInternal._canInitEmulator,
52-
authInternal,
53-
AuthErrorCode.EMULATOR_CONFIG_FAILED
54-
);
55-
5651
_assert(
5752
/^https?:\/\//.test(url),
5853
authInternal,
@@ -66,15 +61,42 @@ export function connectAuthEmulator(
6661
const portStr = port === null ? '' : `:${port}`;
6762

6863
// Always replace path with "/" (even if input url had no path at all, or had a different one).
69-
authInternal.config.emulator = { url: `${protocol}//${host}${portStr}/` };
70-
authInternal.settings.appVerificationDisabledForTesting = true;
71-
authInternal.emulatorConfig = Object.freeze({
64+
const emulator = { url: `${protocol}//${host}${portStr}/` };
65+
const emulatorConfig = Object.freeze({
7266
host,
7367
port,
7468
protocol: protocol.replace(':', ''),
7569
options: Object.freeze({ disableWarnings })
7670
});
7771

72+
// There are a few scenarios to guard against if the Auth instance has already started:
73+
if (!authInternal._canInitEmulator) {
74+
// Applications may not initialize the emulator for the first time if Auth has already started
75+
// to make network requests.
76+
_assert(
77+
authInternal.config.emulator && authInternal.emulatorConfig,
78+
authInternal,
79+
AuthErrorCode.EMULATOR_CONFIG_FAILED
80+
);
81+
82+
// Applications may not alter the configuration of the emulator (aka pass a different config)
83+
// once Auth has started to make network requests.
84+
_assert(
85+
deepEqual(emulator, authInternal.config.emulator) &&
86+
deepEqual(emulatorConfig, authInternal.emulatorConfig),
87+
authInternal,
88+
AuthErrorCode.EMULATOR_CONFIG_FAILED
89+
);
90+
91+
// It's valid, however, to invoke connectAuthEmulator() after Auth has started making
92+
// connections, so long as the config matches the existing config. This results in a no-op.
93+
return;
94+
}
95+
96+
authInternal.config.emulator = emulator;
97+
authInternal.emulatorConfig = emulatorConfig;
98+
authInternal.settings.appVerificationDisabledForTesting = true;
99+
78100
if (!disableWarnings) {
79101
emitEmulatorWarning();
80102
}

0 commit comments

Comments
 (0)