Skip to content

Commit 04b2d33

Browse files
committed
Finish emulator implementation
1 parent 4d9dc69 commit 04b2d33

File tree

12 files changed

+158
-128
lines changed

12 files changed

+158
-128
lines changed

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

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,9 @@ import { mockEndpoint } from '../../test/helpers/api/helper';
2525
import { testAuth, TestAuth } from '../../test/helpers/mock_auth';
2626
import * as mockFetch from '../../test/helpers/mock_fetch';
2727
import { AuthErrorCode } from '../core/errors';
28+
import { ConfigInternal } from '../model/auth';
2829
import {
29-
_performApiRequest,
30-
DEFAULT_API_TIMEOUT_MS,
31-
Endpoint,
32-
HttpHeader,
33-
HttpMethod
30+
_getFinalTarget, _performApiRequest, DEFAULT_API_TIMEOUT_MS, Endpoint, HttpHeader, HttpMethod
3431
} from './';
3532
import { ServerError } from './errors';
3633

@@ -327,4 +324,18 @@ describe('api/_performApiRequest', () => {
327324
}
328325
});
329326
});
327+
328+
context('_getFinalTarget', () => {
329+
it('works properly with a non-emulated environment', () => {
330+
expect(_getFinalTarget(auth, 'host', '/path', 'query=test')).to.eq('mock://host/path?query=test');
331+
});
332+
333+
it('works properly with an emulated environment', () => {
334+
(auth.config as ConfigInternal).emulator = {
335+
hostname: 'localhost',
336+
port: 5000,
337+
};
338+
expect(_getFinalTarget(auth, 'host', '/path', 'query=test')).to.eq('http://localhost:5000/host/path?query=test');
339+
});
340+
});
330341
});

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

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,10 @@
1717

1818
import { FirebaseError, querystring } from '@firebase/util';
1919

