Skip to content

Commit 30edffc

Browse files
committed
Add hooks into auth-next for emulator config (#3716)
* Add hooks into emulator for auth-next * Formatting * PR feedback * Fix broken tests * Formatting
1 parent e7a1282 commit 30edffc

File tree

10 files changed

+144
-18
lines changed

10 files changed

+144
-18
lines changed

packages-exp/auth-exp/src/api/authentication/token.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ describe('requestStsToken', () => {
3535
beforeEach(async () => {
3636
auth = await testAuth();
3737
const { apiKey, tokenApiHost, apiScheme } = auth.config;
38-
endpoint = `${apiScheme}://${tokenApiHost}/${_ENDPOINT}?key=${apiKey}`;
38+
endpoint = `${apiScheme}://${tokenApiHost}${_ENDPOINT}?key=${apiKey}`;
3939
fetch.setUp();
4040
});
4141

packages-exp/auth-exp/src/api/authentication/token.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,15 @@
1919

2020
import { querystring } from '@firebase/util';
2121

22-
import { _performFetchWithErrorHandling, HttpMethod } from '../';
23-
import { AuthCore } from '../../model/auth';
22+
import {
23+
_getFinalTarget,
24+
_performFetchWithErrorHandling,
25+
HttpMethod
26+
} from '../';
2427
import { FetchProvider } from '../../core/util/fetch_provider';
28+
import { AuthCore } from '../../model/auth';
2529

26-
export const _ENDPOINT = 'v1/token';
30+
export const _ENDPOINT = '/v1/token';
2731
const GRANT_TYPE = 'refresh_token';
2832

2933
/** The server responses with snake_case; we convert to camelCase */
@@ -50,10 +54,10 @@ export async function requestStsToken(
5054
'grant_type': GRANT_TYPE,
5155
'refresh_token': refreshToken
5256
}).slice(1);
53-
const { apiScheme, tokenApiHost, apiKey, sdkClientVersion } = auth.config;
54-
const url = `${apiScheme}://${tokenApiHost}/${_ENDPOINT}`;
57+
const { tokenApiHost, apiKey, sdkClientVersion } = auth.config;
58+
const url = _getFinalTarget(auth, tokenApiHost, _ENDPOINT, `key=${apiKey}`);
5559

56-
return FetchProvider.fetch()(`${url}?key=${apiKey}`, {
60+
return FetchProvider.fetch()(url, {
5761
method: HttpMethod.POST,
5862
headers: {
5963
'X-Client-Version': sdkClientVersion,

packages-exp/auth-exp/src/api/index.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import {
2525
import { fail } from '../core/util/assert';
2626
import { Delay } from '../core/util/delay';
2727
import { FetchProvider } from '../core/util/fetch_provider';
28-
import { AuthCore } from '../model/auth';
28+
import { Auth, AuthCore } from '../model/auth';
2929
import { IdTokenResponse, TaggedWithTokenResponse } from '../model/id_token';
3030
import { IdTokenMfaResponse } from './authentication/mfa';
3131
import { SERVER_ERROR_MAP, ServerError, ServerErrorMap } from './errors';
@@ -99,7 +99,7 @@ export async function _performApiRequest<T, V>(
9999
}
100100

101101
return FetchProvider.fetch()(
102-
`${auth.config.apiScheme}://${auth.config.apiHost}${path}?${query}`,
102+
_getFinalTarget(auth, auth.config.apiHost, path, query),
103103
{
104104
method,
105105
headers,
@@ -115,6 +115,7 @@ export async function _performFetchWithErrorHandling<V>(
115115
customErrorMap: Partial<ServerErrorMap<ServerError>>,
116116
fetchFn: () => Promise<Response>
117117
): Promise<V> {
118+
(auth as Auth)._canInitEmulator = false;
118119
const errorMap = { ...SERVER_ERROR_MAP, ...customErrorMap };
119120
try {
120121
const response: Response = await Promise.race<Promise<Response>>([
@@ -183,6 +184,22 @@ export async function _performSignInRequest<T, V extends IdTokenResponse>(
183184
return serverResponse;
184185
}
185186

187+
export function _getFinalTarget(
188+
auth: AuthCore,
189+
host: string,
190+
path: string,
191+
query: string
192+
): string {
193+
const { emulator } = auth.config;
194+
const base = `${host}${path}?${query}`;
195+
196+
if (!emulator) {
197+
return `${auth.config.apiScheme}://${base}`;
198+
}
199+
200+
return `http://${emulator.hostname}:${emulator.port}/${base}`;
201+
}
202+
186203
function makeNetworkTimeout<T>(appName: string): Promise<T> {
187204
return new Promise((_, reject) =>
188205
setTimeout(() => {

packages-exp/auth-exp/src/core/auth/auth_impl.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@ export class AuthImplCompat<T extends User> implements Auth, _FirebaseService {
6363
private idTokenSubscription = new Subscription<T>(this);
6464
private redirectUser: T | null = null;
6565
private isProactiveRefreshEnabled = false;
66+
67+
// Any network calls will set this to true and prevent subsequent emulator
68+
// initialization
69+
_canInitEmulator = true;
6670
_isInitialized = false;
6771
_initializationPromise: Promise<void> | null = null;
6872
_popupRedirectResolver: PopupRedirectResolver | null = null;
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/**
2+
* @license
3+
* Copyright 2020 Google LLC
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
import { expect } from 'chai';
19+
20+
import { Auth, User } from '@firebase/auth-types-exp';
21+
import { FirebaseError } from '@firebase/util';
22+
23+
import { endpointUrl, mockEndpoint } from '../../../test/helpers/api/helper';
24+
import { testAuth, testUser } from '../../../test/helpers/mock_auth';
25+
import * as fetch from '../../../test/helpers/mock_fetch';
26+
import { _getFinalTarget, Endpoint } from '../../api';
27+
import { _castAuth } from './auth_impl';
28+
import { useEmulator } from './initialize';
29+
30+
describe('core/auth/initialize', () => {
31+
let auth: Auth;
32+
let user: User;
33+
let normalEndpoint: fetch.Route;
34+
let emulatorEndpoint: fetch.Route;
35+
36+
beforeEach(async () => {
37+
auth = await testAuth();
38+
user = testUser(_castAuth(auth), 'uid', 'email', true);
39+
fetch.setUp();
40+
normalEndpoint = mockEndpoint(Endpoint.DELETE_ACCOUNT, {});
41+
emulatorEndpoint = fetch.mock(
42+
`http://localhost:2020/${endpointUrl(Endpoint.DELETE_ACCOUNT).replace(
43+
/^.*:\/\//,
44+
''
45+
)}`,
46+
{}
47+
);
48+
});
49+
50+
afterEach(() => {
51+
fetch.tearDown();
52+
});
53+
54+
context('useEmulator', () => {
55+
it('fails if a network request has already been made', async () => {
56+
await user.delete();
57+
expect(() => useEmulator(auth, 'localhost', 2020)).to.throw(
58+
FirebaseError,
59+
'auth/emulator-config-failed'
60+
);
61+
});
62+
63+
it('updates the endpoint appropriately', async () => {
64+
useEmulator(auth, 'localhost', 2020);
65+
await user.delete();
66+
expect(normalEndpoint.calls.length).to.eq(0);
67+
expect(emulatorEndpoint.calls.length).to.eq(1);
68+
});
69+
});
70+
});

packages-exp/auth-exp/src/core/auth/initialize.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,11 @@ import { FirebaseApp } from '@firebase/app-types-exp';
2020
import * as externs from '@firebase/auth-types-exp';
2121

2222
import { Dependencies } from '../../model/auth';
23+
import { AuthErrorCode } from '../errors';
2324
import { Persistence } from '../persistence';
25+
import { assert } from '../util/assert';
2426
import { _getInstance } from '../util/instantiator';
25-
import { AuthImpl } from './auth_impl';
27+
import { _castAuth, AuthImpl } from './auth_impl';
2628

2729
export function initializeAuth(
2830
app: FirebaseApp = getApp(),
@@ -34,6 +36,22 @@ export function initializeAuth(
3436
return auth;
3537
}
3638

39+
export function useEmulator(
40+
authExtern: externs.Auth,
41+
hostname: string,
42+
port: number
43+
): void {
44+
const auth = _castAuth(authExtern);
45+
assert(auth._canInitEmulator, AuthErrorCode.EMULATOR_CONFIG_FAILED, {
46+
appName: auth.name
47+
});
48+
49+
auth.config.emulator = {
50+
hostname,
51+
port
52+
};
53+
}
54+
3755
export function _initializeAuthInstance(
3856
auth: AuthImpl,
3957
deps?: Dependencies

packages-exp/auth-exp/src/core/errors.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@
1919
import * as externs from '@firebase/auth-types-exp';
2020
import { ErrorFactory, ErrorMap } from '@firebase/util';
2121

22-
import { AppName } from '../model/auth';
2322
import { IdTokenMfaResponse } from '../api/authentication/mfa';
23+
import { AppName } from '../model/auth';
2424

2525
/*
2626
* Developer facing Firebase Auth error codes.
@@ -40,6 +40,7 @@ export const enum AuthErrorCode {
4040
DYNAMIC_LINK_NOT_ACTIVATED = 'dynamic-link-not-activated',
4141
EMAIL_CHANGE_NEEDS_VERIFICATION = 'email-change-needs-verification',
4242
EMAIL_EXISTS = 'email-already-in-use',
43+
EMULATOR_CONFIG_FAILED = 'emulator-config-failed',
4344
EXPIRED_OOB_CODE = 'expired-action-code',
4445
EXPIRED_POPUP_REQUEST = 'cancelled-popup-request',
4546
INTERNAL_ERROR = 'internal-error',
@@ -154,6 +155,10 @@ const ERRORS: ErrorMap<AuthErrorCode> = {
154155
'Multi-factor users must always have a verified email.',
155156
[AuthErrorCode.EMAIL_EXISTS]:
156157
'The email address is already in use by another account.',
158+
[AuthErrorCode.EMULATOR_CONFIG_FAILED]:
159+
'Auth instance has already been used to make a network call. Auth can ' +
160+
'no longer be configured to use the emulator. Try calling ' +
161+
'"useEmulator()" sooner.',
157162
[AuthErrorCode.EXPIRED_OOB_CODE]: 'The action code has expired.',
158163
[AuthErrorCode.EXPIRED_POPUP_REQUEST]:
159164
'This operation has been cancelled due to another conflicting popup being opened.',

packages-exp/auth-exp/src/core/user/token_manager.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ describe('core/user/token_manager', () => {
8989
let mock: fetch.Route;
9090
beforeEach(() => {
9191
const { apiKey, tokenApiHost, apiScheme } = auth.config;
92-
const endpoint = `${apiScheme}://${tokenApiHost}/${_ENDPOINT}?key=${apiKey}`;
92+
const endpoint = `${apiScheme}://${tokenApiHost}${_ENDPOINT}?key=${apiKey}`;
9393
mock = fetch.mock(endpoint, {
9494
'access_token': 'new-access-token',
9595
'refresh_token': 'new-refresh-token',

packages-exp/auth-exp/src/model/auth.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,20 @@ export type AppName = string;
2525
export type ApiKey = string;
2626
export type AuthDomain = string;
2727

28+
interface ConfigInternal extends externs.Config {
29+
emulator?: {
30+
hostname: string;
31+
port: number;
32+
};
33+
}
34+
2835
/**
2936
* Core implementation of the Auth object, the signatures here should match across both legacy
3037
* and modern implementations
3138
*/
3239
export interface AuthCore {
3340
readonly name: AppName;
34-
readonly config: externs.Config;
41+
readonly config: ConfigInternal;
3542
languageCode: string | null;
3643
tenantId: string | null;
3744
readonly settings: externs.AuthSettings;
@@ -42,6 +49,7 @@ export interface AuthCore {
4249

4350
export interface Auth extends AuthCore {
4451
currentUser: User | null;
52+
_canInitEmulator: boolean;
4553
_isInitialized: boolean;
4654
_initializationPromise: Promise<void> | null;
4755
updateCurrentUser(user: User | null): Promise<void>;

packages-exp/auth-exp/test/helpers/api/helper.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,14 @@ import { Endpoint } from '../../../src/api';
1919
import { TEST_HOST, TEST_KEY, TEST_SCHEME } from '../mock_auth';
2020
import { mock, Route } from '../mock_fetch';
2121

22+
export function endpointUrl(endpoint: Endpoint): string {
23+
return `${TEST_SCHEME}://${TEST_HOST}${endpoint}?key=${TEST_KEY}`;
24+
}
25+
2226
export function mockEndpoint(
2327
endpoint: Endpoint,
2428
response: object,
2529
status = 200
2630
): Route {
27-
return mock(
28-
`${TEST_SCHEME}://${TEST_HOST}${endpoint}?key=${TEST_KEY}`,
29-
response,
30-
status
31-
);
31+
return mock(endpointUrl(endpoint), response, status);
3232
}

0 commit comments

Comments
 (0)