20-
import {
21-
AUTH_ERROR_FACTORY,
22-
AuthErrorCode,
23-
NamedErrorParams
24-
} from '../core/errors';
20+
import { AUTH_ERROR_FACTORY, AuthErrorCode, NamedErrorParams } from '../core/errors';
2521
import { fail } from '../core/util/assert';
2622
import { Delay } from '../core/util/delay';
23+
import { _emulatorUrl } from '../core/util/emulator';
2724
import { FetchProvider } from '../core/util/fetch_provider';
2825
import { Auth, AuthCore } from '../model/auth';
2926
import { IdTokenResponse, TaggedWithTokenResponse } from '../model/id_token';
@@ -191,14 +188,13 @@ export function _getFinalTarget(
191188
path: string,
192189
query: string
193190
): string {
194-
const { emulator } = auth.config;
195191
const base = `${host}${path}?${query}`;
196192

197-
if (!emulator) {
193+
if (!auth.config.emulator) {
198194
return `${auth.config.apiScheme}://${base}`;
199195
}
200196

201-
return `http://${emulator.hostname}:${emulator.port}/${base}`;
197+
return _emulatorUrl(auth.config, base);
202198
}
203199

204200
function makeNetworkTimeout<T>(appName: string): Promise<T> {

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

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,19 +23,18 @@ import * as sinonChai from 'sinon-chai';
2323
import { FirebaseApp } from '@firebase/app-types-exp';
2424
import { FirebaseError } from '@firebase/util';
2525

26-
import { testUser } from '../../../test/helpers/mock_auth';
26+
import { endpointUrl, mockEndpoint } from '../../../test/helpers/api/helper';
27+
import { testAuth, TestAuth, testUser } from '../../../test/helpers/mock_auth';
28+
import * as fetch from '../../../test/helpers/mock_fetch';
29+
import { Endpoint } from '../../api';
2730
import { Auth } from '../../model/auth';
2831
import { User } from '../../model/user';
2932
import { Persistence } from '../persistence';
3033
import { inMemoryPersistence } from '../persistence/in_memory';
3134
import { _getInstance } from '../util/instantiator';
3235
import * as navigator from '../util/navigator';
3336
import {
34-
_castAuth,
35-
AuthImpl,
36-
DEFAULT_API_HOST,
37-
DEFAULT_API_SCHEME,
38-
DEFAULT_TOKEN_API_HOST
37+
_castAuth, AuthImpl, DEFAULT_API_HOST, DEFAULT_API_SCHEME, DEFAULT_TOKEN_API_HOST
3938
} from './auth_impl';
4039
import { _initializeAuthInstance } from './initialize';
4140

@@ -388,3 +387,47 @@ describe('core/auth/auth_impl', () => {
388387
});
389388
});
390389
});
390+
391+
// These tests are separate because they are using a different auth with
392+
// separate setup and config
393+
describe('core/auth/auth_impl useEmulator', () => {
394+
let auth: TestAuth;
395+
let user: User;
396+
let normalEndpoint: fetch.Route;
397+
let emulatorEndpoint: fetch.Route;
398+
399+
beforeEach(async () => {
400+
auth = await testAuth();
401+
user = testUser(_castAuth(auth), 'uid', 'email', true);
402+
fetch.setUp();
403+
normalEndpoint = mockEndpoint(Endpoint.DELETE_ACCOUNT, {});
404+
emulatorEndpoint = fetch.mock(
405+
`http://localhost:2020/${endpointUrl(Endpoint.DELETE_ACCOUNT).replace(
406+
/^.*:\/\//,
407+
''
408+
)}`,
409+
{}
410+
);
411+
});
412+
413+
afterEach(() => {
414+
fetch.tearDown();
415+
});
416+
417+
context('useEmulator', () => {
418+
it('fails if a network request has already been made', async () => {
419+
await user.delete();
420+
expect(() => auth.useEmulator('localhost', 2020)).to.throw(
421+
FirebaseError,
422+
'auth/emulator-config-failed'
423+
);
424+
});
425+
426+
it('updates the endpoint appropriately', async () => {
427+
auth.useEmulator('localhost', 2020);
428+
await user.delete();
429+
expect(normalEndpoint.calls.length).to.eq(0);
430+
expect(emulatorEndpoint.calls.length).to.eq(1);
431+
});
432+
});
433+
});

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

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,23 +18,16 @@
1818
import { _FirebaseService, FirebaseApp } from '@firebase/app-types-exp';
1919
import * as externs from '@firebase/auth-types-exp';
2020
import {
21-
CompleteFn,
22-
createSubscribe,
23-
ErrorFn,
24-
NextFn,
25-
Observer,
26-
Subscribe,
27-
Unsubscribe
21+
CompleteFn, createSubscribe, ErrorFn, NextFn, Observer, Subscribe, Unsubscribe
2822
} from '@firebase/util';
2923

30-
import { Auth, AuthCore } from '../../model/auth';
24+
import { Auth, AuthCore, ConfigInternal } from '../../model/auth';
3125
import { PopupRedirectResolver } from '../../model/popup_redirect';
3226
import { User, UserParameters } from '../../model/user';
3327
import { AuthErrorCode } from '../errors';
3428
import { Persistence } from '../persistence';
3529
import {
36-
_REDIRECT_USER_KEY_NAME,
37-
PersistenceUserManager
30+
_REDIRECT_USER_KEY_NAME, PersistenceUserManager
3831
} from '../persistence/persistence_user_manager';
3932
import { _reloadWithoutSaving } from '../user/reload';
4033
import { UserImpl } from '../user/user_impl';
@@ -82,7 +75,7 @@ export class AuthImplCompat<T extends User> implements Auth, _FirebaseService {
8275

8376
constructor(
8477
public readonly app: FirebaseApp,
85-
public readonly config: externs.Config,
78+
public readonly config: ConfigInternal,
8679
private readonly _userProvider: UserProvider<T>
8780
) {
8881
this.name = app.name;
@@ -189,6 +182,20 @@ export class AuthImplCompat<T extends User> implements Auth, _FirebaseService {
189182
this.languageCode = _getUserLanguage();
190183
}
191184

185+
useEmulator(
186+
hostname: string,
187+
port: number
188+
): void {
189+
assert(this._canInitEmulator, AuthErrorCode.EMULATOR_CONFIG_FAILED, {
190+
appName: this.name
191+
});
192+
193+
this.config.emulator = {
194+
hostname,
195+
port
196+
};
197+
}
198+
192199
async _delete(): Promise<void> {
193200
// TODO: Determine what we want to do in this case
194201
}

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

Lines changed: 0 additions & 70 deletions
This file was deleted.

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

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,7 @@ 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';
2423
import { Persistence } from '../persistence';
25-
import { assert } from '../util/assert';
2624
import { _getInstance } from '../util/instantiator';
2725
import { _castAuth, AuthImpl } from './auth_impl';
2826

@@ -36,22 +34,6 @@ export function initializeAuth(
3634
return auth;
3735
}
3836

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-
5537
export function _initializeAuthInstance(
5638
auth: AuthImpl,
5739
deps?: Dependencies
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { expect } from 'chai';
2+
3+
import { ConfigInternal } from '../../model/auth';
4+
import { _emulatorUrl } from './emulator';
5+
6+
describe('core/util/emulator', () => {
7+
const config: ConfigInternal = {
8+
emulator: {
9+
hostname: 'localhost',
10+
port: 4000
11+
}
12+
} as ConfigInternal;
13+
14+
it('builds the proper URL with no path', () => {
15+
expect(_emulatorUrl(config)).to.eq('http://localhost:4000');
16+
});
17+
18+
it('builds the proper URL with a path', () => {
19+
expect(_emulatorUrl(config, '/test/path')).to.eq('http://localhost:4000/test/path');
20+
});
21+
22+
it('builds the proper URL with a path missing separator', () => {
23+
expect(_emulatorUrl(config, 'test/path')).to.eq('http://localhost:4000/test/path');
24+
});
25+
});
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { ConfigInternal } from '../../model/auth';
2+
import { debugAssert } from './assert';
3+
4+
export function _emulatorUrl(config: ConfigInternal, path?: string): string {
5+
debugAssert(config.emulator, 'Emulator should always be set here');
6+
const {hostname, port} = config.emulator;
7+
8+
const base = `http://${hostname}:${port}`;
9+
if (!path) {
10+
return base;
11+
}
12+
13+
const sep = path.startsWith('/') ? '' : '/';
14+
return `${base}${sep}${path}`;
15+
}

packages-exp/auth-exp/src/core/util/validate_origin.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ const IP_ADDRESS_REGEX = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/;
2525
const HTTP_REGEX = /^https?/;
2626

2727
export async function _validateOrigin(auth: Auth): Promise<void> {
28+
// Skip origin validation if we are in an emulated environment
29+
if (auth.config.emulator) {
30+
return;
31+
}
32+
2833
const { authorizedDomains } = await _getProjectConfig(auth);
2934

3035
for (const domain of authorizedDomains) {

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

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

28-
interface ConfigInternal extends externs.Config {
28+
export interface ConfigInternal extends externs.Config {
2929
emulator?: {
3030
hostname: string;
3131
port: number;

packages-exp/auth-exp/src/platform_browser/iframe/iframe.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,15 @@ import { querystring } from '@firebase/util';
2121
import { AUTH_ERROR_FACTORY, AuthErrorCode } from '../../core/errors';
2222
import { assert } from '../../core/util/assert';
2323
import { Delay } from '../../core/util/delay';
24+
import { _emulatorUrl } from '../../core/util/emulator';
2425
import { AuthCore } from '../../model/auth';
2526
import { _window } from '../auth_window';
2627
import * as gapiLoader from './gapi';
2728

2829
const PING_TIMEOUT = new Delay(5000, 15000);
30+
const IFRAME_PATH = '__/auth/iframe';
31+
const EMULATED_IFRAME_PATH = 'emulator/auth/iframe';
32+
2933
const IFRAME_ATTRIBUTES = {
3034
style: {
3135
position: 'absolute',
@@ -36,10 +40,12 @@ const IFRAME_ATTRIBUTES = {
3640
};
3741

3842
function getIframeUrl(auth: AuthCore): string {
39-
const url = `https://${auth.config.authDomain!}/__/auth/iframe`;
43+
const config = auth.config;
44+
const url = config.emulator ? _emulatorUrl(config, EMULATED_IFRAME_PATH) :
45+
`https://${auth.config.authDomain!}/${IFRAME_PATH}`;
4046

4147
const params = {
42-
apiKey: auth.config.apiKey,
48+
apiKey: config.apiKey,
4349
appName: auth.name,
4450
v: SDK_VERSION
4551
};

0 commit comments

Comments
 (0)